vercel / storage

Vercel Postgres, KV, Blob, and Edge Config
https://vercel.com/storage
Apache License 2.0
517 stars 58 forks source link

`@vercel/blob` - Non descriptive error message when missing file extension #664

Closed loky-lp closed 7 months ago

loky-lp commented 7 months ago

TL;DR Got Error: Vercel Blob: Access denied, please provide a valid token for this resource but the pathname was missing the file extension

I was trying to perform a client upload in a sveltekit app, i set up the api handler and everything according to the documentation, but this weren't working and i really I had a hard time wrapping my head around the error i was getting every time:

@vercel_blob_client.js?v=e0b9f04e:517 Uncaught (in promise) Error: Vercel Blob: Access denied, please provide a valid token for this resource.
    at getBlobError (@vercel_blob_client.js?v=e0b9f04e:517:15)
    at async @vercel_blob_client.js?v=e0b9f04e:567:31
    at async requestApi (@vercel_blob_client.js?v=e0b9f04e:545:23)
    at async put2 (@vercel_blob_client.js?v=e0b9f04e:1071:22)
    at async onSubmit (+page.svelte:29:10)
    at async sveltekit-superforms.js?v=e0b9f04e:3217:11
    at async HTMLFormElement.handle_submit (forms.js?v=e0b9f04e:150:5)

I tried setting validUntil to a higher value but got nothing. I debugged every steps but everything seemed to work fine and the generated token seemed valid. The fix for me was to add the file extension the to pathname used in the upload method

Setup

"@sveltejs/kit": "^2.0.0",
"@vercel/blob": "^0.23.0",
"svelte": "^4.2.7",

File +server.ts

import type { RequestHandler } from './$types'
import { json } from '@sveltejs/kit'
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'
import { BLOB_READ_WRITE_TOKEN } from '$env/static/private'

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json() as HandleUploadBody

  try {
    const jsonResponse = await handleUpload({
      token: BLOB_READ_WRITE_TOKEN,
      body,
      request,
      onBeforeGenerateToken: async () => {
        return {
          allowedContentTypes: ['image/avif', 'image/png', 'image/jpeg', 'image/webp', 'image/svg+xml'],
          maximumSizeInBytes: 100_000_000, // 100MB
          tokenPayload: JSON.stringify({ data: 'test-data' }),
        }
      },
      onUploadCompleted: async ({ blob, tokenPayload }) => {
        console.log('upload completed', { blob, tokenPayload })
      },
    })

    return json(jsonResponse)
  } catch (e) {
    return json(
      { error: (e as Error).message },
      { status: 400 },
    )
  }
}

onSubmit function in +page.svelte

async function onSubmit({ formData }) {
  // ...                         ⬇️ Use a name like this to see the error
  const imageUrl = await upload('beautiful-name-test', formData.get('image'), {
    access: 'public',
    handleUploadUrl: '/api/form/upload',
  })
  // ...
},

Maybe related issue #456

luismeyer commented 7 months ago

hey @LoKy-dev, thanks for the detailed explanation. I can't really reproduce the issue. When I use your code I end up with the error: message: "\"contentType\" is not allowed"

This happens because you restrict the contentType by passing allowedContentTypes and don't pass a file extension or the contentType param into the upload function. The way our API currently works is: either use the contentType you passed or check the file extension. Because you pass nothing, the API doesn't know the contentType and rejects the request.

We probably can improve further on how the API determines the contentType but for now you would need to either pass the contentType option to upload or add a file extension.

luismeyer commented 7 months ago

we just shipped a new API version. The contentType should be inferred automatically now.

@LoKy-dev could you verify, so we can close this issue? thanks