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 OPTIONS badly
  • a serverless function only implements GET and POST
  • 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.com
  • admin.example.com
  • preview-pr-42.example.pages.dev style 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:

  1. Request Origin
  2. Response Access-Control-Allow-Origin
  3. Whether credentials are involved
  4. Preflight response status
  5. Allowed methods and headers
  6. Proxy/CDN cache behavior
  7. 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 OPTIONS early
  • send Vary: Origin when 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.