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).
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
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.
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:
"origin": ["http://localhost:8080"]
to "origin": ["*"]
in the CORS policy for my storage bucketXMLHttpRequest
instead of fetch
in my frontend codeI 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.