Fait partie de Hébergement par cadricielHébergement · Réseau

Erreurs CORS qui n’arrivent qu’en production : lisez le message, puis corrigez la cause.

En développement, le frontal et l’API partagent souvent une origine : CORS n’entre jamais en jeu. En production, non. Le message du navigateur nomme le problème : une prévérification sans réponse 2xx, une origine qui ne correspond pas, des authentifiants avec le joker *, une redirection ou une erreur sans en-têtes CORS. Corrigez du côté de l’API, pas dans le navigateur.

Faits vérifiés le

Pourquoi ça marche en local

CORS s’applique quand une page d’une origine (schéma, hôte et port) en appelle une autre. En local, votre serveur de développement redirige souvent /api vers le dorsal : le navigateur ne parle qu’à localhost:5173. Ou l’API autorise http://localhost:3000 et rien d’autre. En production, le frontal est sur https://exemple.ca et l’API sur https://api.exemple.ca ou https://votre-api.canner.app : deux origines, et le navigateur demande maintenant la permission à l’API.

Voyez l’« erreur CORS » comme un symptôme. Le navigateur cache la réponse à votre code : un point de terminaison planté, un 404 et une vraie incompatibilité de politique se ressemblent dans la console tant que vous n’ouvrez pas la requête.

Ce que dit le message, et ce qui corrige

Les messages de Chrome commencent par Access to fetch at '…' from origin '…' has been blocked by CORS policy:. La suite de la ligne est ce qui compte :

Le message continueCauseCorrectif
No 'Access-Control-Allow-Origin' header is present on the requested resource. † †La réponse n’avait aucun en-tête CORS : l’origine n’est pas dans la liste de l’API, ou la réponse vient de quelque chose qui s’exécute avant votre intergiciel CORS (un plantage, un 404 pour la route, une page d’erreur du proxy)Ouvrez la requête dans l’onglet Réseau et lisez d’abord son vrai statut. Ajoutez l’origine; enregistrez l’intergiciel CORS avant l’authentification et les routes
Response to preflight request doesn't pass access control check: It does not have HTTP ok status. † †La prévérification OPTIONS a reçu une réponse hors 2xx. Une prévérification ne porte jamais de témoins ni de jetons : l’intergiciel d’authentification répond souvent 401Laissez passer OPTIONS avant l’authentification; répondez 204 avec les en-têtes CORS
The 'Access-Control-Allow-Origin' header has a value 'https://exemple.ca' that is not equal to the supplied origin. † †L’origine ne correspond pas exactement : www ou domaine racine, http ou https, un port, ou une barre oblique finale dans votre configurationComparez l’en-tête Origin de la requête à votre liste, caractère par caractère. Une origine ne finit jamais par /
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. † †credentials: 'include' (ou withCredentials) avec Access-Control-Allow-Origin: *Renvoyez l’origine précise, avec Access-Control-Allow-Credentials: true et Vary: Origin. Ou retirez les authentifiants si vous utilisez des jetons porteurs
Redirect is not allowed for a preflight request. † †L’URL de l’API redirige : http vers https, www vers le domaine racine, ou une redirection de barre oblique finaleAppelez directement l’URL finale. Faites curl -I sur l’URL exacte qu’utilise votre frontal
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response. † †La requête envoie un en-tête que la réponse de prévérification n’a pas listéAjoutez-le à Access-Control-Allow-Headers. Avec des authentifiants, * ne compte pas : listez les noms

† Les détails sur les concurrents proviennent des pages publiées par chaque fournisseur aux dates indiquées; les prix et forfaits changent, vérifiez auprès du fournisseur avant de décider. `No 'Access-Control-Allow-Origin' header is present on the requested resource.` (2026-10-29) · `No 'Access-Control-Allow-Origin' header is present on the requested resource.` (2026-10-29) · `Response to preflight request doesn't pass access control check: It does not have HTTP ok status.` (2026-10-29) · `Response to preflight request doesn't pass access control check: It does not have HTTP ok status.` (2026-10-29) · `The 'Access-Control-Allow-Origin' header has a value 'https://exemple.ca' that is not equal to the supplied origin.` (2026-10-29) · `The 'Access-Control-Allow-Origin' header has a value 'https://exemple.ca' that is not equal to the supplied origin.` (2026-10-29) · `The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.` (2026-10-29) · `The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.` (2026-10-29) · `Redirect is not allowed for a preflight request.` (2026-10-29) · `Redirect is not allowed for a preflight request.` (2026-10-29) · `Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.` (2026-10-29) · `Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.` (2026-10-29)

Quand le navigateur envoie une prévérification

Un GET, HEAD ou POST avec seulement des en-têtes autorisés et un Content-Type application/x-www-form-urlencoded, multipart/form-data ou text/plain passe directement. Tout le reste est d’abord prévérifié : PUT, PATCH, DELETE, un corps JSON, un en-tête Authorization, tout en-tête X- personnalisé. Voilà pourquoi un formulaire fonctionne alors que le même appel par fetch() en JSON échoue.

La prévérification est une requête OPTIONS avec Access-Control-Request-Method et Access-Control-Request-Headers. Access-Control-Max-Age permet au navigateur de garder la réponse en cache; sans lui, la valeur par défaut est de 5 secondes, et une API lente paie deux allers-retours sur la plupart des appels.

Les origines sur Canner : www, domaine racine et URL d’aperçu

  • `www` et le domaine racine sont deux origines. Avec Servir www activé, Canner répond à www.exemple.ca par une redirection 301 vers exemple.ca (domaines personnalisés). Un frontal qui appelle https://www.exemple.ca/api reçoit cette redirection, et une prévérification ne peut pas la suivre. Visez l’hôte qui sert l’appli.
  • Le `http://` simple est redirigé vers le HTTPS par le proxy, ce qui brise aussi une prévérification. Une URL d’API en http:// sur une page HTTPS est de toute façon bloquée comme contenu mixte (en-têtes de proxy et contenu mixte).
  • Les aperçus ont leur propre origine. L’aperçu d’une demande de tirage tourne à branche--projet.canner.app. Si le frontal est un projet distinct, ses aperçus appellent votre API depuis https://<branche>--web.canner.app, absent d’une liste écrite pour la production.
  • Les aperçus partagent les variables d’environnement du projet. Un ALLOWED_ORIGINS défini pour la production s’applique aussi aux aperçus. Canner définit CANNER_ENV=preview et CANNER_URL à l’adresse de l’aperçu (variables d’environnement) : votre code peut les distinguer. Pour un frontal Vite, l’URL de l’API est figée à la compilation (variables Vite en production).
  • **N’autorisez jamais toutes les origines *.canner.app.** Les applis d’autres clients y vivent. Faites correspondre le motif d’aperçu de votre propre projet, comme dans le code ci-dessous.
// Express, with the cors package. ALLOWED_ORIGINS="https://example.ca,https://admin.example.ca"
const allowed = (process.env.ALLOWED_ORIGINS ?? "").split(",").filter(Boolean);
// Pull-request previews of the frontend project "web": https://<branch>--web.canner.app
const preview = /^https:\/\/[a-z0-9-]+--web\.canner\.app$/;

app.use(cors({
  origin: (origin, cb) => cb(null, !origin || allowed.includes(origin) || preview.test(origin)),
  credentials: true, // only if the browser sends cookies
}));

# FastAPI
app.add_middleware(
    CORSMiddleware,
    allow_origins=os.environ.get("ALLOWED_ORIGINS", "").split(","),
    allow_origin_regex=r"https://[a-z0-9-]+--web\.canner\.app",
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Proxys, caches et erreurs sans en-têtes

Le proxy de Canner n’ajoute, ne retire ni ne modifie aucun en-tête Access-Control-*, et il transmet les requêtes OPTIONS à votre appli en marche. Les en-têtes CORS qui arrivent au navigateur sont ceux que votre appli a envoyés. Ça joue dans les deux sens : si votre appli plante et répond 500 depuis un gestionnaire d’erreurs qui s’exécute hors de l’intergiciel CORS, ou si elle ne répond pas et que le proxy renvoie un 502, la réponse n’a pas d’en-têtes CORS et le navigateur signale une erreur CORS. Consultez les journaux de l’appli pour cette requête avant de toucher à la politique.

Les caches sont l’autre piège. Quand l’API renvoie l’origine de la requête, elle doit envoyer Vary: Origin; sinon, un cache partagé peut servir l’Access-Control-Allow-Origin d’un site à un autre. La cache optionnelle de Canner ne conserve jamais une réponse qui varie selon Origin : elle ne peut pas causer ce problème, mais un CDN que vous placez vous-même devant pourrait.

Déboguez dans le navigateur, puis avec curl

  • L’onglet Réseau, pas la console. Trouvez la requête en échec. S’il y a une ligne OPTIONS juste avant, c’est la prévérification : vérifiez son statut et ses en-têtes de réponse.
  • Comparez l’en-tête `Origin` de la requête avec Access-Control-Allow-Origin dans la réponse. Ils doivent être identiques.
  • Clic droit → Copier en tant que cURL pour rejouer la requête hors du navigateur, puis envoyez la prévérification vous-même (ci-dessous). curl ignore CORS : il montre ce que le serveur renvoie vraiment.
  • Mettez les origines autorisées dans une variable pour les changer sans toucher au code : canner env set ALLOWED_ORIGINS=https://exemple.ca,https://admin.exemple.ca avec l’interface en ligne de commande, puis redéployez.
# Send the preflight the browser would send
curl -i -X OPTIONS https://api.example.ca/items \
  -H "Origin: https://example.ca" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type, authorization"

# A good answer: a 2xx status and, at least,
#   Access-Control-Allow-Origin: https://example.ca
#   Access-Control-Allow-Methods: ... POST ...
#   Access-Control-Allow-Headers: content-type, authorization
#   Vary: Origin

Ou évitez CORS complètement

Si le frontal et l’API sont à vous, les servir depuis une seule origine règle le problème : l’API sous /api dans la même appli, ou une réécriture dans le serveur du frontal qui transmet /api au dorsal. Les témoins deviennent plus simples aussi. Des origines distinctes se justifient quand l’API a d’autres clients ou un autre rythme de mise en production. D’autres guides sont dans hébergement par cadriciel.

Questions

Pourquoi CORS fonctionne-t-il en local mais pas en production?

En local, le serveur de développement relaie généralement les appels d’API : le navigateur ne voit qu’une origine. En production, le frontal et l’API ont des noms d’hôte différents, et l’API doit autoriser l’origine exacte du frontal, répondre aux prévérifications par un 2xx et éviter les redirections.

Peut-on corriger une erreur CORS depuis le frontal?

Non. C’est l’API qui décide quelles origines peuvent lire ses réponses. mode: 'no-cors' rend seulement la réponse illisible pour votre code. Corrigez les en-têtes de l’API ou servez les deux depuis une seule origine.

Pourquoi Access-Control-Allow-Origin: * échoue-t-il avec des témoins?

Les navigateurs refusent le joker sur les requêtes avec authentifiants. Renvoyez l’origine précise tirée d’une liste d’autorisation, envoyez Access-Control-Allow-Credentials: true et ajoutez Vary: Origin.

Une erreur CORS est-elle toujours un problème de CORS?

Non. Un 500, un 404 ou une erreur de passerelle sans en-têtes CORS apparaît aussi comme une erreur CORS. Vérifiez d’abord le statut de la requête dans l’onglet Réseau et les journaux de l’API.

Des URL d’aperçu qu’on peut mettre sur une liste d’autorisation.

Chaque demande de tirage obtient une adresse prévisible branche--projet.canner.app, et CANNER_URL dit à votre code laquelle.