How to Change Sec-Fetch-Mode (and Other Sec-Fetch Headers)

Reynaldi
Reynaldi •

All Sec-Fetch-* headers are forbidden headers in the browser, meaning the browser controls them and silently ignores your attempts to override them. In this article, you’ll learn how to set them using a server-side proxy, and why the browser blocks them.

The Code

fetch("https://proxy.corsfix.com/?https://example.com/protected-page", {
headers: {
"x-corsfix-headers": JSON.stringify({
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-User": "?1",
}),
},
});

Instead of setting Sec-Fetch-* headers directly (which the browser ignores), you pass them inside x-corsfix-headers. Corsfix applies them server-side before forwarding the request to the target.

For local development, this works instantly without registration. For live websites, set up your domain (takes 30 seconds).

Indirect Option: Set the mode Option in fetch()

The value of Sec-Fetch-Mode is derived from the mode option you pass to fetch(). You can’t set the header directly, but you can influence it:

// Results in Sec-Fetch-Mode: cors
fetch("https://api.example.com/data", {
mode: "cors",
});
// Results in Sec-Fetch-Mode: no-cors
fetch("https://api.example.com/data", {
mode: "no-cors",
});
// Results in Sec-Fetch-Mode: same-origin
fetch("https://api.example.com/data", {
mode: "same-origin",
});

This works for basic cases, but you can only choose from cors, no-cors, and same-origin. You cannot set it to navigate or websocket — those values are assigned by the browser based on how the request was initiated (clicking a link, typing in the address bar, establishing a WebSocket connection, etc.). There is no fetch() option that produces them.

This also only affects Sec-Fetch-Mode. You have no control over Sec-Fetch-Site, Sec-Fetch-Dest, or Sec-Fetch-User through any fetch() option.

Why You Can’t Set Sec-Fetch Headers Directly

You might try setting the header manually:

// This does NOT work
fetch("https://api.example.com/data", {
headers: {
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-Dest": "document",
},
});

The browser won’t throw an error — it silently ignores those headers. Your request goes out with whatever Sec-Fetch-* values the browser decides, not what you specified.

Any header name starting with Sec- is classified as a forbidden request header by the Fetch specification. Forbidden headers can only be set by the browser (the “user agent”), never by JavaScript. The Sec- prefix stands for “secure” — a namespace reserved for headers that the browser guarantees came from itself, not from your code.

The security model of Sec-Fetch-* headers depends on servers being able to trust them. If JavaScript could set Sec-Fetch-Mode: navigate and Sec-Fetch-Site: none, any malicious script could impersonate a user typing a URL into the address bar.

What Each Sec-Fetch-Mode Value Means

The browser assigns one of these values to Sec-Fetch-Mode:

  • cors — The request uses the CORS protocol. This is what fetch() sends by default for cross-origin requests.
  • no-cors — The request skips CORS. Used when loading resources like images, scripts, or stylesheets via HTML tags. The response will be “opaque” (you can’t read its contents from JavaScript).
  • navigate — The user is navigating to a page. Triggered by clicking links, typing in the address bar, submitting forms, or opening bookmarks.
  • same-origin — The request targets the same origin and will fail if the target is a different origin.
  • websocket — The request is initiating a WebSocket connection.

navigate is the value most servers use to identify “real” browser traffic, and it’s the one you cannot produce from fetch().

All Sec-Fetch Headers Are Forbidden

Sec-Fetch-Mode isn’t the only header you can’t control. The browser sends four Sec-Fetch-* headers, and they’re all forbidden:

Sec-Fetch-Mode tells the server the request’s mode (cors, navigate, no-cors, same-origin, websocket).

Sec-Fetch-Site tells the server the relationship between your origin and the target (same-origin, same-site, cross-site, none).

Sec-Fetch-Dest tells the server what type of resource is being requested (document, image, script, empty, and many more).

Sec-Fetch-User tells the server whether the request was triggered by a user action. Its value is always ?1 when present, and it’s omitted entirely for non-user-initiated requests.

Servers that enforce fetch metadata policies check combinations of these headers. A request that looks like a real user navigating to a page has:

Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-Dest: document
Sec-Fetch-User: ?1

A cross-origin fetch() call from JavaScript looks like this:

Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Sec-Fetch-Dest: empty

These two fingerprints are completely different, and there’s nothing you can do from the browser to make the second one look like the first.

Servers That Check Sec-Fetch Headers

Some servers use these headers to reject requests that don’t look like normal browser navigation. Google’s own guidance recommends using Sec-Fetch-* headers to build a “Resource Isolation Policy” that blocks cross-origin requests that aren’t top-level navigations. This defends against CSRF, cross-site script inclusion, and data leaks.

If you’re calling one of these endpoints from frontend JavaScript — a client-side integration, a third-party service, or a scraping target — your requests get rejected. The server sees Sec-Fetch-Site: cross-site and Sec-Fetch-Mode: cors, and blocks you. From the browser, you can’t change these headers. A server-side proxy like Corsfix can.

Other Ways to Override Sec-Fetch Headers (Dev and Testing)

If you’re not building for production and just need to test or debug, there are simpler options.

Browser extensions like ModHeader or Requestly can intercept outgoing requests and override any header, including forbidden ones. They work at the browser level, below the Fetch API’s restrictions. This is great for testing how a server responds to different Sec-Fetch-* values without writing any code.

curl and Postman don’t have any concept of forbidden headers. You can set whatever you want:

Terminal window
curl "https://example.com/protected-page" \
-H "Sec-Fetch-Mode: navigate" \
-H "Sec-Fetch-Site: none" \
-H "Sec-Fetch-Dest: document" \
-H "Sec-Fetch-User: ?1"

Server-side code (Node.js, Python, Go, etc.) also has no forbidden header restrictions. In fact, server-side HTTP clients don’t even add Sec-Fetch-* headers by default — they’re a browser-only feature. You can set them manually if the target server requires them, or simply omit them (many servers allow requests that don’t include fetch metadata at all, since they need to support older browsers and non-browser clients).

These approaches all work for development and testing, but they don’t help when you need to make requests from client-side JavaScript running in a real user’s browser.

Conclusion

Use a server-side proxy like Corsfix to change Sec-Fetch-Mode and other Sec-Fetch-* headers from JavaScript. Pass the headers you need inside x-corsfix-headers, and Corsfix sets them on the outbound request before forwarding it to the target.

Corsfix handles Sec-Fetch-* overrides along with other forbidden headers server-side, so you can focus on building your app instead of managing proxy infrastructure.

It's time to build great websites without CORS errors

Try our CORS proxy for free, all features included.

Fix CORS errorsNo credit card required.