Skip to content

Commit

Permalink
fix: don't use sse (#480)
Browse files Browse the repository at this point in the history
Co-authored-by: Mark R. Florkowski <[email protected]>
  • Loading branch information
juliusmarminge and markflorkowski committed Nov 20, 2023
1 parent 7440064 commit 67109c8
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 108 deletions.
7 changes: 7 additions & 0 deletions .changeset/curly-lions-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@uploadthing/react": patch
"@uploadthing/shared": patch
"uploadthing": patch
---

fix: serverdata polling timed out and returned 504
16 changes: 11 additions & 5 deletions packages/react/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ export function UploadButton<
return `Choose File${multiple ? `(s)` : ``}`;
};

const getUploadButtonContents = (fileTypes: string[]) => {
if (state !== "uploading") {
return getUploadButtonText(fileTypes);
}
if (uploadProgress === 100) {
return <Spinner />;
}
return `${uploadProgress}%`;
};

const getInputProps = () => ({
type: "file",
ref: fileInputRef,
Expand Down Expand Up @@ -266,11 +276,7 @@ export function UploadButton<
>
<input {...getInputProps()} className="sr-only" />
{contentFieldToContent($props.content?.button, styleFieldArg) ??
(state === "uploading" ? (
<Spinner />
) : (
getUploadButtonText(fileTypes)
))}
getUploadButtonContents(fileTypes)}
</label>
{mode === "manual" && files.length > 0
? renderClearButton()
Expand Down
10 changes: 5 additions & 5 deletions packages/shared/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,18 @@ export function generateUploadThingURL(path: `/${string}`) {
}

export const withExponentialBackoff = async <T>(
doTheThing: () => Promise<T | null>,
doTheThing: () => Promise<T | undefined>,
MAXIMUM_BACKOFF_MS = 64 * 1000,
MAX_RETRIES = 20,
): Promise<T | null> => {
let tries = 0;
let backoffMs = 500;
let backoffFuzzMs = 0;

let result = null;
let result = undefined;
while (tries <= MAX_RETRIES) {
result = await doTheThing();
if (result !== null) return result;
if (result !== undefined) return result;

tries += 1;
backoffMs = Math.min(MAXIMUM_BACKOFF_MS, backoffMs * 2);
Expand Down Expand Up @@ -177,10 +177,10 @@ export async function pollForFileData(
console.error(
`[UT] Error polling for file data for ${opts.url}: ${maybeJson.message}`,
);
return null;
return undefined;
}

if (maybeJson.status !== "done") return null;
if (maybeJson.status !== "done") return undefined;
await callback?.(maybeJson);
});
}
Expand Down
40 changes: 32 additions & 8 deletions packages/uploadthing/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { safeParseJSON, UploadThingError } from "@uploadthing/shared";
import {
safeParseJSON,
UploadThingError,
withExponentialBackoff,
} from "@uploadthing/shared";

import type { UploadThingResponse } from "./internal/handler";
import { uploadPartWithProgress } from "./internal/multi-part";
Expand Down Expand Up @@ -130,8 +134,15 @@ export const DANGEROUS__uploadFiles = async <
});
}

const { presignedUrls, uploadId, chunkSize, contentDisposition, key } =
presigned;
const {
presignedUrls,
uploadId,
chunkSize,
contentDisposition,
key,
pollingUrl,
pollingJwt,
} = presigned;

let uploadedBytes = 0;

Expand Down Expand Up @@ -184,11 +195,24 @@ export const DANGEROUS__uploadFiles = async <
});
}

const serverData = await fetch(opts.url, {
headers: { "x-uploadthing-polling-key": key },
}).then(
(res) => res.json() as Promise<inferEndpointOutput<TRouter[TEndpoint]>>,
);
// wait a bit as it's unsreasonable to expect the server to be done by now
await new Promise((r) => setTimeout(r, 750));

const serverData = (await withExponentialBackoff(async () => {
type PollingResponse =
| {
status: "done";
callbackData: inferEndpointOutput<TRouter[TEndpoint]>;
}
| { status: "still waiting" };

const res = await fetch(pollingUrl, {
headers: { authorization: pollingJwt },
}).then((r) => r.json() as Promise<PollingResponse>);

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return res.status === "done" ? res.callbackData : undefined;
})) as inferEndpointOutput<TRouter[TEndpoint]>;

return {
name: file.name,
Expand Down
16 changes: 2 additions & 14 deletions packages/uploadthing/src/express.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import EventEmitter from "events";
import { Router as ExpressRouter } from "express";
import type {
Request as ExpressRequest,
Expand Down Expand Up @@ -35,8 +34,7 @@ export const createUploadthingExpressHandler = <TRouter extends FileRouter>(
opts: RouterWithConfig<TRouter>,
): ExpressRouter => {
incompatibleNodeGuard();
const ee = new EventEmitter();
const requestHandler = buildRequestHandler<TRouter>(opts, ee);
const requestHandler = buildRequestHandler<TRouter>(opts);
const router = ExpressRouter();

// eslint-disable-next-line @typescript-eslint/no-misused-promises
Expand Down Expand Up @@ -93,20 +91,10 @@ export const createUploadthingExpressHandler = <TRouter extends FileRouter>(

const getBuildPerms = buildPermissionsInfoHandler<TRouter>(opts);

// eslint-disable-next-line @typescript-eslint/no-misused-promises
router.get("/", async (req, res) => {
router.get("/", (_req, res) => {
res.status(200);
res.setHeader("x-uploadthing-version", UPLOADTHING_VERSION);

const clientPollingKey = req.headers["x-uploadthing-polling-key"];
if (clientPollingKey) {
const eventData = await new Promise((resolve) => {
ee.addListener("callbackDone", resolve);
});
ee.removeAllListeners("callbackDone");
return res.send(JSON.stringify(eventData));
}

res.send(JSON.stringify(getBuildPerms()));
});

Expand Down
19 changes: 1 addition & 18 deletions packages/uploadthing/src/fastify.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { EventEmitter } from "events";
import type {
FastifyInstance,
FastifyReply,
Expand Down Expand Up @@ -37,8 +36,7 @@ export const fastifyUploadthingPlugin = <TRouter extends FileRouter>(
done: (err?: Error) => void,
) => {
incompatibleNodeGuard();
const ee = new EventEmitter();
const requestHandler = buildRequestHandler<TRouter>(opts, ee);
const requestHandler = buildRequestHandler<TRouter>(opts);

const POST: RouteHandlerMethod = async (req, res) => {
const proto = (req.headers["x-forwarded-proto"] as string) ?? "http";
Expand Down Expand Up @@ -84,21 +82,6 @@ export const fastifyUploadthingPlugin = <TRouter extends FileRouter>(
const getBuildPerms = buildPermissionsInfoHandler<TRouter>(opts);

const GET: RouteHandlerMethod = async (req, res) => {
const clientPollingKey = req.headers["x-uploadthing-polling-key"];
if (clientPollingKey) {
const eventData = await new Promise((resolve) => {
ee.addListener("callbackDone", resolve);
});
ee.removeAllListeners("callbackDone");

void res
.status(200)
.headers({
"x-uploadthing-version": UPLOADTHING_VERSION,
})
.send(eventData);
return;
}
void res
.status(200)
.headers({
Expand Down
14 changes: 1 addition & 13 deletions packages/uploadthing/src/h3.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import EventEmitter from "events";
import type { H3Event } from "h3";
import {
assertMethod,
Expand Down Expand Up @@ -37,8 +36,7 @@ export const createUploadthing = <TErrorShape extends Json>(
export const createH3EventHandler = <TRouter extends FileRouter>(
opts: RouterWithConfig<TRouter>,
) => {
const ee = new EventEmitter();
const requestHandler = buildRequestHandler(opts, ee);
const requestHandler = buildRequestHandler(opts);
const getBuildPerms = buildPermissionsInfoHandler<TRouter>(opts);

return defineEventHandler(async (event) => {
Expand All @@ -47,16 +45,6 @@ export const createH3EventHandler = <TRouter extends FileRouter>(

// GET
if (event.method === "GET") {
const clientPollingKey =
getRequestHeaders(event)["x-uploadthing-polling"];
if (clientPollingKey) {
const eventData = await new Promise((resolve) => {
ee.addListener("callbackDone", resolve);
});
ee.removeAllListeners("callbackDone");
return eventData;
}

return getBuildPerms();
}

Expand Down
18 changes: 9 additions & 9 deletions packages/uploadthing/src/internal/handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type EventEmitter from "events";

import type { MimeType } from "@uploadthing/mime-types/db";
import {
generateUploadThingURL,
Expand Down Expand Up @@ -104,6 +102,7 @@ const getHeader = (req: RequestLike, key: string) => {

export type UploadThingResponse = {
presignedUrls: string[];
pollingJwt: string;
key: string;
pollingUrl: string;
uploadId: string;
Expand All @@ -116,7 +115,6 @@ export type UploadThingResponse = {

export const buildRequestHandler = <TRouter extends FileRouter>(
opts: RouterWithConfig<TRouter>,
ee?: EventEmitter,
) => {
return async (input: {
req: RequestLike;
Expand Down Expand Up @@ -199,6 +197,8 @@ export const buildRequestHandler = <TRouter extends FileRouter>(
});
}

const utFetch = createUTFetch(preferredOrEnvSecret);

if (uploadthingHook === "callback") {
// This is when we receive the webhook from uploadthing
const maybeReqBody = await safeParseJSON<{
Expand All @@ -216,12 +216,14 @@ export const buildRequestHandler = <TRouter extends FileRouter>(
});
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const res = await uploadable.resolver({
const res = (await uploadable.resolver({
file: maybeReqBody.file,
metadata: maybeReqBody.metadata,
})) as unknown;
await utFetch("/api/serverCallback", {
fileKey: maybeReqBody.file.key,
callbackData: res ?? null,
});
ee?.emit("callbackDone", res ?? null); // fallback to null to ensure JSON compatibility

return { status: 200 };
}
Expand All @@ -237,8 +239,6 @@ export const buildRequestHandler = <TRouter extends FileRouter>(
});
}

const utFetch = createUTFetch(preferredOrEnvSecret);

switch (actionType) {
case "upload": {
const maybeInput = await safeParseJSON<UTEvents["upload"]>(req);
Expand Down Expand Up @@ -379,7 +379,7 @@ export const buildRequestHandler = <TRouter extends FileRouter>(
return {
body: parsedResponse.map((x) => ({
...x,
pollingUrl: generateUploadThingURL(`/api/pollUpload/${x.key}`),
pollingUrl: generateUploadThingURL(`/api/serverCallback`),
})),
status: 200,
};
Expand Down
16 changes: 1 addition & 15 deletions packages/uploadthing/src/next-legacy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// This node import should be fine since it's available in both node and edge runtimes
// https://vercel.com/docs/functions/edge-functions/edge-runtime#compatible-node.js-modules
import { EventEmitter } from "events";
import type { NextApiRequest, NextApiResponse } from "next";

import { getStatusCodeFromError, UploadThingError } from "@uploadthing/shared";
Expand Down Expand Up @@ -32,26 +31,13 @@ export const createNextPageApiHandler = <TRouter extends FileRouter>(
opts: RouterWithConfig<TRouter>,
) => {
incompatibleNodeGuard();
const ee = new EventEmitter();
const requestHandler = buildRequestHandler<TRouter>(opts, ee);
const requestHandler = buildRequestHandler<TRouter>(opts);

const getBuildPerms = buildPermissionsInfoHandler<TRouter>(opts);

return async (req: NextApiRequest, res: NextApiResponse) => {
// Return valid endpoints
if (req.method === "GET") {
const clientPollingKey = req.headers["x-uploadthing-polling-key"];
if (clientPollingKey) {
const eventData = await new Promise((resolve) => {
ee.addListener("callbackDone", resolve);
});
ee.removeAllListeners("callbackDone");

res.setHeader("x-uploadthing-version", UPLOADTHING_VERSION);
res.status(200).json(eventData);
return;
}

const perms = getBuildPerms();
res.status(200).json(perms);
return;
Expand Down
24 changes: 3 additions & 21 deletions packages/uploadthing/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { EventEmitter } from "events";

import { getStatusCodeFromError, UploadThingError } from "@uploadthing/shared";
import type { Json } from "@uploadthing/shared";

Expand Down Expand Up @@ -30,8 +28,7 @@ export const createServerHandler = <TRouter extends FileRouter>(
opts: RouterWithConfig<TRouter>,
) => {
incompatibleNodeGuard();
const ee = new EventEmitter();
const requestHandler = buildRequestHandler<TRouter>(opts, ee);
const requestHandler = buildRequestHandler<TRouter>(opts);

const POST = async (request: Request | { request: Request }) => {
const req = request instanceof Request ? request : request.request;
Expand Down Expand Up @@ -65,23 +62,8 @@ export const createServerHandler = <TRouter extends FileRouter>(

const getBuildPerms = buildPermissionsInfoHandler<TRouter>(opts);

const GET = async (request: Request | { request: Request }) => {
const req = request instanceof Request ? request : request.request;

const clientPollingKey = req.headers.get("x-uploadthing-polling-key");
if (clientPollingKey) {
const eventData = await new Promise((resolve) => {
ee.addListener("callbackDone", resolve);
});
ee.removeAllListeners("callbackDone");

return new Response(JSON.stringify(eventData), {
status: 200,
headers: {
"x-uploadthing-version": UPLOADTHING_VERSION,
},
});
}
const GET = (request: Request | { request: Request }) => {
const _req = request instanceof Request ? request : request.request;

return new Response(JSON.stringify(getBuildPerms()), {
status: 200,
Expand Down

1 comment on commit 67109c8

@vercel
Copy link

@vercel vercel bot commented on 67109c8 Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.