EthioDeploy
DocsPricingDomainsBlogToolsChangelog

Blog/Fix deploy errors

CORS error after deploying frontend and backend separately

BS
Brook SolomonPublished November 7, 2026
Share on XShare on LinkedIn
Access-Control-Allow-Origin

THE SHORT ANSWER

Once your frontend and API are deployed to different domains, the browser treats every API call as cross-origin and blocks the response unless the API sends an Access-Control-Allow-Origin header naming the frontend's exact origin. It worked locally because the Vite dev proxy, or a single localhost origin, hid the problem. The fix goes on the backend: allow the deployed frontend's origin, with scheme and no trailing slash, answer OPTIONS preflight requests, and never combine credentials with a * origin.

Why did CORS break only after deploying?

An origin is the scheme, host and port together. https://app.example.com and https://api.example.com are different origins, and so are two subdomains on any hosting platform. When a page on one origin calls an API on another, the browser sends the request, then checks the response for an Access-Control-Allow-Origin header that allows the page's origin. No header, or the wrong origin, and your JavaScript gets an error instead of the response.

Locally it often never comes up. Vite's server.proxy forwards /api to your backend, so the browser only ever talks to one origin, and Vite's docs say that option applies to the dev server only. After the build there is no proxy, the frontend calls the API's real URL, and CORS starts applying.

CORS is enforced by the browser, not the server. The same request from curl or Postman works, which is why the API can look fine when you test it by hand.

What does each error message mean?

Chrome prints all of these as “Access to fetch at ... has been blocked by CORS policy”, followed by the reason:

The reason Chrome givesWhat it means
No 'Access-Control-Allow-Origin' header is present on the requested resource.The API sent no CORS headers. Either CORS isn't set up, or the request failed before the CORS code ran: a 500, a 502 from a crashed app, or a 404 on the wrong URL.
Response to preflight request doesn't pass access control checkThe browser's OPTIONS request got no CORS headers or a non-2xx status. The OPTIONS route isn't handled, or auth middleware rejected it.
The value of the 'Access-Control-Allow-Origin' header ... must not be the wildcard '*' when the request's credentials mode is 'include'.You send cookies (credentials: 'include', or withCredentials in Axios) and the API allows *. Name the origin instead.
The Access-Control-Allow-Origin header has a value that is not equal to the supplied originThe API allows a different origin: http instead of https, a trailing slash, or www against no www.

Check the first row carefully. If the API crashed, the error page it returns has no CORS headers either, so a server bug shows up in the browser as a CORS error. Open the Network tab and look at the real status code of the failed request, then check the API's logs, before you change any CORS settings.

Why is there an OPTIONS request?

For anything beyond a simple request, the browser first sends an OPTIONS request, the preflight, to ask whether the real one is allowed. A request is simple only if it uses GET, HEAD or POST, and its Content-Type is application/x-www-form-urlencoded, multipart/form-data or text/plain. So a POST with a JSON body, a PUT or DELETE, or any request carrying an Authorization header gets a preflight.

The API must answer that OPTIONS request with a 2xx status and the CORS headers, before any login check. The browser never sends your auth token with the preflight, so middleware that requires one returns 401 and the real request is never sent.

How do I fix it in Express?

Use the cors package and list your frontend's origins. When it runs as app-level middleware, its docs say, “pre-flight requests are already handled for all routes”:

import express from 'express'
import cors from 'cors'

const app = express()

app.use(cors({
  origin: ['https://app.example.com', 'http://localhost:5173'],
  credentials: true, // only if you send cookies
}))

app.use(express.json())
app.use('/api', apiRouter)

Put it before your routes and before any auth middleware. If you copied app.options('*', cors()) from an older example, Express 5 rejects the bare * path, and with app.use(cors()) you don't need that line at all.

How do I fix it in Django?

Install django-cors-headers and put its middleware as high as possible. Its README says it must come before anything that can generate a response, “such as Django's CommonMiddleware or Whitenoise's WhiteNoiseMiddleware”:

INSTALLED_APPS = [
    # ...
    "corsheaders",
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # ...
    "django.middleware.common.CommonMiddleware",
]

CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",  # scheme + host, no trailing slash
]
CORS_ALLOW_CREDENTIALS = True  # only if you send cookies

An origin with a path, including a trailing slash like https://app.example.com/, fails the package's system check with error E014. If you also send cookies, the frontend's origin must be in CSRF_TRUSTED_ORIGINS for POST requests to pass Django's CSRF check.

How do I fix it in FastAPI?

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,  # only if you send cookies
    allow_methods=["*"],
    allow_headers=["*"],
)

allow_methods defaults to ["GET"], so without it a POST fails its preflight. FastAPI's docs also say none of allow_origins, allow_methods and allow_headers can be ["*"] when allow_credentials is True; when that happens the browser rejects the response.

And in Next.js route handlers?

If your API is Next.js route handlers on a different domain from the frontend, set the headers in next.config for every API path. Next.js answers OPTIONS automatically for a route that doesn't define it:

// next.config.ts
export default {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
        ],
      },
    ]
  },
}

CORS passes but cookies still aren't sent?

Cross-site cookies need three things together: credentials: 'include' on the fetch, Access-Control-Allow-Credentials: true with a named origin on the API, and the cookie set with SameSite=None; Secure. MDN notes that SameSite=None requires Secure, so this only works over HTTPS. Django's session cookie defaults to SameSite=Lax, which browsers don't send on cross-site fetches.

Browsers keep tightening third-party cookies, so the more reliable fix is to avoid needing them: put the API on a subdomain of the frontend's domain, use token auth in an Authorization header, or serve the frontend and API from one origin.

What happens on EthioDeploy?

A frontend and an API deployed as two projects get two different addresses, so they are two origins. Allow the frontend's address in the API's CORS settings, and when you add a custom domain, add that origin too. Keeping the allowed origins in an environment variable lets you change them without a code change.

On the frontend side, a Vite app reads the API URL at build time, so set it as a VITE_ variable and redeploy after you change it. See Vite env variables undefined in production.

Questions

CORS is enforced by browsers, not servers. Postman and curl don't check the Access-Control-Allow-Origin header, so the same request succeeds there and is blocked in the browser until the API allows your frontend's origin.

No. The API has to send the CORS headers. In development a proxy like Vite's server.proxy avoids the cross-origin call, but it only runs in the dev server.

For a public API that uses no cookies, it is common. It can't be used with credentials, and for an API behind a login it's better to list the exact frontend origins.

A POST with a JSON body triggers a preflight OPTIONS request, while a plain GET doesn't. If the API doesn't answer OPTIONS with the CORS headers, or requires auth on it, only those requests fail.

No. An origin is scheme, host and port only, such as https://app.example.com. A trailing slash makes it a different string, so the browser doesn't match it, and django-cors-headers rejects it with error E014.

Sources

Checked on November 7, 2026:

  • MDN: Cross-Origin Resource Sharing (CORS)
  • MDN: CORS errors
  • MDN: Credential is not supported if the origin is *
  • MDN: Set-Cookie
  • Chromium: CORS error strings
  • Express: cors middleware
  • Express: Migrating to Express 5
  • django-cors-headers
  • FastAPI: CORS
  • Next.js: Route handlers
  • Next.js: headers
  • Vite: Server options

Push a repo. Get a URL.

  • Deploy an Express app
  • Deploy a FastAPI app
  • Deploy a Django app
  • Environment variables
Start free

Join the EthioDeploy Telegram

Release notes as they ship, plus deploy help from the team and other developers.

Join on Telegram
BS
WRITTEN BYBrook SolomonFounder, EthioDeploy

Brook Solomon founded EthioDeploy and builds its deploy pipeline, build servers and AI coding agent. The posts here come from errors seen in real deploys on the platform.

All posts by Brook →
On this page
  • Why did CORS break only after deploying?
  • What does each error message mean?
  • Why is there an OPTIONS request?
  • How do I fix it in Express?
  • How do I fix it in Django?
  • How do I fix it in FastAPI?
  • And in Next.js route handlers?
  • CORS passes but cookies still aren't sent?
  • What happens on EthioDeploy?
  • Questions
  • Sources
0% read

Keep reading

  • DEBUG = False

    Fix deploy errorsNov 7, 2026

    Django static files 404 in production

    Django only serves static files itself while DEBUG is True. Its docs call that method grossly inefficient and probably insecure, so with DEBUG = False in production every /static/ URL returns 404 and the admin loses its styling. The fix has three parts: set STATIC_ROOT, run python manage.py collectstatic during the build, and serve the collected folder, usually with WhiteNoise added to MIDDLEWARE right after SecurityMiddleware.

  • /* → index.html

    Fix deploy errorsNov 4, 2026

    404 on page refresh in a React or Vite SPA

    A single-page app returns 404 on refresh because the browser asks the server for a path like /dashboard, and the server has no file at that path. Your router only exists in the JavaScript that index.html loads, so the server has to answer every unknown path with index.html and let the router take over. It works locally because Vite's dev server and vite preview already do that. In production you add one rewrite rule to your host: send every path that isn't a real file to /index.html.

  • heap limit

    Fix deploy errorsNov 3, 2026

    JavaScript heap out of memory during next build

    next build fails with "JavaScript heap out of memory" when the Node.js process running the build fills V8's old-space heap. The heap has a size limit of its own, separate from how much RAM the machine has. The quick fix is to raise that limit for the build only, with --max-old-space-size set below the memory the build machine actually has. The lasting fix is to find which build step uses the memory (bundling, TypeScript or static page generation) and turn down that step: source maps, the type check, or webpack's cache.

EthioDeploy

Agentic first development and deployment, started in Ethiopia, built for developers everywhere.

Telegram community
Product
PricingAI coding agentAgent mode docsDomainsCLIFree backend hostingHosting in EthiopiaChangelog
Deploy guides
Next.jsNode.jsPythonSvelteKitTanStack StartDjangoFastAPIFlaskExpressNestJSLaravelRailsGoTelegram botDiscord botLovableBoltReplitv0Google AI StudioBase44Firebase StudioClaude Code & Cursor
Compare
vs Herokuvs Vercelvs Rendervs Railwayvs Fly.iovs Netlifyvs KoyebHeroku alternativesVercel alternativesNetlify alternativesRender alternativesRailway alternativesFly.io alternativesReplit alternatives
Resources
DocsBlogFree toolsAboutSupportContactllms.txt
© 2026 EthioDeployPay by card in USD or bank transfer in ETBTermsPrivacy