Skip to content

Commit

Permalink
fix: prefetch permissions on server to prevent layout flashes (#122)
Browse files Browse the repository at this point in the history
* patch

* best conditional ever

* Create nine-islands-deliver.md

* rm unused
  • Loading branch information
juliusmarminge authored Jun 5, 2023
1 parent 3cf57bb commit 4fde69f
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 68 deletions.
6 changes: 6 additions & 0 deletions .changeset/nine-islands-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@uploadthing/react": patch
"uploadthing": patch
---

fix: prefetch permissions on server to prevent layout flashes
26 changes: 23 additions & 3 deletions packages/react/src/useUploadThing.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import { useState } from "react";
import { use, useRef, useState } from "react";

import { DANGEROUS__uploadFiles } from "uploadthing/client";
import { DANGEROUS__uploadFiles, getUtUrl } from "uploadthing/client";
import type { ExpandedRouteConfig, FileRouter } from "uploadthing/server";

import { useEvent } from "./utils/useEvent";
import useFetch from "./utils/useFetch";

const fetchEndpointData = async () => {
const url = getUtUrl();
const res = await fetch(url);
const data = (await res.json()) as any[];
return data as EndpointMetadata;
};

type EndpointMetadata = {
slug: string;
config: ExpandedRouteConfig;
}[];
const useEndpointMetadata = (endpoint: string) => {
const useEndpointMetadataRSC = (endpoint: string) => {
// Trigger suspense
const promiseRef = useRef<Promise<EndpointMetadata>>();
const data = use((promiseRef.current ??= fetchEndpointData()));

// TODO: Log on errors in dev

return data?.find((x) => x.slug === endpoint);
};

const useEndpointMetadataStd = (endpoint: string) => {
const { data } = useFetch<EndpointMetadata>("/api/uploadthing");

// TODO: Log on errors in dev

return data?.find((x) => x.slug === endpoint);
};

const useEndpointMetadata =
typeof use === "function" ? useEndpointMetadataRSC : useEndpointMetadataStd;

export const useUploadThing = <T extends string>({
endpoint,
onClientUploadComplete,
Expand Down
19 changes: 10 additions & 9 deletions packages/uploadthing/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const DANGEROUS__uploadFiles = async <T extends string>(
"https://uploadthing.com/f/" + encodeURIComponent(fields["key"]);

// Poll for file data, this way we know that the client-side onUploadComplete callback will be called after the server-side version
await pollForFileData(presigned.key)
await pollForFileData(presigned.key);

return {
fileKey: presigned.key,
Expand Down Expand Up @@ -109,13 +109,11 @@ export const classNames = (...classes: string[]) => {
};

export const generateMimeTypes = (fileTypes: string[]) => {
const accepted = fileTypes.map((type) =>
{
if (type === "blob") return "blob"
if (type === "pdf") return "application/pdf"
else return `${type}/*`
}
);
const accepted = fileTypes.map((type) => {
if (type === "blob") return "blob";
if (type === "pdf") return "application/pdf";
else return `${type}/*`;
});

if (accepted.includes("blob")) {
return undefined;
Expand All @@ -131,4 +129,7 @@ export const generateClientDropzoneAccept = (fileTypes: string[]) => {
return Object.fromEntries(mimeTypes.map((type) => [type, []]));
};

export { pollForFileData as DANGEROUS__pollForFileData } from "./src/utils"
export {
pollForFileData as DANGEROUS__pollForFileData,
GET_DEFAULT_URL as getUtUrl,
} from "./src/utils";
92 changes: 39 additions & 53 deletions packages/uploadthing/src/internal/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
} from "../types";
import {
generateUploadThingURL,
GET_DEFAULT_URL,
getTypeFromFileName,
fillInputRouteConfig as parseAndExpandInputConfig,
pollForFileData,
Expand Down Expand Up @@ -84,70 +85,55 @@ const isValidResponse = (response: Response) => {
return true;
};



const conditionalDevServer = async (fileKey: string) => {
if (process.env.NODE_ENV !== "development") return;

const fileData = await pollForFileData(fileKey, async (json: {fileData: FileData})=> {
const file = json.fileData;

let callbackUrl = file.callbackUrl + `?slug=${file.callbackSlug}`;
if (!callbackUrl.startsWith("http")) callbackUrl = "http://" + callbackUrl;

console.log("[UT] SIMULATING FILE UPLOAD WEBHOOK CALLBACK", callbackUrl);

const response = await fetch(callbackUrl, {
method: "POST",
body: JSON.stringify({
status: "uploaded",
metadata: JSON.parse(file.metadata ?? "{}") as FileData["metadata"],
file: {
url: `https://uploadthing.com/f/${encodeURIComponent(fileKey ?? "")}`,
key: fileKey ?? "",
name: file.fileName,
const fileData = await pollForFileData(
fileKey,
async (json: { fileData: FileData }) => {
const file = json.fileData;

let callbackUrl = file.callbackUrl + `?slug=${file.callbackSlug}`;
if (!callbackUrl.startsWith("http"))
callbackUrl = "http://" + callbackUrl;

console.log("[UT] SIMULATING FILE UPLOAD WEBHOOK CALLBACK", callbackUrl);

const response = await fetch(callbackUrl, {
method: "POST",
body: JSON.stringify({
status: "uploaded",
metadata: JSON.parse(file.metadata ?? "{}") as FileData["metadata"],
file: {
url: `https://uploadthing.com/f/${encodeURIComponent(
fileKey ?? "",
)}`,
key: fileKey ?? "",
name: file.fileName,
},
}),
headers: {
"uploadthing-hook": "callback",
},
}),
headers: {
"uploadthing-hook": "callback",
},
});
if (isValidResponse(response)) {
console.log("[UT] Successfully simulated callback for file", fileKey);
} else {
console.error(
"[UT] Failed to simulate callback for file. Is your webhook configured correctly?",
fileKey,
);
}
return file;
});
});
if (isValidResponse(response)) {
console.log("[UT] Successfully simulated callback for file", fileKey);
} else {
console.error(
"[UT] Failed to simulate callback for file. Is your webhook configured correctly?",
fileKey,
);
}
return file;
},
);

if (fileData !== null) return fileData;

console.error(`[UT] Failed to simulate callback for file ${fileKey}`);
throw new Error("File took too long to upload");
};

const GET_DEFAULT_URL = () => {
/**
* Use VERCEL_URL as the default callbackUrl if it's set
* they don't set the protocol, so we need to add it
* User can override this with the UPLOADTHING_URL env var,
* if they do, they should include the protocol
*
* The pathname must be /api/uploadthing
* since we call that via webhook, so the user
* should not override that. Just the protocol and host
*/
const vcurl = process.env.VERCEL_URL;
if (vcurl) return `https://${vcurl}/api/uploadthing`; // SSR should use vercel url
const uturl = process.env.UPLOADTHING_URL;
if (uturl) return `${uturl}/api/uploadthing`;

return `http://localhost:${process.env.PORT ?? 3000}/api/uploadthing`; // dev SSR should use localhost
};

export type RouterWithConfig<TRouter extends FileRouter> = {
router: TRouter;
config?: {
Expand Down
28 changes: 25 additions & 3 deletions packages/uploadthing/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { lookup } from "mime-types";

import type { FileData } from "./internal/types";
import type {
AllowedFileType,
ExpandedRouteConfig,
FileRouterInputConfig,
FileSize,
} from "./types";
import type { FileData } from './internal/types';

function isRouteArray(
routeConfig: FileRouterInputConfig,
Expand Down Expand Up @@ -127,7 +127,10 @@ const withExponentialBackoff = async <T>(
return null;
};

export const pollForFileData = async (fileKey:string, callback?: (json: any)=>Promise<any>) => {
export const pollForFileData = async (
fileKey: string,
callback?: (json: any) => Promise<any>,
) => {
const queryUrl = generateUploadThingURL(`/api/pollUpload/${fileKey}`);

return withExponentialBackoff(async () => {
Expand All @@ -140,4 +143,23 @@ export const pollForFileData = async (fileKey:string, callback?: (json: any)=>Pr

await callback?.(json);
});
}
};

export const GET_DEFAULT_URL = () => {
/**
* Use VERCEL_URL as the default callbackUrl if it's set
* they don't set the protocol, so we need to add it
* User can override this with the UPLOADTHING_URL env var,
* if they do, they should include the protocol
*
* The pathname must be /api/uploadthing
* since we call that via webhook, so the user
* should not override that. Just the protocol and host
*/
const vcurl = process.env.VERCEL_URL;
if (vcurl) return `https://${vcurl}/api/uploadthing`; // SSR should use vercel url
const uturl = process.env.UPLOADTHING_URL;
if (uturl) return `${uturl}/api/uploadthing`;

return `http://localhost:${process.env.PORT ?? 3000}/api/uploadthing`; // dev SSR should use localhost
};

1 comment on commit 4fde69f

@vercel
Copy link

@vercel vercel bot commented on 4fde69f Jun 5, 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.