Lesson 8/8 12 min

CORS and HTTP/2, HTTP/3

Why your fetch works in curl but fails in the browser, and how HTTP/2 then HTTP/3 cut even more round trips. The course capstone.

Le fetch qui marche partout, sauf dans le navigateur

Tu écris une seule ligne : fetch('https://api.autre-site.fr/data'). Dans ton terminal, le même appel en curl renvoie le JSON sans broncher. Dans le navigateur, la console se peint en rouge : « blocked by CORS policy ». Tu n'as rien changé. La même URL, la même requête, deux résultats opposés.

Tout dev a maudit cette erreur au moins une fois. La réaction réflexe est de croire que le serveur distant te refuse, ou que ton code est cassé. Les deux sont faux. Le serveur a répondu. Ton code est correct. Ce qui bloque, c'est le navigateur lui-même, et il le fait pour une bonne raison.

Cette dernière leçon boucle le cours. On va comprendre CORS (pourquoi curl s'en fiche et pas Chrome), puis on remonte dans le temps des protocoles : HTTP/1.1, 2, 3, et comment chaque version supprime encore des allers-retours. À la fin, tu rejoueras le voyage complet d'une URL, chaque étape enfin comprise.

L'origine : scheme + host + port

Souviens-toi de la leçon 2, quand on a démonté l'URL. On y a croisé une notion qui resurgit ici : l'origine. Une origine, c'est le trio scheme + host + port. https://www.web-developpeur.com:443 est une origine. Change l'un des trois, et tu changes d'origine.

  • https://web-developpeur.com et http://web-developpeur.com : origines différentes (le scheme change).
  • https://web-developpeur.com et https://api.web-developpeur.com : origines différentes (le host change).
  • https://web-developpeur.com et https://web-developpeur.com:8443 : origines différentes (le port change).

La règle est simple. Si ta page et l'URL que tu appelles ont la même origine, le navigateur laisse passer sans poser de question. Si l'origine est différente, le navigateur ne tranche pas tout seul : il demande la permission au serveur cible. C'est ça, le cœur de CORS, le « Cross-Origin Resource Sharing », le partage de ressources entre origines.

CORS ne protège pas ton serveur

Voici la confusion la plus répandue sur CORS, celle qui fait perdre des heures : croire que « CORS protège mon serveur ». C'est faux, et comprendre pourquoi change tout.

CORS protège l'utilisateur, pas ton serveur. C'est une règle du navigateur, pas un pare-feu côté serveur. Pourquoi l'utilisateur ? Parce que ton navigateur porte tes cookies de session (rappel de la leçon 6). Sans cette règle, n'importe quel site louche que tu visites pourrait, en arrière-plan, lancer un fetch vers ta banque, et comme ton navigateur joindrait automatiquement tes cookies, lire tes données bancaires. CORS empêche une page d'une origine de piller en douce une autre origine où tu es connecté. La preuve que ce n'est pas un pare-feu serveur : curl n'a pas de CORS. curl récupère tout, parce que curl ne porte pas tes cookies de navigateur et n'expose personne. Source : MDN, CORS.

Retiens l'image : CORS est un videur posté à l'entrée du navigateur, pas du serveur. Il ne protège pas le serveur distant (qui se débrouille avec ses propres défenses), il protège toi, en empêchant les sites que tu visites d'exploiter ta session ailleurs.

Le mécanisme : requête simple ou preflight

Concrètement, le navigateur a deux comportements selon la requête.

Requête « simple ». Un GET ou un POST basique, sans en-tête exotique. Le navigateur envoie d'abord la requête, puis regarde la réponse. Il cherche un en-tête Access-Control-Allow-Origin. S'il est là et autorise ton origine, le résultat t'est livré. Sinon, le navigateur a bien reçu la réponse, mais il refuse de te la donner et peint l'erreur rouge.

Requête « non simple ». Un PUT, un DELETE, un en-tête personnalisé, ou un corps en JSON : là, le navigateur ne prend pas de risque. Il envoie d'abord une requête de reconnaissance, le preflight, avec la méthode OPTIONS. Il demande au serveur : « j'aimerais faire un PUT depuis cette origine, tu autorises ? » Si le serveur répond oui, alors seulement le navigateur envoie la vraie requête. Et oui : ça fait un aller-retour de plus, le fil rouge de tout ce cours.

Deux scénarios CORS côte à côte. À gauche, requête simple : le navigateur envoie un GET, le serveur répond avec l'en-tête Access-Control-Allow-Origin, un seul aller-retour. À droite, requête non simple : le navigateur envoie d'abord un OPTIONS (le preflight), le serveur répond « autorisé », puis le navigateur envoie le vrai PUT et reçoit la réponse, deux allers-retours. Requête simple GET, POST basique Navigateur Serveur GET /data 200 + Allow-Origin 1 aller-retour Requête non simple PUT, DELETE, JSON, en-tête custom Navigateur Serveur OPTIONS (preflight) autorisé PUT /data (la vraie) 200 OK 2 allers-retours
Une requête non simple paie un aller-retour de préflight avant même la vraie requête.
Prédis avant de lire

Ton fetch vers https://api.autre-site.fr échoue avec « CORS policy ». Tu relances la même URL en curl dans ton terminal : ça marche, le JSON s'affiche. Qu'est-ce que ça prouve sur la cause du blocage ?

Voir la réponse

Ça prouve que le serveur n'est pas en cause : il répond très bien, curl récupère tout. Le blocage vient du navigateur, qui applique CORS pour te protéger. curl, lui, n'a pas de notion de CORS (il ne porte pas tes cookies de navigateur, il n'expose personne), donc il ne bloque rien. Conclusion : CORS n'est pas un pare-feu serveur, c'est une règle côté client. Le fix se fait côté serveur (renvoyer le bon Access-Control-Allow-Origin), jamais en bricolant ta requête fetch.

La vraie solution tient en trois en-têtes

Puisque CORS est une permission accordée par le serveur cible, la solution se trouve forcément côté serveur. Trois en-têtes de réponse suffisent :

Access-Control-Allow-Origin: https://ton-site.fr
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

Le premier dit quelles origines ont le droit. Le deuxième, quelles méthodes. Le troisième, quels en-têtes la requête peut joindre. Le navigateur lit ces trois lignes et décide de te livrer la réponse, ou non.

Le piège qui paraît malin : mettre Access-Control-Allow-Origin: * pour « régler » CORS une bonne fois. L'étoile autorise toutes les origines, et avec des requêtes authentifiées par cookies, c'est exactement le trou que CORS était censé fermer. Le volet défensif (qui autoriser, comment gérer les credentials, les risques d'un wildcard) est un sujet de sécurité à part entière : il est traité en détail dans la leçon CORS du cours Sécurité web. Ici, retiens juste le principe : autorise précisément, jamais aveuglément.

HTTP/1.1, 2, 3 : la chasse aux allers-retours

Changeons de sujet pour boucler le fil rouge du cours : les allers-retours. Tout ce qu'on a vu (DNS, handshake TLS, requête, réponse) coûte des allers-retours. Chaque version de HTTP a cherché à en enlever.

HTTP/1.1. Une connexion ne transporte qu'une requête à la fois. Tu demandes le CSS, tu attends, puis l'image, tu attends. Pour masquer la lenteur, les navigateurs ouvraient jusqu'à 6 connexions en parallèle vers le même serveur. Mieux, mais au-delà de 6 ressources, ça faisait quand même la queue.

HTTP/2. La grande idée : le multiplexage. Toutes tes requêtes voyagent en parallèle sur UNE seule connexion. Plus de file d'attente, plus besoin d'ouvrir 6 tuyaux. Une connexion, et tout passe en même temps.

HTTP/3. On s'attaque à la couche en dessous. Jusqu'ici, HTTP roulait sur TCP. HTTP/3 remplace TCP par QUIC, un protocole bâti sur UDP. L'intérêt, c'est le rappel direct de la leçon 4 sur le handshake TLS : avec QUIC, l'établissement de connexion et le handshake de chiffrement sont combinés en une seule étape. Encore moins d'allers-retours avant le premier octet utile. Cerise sur le gâteau : la connexion survit au changement de réseau. Tu passes du wifi à la 4G dans le métro ? Ta connexion QUIC continue, sans tout recommencer.

Où en est HTTP/3 en 2026 ? Cherche le dénominateur

Tu vas tomber sur des chiffres qui semblent se contredire. Apprenons à les lire ensemble, parce que c'est un excellent réflexe de rigueur.

  • Selon w3techs (juin 2026), 39,5 % des sites annoncent qu'ils savent parler HTTP/3.
  • Selon Cloudflare Radar, seulement ~21 % du trafic réel passe en HTTP/3, et HTTP/2 reste majoritaire avec ~51 % des requêtes.

21 % et 39,5 %, qui ment ? Personne. Les deux chiffres sont vrais. Ils ne mesurent simplement pas la même chose : w3techs compte des sites qui annoncent une capacité, Cloudflare compte des requêtes réellement servies. Un site peut annoncer HTTP/3 sans que la majorité de son trafic l'emprunte. Le dénominateur n'est pas le même : « par site » contre « par requête ».

Le réflexe à garder pour la vie : quand deux statistiques se contredisent, ne choisis pas ton camp, cherche le dénominateur. « 40 % » de quoi ? Des sites, des requêtes, des utilisateurs, des octets ? Neuf fois sur dix, les deux chiffres sont exacts et mesurent des populations différentes. Voir le dénominateur, c'est désamorcer 90 % des fausses contradictions.

À ne jamais écrire : « HTTP/3 a remplacé HTTP/2 ». C'est faux. En 2026, HTTP/2 sert encore la majorité des requêtes (~51 % selon Cloudflare Radar). HTTP/3 progresse vite, mais les trois versions cohabitent : un même navigateur négocie la meilleure version disponible avec chaque serveur.

Trois lignes comparées. HTTP/1.1 : des requêtes une par une, en file d'attente sur la connexion. HTTP/2 : les mêmes requêtes en flèches parallèles sur un seul tuyau (multiplexage). HTTP/3 : pareil en parallèle, plus un handshake raccourci au départ grâce à QUIC. HTTP/1.1 une requête à la fois req 1 req 2 req 3 … puis chacune attend son tour → HTTP/2 multiplexage : tout en parallèle une seule connexion HTTP/3 parallèle + handshake combiné (QUIC) handshake raccourci
De la file d'attente au parallèle, puis au handshake combiné : chaque version retire des allers-retours.

Le voyage complet, enfin compris

On a ouvert le cours sur une question simple : que se passe-t-il quand tu tapes une URL et appuies sur Entrée ? Redéroulons-le, en six lignes, chaque étape maintenant claire :

  1. Le navigateur découpe l'URL (scheme, host, port, chemin) : leçon 2.
  2. Le DNS traduit le nom de domaine en adresse IP : leçon 3.
  3. Un handshake TLS ouvre un canal chiffré (et HTTP/3 le combine pour aller plus vite) : leçon 4.
  4. Le navigateur envoie une requête, le serveur renvoie une réponse avec son status code : leçon 5.
  5. Comme HTTP est sans état, des cookies portent ta session d'une requête à l'autre : leçon 6.
  6. Le cache évite de tout redemander, et CORS arbitre ce que ta page a le droit de lire ailleurs : leçons 7 et 8.

La promesse du cours est tenue. Tu ne « tapes » plus une URL : tu sais ce qui part, vers où, et pourquoi ça prend une demi-seconde ou dix. Le réseau du web n'est plus une boîte noire. C'est une suite d'étapes nommées, mesurables, et désormais lisibles pour toi.

The fetch that works everywhere except in the browser

You write a single line: fetch('https://api.other-site.com/data'). In your terminal, the same call in curl returns the JSON without complaint. In the browser, the console turns red: "blocked by CORS policy". You changed nothing. Same URL, same request, two opposite outcomes.

Every dev has cursed this error at least once. The reflex is to think the remote server is refusing you, or that your code is broken. Both are wrong. The server replied. Your code is correct. What blocks you is the browser itself, and it does so for a good reason.

This final lesson closes the loop. We'll understand CORS (why curl doesn't care and Chrome does), then travel back through the protocols: HTTP/1.1, 2, 3, and how each version cuts more round trips. By the end, you'll replay the full journey of a URL, every step finally understood.

The origin: scheme + host + port

Remember lesson 2, when we took the URL apart. We met a notion that resurfaces here: the origin. An origin is the trio scheme + host + port. https://www.web-developpeur.com:443 is an origin. Change any of the three and you change origin.

  • https://web-developpeur.com and http://web-developpeur.com: different origins (the scheme changes).
  • https://web-developpeur.com and https://api.web-developpeur.com: different origins (the host changes).
  • https://web-developpeur.com and https://web-developpeur.com:8443: different origins (the port changes).

The rule is simple. If your page and the URL you call share the same origin, the browser lets it through, no questions asked. If the origin is different, the browser doesn't decide alone: it asks the target server for permission. That's the heart of CORS, "Cross-Origin Resource Sharing".

CORS does not protect your server

Here's the most widespread misconception about CORS, the one that costs hours: believing that "CORS protects my server". It's false, and understanding why changes everything.

CORS protects the user, not your server. It's a browser rule, not a server-side firewall. Why the user? Because your browser carries your session cookies (recall lesson 6). Without this rule, any shady site you visit could, in the background, fire a fetch at your bank, and since your browser would automatically attach your cookies, read your banking data. CORS stops a page from one origin quietly looting another origin where you're logged in. Proof it isn't a server firewall: curl has no CORS. curl fetches everything, because curl doesn't carry your browser cookies and exposes nobody. Source: MDN, CORS.

Keep the image: CORS is a bouncer posted at the browser's door, not the server's. It doesn't protect the remote server (which handles its own defences), it protects you, by stopping the sites you visit from exploiting your session elsewhere.

The mechanism: simple request or preflight

In practice, the browser has two behaviours depending on the request.

"Simple" request. A basic GET or POST, no exotic header. The browser sends first, then looks at the response. It looks for an Access-Control-Allow-Origin header. If it's there and allows your origin, the result is handed to you. Otherwise the browser did receive the response, but refuses to give it to you and paints the red error.

"Non-simple" request. A PUT, a DELETE, a custom header, or a JSON body: now the browser takes no chances. It first sends a scouting request, the preflight, with the OPTIONS method. It asks the server: "I'd like to do a PUT from this origin, do you allow it?" Only if the server says yes does the browser send the real request. And yes: that's one extra round trip, the thread running through this whole course.

Two CORS scenarios side by side. On the left, simple request: the browser sends a GET, the server replies with the Access-Control-Allow-Origin header, a single round trip. On the right, non-simple request: the browser first sends an OPTIONS (the preflight), the server replies "allowed", then the browser sends the real PUT and receives the response, two round trips. Simple request GET, basic POST Browser Server GET /data 200 + Allow-Origin 1 round trip Non-simple request PUT, DELETE, JSON, custom header Browser Server OPTIONS (preflight) allowed PUT /data (the real one) 200 OK 2 round trips
A non-simple request pays a preflight round trip before the real request even starts.
Predict before reading on

Your fetch to https://api.other-site.com fails with "CORS policy". You rerun the same URL in curl in your terminal: it works, the JSON shows up. What does that prove about the cause of the block?

Show the answer

It proves the server isn't the cause: it replies fine, curl fetches everything. The block comes from the browser, which applies CORS to protect you. curl has no notion of CORS (it carries no browser cookies, exposes nobody), so it blocks nothing. Conclusion: CORS isn't a server firewall, it's a client-side rule. The fix is on the server (return the right Access-Control-Allow-Origin), never by tinkering with your fetch request.

The real fix is three headers

Since CORS is a permission granted by the target server, the fix is necessarily server-side. Three response headers are enough:

Access-Control-Allow-Origin: https://your-site.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

The first says which origins are allowed. The second, which methods. The third, which headers the request may attach. The browser reads those three lines and decides to hand you the response, or not.

The trap that looks clever: setting Access-Control-Allow-Origin: * to "fix" CORS once and for all. The star allows every origin, and with cookie-authenticated requests that's exactly the hole CORS was meant to close. The defensive side (whom to allow, how to handle credentials, the risks of a wildcard) is a security topic in its own right: it's covered in detail in the CORS lesson of the Web Security course. Here, just keep the principle: allow precisely, never blindly.

HTTP/1.1, 2, 3: hunting down round trips

Let's switch topics to close the course's thread: round trips. Everything we've seen (DNS, TLS handshake, request, response) costs round trips. Each HTTP version set out to remove some.

HTTP/1.1. A connection carries only one request at a time. You request the CSS, you wait, then the image, you wait. To hide the slowness, browsers opened up to 6 parallel connections to the same server. Better, but beyond 6 resources it still queued up.

HTTP/2. The big idea: multiplexing. All your requests travel in parallel over ONE connection. No more queue, no need to open 6 pipes. One connection, and everything flows at once.

HTTP/3. We tackle the layer below. Until now, HTTP rode on TCP. HTTP/3 replaces TCP with QUIC, a protocol built on UDP. The benefit is the direct callback to lesson 4 on the TLS handshake: with QUIC, connection setup and the encryption handshake are combined into a single step. Even fewer round trips before the first useful byte. The cherry on top: the connection survives a network change. Switch from wifi to 4G on the subway? Your QUIC connection carries on, without starting over.

Where does HTTP/3 stand in 2026? Find the denominator

You'll bump into numbers that seem to contradict each other. Let's learn to read them together, because it's an excellent reflex of rigour.

  • According to w3techs (June 2026), 39.5% of sites advertise that they can speak HTTP/3.
  • According to Cloudflare Radar, only ~21% of real traffic runs over HTTP/3, and HTTP/2 stays the majority at ~51% of requests.

21% and 39.5%, which one lies? Neither. Both numbers are true. They simply don't measure the same thing: w3techs counts sites that advertise a capability, Cloudflare counts requests actually served. A site can advertise HTTP/3 without most of its traffic taking it. The denominator isn't the same: "per site" versus "per request".

The reflex to keep for life: when two statistics contradict each other, don't pick a side, find the denominator. "40%" of what? Sites, requests, users, bytes? Nine times out of ten, both numbers are exact and measure different populations. Seeing the denominator defuses 90% of false contradictions.

Never write: "HTTP/3 has replaced HTTP/2". It's false. In 2026, HTTP/2 still serves the majority of requests (~51% per Cloudflare Radar). HTTP/3 is growing fast, but all three versions coexist: a single browser negotiates the best available version with each server.

Three lines compared. HTTP/1.1: requests one by one, queued on the connection. HTTP/2: the same requests as parallel arrows over a single pipe (multiplexing). HTTP/3: same parallel arrows, plus a shortened handshake at the start thanks to QUIC. HTTP/1.1 one request at a time req 1 req 2 req 3 … then each waits its turn → HTTP/2 multiplexing: all in parallel a single connection HTTP/3 parallel + combined handshake (QUIC) shortened handshake
From queue to parallel, then to a combined handshake: each version removes round trips.

The full journey, finally understood

We opened the course on a simple question: what happens when you type a URL and hit Enter? Let's replay it, in six lines, every step now clear:

  1. The browser splits the URL (scheme, host, port, path) — lesson 2.
  2. The DNS translates the domain name into an IP address — lesson 3.
  3. A TLS handshake opens an encrypted channel (and HTTP/3 combines it to go faster) — lesson 4.
  4. The browser sends a request, the server returns a response with its status code — lesson 5.
  5. Since HTTP is stateless, cookies carry your session from one request to the next — lesson 6.
  6. The cache avoids asking for everything again, and CORS arbitrates what your page may read elsewhere — lessons 7 and 8.

The course's promise is kept. You no longer just "type" a URL: you know what leaves, where to, and why it takes half a second or ten. The web's network is no longer a black box. It's a sequence of named, measurable steps, now readable to you.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Explique à un collègue pourquoi un fetch bloqué par CORS marche pourtant en curl, et qui CORS protège vraiment.

Une bonne explication dit : CORS est une règle du navigateur, pas un pare-feu serveur. Elle protège l'utilisateur, parce que le navigateur porte ses cookies de session : sans CORS, n'importe quel site visité pourrait lire ses données chez sa banque. curl marche parce qu'il n'a pas de CORS (pas de cookies de navigateur, personne d'exposé). Donc le serveur n'est jamais en cause, et le fix se fait côté serveur avec Access-Control-Allow-Origin.
🧠 Rappel libre
Rappel libre

Sans remonter : qu'apporte HTTP/2 par rapport à 1.1, et qu'ajoute HTTP/3 par-dessus ?

HTTP/1.1 : une requête à la fois par connexion (d'où les 6 connexions parallèles des navigateurs). HTTP/2 : le multiplexage, toutes les requêtes en parallèle sur une seule connexion. HTTP/3 : remplace TCP par QUIC (sur UDP), ce qui combine l'ouverture de connexion et le handshake de chiffrement (moins d'allers-retours) et laisse la connexion survivre au changement de réseau (wifi → 4G).
⚖️ Juge le conseil de l'IA
Accepter ou rejeter le conseil de l'IA

Ton fetch vers une API tierce échoue en CORS. L'IA te dit : « Facile, ajoute le header Access-Control-Allow-Origin: * à TA requête fetch côté client pour contourner l'erreur. » Tu acceptes, ou tu rejettes ?

À rejeter, c'est un non-sens. Access-Control-Allow-Origin est un en-tête de réponse, fixé par le serveur cible : le client ne peut pas s'auto-autoriser. Une requête qui se donne elle-même la permission viderait CORS de tout sens (c'est justement le navigateur qui décide, pas ta requête). Le vrai fix : le serveur tiers doit renvoyer le bon Access-Control-Allow-Origin ; si tu ne contrôles pas ce serveur, passe par un proxy côté backend que TU contrôles.
Qui CORS protège-t-il, et qui l'applique ?
Quel est l'apport central de HTTP/2 par rapport à HTTP/1.1 ?
w3techs dit 39,5 % et Cloudflare Radar dit ~21 % pour HTTP/3. Comment lire ça ?
Ton fetch en PUT vers une API tierce déclenche DEUX requêtes dans l'onglet Network (un OPTIONS puis le PUT). Pourquoi ?
Next step

You now know what travels across the web's network: URL, DNS, TLS, requests, cookies, cache, CORS and HTTP versions. The natural next step: the Web Security course, which teaches you to attack all of it (with a real offensive eye) to defend it better.

Course: Web Security →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement