pingdotgg / uploadthing

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

[bug]: startUpload doesn't return a response. It gives an error. Even though in the console(GUI), it shows the upload was successful #853

Closed shehryarbajwa closed 3 months ago

shehryarbajwa commented 3 months ago

Provide environment information

System:
    OS: macOS 11.7.8
    CPU: (8) x64 Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
    Memory: 71.58 MB / 16.00 GB
    Shell: 3.2.57 - /bin/bash
  Binaries:
    Node: 20.13.1 - /usr/local/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 10.5.2 - /usr/local/bin/npm
    pnpm: 9.1.1 - /usr/local/bin/pnpm
  Browsers:
    Chrome: 125.0.6422.142
    Safari: 16.5.2
  npmPackages:
    @uploadthing/react: ^6.6.0 => 6.6.0 
    typescript: ^5.4.5 => 5.4.5 
    uploadthing: ^6.12.0 => 6.12.0

Describe the bug

'use client';

import React, { useState } from 'react';
import Dropzone from 'react-dropzone';
import { Cloud, File, Loader2 } from 'lucide-react';
import { useRouter } from 'next/navigation';

import { Dialog, DialogContent, DialogTrigger } from './ui/dialog';
import { Button } from './ui/button';
import { Progress } from './ui/progress';
import { useUploadThing } from '@/lib/uploadthing';
import { useToast } from './ui/use-toast';
import { trpc } from '@/app/_trpc/client';

const UploadDropzone = () => {
  const [isUploading, setIsUploading] = useState(false);
  const [uploadProgress, setUploadProgress] = useState(0);

  const router = useRouter();

  const { toast } = useToast();

  const { startUpload } = useUploadThing('pdfUploader');

  const { mutate: startPolling } = trpc.getFile.useMutation({
    onSuccess: (file) => {
      console.log('Polling success:', file);
      router.push(`/dashboard/${file.id}`);
    },
    onError: (error) => {
      console.error('Polling error:', error);
      toast({
        title: 'Something went wrong during polling',
        description: 'Please try again later',
        variant: 'destructive',
      });
    },
    retry: true,
    retryDelay: 500,
  });

  const startSimulatedProgress = () => {
    setUploadProgress(0);

    const interval = setInterval(() => {
      setUploadProgress((prevProgress) => {
        if (prevProgress >= 95) {
          clearInterval(interval);
          return prevProgress + 5;
        }

        return prevProgress + 5;
      });
    }, 500);

    return interval;
  };

  return (
    <Dropzone
      multiple={false}
      onDrop={async (acceptedFiles) => {
        console.log('Files dropped:', acceptedFiles);
        setIsUploading(true);

        const progressInterval = startSimulatedProgress();

        try {
          console.log('Starting upload...');
          const res = await startUpload(acceptedFiles);
          console.log('Upload response:', res);

          if (!res || res.length === 0) {
            console.log('Upload failed or no response');
            throw new Error('Upload failed or no response');
          }

          const [fileResponse] = res;

          const key = fileResponse?.key;
          console.log('File key:', key);

          if (!key) {
            throw new Error('File key is missing');
          }

          clearInterval(progressInterval);
          setUploadProgress(100);

          console.log('Starting polling with key:', key);
          startPolling({ key });
        } catch (error) {
          console.error('Error during upload:', error);
          toast({
            title: 'Something went wrong',
            description: 'Please try again later',
            variant: 'destructive',
          });
          clearInterval(progressInterval);
          setUploadProgress(0);
        } finally {
          setIsUploading(false);
        }
      }}
    >
      {({ getRootProps, getInputProps, acceptedFiles }) => (
        <div
          {...getRootProps()}
          className="border h-64 m-4 border-dashed border-gray-300 rounded-lg"
        >
          <div className="flex items-center justify-center h-full w-full">
            <label
              htmlFor="dropzone-file"
              className="flex flex-col items-center justify-center w-full h-full rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100"
            >
              <div className="flex flex-col items-center justify-center pt-5 pb-6">
                <Cloud className="h-6 w-6 text-zinc-500 mb-2" />
                <p className="mb-2 text-sm text-zinc-700">
                  <span className="font-semibold">Click to upload</span> or drag
                  and drop
                </p>
                <p className="text-xs text-zinc-500">PDF up to 16 MB</p>
              </div>

              {acceptedFiles && acceptedFiles[0] ? (
                <div className="max-w-xs bg-white flex items-center rounded-md overflow-hidden outline outline-[1px] outline-zinc-200 divide-x divide-zinc-200">
                  <div className="px-3 py-2 h-full grid place-items-center">
                    <File className="h-4 w-4 text-blue-500" />
                  </div>
                  <div className="px-3 py-2 h-full text-sm truncate">
                    {acceptedFiles[0].name}
                  </div>
                </div>
              ) : null}

              {isUploading ? (
                <div className="w-full mt-4 max-w-xs mx-auto">
                  <Progress
                    color={uploadProgress === 100 ? 'bg-green-500' : ''}
                    value={uploadProgress}
                    className="h-1 w-full bg-zinc-200"
                  />
                  {uploadProgress === 100 ? (
                    <div>
                      <div className="flex gap-1 items-center justify-center text-sm text-zinc-700 text-center pt-2">
                        <Loader2 className="h-3 w-3 animate-spin" />
                        Upload is Complete. Redirecting...
                      </div>
                    </div>
                  ) : null}
                </div>
              ) : null}

              <input
                {...getInputProps()}
                type="file"
                id="dropzone-file"
                className="hidden"
              />
            </label>
          </div>
        </div>
      )}
    </Dropzone>
  );
};

const UploadButton = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <Dialog
      open={isOpen}
      onOpenChange={(v) => {
        if (!v) setIsOpen(v);
      }}
    >
      <DialogTrigger onClick={() => setIsOpen(true)} asChild>
        <Button>Upload PDF</Button>
      </DialogTrigger>

      <DialogContent>
        <UploadDropzone />
      </DialogContent>
    </Dialog>
  );
};

export default UploadButton;

Link to reproduction

https://github.com/shehryarbajwa/quill/blob/master/src/components/UploadButton.tsx

To reproduce

Screen Shot 2024-06-09 at 7 30 16 PM

Here is the uploadthing.ts file and the pdfUploader

import { File } from 'lucide-react';
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server';
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";
import { db } from '@/db';

const f = createUploadthing();

// FileRouter for your app, can contain multiple FileRoutes
export const ourFileRouter = {
  // Define as many FileRoutes as you like, each with a unique routeSlug
  pdfUploader: f({ pdf: { maxFileSize: "16MB" } })
    // Set permissions and file types for this FileRoute
    .middleware(async ({ req }) => {
      // This code runs on your server before upload
      const { getUser } = getKindeServerSession();

      const user = await getUser();

      if (!user || !user.id) throw new Error("UNAUTHORIZED")

      // Whatever is returned here is accessible in onUploadComplete as `metadata`
      return { userId: user.id };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      // This code RUNS ON YOUR SERVER after upload
      console.log("Upload complete for userId:", metadata.userId);

      console.log("file url", file.url);

      // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
      return { uploadedBy: metadata.userId };
    }),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;

Here is the error I get in the console when I try to log

const res = await startUpload(acceptedFiles);
          console.log('Upload response:', res);

Error: Uncaught (in promise) Error: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received

162-5a8605f2ab2e0772.js:1 Error during upload: Error: Upload failed or no response at onDrop (page-17a9efdf319d8eb1.js:1:3431)

Additional information

Not sure what I am doing wrong here

πŸ‘¨β€πŸ‘§β€πŸ‘¦ Contributing

Code of Conduct

markflorkowski commented 3 months ago

The following snippet is incorrect:

console.log('Starting upload...');
const res = await startUpload(acceptedFiles);
console.log('Upload response:', res);

if (!res || res.length === 0) {
  console.log('Upload failed or no response');
  throw new Error('Upload failed or no response');
}

As seen in our docs, the startUpload function is not expected to return anything.

If you would like to have code that runs when an upload fails, you would pass it into useUploadthing via the onUploadError key:

const { startUpload, isUploading, permittedFileInfo } = useUploadThing(
  endpoint,
  opts: {
    onClientUploadComplete: ({fileKey: string, fileUrl: string}[]) => void
    onUploadError: (error: Error) => void
    onUploadAborted: () => void
    onUploadProgress: (progress: number) => void
    onUploadBegin: (fileName: string) => void
  },
);

For more information, check out our documentation on useUploadThing

shehryarbajwa commented 3 months ago

I am trying to use a key which is returned when the network call completes.

const [fileResponse] = res;

          const key = fileResponse?.key;
          console.log('File key:', key);

Should I do this with onClientUploadComplete instead!

markflorkowski commented 3 months ago

Either there, or on the server-side onUploadComplete πŸ‘

shehryarbajwa commented 3 months ago

Hey @markflorkowski I tried on the client side and it works fine. Not sure why I can't see the metadata on the server side. Shouldn't they be working on both? Client side(This call does the console log)

Screen Shot 2024-06-11 at 12 02 34 AM

(This one doesn't even though the middleware works just fine. Doesn't do the db call either)

Screen Shot 2024-06-11 at 12 03 30 AM
markflorkowski commented 3 months ago

This one doesn't even though the middleware works just fine. Doesn't do the db call either

Are you running a production build locally? If so, there is no way for uploadthing callbacks to make it to your server, so the onUploadComplete will never run. If you are running in NODE_ENV=development though, you should be seeing simulated callbacks (with corresponding console logs)

shehryarbajwa commented 3 months ago

Wow this was unbelievable. I feel like this can be added to the docs since Ive seen many posts using this library make this silly mistake. Can I make a pull request to add in the docs?

markflorkowski commented 3 months ago

It's already in the docs: https://docs.uploadthing.com/faq#youre-testing-a-production-build-locally