Vercel Blob Client Upload Error on GenerateClientToken

Hi! I’m loving the power of Vercel & I’m super excited to be apart of it’s growth and Development!

So the feature I am working on right now is uploading resumes, coverletters, and additional attachments to Vercel Blob. To do this I’m using client uploads documented here:
https://vercel.com/docs/storage/vercel-blob/client-upload

Here is my upload request to my handler:

          const coverLetterBlobData = await upload(coverLetter.name, coverLetter, {
            access: 'public',
            handleUploadUrl: '/api/blob/upload',
            clientPayload: JSON.stringify({
              documentType: 'COVER_LETTER',
              startupId: startupId,
              filename: coverLetter.name,
            })
          });

          const resumeBlobData = await upload(resume.name, resume, {
            access: 'public',
            handleUploadUrl: '/api/blob/upload',
            clientPayload: JSON.stringify({
              documentType: 'RESUME',
              startupId: startupId,
              filename: resume.name,
            })
          });

Here is my route handler:

import { handleUpload, HandleUploadBody } from '@vercel/blob/client'
import { getServerSession } from 'next-auth/next'
import { prisma } from '@/src/utils/prisma'
import { z } from 'zod'

import type { NextApiResponse, NextApiRequest } from 'next'

export const uploadPayloadSchema = z.object({
  startupId: z.string(),
  documentType: z.string(),
  filename: z.string(),
})

export const config = {
  api: {
    bodyParser: false,
  },
}

export default async function handler(
  request: NextApiRequest,
  response: NextApiResponse
) {

  const readWriteToken = process.env.BLOB_READ_WRITE_TOKEN

  if (!readWriteToken) {
    return response.status(500).json({ error: 'Missing BLOB_READ_WRITE_TOKEN' })
  }

  const body = request.body as HandleUploadBody

  if (request.method !== 'POST') {
    return response.status(405).json({ error: 'Method not allowed' })
  }

  try {
    const session = await getServerSession(request, response, {
      callbacks: {
        session: async ({ session, token }) => {
          if (session?.user) {
            session.user.userId = token.sub
          }
          return session
        },
      },
    })

    if (!session?.user?.userId) {
      throw new Error('Unauthorized')
    }

    const jsonResponse = await handleUpload({
      body,
      request,
      onBeforeGenerateToken: async (
        pathname,
        clientPayload: string | null,
        multipart: boolean) => {
          
        const payload = clientPayload ? uploadPayloadSchema.parse(JSON.parse(clientPayload)) : null

        const startup = await prisma.startup.findUnique({
          where: { id: payload?.startupId },
          select: { blobKey: true },
        })

        const customPathname = `${startup?.blobKey?.slice(0, 32)}/${session.user.userId
          }_${payload?.documentType}_${payload?.filename}`;

        // Validate user session and authorization here
        return {
          pathname: customPathname,
          callbackUrl: `/api/blob/upload`,
          multipart: multipart,
          allowedContentTypes: [
            'application/pdf',
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
          ],
          maximumSizeInBytes: 1024 * 1024 * 10, // 10MB
          tokenPayload:
            JSON.stringify(
              clientPayload
              //   ? {
              //     ...(typeof clientPayload === 'string'
              //       ? JSON.parse(clientPayload)
              //       : {}),
              //     timestamp: Date.now(),
              //   }
              //   : { timestamp: Date.now() }
            ),
        }
      },
      onUploadCompleted: async ({ blob, tokenPayload }) => {
        console.log('Upload completed:', blob)

        try {
          // Ensure tokenPayload is a valid JSON string
          if (typeof tokenPayload !== 'string') {
            throw new Error('Invalid token payload format')
          }

          const payload = JSON.parse(tokenPayload) as {
            type: 'COVER_LETTER' | 'RESUME'
            startupId: string
            timestamp: number
          }

          // Store metadata in your database here
          console.log('Token payload:', payload)
        } catch (error) {
          console.error('Error processing upload:', error)
          throw new Error('Failed to process upload')
        }
      },
    })

    return response.status(200).json(jsonResponse)
  } catch (error) {
    console.error('Upload error:', error)
    return response.status(400).json({ error: (error as Error).message })
  }
}

The biggest hint to the issue is in my payload and the error it returns.

Payload

{
    "type": "blob.generate-client-token",
    "payload": {
        "pathname": "Mobile Engineer - React native.docx",
        "callbackUrl": "https://codeshares-jkc4iemd6-brezzy1337s-projects.vercel.app/api/blob/upload",
        "clientPayload": "{\"documentType\":\"COVER_LETTER\",\"startupId\":\"cm45ffb890002rsutnuefuhjo\",\"filename\":\"Mobile Engineer - React native.docx\"}",
        "multipart": false
    }
}

Error

error: "Cannot read properties of undefined (reading 'type')"

So the type is set to blob.generate-client-token

Which I believe is the type set under the OnBeforeGenerationToken part of the handleUpload function.

Here is that part of the code cut out from the whole function:

onBeforeGenerateToken: async (
        pathname,
        clientPayload: string | null,
        multipart: boolean) => {
          
        const payload = clientPayload ? uploadPayloadSchema.parse(JSON.parse(clientPayload)) : null

        const startup = await prisma.startup.findUnique({
          where: { id: payload?.startupId },
          select: { blobKey: true },
        })

        const customPathname = `${startup?.blobKey?.slice(0, 32)}/${session.user.userId
          }_${payload?.documentType}_${payload?.filename}`;

        // Validate user session and authorization here
        return {
          pathname: customPathname,
          callbackUrl: `/api/blob/upload`,
          multipart: multipart,
          allowedContentTypes: [
            'application/pdf',
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
          ],
          maximumSizeInBytes: 1024 * 1024 * 10, // 10MB
          tokenPayload:
            JSON.stringify(
              clientPayload
              //   ? {
              //     ...(typeof clientPayload === 'string'
              //       ? JSON.parse(clientPayload)
              //       : {}),
              //     timestamp: Date.now(),
              //   }
              //   : { timestamp: Date.now() }
            ),
        }
      },

Any help or insights on how vercel blob generates token would be very appreciated since I’m pretty sure its that part of my config that is throwing the error.

Thank you!

Update on my issue - Error is being thrown on the retrieveClientToken function which branches out to a few possibilities:

Error submitting proposal: BlobError: Vercel Blob: Failed to  retrieve the client token
    at retrieveClientToken (client.js:239:11)
    at async createPutOptions (chunk-QXAUX3LT.js:740:21)
    at async put (chunk-QXAUX3LT.js:1189:21)
    at async onSubmit (AgreementProposalForm.tsx:268:39)

I’m thinking that something is not being properly passed to the putOptions from the handler causing a undefined type error.

async function retrieveClientToken(options) {
    const {handleUploadUrl, pathname} = options;
    const url = isAbsoluteUrl(handleUploadUrl) ? handleUploadUrl : toAbsoluteUrl(handleUploadUrl);
    const event = {
        type: EventTypes.generateClientToken,
        payload: {
            pathname,
            callbackUrl: url,
            clientPayload: options.clientPayload,
            multipart: options.multipart
        }
    };
    const res = await (0,
    undici__WEBPACK_IMPORTED_MODULE_2__.fetch)(url, {
        method: "POST",
        body: JSON.stringify(event),
        headers: {
            "content-type": "application/json"
        },
        signal: options.abortSignal
    });
    if (!res.ok) {
        throw new _chunk_QXAUX3LT_js__WEBPACK_IMPORTED_MODULE_0__.BlobError("Failed to  retrieve the client token");
    }
    try {
        const {clientToken} = await res.json();
        return clientToken;
    } catch (e) {
        throw new _chunk_QXAUX3LT_js__WEBPACK_IMPORTED_MODULE_0__.BlobError("Failed to retrieve the client token");
    }
}

@brezzy1337 Do you have any interesting console.log errors in your browser console? It looks like your browser can’t make the request to the url so there maybe an interesting error in here, let me know!

Hey! Yes it do:

AgreementProposalForm.tsx:307 Error submitting proposal: BlobError: Vercel Blob: Failed to  retrieve the client token
    at retrieveClientToken (client.js:239:11)
    at async createPutOptions (chunk-QXAUX3LT.js:740:21)
    at async put (chunk-QXAUX3LT.js:1189:21)
    at async onSubmit (AgreementProposalForm.tsx:268:39)

I’m also getting a error on my response:

error: 
"Cannot read properties of undefined (reading 'type')"

hey, it should be enough to set bodyParser: true or remove the config entirely.

Explanation for this is that retrieveClientToken is sending requests with JSON payload to your API route. When you disable the bodyParser the JSON body will be ignored and we can’t read it.

3 Likes

That did the trick! You guys are awesome thank you so much :grin:

3 Likes

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.