Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/uploadthing/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Arr from "effect/Array";
import * as Micro from "effect/Micro";

import type { ExpandedRouteConfig } from "@uploadthing/shared";
import type { ExpandedRouteConfig, FetchEsque } from "@uploadthing/shared";
import {
createIdentityProxy,
FetchContext,
Expand Down Expand Up @@ -112,6 +112,7 @@ export const genUploader = <TRouter extends FileRouter>(
url: resolveMaybeUrlArg(initOpts?.url),
headers: opts.headers,
});
const fetchFn: FetchEsque = initOpts.fetch ?? window.fetch;

const presigneds = await Micro.runPromise(
utReporter("upload", {
Expand All @@ -123,7 +124,7 @@ export const genUploader = <TRouter extends FileRouter>(
type: f.type,
lastModified: f.lastModified,
})),
}).pipe(Micro.provideService(FetchContext, window.fetch)),
}).pipe(Micro.provideService(FetchContext, fetchFn)),
);

const totalSize = opts.files.reduce((acc, f) => acc + f.size, 0);
Expand All @@ -141,7 +142,7 @@ export const genUploader = <TRouter extends FileRouter>(
totalProgress: Math.round((totalLoaded / totalSize) * 100),
});
},
}).pipe(Micro.provideService(FetchContext, window.fetch));
}).pipe(Micro.provideService(FetchContext, fetchFn));

for (const [i, p] of presigneds.entries()) {
const file = opts.files[i];
Expand Down Expand Up @@ -254,6 +255,7 @@ export const genUploader = <TRouter extends FileRouter>(
>,
) => {
const endpoint = typeof slug === "function" ? slug(routeRegistry) : slug;
const fetchFn: FetchEsque = initOpts.fetch ?? window.fetch;

return uploadFilesInternal<TRouter, TEndpoint>(endpoint, {
...opts,
Expand All @@ -263,7 +265,7 @@ export const genUploader = <TRouter extends FileRouter>(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
input: (opts as any).input as inferEndpointInput<TRouter[TEndpoint]>,
})
.pipe((effect) =>
.pipe(Micro.provideService(FetchContext, fetchFn), (effect) =>
Micro.runPromiseExit(effect, opts.signal && { signal: opts.signal }),
)
.then((exit) => {
Expand Down
30 changes: 15 additions & 15 deletions packages/uploadthing/src/internal/upload.browser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { unsafeCoerce } from "effect/Function";
import * as Micro from "effect/Micro";

import type { FetchError } from "@uploadthing/shared";
import { FetchContext, fetchEff, UploadThingError } from "@uploadthing/shared";
import type { FetchContext, FetchError } from "@uploadthing/shared";
import { fetchEff, UploadThingError } from "@uploadthing/shared";

import { version } from "../../package.json";
import type {
Expand Down Expand Up @@ -119,7 +119,8 @@ export const uploadFilesInternal = <
opts: UploadFilesOptions<TRouter[TEndpoint]>,
): Micro.Micro<
ClientUploadedFileData<TServerOutput>[],
UploadThingError | FetchError
UploadThingError | FetchError,
FetchContext
> => {
// classic service right here
const reportEventToUT = createUTReporter({
Expand All @@ -132,16 +133,17 @@ export const uploadFilesInternal = <
const totalSize = opts.files.reduce((acc, f) => acc + f.size, 0);
let totalLoaded = 0;

return reportEventToUT("upload", {
input: "input" in opts ? opts.input : null,
files: opts.files.map((f) => ({
name: f.name,
size: f.size,
type: f.type,
lastModified: f.lastModified,
})),
}).pipe(
Micro.flatMap((presigneds) =>
return Micro.flatMap(
reportEventToUT("upload", {
input: "input" in opts ? opts.input : null,
files: opts.files.map((f) => ({
name: f.name,
size: f.size,
type: f.type,
lastModified: f.lastModified,
})),
}),
(presigneds) =>
Micro.forEach(
presigneds,
(presigned, i) =>
Expand Down Expand Up @@ -174,7 +176,5 @@ export const uploadFilesInternal = <
),
{ concurrency: 6 },
),
),
Micro.provideService(FetchContext, window.fetch),
);
};
19 changes: 19 additions & 0 deletions packages/uploadthing/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,25 @@ export type GenerateUploaderOptions = {
* @default (VERCEL_URL ?? window.location.origin) + "/api/uploadthing"
*/
url?: string | URL;
/**
* Provide a custom fetch implementation.
* @default `globalThis.fetch`
* @example
* ```ts
* fetch: (input, init) => {
* if (input.toString().startsWith(MY_SERVER_URL)) {
* // Include cookies in the request to your API
* return fetch(input, {
* ...init,
* credentials: "include",
* });
* }
*
* return fetch(input, init);
* }
* ```
*/
fetch?: FetchEsque;
/**
* The uploadthing package that is making this request
* @example "@uploadthing/react"
Expand Down
34 changes: 32 additions & 2 deletions packages/uploadthing/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

import { genUploader } from "../src/client";
import { createRouteHandler, createUploadthing } from "../src/express";
import type { ClientUploadedFileData } from "../src/types";
import type {
ClientUploadedFileData,
GenerateUploaderOptions,
} from "../src/types";
import {
appUrlPattern,
doNotExecute,
Expand All @@ -20,7 +23,9 @@
UTFS_IO_URL,
} from "./__test-helpers";

export const setupUTServer = async () => {
export const setupUTServer = async (
clientOpts?: Partial<GenerateUploaderOptions>,
) => {
const f = createUploadthing({
errorFormatter(err) {
return { message: err.message };
Expand Down Expand Up @@ -90,6 +95,7 @@
const { uploadFiles } = genUploader<typeof router>({
package: "vitest",
url: `http://localhost:${port}`,
...clientOpts,
});

return {
Expand Down Expand Up @@ -228,6 +234,30 @@
await close();
});

it("uses custom fetch implementation if set", async () => {
const fetchFn = vi.fn();

const { uploadFiles, close } = await setupUTServer({
fetch: (input, init) => {
fetchFn(input, init);
return window.fetch(input, init);
},
});

const file = new File(["foo"], "foo.txt", { type: "text/plain" });
await uploadFiles("foo", { files: [file] });

expect(fetchFn).toHaveBeenCalledWith(

Check failure on line 250 in packages/uploadthing/test/client.test.ts

View workflow job for this annotation

GitHub Actions / build

test/client.test.ts > uploadFiles > uses custom fetch implementation if set

AssertionError: expected "spy" to be called with arguments: [ Any<String>, ObjectContaining{…} ] Received: 1st spy call: Array [ - Any<String>, - ObjectContaining { - "body": Any<FormData>, + "http://localhost:40803/api/uploadthing?actionType=upload&slug=foo", + Object { + "body": "{\"files\":[{\"name\":\"foo.txt\",\"size\":3,\"type\":\"text/plain\",\"lastModified\":1734968628264}]}", + "headers": Headers { + Symbol(entries): Object { + "content-type": Object { + "name": "Content-Type", + "value": Array [ + "application/json", + ], + }, + "x-uploadthing-package": Object { + "name": "x-uploadthing-package", + "value": Array [ + "vitest", + ], + }, + "x-uploadthing-version": Object { + "name": "x-uploadthing-version", + "value": Array [ + "7.4.1", + ], + }, + }, + }, "method": "POST", + "signal": AbortSignal { + "aborted": false, + "onabort": null, + "reason": null, + Symbol(listeners): Object { + "abort": Array [ + [Function anonymous], + [Function abort], + ], + }, + Symbol(listenerOptions): Object { + "abort": Array [ + Object { + "once": true, + }, + Object { + "once": true, + }, + ], + }, + }, }, ] 2nd spy call: Array [ - Any<String>, - ObjectContaining { - "body": Any<FormData>, - "method": "POST", + "https://fra1.ingest.uploadthing.com/u7rKiJYp268sasdVZyTSrCnsTiNZMfdv79G2LYWweVb0gB4K?expires=1734972228285&x-ut-identifier=app-1&x-ut-file-name=foo.txt&x-ut-file-size=3&x-ut-file-type=text%252Fplain&x-ut-slug=foo&x-ut-content-disposition=inline&signature=hmac-sha256%3Dcce5052c046e10d1260ed3d35a65227998f2a7f450a0bc11278438b6f6dc2862", + Object { + "headers": Headers { + Symbol(entries): Object {}, + }, + "method": "HEAD", + "signal": AbortSignal { + "aborted": false, + "onabort": null, + "reason": null, + Symbol(listeners): Object { + "abort": Array [ + [Function anonymous], + ], + }, + Symbol(listenerOptions): Object { + "abort": Array [ + Object { + "once": true, + }, + ], + }, + }, }, ] Number of calls: 2 ❯ test/client.test.ts:250:21
expect.any(String),
expect.objectContaining({
method: "POST",
body: expect.any(FormData),
}),
);

await close();
});

// Should we retry?
// it("succeeds after retries", { timeout: 15e3 }, async ({ db }) => {
// const { uploadFiles, close } = await setupUTServer();
Expand Down
Loading