Back to blog

How To Set Axios POST Headers and Manage Headers Across All Request Types

Axios POST headers are one of the most important items for JavaScript developers working with HTTP. Configure them incorrectly, and your requests fail, authentication breaks, or data gets rejected. The good news? Axios gives developers several ways to manage headers, including inline on individual requests, globally via defaults, through reusable instances, and dynamically with interceptors. This guide explores how to use Axios to set headers across all request types, covering POST, GET, PUT, and DELETE requests, plus common pitfalls and fixes.

TL;DR

  • Axios headers can be set per request (inline config), globally via axios.defaults, per instance with axios.create(), or dynamically using interceptors
  • For POST requests, pass headers in the third argument: axios.post(url, data, { headers: {...} })
  • Never manually set Content-Type when sending FormData
  • Use Axios interceptors headers for token injection and refresh logic
  • Combine Axios custom headers with proxy rotation for reliable JavaScript web scraping
  • Use per-request headers for one-off calls where you need specific, isolated config
  • Set axios.defaults.headers for headers that every request in your app should carry
  • Create isolated Axios instances with axios.create() when you work with multiple APIs
  • Use request interceptors to inject tokens dynamically, and response interceptors to handle token refresh
  • Never manually set Content-Type: multipart/form-data. Axios handles the boundary automatically.

Installing Axios and basic setup

Axios is an isomorphic, promise-based HTTP client that allows developers to use the same codebase for network requests in both Node.js and browser environments, which matters because some header behaviors differ between the two environments.

Install it via the package manager your project uses:

# npm
npm install axios
# yarn
yarn add axios
# pnpm
pnpm add axios

If you're working browser-only without a build step, use the CDN:

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

Then import the Axios library in your code, depending on your JavaScript environment:

ESM (Modern JavaScript):

import axios from 'axios';

CommonJS (Older Node.js):

const axios = require('axios');
  • Use ESM (import) if you're starting a new project 
  • Use CommonJS (require) only if you're maintaining older codebases

Note: All examples in this guide use Axios v1.0+ and Node.js 18+

CORS restrictions apply in browsers but not in Node.js. That means Axios custom headers that work fine in Node.js might trigger CORS preflight errors in the browser, depending on server configuration. We'll cover more on that in the troubleshooting section.

Setting headers for individual Axios requests

Per-request headers are the most explicit option. You pass a headers object inside the config argument on each call. They apply only to that single request and nothing else.

This is the right choice when you're making a one-off call with unique credentials, testing a specific endpoint, or calling an API that doesn't fit your app's global defaults.

GET request with custom headers

For GET requests, the config object (which holds your headers) is the second argument.

To make this example fully testable, we'll use the JSONPlaceholder, a publicly accessible test API endpoint that returns the headers you send.

import axios from 'axios';
// fetchPost() - uses JSONPlaceholder, a free public REST API for testing
async function fetchPost() {
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts/1',
{
headers: {
'X-API-Key': 'test-key-123',
Accept: 'application/json',
'X-Request-Source': 'dashboard-v3',
'Accept-Language': 'en-US,en;q=0.9'
}
}
);
console.log('Status :', response.status);
console.log('Data :', JSON.stringify(response.data, null, 2));
console.log('Content-Type:', response.headers['content-type']);
} catch (error) {
console.error('Error:', error.message);
}
}
fetchPost();

Run it with:

node get_example.js

Sample output:

Status : 200
Data : { "userId": 1, "id": 1, "title": "sunt aut facere...", "body": "..." }
Content-Type: application/json; charset=utf-8

The code uses Axios to send a request to JSONPlaceholder and fetch a sample post. It then prints the response status, the returned data, and the content type or shows an error if the request fails.

Here are a few things worth knowing here: 

  • Accept: application/json signals to the server that you want JSON back. Many APIs will return a different format (or an error) if this header is missing or wrong. 
  • X-Request-Source is a custom non-standard header; anything prefixed with X- is application-defined, and the server has to be built to read it. 
  • Accept-Language affects localized responses on APIs that support it. It's useful if you're fetching content for a specific location.

These headers exist only for this request. They don't affect the global Axios state, any other pending requests, or requests made elsewhere in your app.

POST request with headers and body data

This is the core pattern behind Axios post headers.

The key thing to get right is: For POST requests, Axios takes three arguments: axios.post(url, data, config)

We'll use httpbin.org/post, a real API testing service specifically designed for request inspection.

import axios from 'axios';
async function sendPostRequest() {
// Request body (data being sent)
const payload = {
title: 'Mechanical Keyboard',
body: 'SKU: KB-2024-MX, quantity: 150',
userId: 1
};
try {
// POST request to httpbin (echo service)
const response = await axios.post(
'https://httpbin.org/post',
payload,
{
headers: {
Authorization: 'Bearer test-token-123',
'Content-Type': 'application/json'
}
}
);
// Print useful debugging info
console.log('Status Code:', response.status);
console.log('Returned JSON Body:', response.data.json);
console.log('Full Response URL:', response.data.url);
} catch (error) {
console.error('POST request failed:', error.message);
}
}
// Run the function
sendPostRequest();

The code sends an HTTP POST request to https://httpbin.org/post using Axios, including a JSON body with user details and custom headers like Content-Type and Authorization. It then logs the response status and data if successful, or prints error details if the request fails.

Note: When you pass a plain JavaScript object as the request body, Axios automatically serializes it to JSON and sets Content-Type: application/json

You don't need to set Content-Type manually in most cases, as Axios handles it. You only need to set it explicitly if you're working with a different format, sending raw strings, or overriding an instance default.

Besides, many REST APIs (Stripe, Shopify, etc.) support the Idempotency-Key header to make POST requests safe to retry. If your request times out and you retry with the same key, the server recognizes it as a duplicate. It then returns the original response instead of creating a second record.

You should also not swap the argument order. For instance, if you pass the config object as the second argument and the data as the third, Axios won't throw an error; it'll silently send an empty body and the config object as the body, which is baffling to debug. 

Always remember: POST/PUT/PATCH are (url, data, config), while GET/DELETE are (url, config).

PUT and DELETE requests with headers

PUT follows the same three-argument pattern as POST. The only difference is semantic. For instance, POST creates a new resource, and PUT replaces an existing one entirely. PATCH updates only specific fields.

Here's a complete script demonstrating both.

import axios from "axios";
async function runExample() {
const baseUrl = "https://httpbin.org";
// -------------------------
// PUT request (full replace)
// -------------------------
const fullResource = {
id: 1,
title: "Wireless Mechanical Keyboard Pro",
body: "category: peripherals, quantity: 200, price: 149.99",
userId: 1
};
try {
const putResponse = await axios.put(
`${baseUrl}/put`,
fullResource,
{
headers: {
Authorization: "Bearer test-token-123",
"If-Match": '"1"' // Simulated ETag (not enforced by httpbin)
}
}
);
console.log("\n=== PUT REQUEST ===");
console.log("Status:", putResponse.status);
console.log("Data:", putResponse.data.json);
console.log("Headers sent:", putResponse.data.headers);
} catch (err) {
console.error("PUT error:", err.message);
}
// -------------------------
// PATCH request (partial update)
// -------------------------
const partialUpdate = {
body: "quantity: 200"
};
try {
const patchResponse = await axios.patch(
`${baseUrl}/patch`,
partialUpdate,
{
headers: {
Authorization: "Bearer test-token-123"
}
}
);
console.log("\n=== PATCH REQUEST ===");
console.log("Status:", patchResponse.status);
console.log("Data:", patchResponse.data.json);
console.log("Headers sent:", patchResponse.data.headers);
} catch (err) {
console.error("PATCH error:", err.message);
}
}
runExample();

A few things worth noting here.

Both PUT and PATCH can include headers such as:

  • Authorization (authentication token)
  • If-Match (used for optimistic concurrency with ETags)

Note: Some testing APIs (like httpbin) don't enforce ETags or concurrency rules, they only echo what you send.

DELETE uses (url, config) like GET, but unlike GET it may include a request body via config.data. when you need to pass extra context to the server. As shown below:

import axios from "axios";
async function runDeleteExamples() {
const baseUrl = "https://httpbin.org";
// ---------------------------------------
// 1. Simple DELETE with headers
// ---------------------------------------
try {
const deleteResponse = await axios.delete(`${baseUrl}/delete`, {
headers: {
Authorization: "Bearer test-token-123",
"X-Reason": "discontinued"
}
});
console.log("\n=== SIMPLE DELETE ===");
console.log("Status:", deleteResponse.status);
console.log("URL:", deleteResponse.data.url);
console.log("Headers received by server:");
console.log(deleteResponse.data.headers);
} catch (err) {
console.error("Simple DELETE error:", err.message);
}
// ---------------------------------------
// 2. DELETE with request body
// ---------------------------------------
try {
const bulkDeleteResponse = await axios.delete(`${baseUrl}/delete`, {
headers: {
Authorization: "Bearer test-token-123",
"Content-Type": "application/json"
},
data: {
ids: [1, 2, 3]
}
});
console.log("\n=== BULK DELETE (WITH BODY) ===");
console.log("Status:", bulkDeleteResponse.status);
console.log("JSON body received by server:");
console.log(bulkDeleteResponse.data.json);
console.log("Headers received by server:");
console.log(bulkDeleteResponse.data.headers);
} catch (err) {
console.error("Bulk DELETE error:", err.message);
}
}
// Run the function
runDeleteExamples();

Request bodies in DELETE requests are technically valid per the HTTP spec, but some servers and proxies don't support them. 

Setting global headers in Axios

Once your app makes more than a handful of requests, setting the same Authorization header on every call can become repetitive and fragile. 

If the token changes, you have to update it everywhere. Axios gives you the following ways to define headers once and have them applied automatically.

Using axios.defaults.headers.common

This sets a header that attaches to every request regardless of HTTP method. The typical pattern is to set it once at application startup, often right after the user logs in, and you have a token.

Here's how to set global Axios default headers after login so every request automatically includes API authentication and app metadata.

import axios from "axios";
/**
* GLOBAL AXIOS INSTANCE (shared state)
*/
function onLoginSuccess(accessToken) {
axios.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`;
axios.defaults.headers.common["X-App-Version"] = "3.1.0";
axios.defaults.headers.common["X-Client-Platform"] = "web";
}
/**
* Isolated Axios instance (recommended alternative)
*/
function createApiClient(accessToken) {
return axios.create({
baseURL: "https://httpbin.org",
headers: {
Authorization: `Bearer ${accessToken}`,
"X-App-Version": "3.1.0",
"X-Client-Platform": "web",
},
});
}
async function sendRequest(label, config, client = axios) {
try {
const res = await client({
url: "https://httpbin.org/anything",
method: config.method,
data: config.data || undefined,
});
console.log(`\n==================== ${label} ====================`);
console.log("HTTP Status:", res.status);
console.log("\nHeaders received by server:");
console.log(res.data.headers);
console.log("\nAuthorization header (if present):");
console.log(
res.data.headers["Authorization"] ||
res.data.headers["authorization"]
);
} catch (err) {
console.error(`Request failed for ${label}`);
console.error(err.message);
}
}
async function main() {
console.log("=== GLOBAL AXIOS DEMO ===");
onLoginSuccess("demo-access-token");
await sendRequest("GET Request (global axios)", { method: "get" });
await sendRequest("POST Request (global axios)", {
method: "post",
data: { title: "test", body: "body", userId: 1 },
});
console.log("\nLogging out (clearing global header)");
delete axios.defaults.headers.common["Authorization"];
await sendRequest("After Logout (no Authorization)", {
method: "get",
});
console.log("\n=== ISOLATED CLIENT DEMO (axios.create) ===");
const apiClient = createApiClient("isolated-token");
await sendRequest(
"GET Request (isolated client)",
{ method: "get" },
apiClient
);
console.log("\nTest complete");
}
main().catch((err) => {
console.error("Fatal error:", err);
});

Sample output:

=== GLOBAL AXIOS DEMO ===
GET Request (global axios) -> Authorization: Bearer demo-access-token
POST Request (global axios) -> Authorization: Bearer demo-access-token
Logging out (clearing global header)
After Logout (no Authorization) -> Authorization: undefined
=== ISOLATED CLIENT DEMO (axios.create) ===
GET Request (isolated client) -> Authorization: Bearer isolated-token
Test complete

The code shows how to set global headers in Axios so that every HTTP request automatically includes things like an authorization token and app information after a user logs in. 

It then sends test requests to a demo API (httpbin) to confirm the headers are being sent correctly, and demonstrates how removing or isolating those headers affects later requests.

Here's something worth noting: axios.defaults modifies the global Axios instance. Every request in your app inherits these headers, including requests made by third-party libraries that use Axios internally. This can cause unexpected header leakage. If you need isolation, use axios.create() instead.

Moreover, deleting a default header on logout is important because if you just set it to an empty string, Axios will still send the header, just with an empty value. The server may accept it, reject it, or behave unpredictably.

Method-specific global defaults

You can also set defaults that only apply to a specific HTTP method.

import axios from "axios";
async function main() {
// Method-specific global defaults
axios.defaults.headers.common["Accept"] = "application/json";
axios.defaults.headers.common["X-App-Version"] = "3.1.0";
axios.defaults.headers.post["Content-Type"] = "application/json";
axios.defaults.headers.put["Content-Type"] = "application/json";
axios.defaults.headers.get = {
...axios.defaults.headers.get,
"Cache-Control": "no-cache",
};
axios.interceptors.request.use((config) => {
if (config.method === "post") {
config.headers["X-Mutation-Source"] = "user-action";
}
return config;
});
try {
// Use an API testing endpoint that echoes request data
const res = await axios.get("https://httpbin.org/headers");
console.log("Status:", res.status);
console.log("Headers received by server:");
console.log(res.data.headers);
console.log("\nClient-side config headers snapshot:");
console.log(res.config.headers);
} catch (err) {
console.error("Request failed:", err.message);
}
}
main();

The code sets up default settings in Axios so that all HTTP requests automatically include certain headers, like sending JSON for POST and PUT requests and accepting JSON responses for all requests. 

It also adds a rule that automatically adds an extra header for POST requests and then sends a test request to show what headers are actually being sent and received.

Creating Axios instances with axios.create()

The cleanest solution for apps that talk to multiple APIs is creating separate Axios instances using axios.create(). Each instance has its own defaults, its own interceptors, and no connection to the global instance.

It's the right pattern you can use for any application that talks to more than one API, which is almost every real application.

import axios from 'axios';
// Instance 1: internal REST API (JSONPlaceholder as stand-in)
const internalAPI = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 5000,
headers: {
Authorization: 'Bearer internal-demo-token',
'Content-Type': 'application/json',
'X-Service-Name': 'inventory-dashboard'
}
});
// Instance 2: httpbin echoes request details - useful for header inspection
const inspectAPI = axios.create({
baseURL: 'https://httpbin.org',
timeout: 8000,
headers: {
'X-API-Key': 'weather-demo-key',
Accept: 'application/json',
'Accept-Encoding': 'gzip'
}
});
// Instance 3: payment-like endpoint with strict timeout
const paymentsAPI = axios.create({
baseURL: 'https://httpbin.org',
timeout: 15000,
headers: {
Authorization: 'Bearer payments-demo-token',
'X-API-Version': '2023-10-16'
}
});
async function main() {
const products = await internalAPI.get('/posts');
console.log('Products count:', products.data.length);
const headers = await inspectAPI.get('/headers');
console.log('X-API-Key echoed:', headers.data.headers['X-Api-Key']);
const charge = await paymentsAPI.post('/post', { amount: 4999 });
console.log('Charge status :', charge.status);
}
main();

This code creates three separate Axios clients, each configured for a different API with its own settings like base URL, timeout, and headers. 

It then makes requests using each client to fetch data, inspect request headers, and send a sample payment request, printing the results to the console.

Each instance keeps its configuration isolated. For example, adding an interceptor to internalAPI doesn't affect weatherAPI. Equally, changing internalAPI's Authorization header doesn't touch paymentsAPI. This is crucial when you're building applications that authenticate differently with different third-party services.

Pro tip: When building scrapers or data pipelines, create a dedicated Axios instance for scraping. That instance gets the browser-like headers, the proxy agent, and any retry logic separate from the instances handling your internal API calls.

Common header scenarios: Authorization, content types, and sessions

Syntax aside, the headers you'll actually spend time on in real projects fall into a few well-defined categories. Here's how to handle each one correctly.

Authorization and bearer tokens

The Authorization header is the most frequent use case of Axios custom headers. There are three common patterns depending on how the API authenticates, as shown here:

// To run this file:
// 1. npm install axios
// 2. Ensure package.json has: "type": "module"
// 3. Run: node auth_examples.mjs
import axios from 'axios';
async function authExamples() {
try {
console.log('--- Starting API header tests ---\n');
// 1. Bearer token (OAuth/JWT style)
const bearerRes = await axios.get(
'https://jsonplaceholder.typicode.com/posts/1',
{
headers: {
Authorization: 'Bearer test-token-123'
}
}
);
console.log('1. Bearer Token Test');
console.log('Status:', bearerRes.status);
console.log('Title :', bearerRes.data.title);
console.log('----------------------\n');
// 2. API key in custom header
const apiKeyRes = await axios.get(
'https://httpbin.org/headers',
{
headers: {
'X-API-Key': 'my-secret-key'
}
}
);
console.log('2. X-API-Key Header Test');
console.log('Echoed key:', apiKeyRes.data.headers['X-Api-Key']);
console.log('----------------------\n');
// 3. API key in Authorization header
const apiKeyAuthRes = await axios.get(
'https://httpbin.org/headers',
{
headers: {
Authorization: 'ApiKey my-secret-key'
}
}
);
console.log('3. Authorization ApiKey Test');
console.log('Echoed auth:', apiKeyAuthRes.data.headers['Authorization']);
console.log('----------------------\n');
// 4. Basic Auth (Axios handles Base64 encoding automatically)
const basicRes = await axios.get(
'https://httpbin.org/basic-auth/user/pass',
{
auth: {
username: 'user',
password: 'pass'
}
}
);
console.log('4. Basic Auth Test');
console.log('Status:', basicRes.status); console.log('Authenticated:', basicRes.data.authenticated); console.log('\n--- All tests completed successfully ---');
} catch (error) {
console.error('\nRequest failed:');
console.error(error.message);
}
}
// Run the function
authExamples();

Sample output:

1. Bearer Token Test
Status: 200
Title : sunt aut facere repellat provident occaecati
----------------------
2. X-API-Key Header Test
Echoed key: my-secret-key
----------------------
3. Authorization ApiKey Test
Echoed auth: ApiKey my-secret-key
----------------------
4. Basic Auth Test
Status: 200
Authenticated: true
--- All tests completed successfully -

This code sends four different HTTP requests using Axios to demonstrate common API authentication methods, including Bearer tokens, API keys in headers, and Basic Auth.

To run it, ensure you have installed Axios with npm install axios, ensure Node.js is set to use ES modules, then execute the file with node auth_examples.mjs.

Content-Type for JSON, forms, and file uploads

Content-Type tells the server how to interpret the request body. If you get this wrong, your data can arrive as garbled bytes the server can't deserialize. Here's when to use each value:

Content-Type

When to use it

How to set it in Axios

application/json

Sending a JavaScript object as JSON

Automatic when the body is a plain object. Set manually only if overriding.

application/x-www-form-urlencoded

Traditional HTML form submissions, OAuth token exchanges

Encode body with new URLSearchParams(data). Set Content-Type manually.

multipart/form-data

File uploads, mixed file and field submissions

DON'T set manually. Axios plus the runtime add the boundary parameter automatically.

text/plain

Sending raw text, webhooks that expect plain strings

Set manually. Pass a string as the body, not an object.

application/octet-stream

Streaming binary data, large file uploads

Set manually. Pass a buffer or stream as the body.

The multipart/form-data rule is important enough to show both the wrong and correct approaches side by side.

Node.js (using form-data package)

In Node.js, use the form-data package and call form.getHeaders() to get the correct Content-Type with boundary included automatically.

const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');
async function uploadFile() {
// Create a small test file so the script runs immediately
fs.writeFileSync('./test-upload.txt', 'Hello from Axios FormData!');
const form = new FormData();
form.append('avatar', fs.createReadStream('./test-upload.txt'), {
filename: 'test-upload.txt',
contentType: 'text/plain'
});
form.append('userId', '12345');
// httpbin.org/post echoes back the full request - great for testing
const response = await axios.post('https://httpbin.org/post', form, {
headers: { ...form.getHeaders() } // includes boundary automatically
});
console.log('Status :', response.status);
console.log('Files :', response.data.files);
console.log('Form :', response.data.form);
}
uploadFile();

This script creates a small text file, then uploads it from a Node.js environment using the form-data package and Axios. It sends both the file and a userId to httpbin.org, then prints the server's response so you can verify the file and form data were received correctly.

Not all APIs use bearer tokens. Some use server-side sessions with cookies, others use custom session headers, and many modern web apps need CSRF protection on top of sessions.

You'll need to install dependencies if you haven't done that already:

import axios from "axios";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
// Create cookie jar for session persistence
const jar = new CookieJar();
// Wrap axios so it supports cookies
const client = wrapper(
axios.create({
jar,
withCredentials: true
})
);
async function runSessionDemo() {
console.log("=== SESSION + COOKIE + HEADER DEMO START ===\n");
try {
// -----------------------------
// 1. COOKIE-BASED SESSION TEST
// -----------------------------
console.log("1) Setting cookie via httpbin...");
const setCookieResponse = await client.get(
"https://httpbin.org/cookies/set/session_id/abc123"
);
console.log("Set-cookie HTTP status:", setCookieResponse.status);
// Verify cookie persistence
const cookieCheck = await client.get(
"https://httpbin.org/cookies"
);
console.log("Cookies stored on client:");
console.log(JSON.stringify(cookieCheck.data, null, 2));
console.log("\n-----------------------------\n");
// -----------------------------
// 2. CUSTOM SESSION HEADERS
// -----------------------------
console.log("2) Sending custom session headers...");
const headerResponse = await client.get(
"https://httpbin.org/headers",
{
headers: {
"X-Session-ID": "sess-abc-123",
"X-Session-Token": "tok-xyz-456"
}
}
);
console.log("Headers echoed back by server:");
const headers = headerResponse.data.headers;
console.log("X-Session-ID :", headers["X-Session-Id"] || headers["x-session-id"]);
console.log("X-Session-Token:", headers["X-Session-Token"] || headers["x-session-token"]);
console.log("\n=== DEMO COMPLETE ===");
} catch (error) {
console.error("Error during session demo:", error.message);
}
}
// Run the demo
runSessionDemo();

This snippet shows two common authentication patterns in Axios: cookie-based sessions and header-based sessions.

CSRF tokens are required by most server-rendered web frameworks, such as Rails, Django, and Laravel, for state-changing requests. The server embeds the token in the page HTML; your JavaScript reads it and sends it back:

import axios from 'axios';
// --- Node.js testable version ---
// In a real browser app, read the token from the DOM:
// const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
// For this test we simulate a token value directly.
const csrfToken = 'simulated-csrf-token-abc123';
// Attach to all state-changing requests as a global default
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken;
// httpbin.org/post echoes back headers and body - confirms the token is sent
async function submitSettings() {
const res = await axios.post(
'https://httpbin.org/post',
{ theme: 'dark' }
);
console.log('Status :', res.status);
console.log('CSRF token sent :', res.data.headers['X-Csrf-Token']);
}
// --- Interceptor approach (refreshes token before each mutating request) ---
axios.interceptors.request.use(config => {
if (['post', 'put', 'patch', 'delete'].includes(config.method)) {
// In a browser this would read from the DOM; here we use the variable
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
submitSettings();

The interceptor approach is more robust when the server rotates the CSRF token on each response (which some frameworks do for extra security). 

Instead of setting the header once at startup, the interceptor reads the token freshly from the DOM before every mutating request, so you always send a valid token even after the server has rotated it.

Using interceptors to manage headers dynamically

Interceptors are middleware for your Axios instance. A request interceptor runs before Axios sends each request; a response interceptor runs after the response arrives. They're the right tool when header logic needs to be dynamic; reading from state, reacting to responses, or applying conditional rules.

Unlike defaults, which are static values set at configuration time, interceptors are functions that run at request time and can read current application state, call async functions, or make decisions based on what request is being sent.

Here's how request and response interceptors let you inject or modify headers automatically. 

Request interceptors for injecting headers

A single request interceptor runs just before Axios sends each request. You get full access to the config object, including headers:

import axios from "axios";
import { randomUUID } from "crypto";
// Create Axios instance pointing to a real API testing service
const api = axios.create({
baseURL: "https://httpbin.org",
});
// Simulated token store (replace with real auth in production)
const tokenStore = {
getAccessToken: () => "live-access-token-xyz",
};
// REQUEST INTERCEPTOR
api.interceptors.request.use(
(config) => {
config.headers = config.headers || {};
const token = tokenStore.getAccessToken();
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
config.headers["X-Trace-ID"] = randomUUID();
config.headers["X-Request-At"] = new Date().toISOString();
return config;
},
(error) => Promise.reject(error)
);
// MAIN TEST FUNCTION
async function runTest() {
try {
const response = await api.get("/headers");
console.log("=== RESPONSE INFO ===");
console.log("Status :", response.status);
console.log("\n=== HEADERS SENT TO SERVER ===");
console.log("Authorization :", response.data.headers["Authorization"]);
console.log("X-Trace-ID :", response.data.headers["X-Trace-Id"] || response.data.headers["X-Trace-ID"]);
console.log("X-Request-At :", response.data.headers["X-Request-At"]);
} catch (err) {
console.error("Request failed:", err.message);
}
}
// EXECUTE
runTest();

This code creates an Axios instance and uses a request interceptor to automatically add headers before every request is sent. It injects an authorization token plus custom headers like a unique trace ID and timestamp into each outgoing request.

It then makes a GET request to https://httpbin.org/headers and prints back the headers that were received by the server. This helps demonstrate how interceptors can dynamically modify requests without changing each request manually.

You must not forget the return config object. Otherwise, if your interceptor doesn't return the config object, Axios receives undefined, and the request hangs or throws a cryptic error. Always return config or a modified copy of it.

You can register multiple interceptors as well; they execute in the order they were added. This lets you compose header logic: one interceptor for auth, another for logging, another for tracing.

When you run this code via:

// Run: node multi-interceptor-test.mjs

You'll first see:

STARTING HTTPBIN INTERCEPTOR TEST

Then for each request, you'll see logs like:

GET request log

REQUEST OUTGOING
URL: https://httpbin.org/headers
METHOD: GET
HEADERS: {
Accept: 'application/json, text/plain, */*',
Authorization: 'Bearer ***hidden***'
}

POST request log

REQUEST OUTGOING
URL: https://httpbin.org/post
METHOD: POST
HEADERS: {
Accept: 'application/json, text/plain, */*',
Authorization: 'Bearer ***hidden***'
}

Each successful request triggers:

RESPONSE RECEIVED
STATUS: 200

Printed twice: once for GET, once for POST.

Then the final output:

TEST COMPLETE

This code sets up several request interceptors that each add different behavior: one adds an auth token, one adds tracing information, and one logs the request details. It also adds a response interceptor that logs successful responses or errors.

Then it runs a GET and a POST request to httpbin.org to show how all interceptors work together, automatically modifying requests and handling responses in a structured way.

This pattern is similar to how request handling works in other ecosystems. For example, in Python, you might use the Requests library.

Response interceptors for token refresh

The token refresh pattern is the most important real-world use of response interceptors. When an access token expires, the server returns a 401 Unauthorized. Instead of propagating the error to your UI, the interceptor catches it, refreshes the token, and retries the original request, transparently:

import axios from "axios";
/**
* Fake token storage (simulates localStorage/session/cookie layer)
*/
const tokenStore = {
access: "expired-access-token",
refresh: "valid-refresh-token",
getAccessToken() {
return this.access;
},
getRefreshToken() {
return this.refresh;
},
setAccessToken(token) {
this.access = token;
},
clear() {
this.access = null;
this.refresh = null;
},
};
/**
* Axios instances
*/
const api = axios.create();
const refreshClient = axios.create();
let refreshPromise = null;
/**
* Response interceptor
*/
api.interceptors.response.use(
(res) => res,
async (error) => {
const originalRequest = error.config;
console.log("Request failed with status:", error.response?.status);
// Only handle 401 and avoid infinite retry loops
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
originalRequest._retry = true;
try {
console.log("Refreshing token...");
// Deduplicate refresh requests
if (!refreshPromise) {
refreshPromise = refreshClient
.post("https://httpbin.org/post", {
refreshToken: tokenStore.getRefreshToken(),
})
.then(() => {
const newToken = "access-token-" + Date.now();
tokenStore.setAccessToken(newToken);
console.log("Token refreshed:", newToken);
return newToken;
})
.finally(() => {
refreshPromise = null;
});
}
const newToken = await refreshPromise;
// Attach new token
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
console.log("Retrying original request...");
// Retry request (switch to success endpoint for demo)
originalRequest.url = "https://httpstat.us/200";
return api(originalRequest);
} catch (err) {
console.log("Refresh failed. Logging out user.");
tokenStore.clear();
return Promise.reject(err);
}
}
);
/**
* Test runner
*/
async function testFlow() {
console.log("Starting request to protected endpoint...\n");
try {
const response = await api.get("https://httpstat.us/401");
console.log("\nFinal response received!");
console.log("Status:", response.status);
console.log("Token in store:", tokenStore.getAccessToken());
} catch (err) {
console.error("Request ultimately failed:", err.message);
}
}
testFlow();

This code sets up an Axios HTTP client that automatically handles expired authentication tokens by refreshing them and retrying failed requests while keeping the user logged in without interruption.

Without the refreshPromise deduplication, if five requests fail with 401 at the same time, you'd fire five parallel refresh requests. 

Most auth servers accept only one refresh per token and invalidate the refresh token after its first use. 

The second through fifth refresh calls would fail with invalid_grant, logging the user out even though they're still active. The shared promise keeps all five retried requests waiting for the same single refresh call.

Conditional headers based on request URL or method

Interceptors can inspect config.url and config.method to decide which headers to attach. This is cleaner than putting conditional logic in every individual API call:

import axios from 'axios';
// Create Axios instance pointing to a real API testing service
const api = axios.create({
baseURL: 'https://httpbin.org'
});
// Simulated token store
const adminTokenStore = {
get: () => 'admin-privilege-token'
};
// Request interceptor
api.interceptors.request.use((config) => {
const url = config.url || '';
const method = (config.method || '').toLowerCase();
// Ensure headers object exists
config.headers = config.headers || {};
// Admin-only paths
if (url.startsWith('/anything/admin/') || url.startsWith('/anything/internal/')) {
config.headers['X-Admin-Token'] = adminTokenStore.get();
}
// Idempotency key for write operations
if (['post', 'put', 'patch'].includes(method)) {
if (!config.headers['Idempotency-Key']) {
config.headers['Idempotency-Key'] =
`${method}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
}
// Public paths shouldn't carry Authorization
if (url.startsWith('/anything/public/')) {
delete config.headers['Authorization'];
}
return config;
});
// Helper to print response headers from httpbin
function printResult(label, res) {
console.log(`\n--- ${label} ---`);
console.log('URL:', res.data.url);
console.log('Idempotency-Key:', res.data.headers['Idempotency-Key'] || 'none');
console.log('X-Admin-Token:', res.data.headers['X-Admin-Token'] || 'none');
console.log('Authorization:', res.data.headers['Authorization'] || 'none');
}
// Test function
async function runTests() {
try {
// 1. POST request (should generate Idempotency-Key)
const postRes = await api.post('/anything/create', {
action: 'create-item'
});
printResult('POST /create', postRes);
// 2. Admin request (should attach X-Admin-Token)
const adminRes = await api.get('/anything/admin/dashboard');
printResult('GET /admin/dashboard', adminRes);
// 3. Public request (should strip Authorization)
api.defaults.headers.common['Authorization'] = 'Bearer should-be-removed';
const publicRes = await api.get('/anything/public/info');
printResult('GET /public/info', publicRes);
// 4. Override Idempotency-Key manually
const customRes = await api.post(
'/anything/create',
{ action: 'custom-idempotent' },
{
headers: {
'Idempotency-Key': 'fixed-key-123'
}
}
);
printResult('POST with custom Idempotency-Key', customRes);
} catch (err) {
console.error('Error:', err.message);
}
}
// Run tests
runTests();

This is a solid Axios request-interceptor example for conditionally injecting headers based on route using HTTP method.

The pattern of checking if (!config.headers['Idempotency-Key']) before setting a header is deliberate. It lets individual callers override the interceptor's default. 

If a specific POST call sets its own Idempotency-Key (because it needs a deterministic key for retry logic), the interceptor won't clobber it. This is the right way to build interceptors: add defaults, don't override explicit caller choices.

Advanced header behaviors and troubleshooting

Most Axios header problems come from a small set of root causes. This section covers the tricky parts: merge order, CORS rules, casing behavior, and the FormData pitfall to save your debugging time.

How Axios merges headers from different sources

When you set headers in multiple places, Axios merges them in a specific order. You need to understand the order because each layer can override the ones before it.

Priority

Source

Override behavior

1 (lowest)

Axios library defaults

Built-in defaults like Accept: application/json

2

axios.defaults.headers.common

Applied to all methods; overrides library defaults

3

axios.defaults.headers[method]

e.g., axios.defaults.headers.post — method-specific

4

Instance defaults (axios.create headers)

Overrides global defaults for this instance

5

Per-request config headers

Passed inline to .get/.post/etc.

6 (highest)

Request interceptor

Runs last, can override everything above it

You may encounter a situation where you set an Authorization header in your per-request config, but the interceptor unconditionally overwrites it with a different token. The interceptor always wins. 

Fix it by guarding in the interceptor:

import axios from 'axios';
const tokenStore = { get: () => 'global-token-from-store' };
// --- Bad: interceptor always overwrites, even per-request overrides ---
const badAPI = axios.create({ baseURL: 'https://httpbin.org' });
badAPI.interceptors.request.use(config => {
config.headers['Authorization'] = `Bearer ${tokenStore.get()}`;
return config;
});
// --- Good: interceptor only sets if the caller has not already set it ---
const goodAPI = axios.create({ baseURL: 'https://httpbin.org' });
goodAPI.interceptors.request.use(config => {
if (!config.headers['Authorization'] && !config.headers['authorization']) {
config.headers['Authorization'] = `Bearer ${tokenStore.get()}`;
}
return config;
});
async function testMergeGuard() {
const CUSTOM = 'Bearer my-per-request-token';
// Bad API: per-request token is overwritten
const badRes = await badAPI.get('/headers',
{ headers: { Authorization: CUSTOM } }
);
console.log('Bad API token :', badRes.data.headers['Authorization']);
// Expected: Bearer global-token-from-store (overwritten!)
// Good API: per-request token is preserved
const goodRes = await goodAPI.get('/headers',
{ headers: { Authorization: CUSTOM } }
);
console.log('Good API token:', goodRes.data.headers['Authorization']);
// Expected: Bearer my-per-request-token (preserved)
}
testMergeGuard();

This code shows how to avoid clobbering per-request headers with a global interceptor.

Setting headers dynamically from environment variables

Never hardcode API keys or tokens in source code because they will eventually be committed to a repository. 

Create a .env file with the following information:

API_BASE_URL=https://httpbin.org
API_KEY=test-key-123

Import environment variables into your code, as shown below:

import 'dotenv/config';
import axios from 'axios';
// Node.js: read from .env via dotenv (or native process.env in CI/CD)
const apiClient = axios.create({
baseURL: process.env.API_BASE_URL ?? 'https://httpbin.org',
headers: {
'X-API-Key': process.env.API_KEY ?? 'fallback-key'
}
});
// Vite (browser build) - variables must be prefixed VITE_
// const apiClient = axios.create({
// baseURL: import.meta.env.VITE_API_BASE_URL,
// headers: { 'X-API-Key': import.meta.env.VITE_API_KEY }
// });
// Create React App - variables must be prefixed REACT_APP_
// headers: { 'X-API-Key': process.env.REACT_APP_API_KEY }
// Test: httpbin.org/headers echoes the key back to confirm it was sent
async function testEnvHeaders() {
const res = await apiClient.get('/headers');
console.log('Status :', res.status);
console.log('X-Api-Key :', res.data.headers['X-Api-Key']);
}
testEnvHeaders();
// Run: node env_vars.mjs
//
// Sample output:
// Status : 200
// X-Api-Key : test-key-123

The code creates an Axios HTTP client that automatically reads an API base URL and API key from environment variables instead of hardcoding them in the source code. It then sends a request to https://httpbin.org/headers and prints the response so you can confirm that the API key was correctly included in the request headers.

Note: Environment variables set in the browser at build time are bundled into the JavaScript output and visible to users. Don't expose secret keys this way. In browser apps, sensitive API calls should go through a backend proxy that holds the real credentials.

CORS restrictions and browser vs. Node.js differences

CORS (Cross-Origin Resource Sharing) is a browser security feature. The browser blocks cross-origin requests that the server hasn't explicitly allowed. Node.js has no such restriction. That means you can send any header to any server from Node.js without a CORS preflight.

This distinction matters when you're testing an Axios request in Node.js that you'll later run in a browser, or when you're wondering why your scraper works fine but the same logic fails in the browser.

Here's what that means for headers:

  • In Node.js, you can send any header to any server without CORS restrictions
  • In browsers: if a custom header triggers a CORS preflight (an OPTIONS request), the server must respond with Access-Control-Allow-Headers listing that header. Otherwise, the request fails before Axios sends it
  • Common custom headers like Authorization, Content-Type with non-standard values, and X-* headers typically trigger preflights
  • withCredentials: true requires the server to send Access-Control-Allow-Credentials: true and a specific (not wildcard) origin

If you're hitting CORS errors, this issue is always a server configuration problem, never an Axios problem. Axios can't change the browser's security rules. To fix this, you need to update the CORS middleware to allow your headers and origin.

Header casing issues

HTTP/1.1 defines headers as case-insensitive, and Axios normalizes header names internally (converting them to lowercase before merging). But a few edge cases can still bite you:

  • Some legacy servers running custom HTTP parsers are case-sensitive. Always use standard HTTP casing to avoid this: Content-TypeAuthorizationX-API-Key
  • When you read response headers from response.headers, Axios returns them lowercased regardless of what the server sent. So check response.headers['content-type'] not Content-Type.
  • If you set the same header twice with different casing, say authorization and Authorization in the same config object, the last one wins after Axios normalizes both to lowercase
  • HTTP/2 (used by many modern APIs) requires lowercase header names by spec. Axios handles this for you under the hood via the underlying http2 module, but if you're doing raw HTTP/2 elsewhere, use lowercase.

To avoid edge cases, use standard HTTP header casing as shown here:

headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
'X-API-Key': 'your_key',
'Accept-Language': 'en-US,en;q=0.9'
}
// Avoid inconsistent casing like:
// 'content-type', 'AUTHORIZATION', 'x-api-key'

File upload Content-Type pitfall

This is one of the most common Axios bugs. When you create a FormData object, the browser will generate a unique boundary string like ----WebKitFormBoundaryXyZ123. This boundary is what separates each form field in the request body. The full Content-Type header looks like:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXyZ123

When you manually set Content-Type: multipart/form-data in your Axios config, you're replacing that full header with just the type without the boundary. The server receives a multipart body but has no delimiter to split it by, so it can't parse any of the fields.

The server error you'll get depends on the backend framework, but common ones are: "no file received", "missing required field", "400 Bad Request", or a silent failure where fields are empty on the server side.

// requires: npm install axios form-data
const axios = require('axios');
const fs = require('fs');
const FormData = require('form-data');
async function uploadProductImage() {
// Write a placeholder file - replace with a real image path in production
fs.writeFileSync('./product.txt', 'product image placeholder bytes');
const form = new FormData();
form.append('product_image', fs.createReadStream('./product.txt'), {
filename: 'product.txt',
contentType: 'text/plain'
});
form.append('product_id', 'KB-2024-MX');
form.append('alt_text', 'Wireless Mechanical Keyboard Hero Shot');
// httpbin.org/post echoes back everything - ideal for verifying headers & payload
const response = await axios.post(
'https://httpbin.org/post',
form,
{
headers: {
...form.getHeaders(), // correct Content-Type + boundary
Authorization: 'Bearer test-token-123'
},
maxBodyLength: Infinity, // raise Axios's 10 MB default
maxContentLength: Infinity
}
);
console.log('Status :', response.status);
console.log('Form fields:', response.data.form);
console.log('Files :', Object.keys(response.data.files));
}
uploadProductImage();

This code uploads a file (an image) and some related data to a remote API using a multipart/form-data HTTP request.

The maxBodyLength and maxContentLength settings are also commonly needed for file uploads. Axios has a 10MB limit on body size by default. 

Uploading a video, a high-resolution image, or a large dataset without raising these limits can result in a 'Request body larger than maxBodyLength' error that looks like an Axios bug but is actually a configuration limit.

Using Axios with proxies for web scraping

Axios handles web scraping requests well in Node.js, but scraping requires more care with headers than standard API calls. This is because servers actively inspect request metadata to identify and block bots. 

The right header strategy, combined with proxy rotation, can determine whether your scraper works reliably or gets blocked within minutes.

Setting browser-like headers for scraping

When a browser makes a request, it sends a predictable set of headers that identify its version, platform, language preference, and accepted formats. A default Axios request sends almost none of these. That mismatch is the primary signal servers use to detect non-human traffic.

Here's a browser header profile to set on your scraping instance:

import axios from "axios";
// File: scraping_headers.mjs
// npm install axios
/**
* Create a reusable Axios instance
* This simulates a browser-like request environment
*/
const client = axios.create({
timeout: 15000,
maxRedirects: 5,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Accept":
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
});
/**
* Test 1: Basic GET request (shows query + headers echoed back)
*/
async function testGetRequest() {
try {
console.log("\n==============================");
console.log("TEST 1: GET /get");
console.log("==============================\n");
const response = await client.get("https://httpbin.org/get", {
params: {
message: "Hello from Axios",
test: true
}
});
console.log("STATUS CODE:", response.status);
console.log("\n--- QUERY PARAMS RECEIVED BY SERVER ---");
console.log(response.data.args);
console.log("\n--- HEADERS RECEIVED BY SERVER ---");
console.log(response.data.headers);
console.log("\n--- ORIGIN IP ---");
console.log(response.data.origin);
} catch (error) {
console.error("GET request failed:", error.message);
}
}
/**
* Test 2: Header inspection endpoint
*/
async function testHeaders() {
try {
console.log("\n==============================");
console.log("TEST 2: /headers");
console.log("==============================\n");
const response = await client.get("https://httpbin.org/headers");
console.log("STATUS CODE:", response.status);
console.log("\n--- HEADERS SENT FROM AXIOS ---");
console.log(JSON.stringify(response.data.headers, null, 2));
console.log("\n--- USER-AGENT CHECK ---");
console.log(response.data.headers["User-Agent"]);
} catch (error) {
console.error("Headers test failed:", error.message);
}
}
/**
* Run all tests sequentially
*/
async function runTests() {
console.log("Starting Axios API tests...\n");
await testGetRequest();
await testHeaders();
console.log("\nAll tests completed.");
}

Here's what these headers actually mean:

  • User-Agent. It identifies the browser and OS. Servers use this for compatibility decisions (mobile vs desktop rendering, etc.). 
  • Accept. Tells the server what response formats the client can handle, such as HTML, XML, and images. 
  • Accept-Language. Preferred language for content. Helps with localization. 
  • Accept-Encoding. Compression formats supported, such as gzip, deflate, and br. Browsers use this to reduce bandwidth. 
  • Connection: keep-alive. Reuses TCP connections instead of opening a new one each time. 
  • Upgrade-Insecure-Requests. Signals preference for HTTPS over HTTP when available. 

Configuring proxy settings in Axios

Routing requests through a proxy hides your real IP address and can help with privacy, debugging, or accessing region-restricted services. Axios supports proxy configuration in two main ways: 

  • A simple built-in proxy option for quick setup
  • A more flexible HTTPS agent when you need advanced control, like custom certificates or connection tuning

To route requests through a proxy server, you can use the proxy configuration option directly in your Axios request or instance. Here's a Decodo example:

// npm install axios https-proxy-agent
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";
async function testWithProxyAgent() {
console.log("\n=== WITH HTTPS PROXY AGENT ===\n");
// Replace with real proxy if available
const proxyUrl = "http://username:password@gate.decodo.com:7000";
const agent = new HttpsProxyAgent(proxyUrl);
try {
const res = await axios.get("https://httpbin.org/ip", {
httpsAgent: agent,
proxy: false // IMPORTANT: disables Axios native proxy handling
});
console.log("Response status:", res.status);
console.log("IP via agent:", res.data.origin);
} catch (err) {
console.error("Agent proxy failed:", err.message);
}
}
testWithProxyAgent();

This code shows two different ways to send Axios HTTP requests through a proxy server instead of directly from your machine.

Use the agent approach when you need HTTPS tunneling, need to rotate agents dynamically, or when the native proxy option doesn't work with your proxy provider's setup. The proxy: false is intentional when using an agent. Without it, Axios may try to apply both proxy mechanisms, and the behavior gets unpredictable.

Configuring rotating proxies to avoid rate limits

Sending many requests from a single IP to the same target can trigger rate limits and bans quickly. Normally, sites typically block an IP after 50–200 requests per session, depending on their protection level. 

Rotating proxies cycle through different IP addresses so each request appears to come from a different location.

You can integrate Decodo residential proxies with Axios by configuring the proxy option with Decodo credentials to enable automated IP rotation and stable connections for web requests.

This setup helps you avoid rate limits, improve anonymity, and maintain consistent scraping performance across multiple endpoints when collecting structured or unstructured data from websites that enforce strict access controls or geo restrictions.

First, install dependencies:

npm install axios https-proxy-agent dotenv

Then:

import axios from "axios";
import dotenv from "dotenv";
import { HttpsProxyAgent } from "https-proxy-agent";
dotenv.config();
// Decodo proxy credentials
const PROXY_USER = process.env.PROXY_USER;
const PROXY_PASS = process.env.PROXY_PASS;
const PROXY_HOST = "gate.decodo.com";
const PROXY_PORT = 7000;
// Create a new proxy agent per request to force rotation/session change
function createProxyAgent(sessionId) {
const proxyUrl = `http://${PROXY_USER}:${PROXY_PASS}@${PROXY_HOST}:${PROXY_PORT}`;
return new HttpsProxyAgent(proxyUrl);
}
// Axios base config (no fixed agent here)
const client = axios.create({
proxy: false,
timeout: 20000,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36",
Accept: "application/json",
},
});
// API test endpoint
const TEST_URL = "https://httpbin.org/ip";
// Run multiple requests to verify IP rotation
async function runRotationTest() {
for (let i = 1; i <= 5; i++) {
try {
const agent = createProxyAgent(i);
console.log(`Request ${i}: sending through Decodo proxy...`);
const res = await client.get(TEST_URL, {
httpsAgent: agent,
});
console.log(`Request ${i} status:`, res.status);
console.log(`Request ${i} IP result:`, res.data);
console.log("--------------------------------------");
} catch (err) {
console.error(`Request ${i} failed:`);
console.error(err.message);
console.log("--------------------------------------");
}
}
}
runRotationTest();

For each request (1 → 5), you'll see something like:

Request 1: sending through Decodo proxy...
Request 1 status: 200
Request 1 IP result: { origin: "203.0.113.10" }
--------------------------------------
Request 2: sending through Decodo proxy...
Request 2 status: 200
Request 2 IP result: { origin: "198.51.100.22" }

When manual header management gets too complex

Some sites go beyond IP blocking and header inspection. They identify Axios by its TLS handshake signature before even looking at headers, check JS APIs like canvas rendering and WebGL, JavaScript challenges, and CAPTCHA challenges.

At that point, managing Axios post headers and proxy settings yourself no longer solves the problem since the block happens at a layer below headers. 

The Decodo Web Scraping API handles all of these layers. It routes requests through residential proxies, uses real browser fingerprints, executes JavaScript challenges, and returns the final rendered HTML or extracted data. You make one Axios call to the API and get clean output back:

import axios from "axios";
import dotenv from "dotenv";
dotenv.config();
async function runApiTests() {
console.log("STEP 1: Running API endpoint tests...\n");
try {
const ipResponse = await axios.get("https://httpbin.org/ip");
console.log("GET /ip test - Status:", ipResponse.status);
console.log("Response:", ipResponse.data);
console.log("------------------------");
} catch (error) {
console.error("API test failed:", error.message);
throw error;
}
}
async function runScraper() {
console.log("STEP 2: Running scraping request...\n");
// Use the exact token string from your working 'fetch' example
const API_KEY = process.env.SCRAPING_API_KEY || "your_token_here";
try {
const response = await axios.post(
"https://scraper-api.decodo.com/v2/scrape",
{
url: "https://books.toscrape.com/",
headless: "html", // Changed from 'render' to 'headless' to match your working fetch
},
{
headers: {
// Changed 'Bearer' to 'Basic' to match your working fetch
"Authorization": `Basic ${API_KEY}`,
"Content-Type": "application/json",
},
timeout: 30000,
}
);
// FIX: Axios puts the body in .data, not .json
const responseData = response.data;
if (!responseData) {
throw new Error("No content returned from scraping API");
}
console.log("Scraping successful");
console.log("Status:", response.status);
console.log("Full Data Response:", responseData);
return responseData;
} catch (error) {
console.error("Scraping failed:");
// Axios puts server error messages in error.response.data
console.error(error.response?.data || error.message);
throw error;
}
}
async function main() {
try {
await runApiTests();
await runScraper();
console.log("\nPipeline completed successfully.");
} catch (error) {
console.error("\nPipeline execution halted.");
}
}
main();

This approach is especially useful for dynamic sites built with React, Vue, or Angular, where the data you want isn't in the initial HTML but loaded by JavaScript after the page renders.

Axios + proxies, sorted

Plug Decodo's rotating residential proxies into your Axios config and stop worrying about IP bans.

Final thoughts

Managing Axios post headers well is mostly about choosing the right tool for the scope of the problem. Use per-request headers when you need something for one call, axios.defaults, or axios.create() when the same header applies to many requests. Meanwhile, use interceptors when headers need to be dynamic, such as reading from a store, refreshing on 401, or varying by route.

Most Axios headers bugs trace back to a handful of root causes: the wrong merge order overriding what you set, missing CORS configuration on the server side, or manually setting Content-Type on a FormData upload and stripping the boundary. Keep these in mind, and you'll avoid 90% of the common pitfalls.

Lastly, when you combine proper Axios set headers practices with proxy rotation, Axios becomes a capable tool for both API integration and large-scale data collection.

Skip the boilerplate

Decodo's Web Scraping API returns ready-to-parse data – no proxy agents, no retry interceptors, no CAPTCHA logic.

About the author

Justinas Tamasevicius

Director of Engineering

Justinas Tamaševičius is Director of Engineering with over two decades of expertise in software development. What started as a self-taught passion during his school years has evolved into a distinguished career spanning backend engineering, system architecture, and infrastructure development.


Connect with Justinas via LinkedIn.

All information on Decodo Blog is provided on an as is basis and for informational purposes only. We make no representation and disclaim all liability with respect to your use of any information contained on Decodo Blog or any third-party websites that may belinked therein.

Frequently asked questions

What are headers in Axios?

Headers in Axios are key-value metadata sent with every HTTP request. They tell the server things like who you are via Authorization, what format you're sending via Content-Type, what format you expect back via Accept, and custom application data. You can set Axios headers per request, globally, on a per-instance basis, or dynamically via interceptors.

What is the main purpose of headers?

Headers carry metadata that controls how requests and responses are processed. On the request side, they authenticate the caller, describe the body format, and communicate client capabilities.

On the response side, they tell the client how to interpret the body, whether to cache it, and what CORS permissions apply. They're separate from the body in that the body is the payload, headers are the envelope.

Are Axios headers case-sensitive?

Axios normalizes header names internally, so 'content-type' and 'Content-Type' are treated as the same header within Axios.

However, some legacy or strictly-compliant servers are case-sensitive. The safe practice is to always use standard HTTP header casing (Content-Type, Authorization, X-API-Key) to avoid issues with strict backends.

What is the maximum header size in Axios?

Axios itself doesn't enforce a header size limit. The limit is set by the server and in Node.js, by the underlying http module. Node.js defaults to 8KB for incoming headers. Most servers, including Nginx and Apache, have limits in the range of 8KB–16KB per request.

Sending very large headers, for example, embedding large JWT tokens or long lists of values, can trigger 431 Request Header Fields Too Large errors. Always keep header values compact and store large data in the body, not in headers.

JavaScript Web Scraping Tutorial (2026)

Ever wished you could make the web work for you? JavaScript web scraping allows you to gather valuable information from websites in an automated way, unlocking insights that would be difficult to collect manually. In this guide, you'll learn the key tools, techniques, and best practices to scrape data efficiently, whether you're a beginner or a developer looking to streamline data collection.

How to Send a POST Request With cURL?

Sending a POST request with cURL is a common task in web development and API interactions. When making a POST request, cURL allows you to send data to a server, often to submit forms or interact with APIs. Understanding how to craft and send POST requests using cURL is essential for testing APIs, debugging, and automating web interactions.


In this guide, we'll explore how to use cURL to send POST requests effectively, with information updated to reflect the latest version of cURL and its current best practices.

Mastering Python Requests - Hero

Mastering Python Requests: A Comprehensive Guide to Using Proxies

When using Python's Requests library, proxies can help with tasks like web scraping, interacting with APIs, or accessing geo-restricted content. Proxies route HTTP requests through different IP addresses, helping you avoid IP bans, maintain anonymity, and bypass restrictions. This guide covers how to set up and use proxies with the Requests library. Let’s get started!

© 2018-2026 decodo.com (formerly smartproxy.com). All Rights Reserved