If you work on the backend, you deal with HTTP every day, yet few people can answer “why is HTTP designed this way?”. This note starts from a few core principles and uses them to explain messages, methods, status codes and caching — because most of these follow directly from those principles. CORS comes last: it is not part of HTTP, but a browser rule built on top of it.
Core principles of HTTP
HTTP (Hypertext Transfer Protocol) is the protocol clients and servers use to exchange data on the web. The whole protocol revolves around the six principles below.
Client and server
HTTP has only two roles: the client sends a request, and the server processes it and returns a result.
A client is not just a browser. A mobile app, curl, Postman, or one backend service calling another — all of these are clients. The key point to remember: the server does not know, and does not need to know, who is calling. It relies only on what is inside the request.
Request and response
Every exchange in HTTP follows one pattern: the client sends one request, and the server returns exactly one response.
This pattern has two consequences worth knowing:
- The client is always the one that sends the request. The server only sends data as the response to a request.
- Every request gets a response. Even when processing fails, the server still returns a response that reports the error.
So what if the server wants to push data to the client on its own — a real-time notification, for example? Plain HTTP cannot do that, and that is where WebSockets and similar techniques come in. They are covered in the WebSockets note.
Statelessness
HTTP is stateless: the server remembers nothing about earlier requests. Each request must carry everything the server needs to process it.
This may sound like a limitation, but it is one of the most important design decisions in HTTP. Look at its two consequences:
- Any instance can handle any request. Because the server keeps no state between requests, you can run ten identical instances behind a load balancer, and a request can land on any of them. This is the foundation of horizontal scaling.
- The client must resend its context with every request. For example, the server does not “remember” that you logged in. On every request, the client sends a cookie or a token to prove who it is. The authentication note goes deeper into this.
Note: stateless does not mean the system stores nothing. The database still stores users and orders as usual. Stateless only means that the protocol itself keeps no state between requests.
Self-descriptive messages
This principle follows directly from statelessness. Since the server remembers nothing, each message must say everything about itself. Besides the data, a message carries a description of that data: is it JSON or HTML, how many bytes long, what language, can it be cached. A message like this is called self-descriptive.
All of that descriptive information lives in headers — we will come back to them when we look at the structure of a message.
Intermediaries
In practice, a request rarely goes straight from the client to the server. On the way, it usually passes through a few intermediaries, such as a proxy, a load balancer, an API gateway or a CDN.
The diagram below shows the path of one call: the request goes from the client, through the intermediaries, to the server; the response travels back along the same path.
flowchart LR
C["Client<br/>browser, app, service"] -->|request| I["Intermediaries<br/>proxy, load balancer, CDN"]
I -->|request| S["Server"]
S -.->|response| I
I -.->|response| C
classDef main fill:#dbeafe,stroke:#1e40af,color:#1e3a8a
classDef infra fill:#f4f4f5,stroke:#52525b,color:#27272a
class C,S main
class I infra
Here is an interesting question: these intermediaries know nothing about your application’s logic, so how do they do the right thing? The answer is the self-descriptive principle. Everything they need is already in the message. A CDN that sees Cache-Control: max-age=600 on a response knows it may cache it for 10 minutes, without asking the server. A load balancer reads the Host header and knows which service should get the request.
For this to work, every party on the path must understand methods, status codes and headers in the same way. That is why HTTP standardizes their meaning in RFCs.
Extensibility
HTTP dates from the early 1990s, yet new headers and new status codes are still being added today. What is remarkable is that these additions do not break software that was already running. In other words, HTTP was designed from the start to be extensible and backward compatible.
The trick is simple: software that meets a header it does not understand just ignores it instead of failing. Thanks to this rule, CORS — a mechanism that came much later than HTTP — could be added with just a few new headers, without changing the protocol.
How the principles fit together
Looking at all six principles together, you can see they do not stand alone — they depend on each other:
| Principle | In simple terms | What it makes possible |
|---|---|---|
| Client–server | Two clear roles | One server can serve many kinds of clients |
| Request–response | Each request gets exactly one response | Simple, predictable exchanges |
| Stateless | Each request carries all its information | Scaling by adding more instances |
| Self-descriptive | A message describes its own content | Caches and proxies work without asking the app |
| Intermediaries | Everyone reads methods, codes, headers the same way | Load balancers, CDNs and gateways in between |
| Extensible | Unknown parts are ignored | New features without breaking old ones |
Where HTTP sits in the network
A common misunderstanding: HTTP does not move data across the network by itself. It only defines what a message means. Getting each byte from one machine to another is the job of the layers below it.
| Layer | Responsible for | Examples |
|---|---|---|
| Application | The meaning of messages | HTTP |
| Security | Encrypting data, verifying the server | TLS |
| Transport | Delivering data completely and in order | TCP, QUIC |
| Network | Routing packets between machines | IP |
This separation is what lets the meaning of HTTP be defined once, in RFC 9110. HTTP/1.1, HTTP/2 and HTTP/3 share the same methods, status codes and headers; they only differ in how they carry messages over the network. The end of this note compares the three versions.
The structure of an HTTP message
Back to the self-descriptive principle: for a message to describe itself, it needs a fixed structure that every party can read. Every request and every response has four parts:
- Start line: the first line, saying what the request is or how it turned out.
- Headers: information that describes the message.
- An empty line: signals that the headers are over.
- Body: the main data, which may be absent.
The request
POST /orders HTTP/1.1
Host: api.shop.test
Content-Type: application/json
Accept: application/json
Content-Length: 25
{"sku": "A-42", "qty": 1}
The start line of a request is called the request line. It has three parts:
| Part | In the example | Meaning |
|---|---|---|
| Method | POST | What the client wants to do |
| Target | /orders | Which resource to do it on |
| Version | HTTP/1.1 | Which format the rest of the message follows |
Notice the Host header: it tells the server which domain the request is for. HTTP/1.1 requires it, because one server often runs many domains on the same IP address.
The response
HTTP/1.1 201 Created
Content-Type: application/json
Location: /orders/9001
Content-Length: 33
{"id": 9001, "status": "pending"}
The start line of a response is called the status line: the version, a three-digit status code and a short reason phrase. Your code should act on the status code; the reason phrase is only there to make logs easier for humans to read.
Headers
Headers are name–value pairs, and their names are not case-sensitive. There are many headers, but grouped by purpose there are only a few kinds:
| Group | Answers the question | Examples |
|---|---|---|
| Request information | Who is calling, and calling what? | Host, User-Agent, Authorization |
| Content negotiation | Which forms of data can the client accept? | Accept, Accept-Language, Accept-Encoding |
| Representation | What format is the body, and how long is it? | Content-Type, Content-Length, Content-Encoding |
| Caching | Can it be cached, and for how long? | Cache-Control, ETag, If-None-Match, Vary |
| Response information | What else does the server want to tell you? | Location, Retry-After, Allow |
The body and its length
A question that sounds simple: how does the receiver know where the body ends? HTTP/1.1 has two ways:
Content-Length: states how many bytes the body has. The receiver reads exactly that many bytes and stops.Transfer-Encoding: chunked: sends the body in chunks, each with its own size, ending with an empty chunk. This is used when the server starts sending before it knows the total size — for example, when streaming a large export.
With HTTP/2 and HTTP/3, messages are split into frames by the protocol itself, so these headers are not needed to find the end of the body.
Content negotiation
The same resource can be returned in several forms: JSON or XML, English or Vietnamese, compressed or not. Each of these forms is a representation. Content negotiation is how the client and the server agree on which one to send.
It works in a very intuitive way: the client says which forms it accepts, and the server picks one and states its choice in the response.
| The client sends | The server answers with |
|---|---|
Accept: application/json | Content-Type: application/json |
Accept-Language: vi, en;q=0.8 | Content-Language: vi |
Accept-Encoding: gzip, br | Content-Encoding: br |
The q value is a priority from 0 to 1. In the example above, the client prefers Vietnamese, and English is fine as a second choice.
Methods
The first word of the request line is the method. The method tells the server what the client wants to do with the resource.
Two properties: safe and idempotent
When you learn about methods, do not just memorize their names. What matters more are two properties the HTTP standard defines for them:
- Safe: the method only reads. It does not ask the server to change any data.
- Idempotent: calling it once or calling it many times leaves the server in the same final state.
It is easy to see that a safe method is always idempotent: if it changes nothing, calling it again changes nothing either.
The common methods
| Method | Used to | Safe | Idempotent |
|---|---|---|---|
GET | Read a resource | yes | yes |
HEAD | Like GET, but headers only | yes | yes |
OPTIONS | Ask the server what it supports | yes | yes |
POST | Create a new resource or run an action | no | no |
PUT | Replace the whole resource at a specific URL | no | yes |
PATCH | Change part of a resource | no | not guaranteed |
DELETE | Delete a resource | no | yes |
PATCH is defined in RFC 5789; the other methods are in RFC 9110.
Note: idempotent is about the end state on the server, not about the response. Call
DELETEtwice and you may get204the first time and404the second — different responses, but the same end state on the server: the resource is gone.
Why these properties matter
These properties are not just theory. They tell everyone on the path which requests may be sent again automatically.
- With a safe method, a browser can prefetch a page and a cache can store the response, without any risk of changing data.
- With an idempotent method, the client can retry freely. Imagine the connection drops before the response arrives: the client cannot tell whether the server processed the request. If the method is idempotent, it can simply send it again.
POSThas neither property. Retrying aPOSTmay create a second record. To make retries safe, we use an idempotency key: the client sends a unique value with the request, and the server uses it to recognize a repeated request. The REST API design note covers this in detail.
Status codes
Every request gets a response, and the status code is the quickest way to know the outcome. It is a three-digit number; the first digit tells you which class it belongs to.
The five classes
| Class | Meaning | What the client should do |
|---|---|---|
1xx | Informational | Keep waiting |
2xx | Success | Nothing more |
3xx | Redirection | Call a different address |
4xx | Client error | Fix the request before sending it again |
5xx | Server error | Wait for the server to recover; a retry may work |
Of the five classes, the line between 4xx and 5xx is the one you most need to understand, because it is a line of responsibility. A 4xx means the problem is in the request — sending it again unchanged will not help. A 5xx means the problem is on the server — trying again later may succeed. Retry logic and monitoring both depend on this line. Return a 500 by mistake for a request with bad data, and you may page your whole on-call team for nothing.
Common status codes
| Code | Name | Meaning |
|---|---|---|
200 | OK | Success, with a body |
201 | Created | A new resource was created; its URL is in Location |
202 | Accepted | The request was received and will be processed later |
204 | No Content | Success, no body |
301 | Moved Permanently | The resource has moved to a new URL for good |
302 | Found | The resource is temporarily at another URL |
304 | Not Modified | The client’s cached copy is still usable |
307 | Temporary Redirect | Like 302, but the method and body must stay the same |
308 | Permanent Redirect | Like 301, but the method and body must stay the same |
400 | Bad Request | The request is malformed |
401 | Unauthorized | Not authenticated |
403 | Forbidden | Authenticated, but not allowed |
404 | Not Found | The resource was not found |
405 | Method Not Allowed | The resource does not support this method |
409 | Conflict | The request conflicts with the resource’s current state |
415 | Unsupported Media Type | The server does not accept this body format |
422 | Unprocessable Content | The format is valid, but the data is not |
429 | Too Many Requests | Too many requests; the client is rate limited |
500 | Internal Server Error | The server hit an unexpected error |
502 | Bad Gateway | A proxy got an invalid response from the server behind it |
503 | Service Unavailable | The server is temporarily unable to serve requests |
504 | Gateway Timeout | A proxy waited too long for the server behind it |
Note: this is where many people get confused. Despite its name,
401 Unauthorizedactually means not authenticated — the server does not know who you are yet.403means the server knows who you are, but you are not allowed to do this.
Caching
Now you can clearly see why the self-descriptive and intermediary principles matter: they are what give HTTP a built-in caching mechanism. A cache stores a response and reuses it for later requests, so the server does not have to process them again from scratch.
To cache correctly, two questions must be answered. First, how long can this response be reused? Second, once it expires, has the data actually changed? HTTP answers them with two matching mechanisms: freshness and validation.
Freshness
The server says how long a response can be reused through the Cache-Control header.
| Directive | Meaning |
|---|---|
max-age=600 | Can be reused for 600 seconds |
no-cache | May be stored, but must be checked with the server before each use |
no-store | Must not be stored |
private | Only the user’s browser may store it, not a CDN |
public | Shared caches such as a CDN may store it too |
Note: the name
no-cacheis misleading. It does not mean “do not cache”; it means “you may store it, but check with the server before using it”. To forbid storing completely, you needno-store.
Validation
When a cached copy has expired (it is stale), the cache does not have to download everything again. It only needs to ask the server one question: “has this changed?”
To make that question possible, the server gives each version of a response an ETag — a short code that stands for exactly that content. Next time, the cache sends this code back in the If-None-Match header. If the data has not changed, the server just returns 304 Not Modified with no body, and the cache keeps using its copy. For a large response, this saves a lot of bandwidth.
Responses that vary
There is one detail that is easy to miss. Because of content negotiation, the same URL can return different representations. A cache must never give the Vietnamese version to someone who asked for English. The Vary header solves this: it tells the cache which request headers the response depends on, for example Vary: Accept-Language. The cache then stores a separate copy for each value of that header.
Server-side caching is a big topic of its own and is covered in the caching note.
HTTP versions
As mentioned earlier, the meaning of HTTP is the same in every version. What changes from version to version is how messages travel over the network — specifically, how several requests share one connection.
HTTP/1.1
HTTP/1.1 sends messages as text over TCP. A connection can be reused for many requests, but it handles only one request at a time. The next request has to wait until the previous response has fully arrived.
This problem is called head-of-line blocking: a single slow response makes every request queued behind it wait as well. Browsers work around it by opening several connections to the same server in parallel — a workaround rather than a real fix.
HTTP/2
HTTP/2 solves this by splitting messages into binary frames. Frames from many requests are interleaved on the same connection, and each request belongs to its own stream. As a result, many requests run in parallel over a single connection. HTTP/2 also compresses headers to reduce the amount of data sent.
However, head-of-line blocking has not completely disappeared — it has only moved down a layer. All streams still share one TCP connection, and TCP requires data to arrive in order. If one packet is lost, every stream has to wait until that packet is sent again.
HTTP/3
HTTP/3 fixes the remaining problem by dropping TCP and moving to QUIC — a transport protocol built on UDP. QUIC handles each stream independently, so a lost packet only slows down the stream it belongs to. QUIC also combines the connection handshake with the TLS handshake, so a new connection is ready sooner.
Comparison
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Format | Text | Binary frames | Binary frames |
| Requests on one connection | One after another | In parallel | In parallel |
| Head-of-line blocking | At the HTTP level | At the TCP level | Only within one stream |
| Specification | RFC 9112 | RFC 9113 | RFC 9114 |
Origins and the same-origin policy
Everything we have covered so far is true for any HTTP client. HTTP itself has no rule about who may read a response. A browser, however, needs such a rule. The reason is that a browser runs code from many different websites in one place, and it holds your login cookies for all of those websites. To understand this rule, we first need to know what an origin is.
Origin
The origin of a URL is made of three parts: scheme, host and port (RFC 6454). Two URLs have the same origin only when all three parts are the same.
| URL | Same origin as https://shop.test? | Reason |
|---|---|---|
https://shop.test/cart | yes | The path is not part of the origin |
http://shop.test | no | Different scheme |
https://api.shop.test | no | Different host |
https://shop.test:8443 | no | Different port |
One consequence every developer has run into: a frontend on localhost:5173 and an API on localhost:8080 are two different origins, even on the same machine.
The same-origin policy
The same-origin policy is a browser rule: JavaScript on a page may send a request to another origin, but may not read the response unless that other origin allows it.
Why block reading? The answer is cookies. When page A sends a request to origin B, the browser automatically attaches your cookies for B. If JavaScript on page A could read the response, any website could read your personal data on every other website where you are logged in — your email, your bank account, everything.
Sending requests to another origin, on the other hand, has always been allowed, because the web needs it to work: <img> tags, forms and scripts loaded from a CDN are all cross-origin requests. The only thing blocked is reading the response.
CORS
The same-origin policy protects users very well, but it is also very strict. In real projects, a frontend on one origin often needs to read data from an API on another. CORS was created to open an exception for these cases — in a controlled way.
CORS (Cross-Origin Resource Sharing) is a mechanism, defined in the Fetch Standard, that lets a server tell the browser which origins may read its responses. And here you can see the extensibility principle from the start of this note at work: CORS runs entirely on a few new HTTP headers.
The diagram below summarizes how a browser handles a cross-origin request. The sections that follow explain each step.
flowchart TD
A["Page calls another origin"] --> B{"Simple request?"}
B -->|yes| C["Send request with Origin"]
B -->|no| D["Send OPTIONS preflight"]
D --> E{"Server allows<br/>method and headers?"}
E -->|no| F["Stop: real request<br/>is not sent"]
E -->|yes| C
C --> G{"Response allows<br/>this origin?"}
G -->|yes| H["JavaScript can read the response"]
G -->|no| X["JavaScript cannot<br/>read the response"]
classDef main fill:#dbeafe,stroke:#1e40af,color:#1e3a8a
classDef infra fill:#f4f4f5,stroke:#52525b,color:#27272a
classDef ok fill:#dcfce7,stroke:#166534,color:#14532d
classDef bad fill:#fee2e2,stroke:#991b1b,color:#7f1d1d
class B,E,G main
class A,C,D infra
class H ok
class F,X bad
What CORS is and is not
Before going into details, one thing needs to be clear: CORS can only relax the same-origin policy, never make it stricter. If the server sends no CORS headers, the browser applies its default restriction — which is the strictest one.
From this come two points that many developers get wrong:
- CORS protects users, not servers. It stops an unknown website from reading data the browser fetched with the user’s cookies.
- Only browsers apply CORS.
curl, Postman or another backend service ignore CORS completely. So never treat CORS as a security layer for your API. Protecting the API is the job of authentication and authorization.
Simple requests
Some cross-origin requests could already be sent by HTML forms before CORS existed. Since the web already allowed them, the browser sends these requests directly, without asking first. They are called simple requests.
A request is simple when it meets all three conditions:
- The method is
GET,HEADorPOST. - It only uses headers from the safelisted set, such as
AcceptorContent-Language. - If it has a body, the
Content-Typeisapplication/x-www-form-urlencoded,multipart/form-dataortext/plain.
For a simple request, the browser adds the Origin header, sends it, and only then checks the CORS headers in the response. Pay attention to this point: if the origin is not allowed, JavaScript cannot read the response — but the server has already received and processed the request.
Preflight requests
The remaining requests can do things an HTML form never could, such as sending JSON or custom headers. For these, the browser is more careful: it asks the server before sending. That asking request is called the preflight.
The preflight is an OPTIONS request that tells the server which method and headers the real request will use. The server answers whether they are allowed. Only if they are does the browser send the real request.
In practice, most API calls need a preflight, because any one of these is enough:
- The body is JSON (
Content-Type: application/json). - There is an
Authorizationheader or another custom header. - The method is
PUT,PATCHorDELETE.
sequenceDiagram
participant B as Browser (page on app.shop.test)
participant S as Server (api.shop.test)
B->>S: OPTIONS /orders/9001<br/>Origin: https://app.shop.test<br/>Access-Control-Request-Method: PUT<br/>Access-Control-Request-Headers: content-type
S-->>B: 204 No Content<br/>Access-Control-Allow-Origin: https://app.shop.test<br/>Access-Control-Allow-Methods: PUT<br/>Access-Control-Allow-Headers: content-type
Note over B: The server allowed it
B->>S: PUT /orders/9001
S-->>B: 200 OK<br/>Access-Control-Allow-Origin: https://app.shop.test
If the preflight is refused, the real request is never sent.
| Simple request | Preflighted request | |
|---|---|---|
| When | Requests an HTML form could send | All other cases |
| Calls to the server | 1 | 2 (1 if the preflight result is cached) |
| If not allowed | The server still processes it; JavaScript cannot read it | The real request is not sent |
The CORS headers
| Header | Sent by | Meaning |
|---|---|---|
Origin | Browser | The origin of the calling page |
Access-Control-Request-Method | Browser, in a preflight | The method of the real request |
Access-Control-Request-Headers | Browser, in a preflight | The non-safelisted headers the real request will use |
Access-Control-Allow-Origin | Server | The origin allowed to read the response, or * for any |
Access-Control-Allow-Methods | Server, in a preflight | The allowed methods |
Access-Control-Allow-Headers | Server, in a preflight | The allowed request headers |
Access-Control-Allow-Credentials | Server | Allows reading the response when cookies are sent |
Access-Control-Expose-Headers | Server | Extra response headers JavaScript may read |
Access-Control-Max-Age | Server, in a preflight | How many seconds the browser may cache the preflight result |
Credentials
In CORS, credentials are what the browser attaches to a request automatically: cookies, HTTP authentication and client certificates. Be careful with the distinction: a token that your JavaScript puts in the Authorization header does not count as a credential in this sense.
Because credentials open the door to a user’s private data, the rules for them are much stricter:
- The page must turn them on explicitly, for example
fetch(url, { credentials: "include" }). - The server must answer with
Access-Control-Allow-Credentials: true. Access-Control-Allow-Originmust name one specific origin;*is not allowed.
A question that comes up often is “is it OK to use *?”. The answer depends on how the API identifies users:
| The API identifies users with | Can it use Access-Control-Allow-Origin: *? |
|---|---|
| Nothing (public data) | Yes |
A token in the Authorization header | Yes |
| Cookies | No — name the exact origin |
Allowing several origins
Access-Control-Allow-Origin can hold only one origin. So what if you have several frontends? The server keeps a list of allowed origins. For each request, it compares the Origin header with the list and, if it matches, returns that exact origin.
Now the response changes depending on the request’s Origin header. Remember the caching section: a response that varies by a request header must declare Vary: Origin. If you forget it, a CDN may store the response meant for origin A and serve it to origin B by mistake — a bug that is very hard to track down.
Configuring CORS on a server
At this point, the rules above translate directly into code. The example below uses plain Node.js without a framework, so you can see every header.
import { createServer } from "node:http";
const ALLOWED_ORIGINS = new Set([
"https://app.shop.test",
"http://localhost:5173",
]);
createServer((req, res) => {
const origin = req.headers.origin;
// The answer depends on the Origin header, so caches must keep them apart.
res.setHeader("Vary", "Origin");
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
// Needed only if the API identifies users with cookies.
res.setHeader("Access-Control-Allow-Credentials", "true");
}
// Preflight: answer before any authentication, without a body.
if (
req.method === "OPTIONS" &&
req.headers["access-control-request-method"]
) {
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
res.setHeader("Access-Control-Max-Age", "600");
res.writeHead(204).end();
return;
}
// Authentication and the real request handling go here.
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
}).listen(8080);
Each part of the code matches a rule we have already covered:
| In the code | Why |
|---|---|
Vary: Origin | A response that varies by a request header must tell caches |
| An allowlist with exact matching | With cookies, Allow-Origin must be one specific origin |
| No CORS headers for unknown origins | Without the headers, the browser keeps its default restriction |
| Preflight handled before authentication | The browser never sends cookies or tokens with a preflight |
Access-Control-Max-Age | The browser does not have to send a preflight for every request |
Reading a CORS error
When you hit a CORS error, remember that it is reported by the browser, not by the server. Open the browser’s Network tab and look at the matching request; you will usually see the cause right away:
| The Network tab shows | Cause |
|---|---|
The OPTIONS request gets 401 or 403 | The preflight was blocked by authentication, but it carries no cookie or token |
OPTIONS succeeds, but the real request is never sent | The method or a header is missing from Allow-Methods / Allow-Headers |
The response has no Access-Control-Allow-Origin | The origin is not allowed, or this route does not return CORS headers |
Access-Control-Allow-Origin: * while using cookies | With cookies, you must name the exact origin |
Summary
- HTTP rests on six principles: client–server, request–response, statelessness, self-descriptive messages, intermediaries and extensibility. The rest of the protocol grows out of them.
- A message has a start line, headers, an empty line and an optional body. Headers describe the message so that every party on the path can understand it.
- For methods, the two properties to remember are: safe means read-only, and idempotent means calling it again leaves the server in the same state.
- The status code tells the client what to do next:
4xxmeans fix the request,5xxmeans the server failed and a retry may work later. - Caching relies on freshness (
Cache-Control) and validation (ETag,304).Varykeeps the cache from serving the wrong representation. - HTTP/1.1, HTTP/2 and HTTP/3 share the same meaning and only differ in how they carry messages over the network.
- The same-origin policy is a browser rule: it blocks reading cross-origin responses, not sending requests.
- CORS lets a server open exceptions for chosen origins. It protects users, not servers, and only browsers apply it.