File upload in R2 is not working (CORS issue)


I’m having a hard time developing a simple file uploader form with Javascript (Svelte).

I’d like to achieve the file upload in two steps:

  1. Request a signed URL to my backend
  2. fetch() POST on this URL the file

I’ve been through a lot of articles and tutorials, but I’m blocked by a CORS policy issue.

This is how I do it. Below is the backend code to retrieve a signed URL based on the fileName provided as a URL param:

import { env } from '$env/dynamic/private';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { error, json, type RequestHandler } from '@sveltejs/kit';

export const GET: RequestHandler = async ({ url }) => {
  const fileName = url.searchParams.get('fileName');
  if (!fileName) throw error(404);

  const s3Client = new S3Client({
    region: 'auto',
    endpoint: `https://${env.R2_ACCOUNT_ID}`,
    credentials: {
      accessKeyId: env.R2_ACCESS_KEY_ID,
      secretAccessKey: env.R2_SECRET_ACCESS_KEY,

  const signedUrl = await getSignedUrl(
    new PutObjectCommand({
      Bucket: env.R2_BUCKET_NAME,
      Key: fileName,
    { expiresIn: 3600 }

  return json(signedUrl);

I successfully retrieve a signedUrl in this format:

Note: if I perform curl -X PUT <SIGNED_URL> -F "[email protected]" in a Terminal, it works and I can see the file in my bucket.

In the following code, I handle

  1. The input file when it changes
  2. The submission of the form when submitted
function handleFileChange(event: Event) {
  const target = as HTMLInputElement;
  if (target.files && target.files.length > 0) {
    fileToUpload = target.files[0];

async function handleSubmit() {
  if (fileToUpload) {
    console.log({ fileToUpload });
    const fileName = `${$}-${}`;

    let formData = new FormData();
    formData.append('size', String(fileToUpload.size)); // size must be first!
    formData.append('file', fileToUpload);
    formData.append('name', fileName);
    formData.append('mimeType', fileToUpload.type);

    // get signed url 
    const res = await fetch(`/api/aws-pre-signed?fileName=${fileName}`);
    const signedUrl = await res.json();

    await fetch(signedUrl, {
      method: 'POST',
      body: formData,
      /* Handles the response */
      .then((res) => {
        if (!res.ok) {
          // the response has a bad status (500..)
          throw new Error(
            'upload error status ' +
              res.status +
              ', status text: ' +
        } else {
          console.log(JSON.stringify(res, null, 2));
          alert(`Done! See the uploaded file in the bucket.`);
      .catch((err) => alert('Ooops: ' + err));

This is what I get when I submit the form: I successfully receive a signedUrl but calling the fetch POST fail, with an alert box Ooops: TypeError: Failed to fetch.

I followed the suggestion to add mode: 'no-cors' to the fetch() options, but then I have a different issue: Ooops: Error: upload error status 0, status text: and a 403 Forbidden in the Network tab.

Any help to decipher all of this would be greatly appreciated :pray:

1 Like

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