diff --git a/.changeset/curvy-rings-own.md b/.changeset/curvy-rings-own.md new file mode 100644 index 0000000000..c93246c12f --- /dev/null +++ b/.changeset/curvy-rings-own.md @@ -0,0 +1,7 @@ +--- +"uploadthing": patch +--- + +fix: improve error handling in utapi + +sometimes errors got silently ignored when they shouldn't have \ No newline at end of file diff --git a/packages/uploadthing/src/_internal/upload-server.ts b/packages/uploadthing/src/_internal/upload-server.ts index 6069d05f08..e0f238ed0c 100644 --- a/packages/uploadthing/src/_internal/upload-server.ts +++ b/packages/uploadthing/src/_internal/upload-server.ts @@ -6,6 +6,7 @@ import { UploadThingError } from "@uploadthing/shared"; import { version } from "../../package.json"; import type { FileEsque } from "../sdk/types"; +import { logHttpClientError } from "./logger"; import type { UploadPutResult } from "./types"; export const uploadWithoutProgress = ( @@ -14,7 +15,7 @@ export const uploadWithoutProgress = ( ) => Effect.gen(function* () { const formData = new FormData(); - formData.append("file", file as Blob); // File data **MUST GO LAST** + formData.append("file", file as Blob); const httpClient = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk, @@ -24,6 +25,7 @@ export const uploadWithoutProgress = ( HttpClientRequest.setHeader("Range", "bytes=0-"), HttpClientRequest.setHeader("x-uploadthing-version", version), httpClient.execute, + Effect.tapError(logHttpClientError("Failed to upload file")), Effect.mapError( (e) => new UploadThingError({ diff --git a/packages/uploadthing/src/sdk/utils.ts b/packages/uploadthing/src/sdk/utils.ts index 06f3dba17c..e3b7a66943 100644 --- a/packages/uploadthing/src/sdk/utils.ts +++ b/packages/uploadthing/src/sdk/utils.ts @@ -120,6 +120,9 @@ export const uploadFile = ( opts.contentDisposition ?? "inline", opts.acl, ).pipe( + Effect.catchTag("UploadThingError", (e) => + Effect.fail(UploadThingError.toObject(e)), + ), Effect.catchTag("ConfigError", () => Effect.fail({ code: "INVALID_SERVER_CONFIG", @@ -128,6 +131,9 @@ export const uploadFile = ( ), ); const response = yield* uploadWithoutProgress(file, presigned).pipe( + Effect.catchTag("UploadThingError", (e) => + Effect.fail(UploadThingError.toObject(e)), + ), Effect.catchTag("ResponseError", (e) => Effect.fail({ code: "UPLOAD_FAILED", diff --git a/packages/uploadthing/test/__test-helpers.ts b/packages/uploadthing/test/__test-helpers.ts index f018623b6b..7acf7074eb 100644 --- a/packages/uploadthing/test/__test-helpers.ts +++ b/packages/uploadthing/test/__test-helpers.ts @@ -63,7 +63,7 @@ export const baseHeaders = { * Call this in each MSW handler to spy on the request * and provide an easy way to assert on the request */ -const callRequestSpy = async (request: StrictRequest) => +export const callRequestSpy = async (request: StrictRequest) => requestSpy(new URL(request.url).toString(), { method: request.method, headers: Object.fromEntries(request.headers), diff --git a/packages/uploadthing/test/sdk.test.ts b/packages/uploadthing/test/sdk.test.ts index f270f6d143..3c46673a59 100644 --- a/packages/uploadthing/test/sdk.test.ts +++ b/packages/uploadthing/test/sdk.test.ts @@ -13,6 +13,7 @@ import { UTApi, UTFile } from "../src/sdk"; import type { UploadFileResult } from "../src/sdk/types"; import { API_URL, + callRequestSpy, handlers, INGEST_URL, requestSpy, @@ -97,6 +98,46 @@ describe("uploadFiles", () => { expect.objectContaining({ method: "PUT" }), ); }); + + it("gracefully handles failed requests", async () => { + const mockedIngestUrl = "https://mocked.ingest.uploadthing.com"; + msw.use( + http.all<{ key: string }>( + `${mockedIngestUrl}/:key`, + async ({ request }) => { + await callRequestSpy(request); + return HttpResponse.json({ error: "Upload failed" }, { status: 400 }); + }, + ), + ); + + const utapi = new UTApi({ + token: testToken.encoded, + /** + * Explicitly set the ingestUrl to the mocked one + * to ensure the request is made to the mocked ingest + * endpoint that yields a 400 error. + */ + ingestUrl: mockedIngestUrl, + }); + const result = await utapi.uploadFiles(fooFile); + expect(result).toStrictEqual({ + data: null, + error: { + code: "UPLOAD_FAILED", + data: undefined, + message: "Failed to upload file", + }, + }); + + expect(requestSpy).toHaveBeenCalledTimes(1); + expect(requestSpy).toHaveBeenCalledWith( + expect.stringContaining(mockedIngestUrl), + expect.objectContaining({ method: "PUT" }), + ); + + msw.use(...handlers); + }); }); describe("uploadFilesFromUrl", () => {