The Browser Does Not Give JavaScript Full Control over HTTP
- date
- category
- Frontend
- also in
- Application Security · Backend
- reading
- 2 min / 370 words
fetch() looks like a request generator:
fetch(url, options);
But the frontend does not get a raw HTTP socket.
It gets a controlled browser interface.
The browser adds state of its own
This code:
fetch("/api/me");
can lead to a request carrying cookies, negotiation headers and other fields set by the user agent.
JavaScript does not have to provide them explicitly.
So the model is:
application intent
|
v
Fetch API
|
v
browser policy + browser state
|
v
HTTP request
Not every header is ours to set
The browser blocks or controls some headers, because JavaScript running on a page cannot be granted the full ability to impersonate the user agent or interfere with the transport.
That is why a request from a browser is not the equivalent of:
curl ...
curl is an HTTP client controlled by the user.
Page code runs inside the browser's security model.
That is a fundamental difference.
Cookies are not an ordinary application header either
The frontend should not build this:
headers: {
Cookie: "session=..."
}
The browser manages the cookie store and decides which cookies qualify for a given request.
Fetch has an option:
fetch(url, {
credentials: "same-origin"
});
By default credentials are attached for same-origin requests.
We can also use:
credentials: "omit"
credentials: "include"
But even include does not bypass rules such as SameSite.
Cross-origin can add a request we never wrote
This code:
fetch("https://api.example.net/users", {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: "{}"
});
can cause an earlier request:
OPTIONS /users
that is, a CORS preflight.
The frontend wrote one fetch().
The Network panel shows two requests.
Another example of:
JavaScript operation
!=
one network request
CORS gets its own treatment in the security series. What matters here is the model: the browser can introduce an extra step before the actual request.
The cache also sits between the code and the network
This code:
await fetch("/logo.svg");
does not guarantee that any data travels over the network.
The browser may hold a fresh response in its cache.
From the application's point of view we still performed a fetch.
From the origin server's point of view the request may never arrive.
fetch()
|
v
browser cache?
├── hit -> Response
└── miss -> network
Caching deserves a series of its own. Here one thing matters: fetch() is a platform operation, not a direct call to the network.
The same goes for connections
Even when the request does reach the network, the browser does not have to create a new connection for it.
It can reuse an existing one.
And connection reuse is exactly what makes the differences between HTTP/1.1, HTTP/2 and HTTP/3 understandable.