CORS bugs on Scaleway usually aren’t really “Scaleway bugs.” They’re config mismatches between your browser app, your API, your object storage, and whatever proxy sits in front. I’ve seen teams burn hours blaming the platform when the actual problem was one missing header or a wildcard used in the wrong place.
If you deploy frontends, APIs, or static assets on Scaleway, these are the mistakes that show up over and over.
Mistake 1: Treating CORS like auth
CORS does not protect your API. It only tells browsers whether frontend JavaScript can read a response.
I still see setups like this:
app.use((req, res, next) => {
const origin = req.headers.origin
if (origin === "https://app.example.com") {
res.setHeader("Access-Control-Allow-Origin", origin)
}
next()
})
And then someone says, “only our frontend can call the API.”
Nope. Curl, mobile apps, server-side code, and bots don’t care about CORS. If your Scaleway API is public, anyone can hit it unless you use real authentication and authorization.
Fix
Use CORS for browser access control only. Use proper auth for everything else.
A sane Express setup looks like this:
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
])
app.use((req, res, next) => {
const origin = req.headers.origin
if (origin && allowedOrigins.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin)
res.setHeader("Vary", "Origin")
res.setHeader("Access-Control-Allow-Credentials", "true")
}
next()
})
That Vary: Origin matters. Without it, caches can serve the wrong CORS response to the wrong site.
Mistake 2: Using * with credentials
This one breaks constantly in production. Your frontend sends cookies or Authorization, and your API responds with:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Browsers reject this. Wildcard origin and credentials do not mix.
You can see a real-world example of a public API using wildcard correctly in GitHub’s headers:
access-control-allow-origin: *
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
That works because GitHub’s public API response is designed for broad access. If your Scaleway app uses session cookies, don’t copy this pattern blindly.
Fix
Reflect the specific allowed origin instead of using *.
const allowedOrigins = [
"https://app.example.com",
"https://staging.example.com",
]
app.use((req, res, next) => {
const origin = req.headers.origin
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin)
res.setHeader("Access-Control-Allow-Credentials", "true")
res.setHeader("Vary", "Origin")
}
next()
})
And on the frontend:
fetch("https://api.example.com/me", {
credentials: "include",
})
If you don’t need cookies, don’t enable credentials. Simpler is better.
Mistake 3: Forgetting preflight requests
Your frontend sends a PUT, PATCH, DELETE, custom header, or JSON request with auth. The browser sends an OPTIONS preflight first. Your app returns 404, 405, or some redirect page. Browser says “CORS failed.”
On Scaleway, this often happens when:
- a reverse proxy handles
OPTIONSbadly - a serverless function only implements
GETandPOST - an ingress/controller blocks methods you forgot about
Fix
Handle OPTIONS explicitly and return the right headers.
Example in Express:
const allowedOrigins = new Set(["https://app.example.com"])
const allowedMethods = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
const allowedHeaders = "Content-Type, Authorization"
app.use((req, res, next) => {
const origin = req.headers.origin
if (origin && allowedOrigins.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin)
res.setHeader("Access-Control-Allow-Credentials", "true")
res.setHeader("Access-Control-Allow-Methods", allowedMethods)
res.setHeader("Access-Control-Allow-Headers", allowedHeaders)
res.setHeader("Vary", "Origin")
}
if (req.method === "OPTIONS") {
return res.status(204).end()
}
next()
})
Test preflight directly:
curl -i -X OPTIONS https://api.example.com/profile \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PATCH" \
-H "Access-Control-Request-Headers: content-type,authorization"
If this fails, your browser request was never going to work.
Mistake 4: Configuring CORS on the app, but not on Scaleway Object Storage
This one bites teams serving fonts, images, PDFs, or direct-upload assets from Scaleway Object Storage. The app API has perfect CORS headers, but the browser is loading files from the bucket domain, not your API domain.
Different origin, different CORS policy.
Common symptoms:
- web fonts fail in production
- canvas becomes tainted after drawing an image
- direct uploads from the browser fail
- frontend can fetch metadata from API but not the actual asset URL
Fix
Set bucket CORS rules on the storage layer itself, not just in your backend.
Typical rules should match your actual frontend origin, methods, and headers. Keep them narrow.
For example, if your app uploads directly from https://app.example.com:
- allow origin:
https://app.example.com - allow methods:
GET, PUT, POST - allow headers:
Content-Type, Authorization - expose headers if the frontend needs them
If you’re using Scaleway’s S3-compatible API, treat bucket CORS like you would on any S3-style object store: the browser talks to the bucket endpoint, so the bucket must answer with the right CORS headers.
Check Scaleway’s official docs for the exact bucket CORS configuration format in your chosen tooling.
Mistake 5: Missing Access-Control-Expose-Headers
Your request works, but frontend code can’t read headers you know are present. DevTools shows them. JavaScript doesn’t.
That’s expected. Browsers only expose a limited set of response headers unless you explicitly allow more.
GitHub does this well. Their API exposes useful headers like:
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Fix
Expose only the headers your frontend actually needs.
res.setHeader(
"Access-Control-Expose-Headers",
"ETag, Location, X-Request-Id, X-RateLimit-Remaining"
)
Frontend example:
const res = await fetch("https://api.example.com/items")
console.log(res.headers.get("ETag"))
console.log(res.headers.get("X-RateLimit-Remaining"))
Without Access-Control-Expose-Headers, those calls usually return null.
Mistake 6: Caching the wrong CORS response
This gets ugly on multi-origin deployments. Maybe you have:
app.example.comadmin.example.compreview-pr-42.example.pages.devstyle preview origins- staging and production on separate domains
If your API or proxy dynamically reflects the request origin, but you forget Vary: Origin, a cache may serve a response generated for one origin to another.
That creates flaky behavior that’s hard to reproduce.
Fix
Whenever Access-Control-Allow-Origin is dynamic, send:
Vary: Origin
And if preflight varies by requested method or headers, also consider:
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
This matters a lot when you put CDNs, load balancers, or reverse proxies in front of Scaleway services.
Mistake 7: Allowing every preview environment forever
Preview deployments are great until your CORS allowlist turns into a junk drawer.
I’ve seen this in startups: every generated subdomain gets added to CORS manually, nobody removes old ones, and six months later your API trusts a pile of stale environments.
Fix
Use a controlled pattern or a deployment-generated allowlist.
Example:
function isAllowedOrigin(origin) {
if (!origin) return false
if (origin === "https://app.example.com") return true
if (origin === "https://staging.example.com") return true
return /^https:\/\/preview-[a-z0-9-]+\.example\.com$/.test(origin)
}
Be careful with regex. Don’t accidentally allow https://preview-foo.example.com.attacker.com.
If preview environments don’t need cookies, disable credentialed CORS for them.
Mistake 8: Redirecting preflight or auth requests
A classic proxy mistake: HTTP to HTTPS redirect, trailing slash redirect, or auth middleware intercepts OPTIONS and sends a login page.
Browsers hate this. Preflight requests should get a clean response, not a redirect chain.
Fix
Terminate redirects before they affect browser API traffic, or exempt OPTIONS.
Bad pattern:
app.use((req, res, next) => {
if (!req.user) {
return res.redirect("/login")
}
next()
})
Better:
app.use((req, res, next) => {
if (req.method === "OPTIONS") {
return res.status(204).end()
}
if (!req.user) {
return res.status(401).json({ error: "unauthorized" })
}
next()
})
For APIs on Scaleway behind a proxy or ingress, make sure the proxy and the app agree on HTTPS handling and method routing.
Mistake 9: Debugging from browser errors only
Browser console errors are useful, but they’re often vague. “Blocked by CORS policy” can mean:
- missing
Access-Control-Allow-Origin - invalid credentialed wildcard
- failed preflight
- 500 error with no CORS headers
- proxy timeout
- DNS mismatch
- redirect on
OPTIONS
Fix
Inspect the actual network exchange.
I usually check these in order:
- Request Origin
- Response
Access-Control-Allow-Origin - Whether credentials are involved
- Preflight response status
- Allowed methods and headers
- Proxy/CDN cache behavior
- Redirects
Useful curl commands:
curl -i https://api.example.com/data \
-H "Origin: https://app.example.com"
curl -i -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type,authorization"
That tells you more than five minutes of staring at a red browser error.
A practical baseline for Scaleway deployments
If you want a boring, reliable setup:
- keep frontend and API on the same site when possible
- use explicit origins, not
*, for authenticated APIs - handle
OPTIONSearly - send
Vary: Originwhen origin is dynamic - configure bucket CORS separately for Object Storage
- expose only the response headers your frontend needs
- don’t use CORS as a security boundary
And if you’re tightening your overall header policy, CORS is only one piece. For broader response-header hardening, the official docs for your app framework and platform are the first stop, and for a practical reference on related headers you can also check https://csp-guide.com.
For Scaleway-specific setup details, the official documentation is where you should verify the exact configuration model for your service, whether that’s Object Storage, serverless products, containers, or load balancing. The CORS rules themselves are standard. The place you attach them is what changes.