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:3000≠https://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:
| Flow | Responsible | Where to Configure |
|---|---|---|
| Browser → API Gateway → Backend | API Gateway | API Gateway or backend middleware |
| Browser → GCS | GCS bucket | cors-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 Type | Direct Browser Access | CORS Needed |
|---|---|---|
| User upload bucket | Yes | Required |
| Internal data exchange | No | Not needed |
| Cloud Build artifacts | No | Not 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
- Add it to the
originarray incors-config.json - Apply it:
gsutil cors set cors-config.json gs://my-bucket
- 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.
Backend CORS (Recommended)
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
| Error | Cause | Solution |
|---|---|---|
| No Access-Control-Allow-Origin | Origin not allowed | Add origin |
| Preflight fails | OPTIONS not handled | Handle OPTIONS |
| Authorization not allowed | Wrong header for GCS | Remove header |
| Wildcard with credentials | Invalid config | Use 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.