Configure CORS for a SPA
If your single-page app at https://app.example.com needs to fetch from or upload to a bucket at your-bucket.s3.filebase.io, you need CORS. Without it, the browser blocks the cross-origin request.
This is a complete recipe — bucket CORS configuration, the application-side fetch, and how to debug when it fails.
Step 1 — Decide what your SPA needs
Pick the smallest set of methods and origins that gets the job done:
- Reading from a public bucket →
GET,HEAD. - Uploading via pre-signed PUT →
PUT, plusGET/HEADif you also read. - Uploading via pre-signed POST →
POST, plusGET/HEAD. - Multipart browser upload (e.g. Uppy) →
PUT,POST,GET,HEAD.
Step 2 — Write the CORS rule
{
"CORSRules": [
{
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
"AllowedOrigins": [
"https://app.example.com",
"http://localhost:5173"
],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag", "x-amz-request-id"],
"MaxAgeSeconds": 3000
}
]
}
What each field does:
- AllowedMethods — the HTTP methods the browser is allowed to make cross-origin. Must include every method your code uses.
- AllowedOrigins — the origins permitted to make cross-origin requests. Add
localhostfor local dev. Avoid*for application buckets. - AllowedHeaders — request headers the browser may send.
*is convenient; for stricter security, list explicitly:["Authorization", "Content-Type", "x-amz-content-sha256", "x-amz-date"]. - ExposeHeaders — response headers that your JavaScript can read (
ETagmatters for many upload UX patterns). - MaxAgeSeconds — how long the browser caches the preflight response. 3000 (50 min) is a reasonable default.
Step 3 — Apply the rule
aws --endpoint https://s3.filebase.io s3api put-bucket-cors \
--bucket your-bucket \
--cors-configuration file://cors.json
Verify:
aws --endpoint https://s3.filebase.io s3api get-bucket-cors --bucket your-bucket
Step 4 — SPA-side fetch
If reading directly from a public bucket:
const res = await fetch(
'https://your-bucket.s3.filebase.io/data/app.json',
);
const data = await res.json();
If using a pre-signed PUT to upload:
const { url } = await fetch('/api/sign-upload', { ... }).then(r => r.json());
await fetch(url, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
Debugging
When CORS fails, the browser reports specifics in the console. Common failures:
"Method not allowed"
Your CORS rule doesn't list the method you're using. Add PUT (or POST, etc.) to AllowedMethods.
"Origin not allowed"
The origin (scheme + host + port) doesn't match anything in AllowedOrigins. Check carefully:
http://localhost:5173is not the same ashttp://localhost:3000.https://example.comis not the same ashttps://www.example.com.
"Header not allowed in preflight"
Your request sets a header (commonly x-amz-... headers from a signed request, or a custom auth header) that's not in AllowedHeaders. Add it, or use ["*"].
Preflight succeeds but the actual request fails
Your AllowedHeaders is correct for OPTIONS but the actual request includes a different set. Use ["*"] to cover both directions.
Inspecting preflight by hand
Simulate the browser's preflight with curl:
curl -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: content-type" \
-i \
https://your-bucket.s3.filebase.io/test
The response's Access-Control-Allow-* headers tell you what's permitted. Compare against what the browser actually sent.
Step 5 — Lock down for production
When you ship to production, narrow AllowedOrigins to your real domains (drop the localhost entry). Keep the dev origin in a separate dev/staging bucket if you want to keep your config tight.