Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the headers option when calling fetch() in a web page: fetch(url, { headers: { "X-Client-Version": "1.2.3" } }). This JavaScript runs in a browser, not in Node.js merely because the project uses Node tooling. Browser code can set application headers such as X-Request-Id and, in ordinary cases, Authorization, but the browser controls fields such as Cookie, Host, and Origin. A cross-origin custom header can also cause an OPTIONS CORS preflight that the API must approve.

First identify where the request runs

“Node.js browser request” combines two different environments. A script bundled by a Node-based build tool and executed by a web page is browser code. It is governed by the browser’s CORS policy and forbidden-header rules. A script executed by a Node.js process is server-side JavaScript and uses Node’s networking APIs without the browser’s page security model.

The examples below label the runtime explicitly. Start with browser fetch(), then see XMLHttpRequest and Node.js alternatives.

Add headers with browser fetch()

Pass either a plain object or a Headers instance in the second argument to fetch(). The following GET request sends two application headers and checks both network success and the HTTP status:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const response = await fetch("https://api.example.com/items", {
  method: "GET",
  headers: {
    "X-Client-Version": "1.2.3",
    "Authorization": "Bearer YOUR_TOKEN",
  },
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const data = await response.json();
console.log(data);

fetch() resolves to a response even for statuses such as 401 or 500, so test response.ok (or inspect response.status) before parsing a successful payload. Network failures and a rejected CORS request reject the promise instead.

POST JSON with custom headers

When sending JSON, declare the media type and serialize the body yourself:

const response = await fetch("https://api.example.com/items", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Request-Id": "abc123",
  },
  body: JSON.stringify({ name: "Example" }),
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const created = await response.json();

A JSON content type and most non-simple custom headers commonly make a cross-origin request non-simple, which leads to a preflight described below.

Build and update a Headers object

A Headers object is useful when conditional code adds or replaces values. Header names are normalized and surrounding whitespace in values is trimmed:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const headers = new Headers();
headers.set("X-Client-Version", "1.2.3");
headers.set("Authorization", "Bearer YOUR_TOKEN");

const response = await fetch("https://api.example.com/items", { headers });

Use headers.set() to replace a value, headers.append() when the protocol intentionally permits multiple values, and headers.has() or headers.get() for inspection.

Set headers with XMLHttpRequest

XMLHttpRequest uses a sequence rather than one options object. Call setRequestHeader() only after open() and before send():

const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/items");
xhr.setRequestHeader("X-Client-Version", "1.2.3");
xhr.setRequestHeader("Authorization", "Bearer YOUR_TOKEN");

xhr.onload = () => {
  if (xhr.status < 200 || xhr.status >= 300) {
    console.error(`HTTP ${xhr.status}`);
    return;
  }
  const data = JSON.parse(xhr.responseText);
  console.log(data);
};
xhr.onerror = () => console.error("Network or CORS failure");
xhr.send();

Calling setRequestHeader() repeatedly with the same name appends values rather than silently replacing the earlier value. XHR and Fetch share the same browser restrictions and CORS enforcement; changing APIs does not bypass them.

Why the browser removes or rejects a custom header

Forbidden request headers

Page JavaScript cannot freely control every HTTP field. The browser manages security- and transport-sensitive headers, including Cookie, Host, Origin, Content-Length, Connection, and names beginning with Sec-. Attempts to set such fields are blocked or ignored. There is no alternate spelling or Fetch syntax that grants page code control.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Cookies are attached according to cookie attributes and the request’s credential mode, not by constructing a Cookie header. The browser generates Origin from the requesting context. Likewise, connection framing and content length are calculated by the user agent.

Authorization is allowed, but protect it

An Authorization header can normally be set from browser JavaScript. Treat bearer tokens as secrets: avoid exposing long-lived credentials in URLs, logs, or publicly shipped source, and send them only to the intended origin. If a request is redirected cross-origin, XMLHttpRequest documentation notes that the authorization value may be removed.

How CORS preflight affects custom headers

CORS is enforced by the browser and configured by the server that owns the target resource. A cross-origin request that is not “simple”—for example, one using many custom headers, JSON content type, or a non-simple method—causes the browser to send an OPTIONS preflight first.

What the preflight contains

The browser identifies the requesting origin and asks whether the method and intended headers are permitted. The API must answer with matching CORS response headers, including an Access-Control-Allow-Headers value that lists your custom header (for example, X-Client-Version), plus an allowed method and origin. If the preflight fails, the actual GET or POST is never sent.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What to configure on your server

If you control the API, allow the exact frontend origin, method, and headers your application uses. For credentialed requests, the server must explicitly allow the requesting origin and credentials; a wildcard origin is not valid with credentials. Cookies remain subject to browser cookie policy.

If you do not control the API, changing client-side JavaScript cannot grant permission. Use a server-side proxy that you control, or ask the API owner to update its CORS policy.

Why no-cors is not a fix

mode: "no-cors" is not a workaround for an API that requires custom headers or a readable response. It restricts methods and headers and returns an opaque response whose body and headers are unavailable to JavaScript. You cannot use it to read JSON or verify an API status.

Browser fetch versus XMLHttpRequest

Concern fetch() XMLHttpRequest
Configuration One options object containing headers, method, body, and credentials Call open(), then setRequestHeader(), then send()
Control flow Promise and async/await Load, error, progress, and timeout event handlers
Response handling Read with json(), text(), or another body method Read responseText, responseXML, or configured response types
Browser restrictions Forbidden headers and CORS apply The same forbidden-header and CORS rules apply

Fetch is the modern Promise-based interface for new code. XHR remains useful when an existing application depends on its event model or progress callbacks.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What changes in a Node.js process

Code running in Node.js is not a page request. Node.js documents global fetch as available from v18.0.0 and the global Headers class as no longer experimental from v21.0.0. The browser’s page CORS enforcement and forbidden request-header list should not be assumed to behave identically in Node; use the current Node.js documentation for the HTTP client and version you deploy.

Node.js global fetch example

const response = await fetch("https://api.example.com/items", {
  headers: {
    "X-Client-Version": "1.2.3",
    "Authorization": "Bearer YOUR_TOKEN",
  },
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const data = await response.json();
console.log(data);

This code belongs in a Node.js process (for example, a server, worker, or command-line script), not in a browser page. Keep secrets here rather than embedding them in frontend bundles whenever the architecture permits.

Equivalent command-line and Python requests

curl https://api.example.com/items 
  -H 'X-Client-Version: 1.2.3' 
  -H 'Authorization: Bearer YOUR_TOKEN'
import requests

response = requests.get(
    "https://api.example.com/items",
    headers={
        "X-Client-Version": "1.2.3",
        "Authorization": "Bearer YOUR_TOKEN",
    },
    timeout=30,
)
response.raise_for_status()
print(response.json())

These server-side examples can send headers that a browser page cannot. That difference is why a request succeeding in curl or Python does not prove that frontend Fetch will succeed: the browser may preflight it or block a forbidden field.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Debugging checklist

The header is absent in DevTools

  • Confirm the code runs after the Fetch call is constructed and that the header name is spelled correctly.
  • Check whether the name is browser-forbidden, such as Origin, Cookie, or Host. Remove it and use the browser’s supported mechanism instead.
  • Inspect the actual request and any preceding OPTIONS request in the Network panel. A failed preflight means there may be no actual request to inspect.

The console reports a CORS error

  • Look at the preflight response and configure the API to allow the exact origin, method, and custom header.
  • Do not try no-cors if your code needs a response body.
  • If the API is third-party and cannot be configured, call it from your own backend and have the browser call that backend.

The server returns 401 or 403

  • Verify the token scheme, spelling, and expiry; Authorization usually needs the expected Bearer format.
  • Check that a redirect did not move the request to another origin and remove authorization.
  • Inspect server authentication logs rather than assuming a browser header was accepted.

The request fails only after adding JSON

Content-Type: application/json can make a cross-origin request require preflight. Add that content type to the server’s allowed headers and ensure OPTIONS is routed without authentication that rejects the preflight.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

If your goal is a clean image or PDF of a URL rather than an API call from page JavaScript, ScreenshotNeo provides a single HTTP request and an MCP server for AI agents. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.

Use the API documentation at https://screenshotneo.com/docs/ for options such as custom headers, cookies, user agents, waits, blocking rules, full-page capture, PDF output, and signed links. A minimal call is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

There is also an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Sign up for ScreenshotNeo’s free plan.

FAQ

Can JavaScript set the User-Agent header?

Browser page code cannot freely replace browser-controlled networking fields. Set a user agent only in a server-side client or a controlled browser automation environment.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does adding a custom header always trigger preflight?

No. The browser evaluates the complete method, header set, and content type against the CORS “simple request” rules. Headers outside that safelist commonly require preflight.

Should I put an API key in a frontend header?

Only when the API is designed for a public client credential and its exposure is acceptable. A secret key shipped to a browser can be copied; keep privileged credentials on a server.

Frequently Asked Questions

Can JavaScript set the User-Agent header?

Browser page code cannot freely replace browser-controlled networking fields. Set a user agent only in a server-side client or a controlled browser automation environment.

Does adding a custom header always trigger preflight?

No. The browser evaluates the complete method, header set, and content type against the CORS “simple request” rules. Headers outside that safelist commonly require preflight.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I put an API key in a frontend header?

Only when the API is designed for a public client credential and its exposure is acceptable. A secret key shipped to a browser can be copied; keep privileged credentials on a server.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.