Software Engineer's Blog

Complete Guide to CORS in GCP

Complete Guide to CORS in GCP

What is CORS?

Browsers, for security reasons, block requests to a different origin by default.

CORS (Cross-Origin Resource Sharing) is a mechanism that allows the server to explicitly say,
“This origin is allowed.”

Origin = protocol + domain + port
http://localhost:3000https://myapp.example.com → different origins

Request Flow in a GCP Architecture

In a typical setup using Next.js + GCP API Gateway + GCS, the request flow looks like this:

Browser

  ├─── API Request ────────────────> GCP API Gateway
  │                                   └─> Backend service (Cloud Run, etc.)

  └─── File Upload/Download ──────> GCS (storage.googleapis.com)
                                      └─> Direct access via Signed URL

CORS needs to be configured in two different places:

FlowResponsibleWhere to Configure
Browser → API Gateway → BackendAPI GatewayAPI Gateway or backend middleware
Browser → GCSGCS bucketcors-config.json

These two paths are completely independent,
so they must be configured separately.

1. GCS CORS Configuration

Why is this needed?

When uploading or downloading files, the browser sends requests directly to GCS.

Since the request does not go through API Gateway,
CORS must be configured on the GCS bucket itself.

Browser ── PUT -> storage.googleapis.com/my-bucket/uploads/file.pdf
                  (Using Signed URL, bypassing API Gateway)

Which buckets need CORS?

Only buckets that are directly accessed from the browser need it.

Bucket TypeDirect Browser AccessCORS Needed
User upload bucketYesRequired
Internal data exchangeNoNot needed
Cloud Build artifactsNoNot needed

cors-config.json

[
  {
    "maxAgeSeconds": 3600,
    "method": ["GET", "HEAD", "PUT", "POST", "DELETE"],
    "origin": [
      "http://localhost:3000",
      "https://myapp.example.com",
      "https://myapp-staging.example.com",
      "https://my-frontend-service-hash-region.run.app"
    ],
    "responseHeader": [
      "Content-Type",
      "Content-Length",
      "Content-Disposition",
      "Content-Range",
      "x-goog-resumable"
    ]
  }
]

Adding a new domain

  1. Add it to the origin array in cors-config.json
  2. Apply it:
gsutil cors set cors-config.json gs://my-bucket
  1. Verify:
gsutil cors get gs://my-bucket

Applying to multiple buckets

#!/bin/bash
# apply-cors.sh

BUCKETS=(
  "gs://my-upload-bucket"
  "gs://my-user-bucket"
)

for bucket in "${BUCKETS[@]}"; do
  echo "Setting CORS on $bucket..."
  gsutil cors set cors-config.json "$bucket"
done

echo ""
echo "=== Verifying ==="
for bucket in "${BUCKETS[@]}"; do
  echo "[$bucket]"
  gsutil cors get "$bucket"
done

Signed URL and CORS (Important)

A Signed URL includes authentication information in the URL itself.

https://storage.googleapis.com/my-bucket/file.pdf
  ?X-Goog-Algorithm=GOOG4-RSA-SHA256
  &X-Goog-Credential=...
  &X-Goog-SignedHeaders=content-type;host
  &X-Goog-Signature=...

Key restriction:
Only headers listed in X-Goog-SignedHeaders can be included in the request.

If you include any additional headers, GCS will reject the request with a CORS error.

A very common mistake is using an API client that automatically adds an Authorization header.

Since Signed URLs already contain authentication information,
this header is not needed and will cause the request to fail.

✅ Correct upload

const xhr = new XMLHttpRequest();
xhr.open("PUT", signedUrl, true);
xhr.setRequestHeader("Content-Type", file.type);
xhr.send(file);

❌ Incorrect approach

await apiClient.put(signedUrl, file);
// Authorization header gets added → request fails

2. GCP API Gateway CORS Configuration

Why is this needed?

For normal API requests (data retrieval, creation, etc.),
requests go through API Gateway to the backend service.

The response must include:

Access-Control-Allow-Origin

Otherwise, the browser will block the request.

Browser ── GET /api/v1/resource ───> API Gateway ──> Cloud Run
         <─── 200 OK + CORS headers ─────────────

Handling CORS in API Gateway

API Gateway is defined using an OpenAPI spec.

You need to explicitly handle preflight (OPTIONS) requests.

paths:
  /api/v1/resource:
    options:
      summary: CORS preflight
      operationId: corsPreflightResource
      responses:
        "204":
          description: CORS preflight response
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: string
            Access-Control-Allow-Methods:
              schema:
                type: string
            Access-Control-Allow-Headers:
              schema:
                type: string
      x-google-backend:
        address: https://my-backend-service.run.app

In practice, it’s more common (and simpler) to handle CORS in the backend.

FastAPI

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:3000",
        "https://myapp.example.com",
        "https://my-frontend-service-hash-region.run.app",
    ],
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type"],
    allow_credentials=True,
)

Spring Boot

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins(
                    "http://localhost:3000",
                    "https://myapp.example.com",
                    "https://my-frontend-service-hash-region.run.app"
                )
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Authorization", "Content-Type")
                .allowCredentials(true);
    }
}

3. Debugging CORS Errors

Step 1 — Check the request URL

- storage.googleapis.com ─> GCS issue
- api.myapp.com          ─> API Gateway / backend issue

Step 2 — Use Chrome DevTools

Network tab ─> Click failed request ─> Headers

Check:
- Request Headers ─> Origin
- Response Headers ─> Access-Control-Allow-Origin

Step 3 — Check preflight (OPTIONS)

The browser sends an OPTIONS request before the actual request.

OPTIONS fails ─> actual request is blocked
OPTIONS succeeds ─> check request headers

Common error patterns

ErrorCauseSolution
No Access-Control-Allow-OriginOrigin not allowedAdd origin
Preflight failsOPTIONS not handledHandle OPTIONS
Authorization not allowedWrong header for GCSRemove header
Wildcard with credentialsInvalid configUse explicit origin

4. Environment Checklist

When adding a new domain:

GCS

  • Add domain to cors-config.json
  • Apply to all relevant buckets
  • Verify with gsutil cors get

Backend

  • Add domain to allow_origins
  • Redeploy service

Firebase (if used)

  • Add domain in Authentication settings

Frontend

  • Check .env
  • Rebuild and redeploy

5. Summary

When a new domain is added:

1. GCS (for direct file access)
   ─> Update cors-config.json → apply

2. Backend (for API requests)
   ─> Update allow_origins → redeploy

3. Firebase (if used)
   ─> Add domain in console

If you check all three, most CORS issues will be resolved.