HTTP Headers: Request and Response Headers

HTTP headers are the unsung heroes of web communication. Every time your browser requests a resource or a server sends a response, headers carry crucial metadata that determines how that exchange...

Key Insights

  • HTTP headers are metadata key-value pairs that control everything from authentication and caching to security and content negotiation—understanding them is essential for building robust web applications
  • Request headers tell the server what the client wants and who it is, while response headers tell the client how to handle the data and enforce security policies
  • Modern JavaScript provides powerful APIs like fetch and the Headers interface for manipulating headers, but developers must understand header size limits, security implications, and CORS requirements to avoid common pitfalls

Introduction to HTTP Headers

HTTP headers are the unsung heroes of web communication. Every time your browser requests a resource or a server sends a response, headers carry crucial metadata that determines how that exchange behaves. Think of them as the envelope and routing information around a letter—the letter itself is the body, but the headers tell you who sent it, where it’s going, and how to handle it.

Headers come in two flavors: request headers (sent from client to server) and response headers (sent from server to client). Request headers might tell the server “I accept JSON data” or “Here’s my authentication token,” while response headers might say “This data expires in 5 minutes” or “Don’t embed this page in an iframe.”

Understanding headers is critical because they control authentication, caching, security policies, content negotiation, and CORS behavior. A misconfigured header can expose your application to security vulnerabilities or break critical functionality.

Here’s what a basic fetch request looks like, along with the headers it generates:

fetch('https://api.example.com/users')
  .then(response => response.json())
  .then(data => console.log(data));

// In DevTools Network tab, you'll see request headers like:
// Accept: */*
// User-Agent: Mozilla/5.0...
// Accept-Encoding: gzip, deflate, br

Common Request Headers

Request headers tell the server what the client needs and provide context about the request. Here are the headers you’ll work with most frequently:

Content-Type specifies the media type of the request body. When sending JSON to an API, you’ll set this to application/json. For form submissions, it’s typically application/x-www-form-urlencoded or multipart/form-data.

Authorization carries authentication credentials, most commonly in the format Bearer <token> for JWT-based authentication.

User-Agent identifies the client software making the request. While browsers set this automatically, you might customize it for API clients or web scrapers.

Accept tells the server what content types the client can process. Setting this to application/json signals that you want JSON responses.

Custom headers should use a descriptive name without the deprecated X- prefix. Modern convention favors names like API-Key or Request-ID over X-API-Key.

CORS-related headers like Origin are set automatically by browsers during cross-origin requests to trigger CORS checks.

Here’s how to set request headers with different JavaScript libraries:

// Using fetch API
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
    'Accept': 'application/json',
    'API-Version': 'v2'
  },
  body: JSON.stringify({ name: 'John Doe', email: 'john@example.com' })
});

// Using axios
import axios from 'axios';

axios.post('https://api.example.com/users', 
  { name: 'John Doe', email: 'john@example.com' },
  {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
      'API-Version': 'v2'
    }
  }
);

Common Response Headers

Response headers provide instructions to the client about how to handle the response and enforce security policies.

Content-Type tells the client what type of data is being returned. The server should set this accurately—application/json for JSON, text/html for HTML, etc.

Content-Length specifies the size of the response body in bytes, allowing clients to track download progress.

Cache-Control dictates caching behavior. Values like no-cache, max-age=3600, or public control whether and how long responses can be cached.

Security headers are critical for protecting your application:

  • Strict-Transport-Security forces HTTPS connections
  • X-Frame-Options prevents clickjacking by controlling iframe embedding
  • Content-Security-Policy restricts resource loading to prevent XSS attacks
  • X-Content-Type-Options: nosniff prevents MIME-type sniffing

CORS headers control cross-origin access:

  • Access-Control-Allow-Origin specifies which origins can access the resource
  • Access-Control-Allow-Methods lists permitted HTTP methods
  • Access-Control-Allow-Headers defines allowed request headers

Here’s how to set response headers in Express:

const express = require('express');
const app = express();

app.use((req, res, next) => {
  // Security headers
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('Content-Security-Policy', "default-src 'self'");
  
  // CORS headers
  res.setHeader('Access-Control-Allow-Origin', 'https://trusted-domain.com');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  next();
});

app.get('/api/users', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Cache-Control', 'max-age=300'); // Cache for 5 minutes
  res.json({ users: [{ id: 1, name: 'John' }] });
});

Working with Headers in JavaScript

JavaScript provides robust APIs for working with headers on both client and server.

Reading request headers in Node.js/Express is straightforward:

app.get('/api/data', (req, res) => {
  const authHeader = req.headers.authorization;
  const userAgent = req.headers['user-agent'];
  const customHeader = req.headers['api-version'];
  
  console.log('Authorization:', authHeader);
  console.log('User Agent:', userAgent);
  
  res.json({ received: true });
});

Accessing response headers from fetch or axios:

// With fetch
fetch('https://api.example.com/users')
  .then(response => {
    const contentType = response.headers.get('Content-Type');
    const rateLimit = response.headers.get('X-RateLimit-Remaining');
    console.log('Content-Type:', contentType);
    console.log('Rate Limit Remaining:', rateLimit);
    return response.json();
  });

// With axios
axios.get('https://api.example.com/users')
  .then(response => {
    console.log('All headers:', response.headers);
    console.log('Content-Type:', response.headers['content-type']);
  });

The Headers API provides a clean interface for manipulating headers:

const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');

// Check if header exists
if (headers.has('Authorization')) {
  console.log('Auth header present');
}

// Iterate over headers
for (let [key, value] of headers.entries()) {
  console.log(`${key}: ${value}`);
}

fetch('https://api.example.com/users', { headers });

Practical Use Cases

Authentication with Bearer tokens is the most common use case for custom headers:

// Client-side: Login and store token
async function login(email, password) {
  const response = await fetch('https://api.example.com/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });
  
  const { token } = await response.json();
  localStorage.setItem('authToken', token);
  return token;
}

// Client-side: Use token for authenticated requests
async function fetchUserProfile() {
  const token = localStorage.getItem('authToken');
  
  const response = await fetch('https://api.example.com/profile', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Accept': 'application/json'
    }
  });
  
  return response.json();
}

// Server-side: Validate token
const jwt = require('jsonwebtoken');

app.use('/api/protected', (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  const token = authHeader.substring(7);
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

Content negotiation allows clients to request different formats:

app.get('/api/users', (req, res) => {
  const acceptHeader = req.headers.accept;
  const users = [{ id: 1, name: 'John' }];
  
  if (acceptHeader.includes('application/xml')) {
    res.setHeader('Content-Type', 'application/xml');
    res.send(`<users><user><id>1</id><name>John</name></user></users>`);
  } else {
    res.setHeader('Content-Type', 'application/json');
    res.json({ users });
  }
});

Custom rate limiting headers inform clients about their API usage:

app.use((req, res, next) => {
  const remaining = getRateLimitRemaining(req.ip); // Your rate limit logic
  
  res.setHeader('X-RateLimit-Limit', '100');
  res.setHeader('X-RateLimit-Remaining', remaining.toString());
  res.setHeader('X-RateLimit-Reset', Date.now() + 3600000);
  
  if (remaining <= 0) {
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }
  
  next();
});

Best Practices and Security Considerations

Never put sensitive data in headers unless encrypted. Headers are logged by proxies, load balancers, and servers. While authorization tokens are acceptable (and expected), avoid putting passwords, credit card numbers, or other sensitive information in custom headers.

Be aware of header size limitations. Most servers limit total header size to 8KB-16KB. Browsers and proxies may have stricter limits. If you’re hitting limits, you’re probably doing something wrong—consider moving data to the request body.

Security headers checklist for production applications:

  • Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • X-Frame-Options: DENY or SAMEORIGIN
  • X-Content-Type-Options: nosniff
  • Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
  • Referrer-Policy: strict-origin-when-cross-origin

Common pitfalls:

  • Setting Access-Control-Allow-Origin: * with credentials (not allowed)
  • Forgetting to handle preflight OPTIONS requests for CORS
  • Using deprecated X- prefix for custom headers (modern practice omits it)
  • Not validating header values, leading to header injection attacks
  • Sending different Content-Type than actual content

Headers are the control plane of HTTP communication. Master them, and you’ll build more secure, efficient, and robust web applications. Set them carelessly, and you’ll create security vulnerabilities and frustrating bugs. Treat headers with the respect they deserve.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.