pingdotgg / uploadthing

File uploads for modern web devs
https://uploadthing.com
MIT License
3.99k stars 294 forks source link

[bug]: file uploads work locally but not in production using the default setup guide for nextjs 14 & app router #519

Closed kaspnilsson closed 9 months ago

kaspnilsson commented 9 months ago

Provide environment information

System:
    OS: macOS 14.1.2
    CPU: (11) arm64 Apple M3 Pro
    Memory: 62.03 MB / 18.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.10.0 - ~/.nvm/versions/node/v20.10.0/bin/node
    Yarn: 1.22.21 - /opt/homebrew/bin/yarn
    npm: 10.2.3 - ~/.nvm/versions/node/v20.10.0/bin/npm
    pnpm: 8.12.0 - /opt/homebrew/bin/pnpm
  Browsers:
    Chrome: 120.0.6099.71
    Safari: 17.1.2
  npmPackages:
    @uploadthing/react: ^6.0.2 => 6.0.2
    typescript: ^5 => 5.3.3
    uploadthing: ^6.1.0 => 6.1.0

Describe the bug

I'm using the dropzone component thusly, within a form:

<UploadDropzone
  content={{ label: 'Upload your spec files' }}
  endpoint="specUploader"
  onUploadError={onUploadError}
  onClientUploadComplete={(res) => {
    field.onChange([
      ...(field.value || []),
      ...res.map((r) => ({ url: r.url, name: r.name })),
    ]);
  }}
  config={{ mode: 'auto' }}
/>

File router:

import { createUploadthing, type FileRouter } from 'uploadthing/next';

const f = createUploadthing();

export const ourFileRouter = {
  specUploader: f({ image: { maxFileCount: 10 }, pdf: { maxFileCount: 10 } })
    .onUploadComplete(async ({ file }) => {
      return file;
    }),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;

Files upload fine locally.

In production (using Vercel, on preview deployments), files upload fine, but the callback never executes on the server. Here's an example set of server logs:

DEC 12 18:59:16.00
401
umagine-sv666aelg-umagine-team.vercel.app
[POST] /api/uploadthing

DEC 12 18:59:15.13
200
umagine-sv666aelg-umagine-team.vercel.app
[POST] /api/uploadthing
[POST] /api/uploadthing?slug=specUploader&actionType=multipart-complete status=200

DEC 12 18:59:13.78
200
umagine-sv666aelg-umagine-team.vercel.app
[POST] /api/uploadthing
[POST] /api/uploadthing?slug=specUploader&actionType=upload status=200

DEC 12 18:59:10.88
200
umagine-sv666aelg-umagine-team.vercel.app
[GET] /api/uploadthing
[GET] /api/uploadthing status=200

The client is stuck in a loading state, with this message seemingly polling forever:

[UT] Call unsuccessful after 11 tries. Retrying in 64 seconds...

I do not think any of the scenarios in the docs pertain to my case, but maybe there's some complexity in dealing with Vercel preview deployments?

Link to reproduction

Follow instructions above -- issue is with deployment

To reproduce

Follow instructions above

Additional information

No response

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributing

Code of Conduct

kaspnilsson commented 9 months ago

My prod deployments work fine.

markflorkowski commented 9 months ago

I think preview deployments on vercel require auth. Our callbacks wouldn't have that auth token, thus would fail.

I feel like we ran into this in the past, trying to remember the fix. cc @juliusmarminge

markflorkowski commented 9 months ago

You can disable this feature in the vercel dashboard:

image

Obviously that comes with it's own security implications.

juliusmarminge commented 9 months ago

Or add this header in your app settings: https://vercel.com/docs/security/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation (if you have the feature on vercel)

kaspnilsson commented 9 months ago

Thanks, that is indeed the issue!

IamAnderson commented 1 month ago

Even after disabling the header and also later adding the header, uploadthing keeps returning 200 with a servercallback message and a response of {"status":"still waiting"}

I started wondering, perhaps I did not add something in the code or perhaps because I did not add middleware authentication to the fileRouter

This is the core.ts: import { createUploadthing, type FileRouter } from "uploadthing/next";

const f = createUploadthing();

export const ourFileRouter = { imageUploader: f({ image: { maxFileSize: "4MB", minFileCount: 1, maxFileCount: 10 }, }).onUploadComplete(async ({ metadata, file }) => { return { fileUrl: file.url }; }), courseAttachment: f([ "text", "image", "video", "audio", "pdf", ]).onUploadComplete(() => {}), } satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;

Nebiant Admin - Google Chrome 03_08_2024 06_11_40

Nebiant Admin - Google Chrome 03_08_2024 06_11_57

Please I need your help @kaspnilsson @markflorkowski

IamAnderson commented 1 month ago

Hiii @kaspnilsson @markflorkowski

I fixed itttttttttt.. hahah

Middleware on the core.ts is necessary, even though it returns an empty object.

import { createUploadthing, type FileRouter } from "uploadthing/next";

const f = createUploadthing();

export const ourFileRouter = { imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 10 } }) .middleware(async ({ req }) => { // This code runs on your server before upload console.log("Middleware for imageUploader"); return {}; // Return an empty object if no metadata is needed }) .onUploadComplete(async ({ metadata, file }) => { console.log("Upload completed:", file.url); return { fileUrl: file.url }; }),

courseAttachment: f(["text", "image", "video", "audio", "pdf"]) .middleware(async ({ req }) => { console.log("Middleware for courseAttachment"); return {}; }) .onUploadComplete(async ({ metadata, file }) => { console.log("Course attachment upload completed:", file); }), } satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;

Also added this to next.config.mjs reactStrictMode: true, webpack: (config) => { config.externals = [...config.externals, { canvas: "canvas" }]; // required for uploadthing return config; },

Hopfully this helps someone, someday :)

juliusmarminge commented 1 month ago

Middleware on the core.ts is necessary, even though it returns an empty object

It shouldn't, interested to see a reproduction repo I can run that doesn't work without middleware

markflorkowski commented 1 month ago

I actually do think that is intentional, but should probably be better documented.

middleware should probably be called something like auth, because we are expecting you to validate whether the upload should be allowed at that point or not. An empty middleware function should feel bad, because it means you are not doing auth when you should.