diff --git a/packages/uploadthing/src/_internal/handler.ts b/packages/uploadthing/src/_internal/handler.ts index 6361de5382..cc591dcada 100644 --- a/packages/uploadthing/src/_internal/handler.ts +++ b/packages/uploadthing/src/_internal/handler.ts @@ -8,6 +8,7 @@ import { HttpServerRequest, HttpServerResponse, } from "@effect/platform"; +import type { ResponseError } from "@effect/platform/HttpClientError"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -601,6 +602,42 @@ const handleUploadAction = (opts: { Effect.flatMap(httpClient.execute), ); + const handleDevStreamError = Effect.fn("handleDevStreamError")(function* ( + err: ResponseError, + chunk: string, + ) { + const schema = Schema.parseJson( + Schema.Struct({ file: UploadedFileData }), + ); + const parsedChunk = yield* Schema.decodeUnknown(schema)(chunk); + const key = parsedChunk.file.key; + + yield* Effect.logError( + "Failed to forward callback request from dev stream", + ).pipe(Effect.annotateLogs({ fileKey: key, error: err.message })); + + const httpResponse = yield* HttpClientRequest.post( + "/callback-result", + ).pipe( + HttpClientRequest.prependUrl(ingestUrl), + HttpClientRequest.setHeaders({ + "x-uploadthing-api-key": Redacted.value(apiKey), + "x-uploadthing-version": pkgJson.version, + "x-uploadthing-be-adapter": beAdapter, + "x-uploadthing-fe-package": fePackage, + }), + HttpClientRequest.bodyJson({ + fileKey: key, + error: `Failed to forward callback request from dev stream: ${err.message}`, + }), + Effect.flatMap(httpClient.execute), + ); + + yield* logHttpClientResponse("Reported callback error to UploadThing")( + httpResponse, + ); + }); + // Send metadata to UT server (non blocking as a daemon) // In dev, keep the stream open and simulate the callback requests as // files complete uploading @@ -624,14 +661,14 @@ const handleUploadAction = (opts: { HttpBody.text(chunk.payload, "application/json"), ), httpClient.execute, - Effect.tapBoth({ - onSuccess: logHttpClientResponse( + Effect.tap( + logHttpClientResponse( "Successfully forwarded callback request from dev stream", ), - onFailure: logHttpClientError( - "Failed to forward callback request from dev stream", - ), - }), + ), + Effect.catchTag("ResponseError", (err) => + handleDevStreamError(err, chunk.payload), + ), Effect.annotateLogs(chunk), Effect.asVoid, Effect.ignoreLogged, diff --git a/packages/uploadthing/src/_internal/upload-browser.ts b/packages/uploadthing/src/_internal/upload-browser.ts index 395b161f59..82d22b1a74 100644 --- a/packages/uploadthing/src/_internal/upload-browser.ts +++ b/packages/uploadthing/src/_internal/upload-browser.ts @@ -1,5 +1,6 @@ import { unsafeCoerce } from "effect/Function"; import * as Micro from "effect/Micro"; +import { hasProperty, isRecord } from "effect/Predicate"; import type { FetchContext, FetchError } from "@uploadthing/shared"; import { fetchEff, UploadThingError } from "@uploadthing/shared"; @@ -37,13 +38,27 @@ const uploadWithProgress = ( previousLoaded = loaded; }); xhr.addEventListener("load", () => { - resume( - xhr.status >= 200 && xhr.status < 300 - ? Micro.succeed(xhr.response) - : Micro.die( - `XHR failed ${xhr.status} ${xhr.statusText} - ${JSON.stringify(xhr.response)}`, - ), - ); + if (xhr.status >= 200 && xhr.status < 300 && isRecord(xhr.response)) { + if (hasProperty(xhr.response, "error")) { + resume( + new UploadThingError({ + code: "UPLOAD_FAILED", + message: String(xhr.response.error), + data: xhr.response as never, + }), + ); + } else { + resume(Micro.succeed(xhr.response)); + } + } else { + resume( + new UploadThingError({ + code: "UPLOAD_FAILED", + message: `XHR failed ${xhr.status} ${xhr.statusText}`, + data: xhr.response as never, + }), + ); + } }); // Is there a case when the client would throw and diff --git a/playground/middleware.ts b/playground/middleware.ts new file mode 100644 index 0000000000..d777adaa93 --- /dev/null +++ b/playground/middleware.ts @@ -0,0 +1,22 @@ +import { + MiddlewareConfig, + NextResponse, + type NextMiddleware, +} from "next/server"; + +import { getSession } from "./lib/data"; + +export default (async (req) => { + if (req.nextUrl.pathname !== "/") { + const sesh = await getSession(); + if (!sesh) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + } + + return NextResponse.next(); +}) satisfies NextMiddleware; + +export const config = { + matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], +} satisfies MiddlewareConfig;