Cloud storage CORS errors during uploads (S3 and GCS)

Prev Next

Cloud storage CORS errors during uploads (S3 and GCS)

:::info
Category: Environment & Configuration Issues

Applies to: Direct browser uploads using pre-signed URLs for Amazon S3 or Google Cloud Storage (GCS)

Quick Detection: Signed URL generation succeeds, but the browser blocks the upload request with a preflight CORS error mentioning missing Access-Control-Allow-Origin.
:::

Symptoms

  • Upload starts but fails before the first chunk or part completes
  • Browser console shows an error similar to:
Access to fetch at '<signed-storage-url>' from origin '<cms-origin>' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • In Network tab, the preflight (OPTIONS) or upload request to the storage domain fails
  • API call that returns the signed URL succeeds, but upload to bucket fails
  • Same issue can occur with both S3 and GCS

Likely Causes

  • Bucket CORS policy does not include the CMS origin (for example https://onecms.staging.viewlift.com)
  • Allowed methods are missing the upload method (PUT and sometimes POST)
  • Allowed request headers are too restrictive for uploader/browser headers
  • Environment domains (dev/staging/prod) were not all added to bucket CORS
  • CORS policy was updated recently and changes have not propagated yet

What to Check

  1. Confirm the failing request host
    • If URL points to storage (s3.amazonaws.com, *.s3.*.amazonaws.com, storage.googleapis.com), this is bucket CORS, not backend ALLOWED_ORIGINS.
  2. Inspect preflight response in DevTools
    • Open Network and select failing OPTIONS request.
    • Check response headers for Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers.
  3. Verify bucket CORS configuration
    • Compare exact CMS origin and upload method/header set with bucket rules.
    • Confirm the target environment origin is present.
  4. Validate signed URL state
    • Ensure URL is not expired and host points to expected bucket/region.
    • Expired URLs cause different errors, but can be confused with CORS during retries.

How to Fix

Apply bucket-level CORS rules for each CMS environment origin.

Amazon S3 example

[
  {
    "AllowedOrigins": ["https://onecms.staging.viewlift.com"],
    "AllowedMethods": ["GET", "HEAD", "PUT", "POST"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag", "x-amz-request-id", "x-amz-id-2"],
    "MaxAgeSeconds": 3000
  }
]

Google Cloud Storage example

[
  {
    "origin": ["https://onecms.staging.viewlift.com"],
    "method": ["GET", "HEAD", "PUT", "POST"],
    "responseHeader": ["Content-Type", "ETag", "x-goog-resumable"],
    "maxAgeSeconds": 3600
  }
]

After updating CORS:

  1. Apply configuration to the target bucket.
  2. Wait for propagation (can take a few minutes).
  3. Retry upload from browser with DevTools open.
  4. Confirm preflight/upload responses contain expected Access-Control-Allow-* headers.

Prevention Tips

  • Maintain a per-environment origin matrix and update it for every new CMS domain
  • Add storage bucket CORS verification (S3 and/or GCS) to deployment checklists
  • Prefer explicit environment origins in production over broad wildcards
  • Add a post-deploy smoke test for one signed browser upload path

Related Issues

  • Backend API CORS looks correct, but direct storage upload still fails
  • Upload works in non-browser tools but fails in browser (non-browser clients do not enforce CORS)
  • Multipart upload fails on first part with preflight errors

ℹ️ Note
This is usually a storage-layer configuration issue and is separate from management service ALLOWED_ORIGINS settings.