Search code examples
google-cloud-platformcorsgoogle-cloud-storagerails-activestorage

Google Cloud Storage request blocked by CORS: not allowed by Access-Control-Allow-Origin


What am I trying to accomplish?

I am trying to make a direct file upload to a Google Cloud Storage bucket. I'm successfully creating a direct upload link via my Ruby on Rails API ActiveStorage setup.

Using the created direct upload link, I am trying to make a PUT request to my Google Cloud Storage bucket in my frontend client (Next.js).

What issue am I encountering?

In my frontend client, I'm making a PUT request with fetch and receiving the following CORS error:

In Chrome:

Access to fetch at 'https://storage.googleapis.com//?GoogleAccessId=<ACCESS_ID>&Expires=1678985734&Signature=' from origin 'http://localhost:8080' 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. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

In Safari:

Origin http://localhost:8080 is not allowed by Access-Control-Allow-Origin. Status code: 200 Failed to load resource: Origin http://localhost:8080 is not allowed by Access-Control-Allow-Origin. Status code: 200

Code Details

As per Google Cloud's CORS configuration, this is the CORS policy for my storage bucket:

[
  {
    "origin": ["http://localhost:8080"],
    "method": ["GET", "PUT"],
    "responseHeader": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"],
    "maxAgeSeconds": 3600
  }
]

I have confirmed this CORS configuration is present by running: gcloud storage buckets describe gs://<BUCKET_NAME> --format="default(cors)"

My direct upload fetch in my frontend client is pretty simple:

const directUpload = async (directUpload: DirectUpload) => {
  const response = await fetch(directUpload.url, {
    method: 'PUT',
    headers: JSON.parse(directUpload.headers),
    body: file,
  });

  return response;
}

The request headers for PUT (from the network tab) are as follows:

Content-Disposition: inline; filename="logo.png"; filename*=UTF-8''logo.png
Content-Type: image/png
Origin: http://localhost:8080
Referer: http://localhost:8080/
Accept: */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15
Content-MD5: y+5qHSqBo9Kmlkln9P0vAQ==

The response headers from GCS from the PUT request (Status code 403):

alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
content-length: 363
content-type: application/xml; charset=UTF-8
date: Mon, 20 Mar 2023 16:39:30 GMT
server: UploadServer
x-guploader-uploadid: <UPLOAD_ID>

The request headers of the preflight request:

:authority: storage.googleapis.com
:method: OPTIONS
:path: <DIRECT_UPLOAD_PATH>
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
access-control-request-headers: content-disposition,content-md5,content-type
access-control-request-method: PUT
cache-control: no-cache
origin: http://localhost:8080
pragma: no-cache
referer: http://localhost:8080/
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36

The response headers from GCS of the preflight request:

access-control-allow-headers: Content-Type,Content-MD5,Content-Disposition
access-control-allow-methods: GET,PUT
access-control-allow-origin: http://localhost:8080
access-control-max-age: 3600
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 0
content-type: text/html; charset=UTF-8
date: Thu, 16 Mar 2023 19:26:23 GMT
expires: Thu, 16 Mar 2023 19:26:23 GMT
server: UploadServer
vary: Origin
x-guploader-uploadid: <UPLOAD_ID>

I can see that the Origin header should match the origin setup in my bucket CORS policy.

Attempted Solutions

I have seen numerous other posts with similar CORS errors of not allowed by Access-Control-Allow-Origin. From these posts I have experimented with:

  • Changing "origin": ["http://localhost:8080"] to "origin": ["*"] in the CORS policy for my storage bucket
  • Using a XMLHttpRequest instead of fetch in my frontend code
  • Checking the Troubleshoot CORS requests suggestions

Solution

  • I was able to successfully make the PUT direct upload request to Google Cloud Storage after making a change found in this write up: https://finnian.io/blog/uploading-files-to-s3-react-native-ruby-on-rails

    Finally I was able to make it work by forcing Active Storage to use v4 of the GCS presigned post URL. You can achieve this by setting the cache_control property of your service in storage.yml. I’m not sure why this setup doesn’t seem to work with v2, but there you go. Maybe it’ll help you.

    This involved adding cache_control: "public, max-age=3600" to my google service in storage.yml as per active storage guide here: https://edgeguides.rubyonrails.org/active_storage_overview.html#google-cloud-storage-service

    I also added "Cache-Control" to the responseHeader list uploaded to my GCS bucket CORS policy.

    This resulted in the successful PUT request to GCS which uploaded the file to my bucket.

    Adding cache_control in Rails forces using v4 (instead of v2) for generating signed URLs. More information on the signing URLs can be found here: https://cloud.google.com/storage/docs/access-control/signed-urls#types

    • V4 signing with service account authentication: This signing mechanism is described below.

    • V2 signing with service account authentication: This is a legacy mechanism for creating signed URLs, and its usage is not recommended.