Skip to content

Commit

Permalink
chore: gracefully handle dev callback failed (#1081)
Browse files Browse the repository at this point in the history
  • Loading branch information
juliusmarminge authored Jan 4, 2025
1 parent e14e353 commit 95388b5
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 13 deletions.
49 changes: 43 additions & 6 deletions packages/uploadthing/src/_internal/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
29 changes: 22 additions & 7 deletions packages/uploadthing/src/_internal/upload-browser.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions playground/middleware.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 95388b5

Please sign in to comment.