HTTP 401 Unauthorized — Why Your API Request Lacks Valid Authentication
Quick answer
💡HTTP 401 Unauthorized means the server requires authentication and the request did not provide valid credentials. Despite the confusing name from the HTTP spec, it means authentication is missing or invalid — not that you lack permission (that is 403). Check the WWW-Authenticate response header to see what authentication scheme the server expects, then verify your Authorization header format and token validity. Use the HTTP Request Builder to test authenticated requests.
Error symptoms
- ✕
HTTP 401 Unauthorized response status code - ✕
WWW-Authenticate: Bearer error="invalid_token" in response headers - ✕
Response body: { "error": "Unauthorized" } or { "message": "Token expired" } - ✕
JsonWebTokenError: jwt malformed in server logs - ✕
TokenExpiredError: jwt expired in server logs - ✕
Request works in Postman but returns 401 from application code
Common causes
- •Authorization header missing entirely from the request
- •Wrong format: 'Bearer' prefix missing or token sent without the scheme prefix
- •JWT token expired — exp claim in the payload is in the past
- •Wrong API key for the environment — using a development key against a production endpoint
- •NEXT_PUBLIC_ prefix on an environment variable exposes a private key to the browser
- •OAuth access token not refreshed before expiry — refresh_token flow not implemented
When it happens
- •On first deployment to a new environment when environment variables are not configured
- •After a JWT access token expires and the application has no refresh token logic
- •When switching between development and production environments with different API keys
- •After a key rotation where the old key still exists in environment variables or secrets
- •When server-side code accidentally sends no Authorization header due to a conditional bug
Examples and fixes
The Authorization header format must be exact — a single space, capital B, correct prefix.
Bearer token format mistakes
❌ Wrong
// Wrong — missing 'Bearer ' prefix
fetch('https://api.example.com/user', {
headers: {
'Authorization': token // Just the token value
}
});
// Also wrong — lowercase and double space
fetch('https://api.example.com/user', {
headers: {
'Authorization': 'bearer ' + token // lowercase + double space
}
});✅ Fixed
// Correct Bearer token format
fetch('https://api.example.com/user', {
headers: {
'Authorization': `Bearer ${token}`
// ^ Capital B, single space, then the token — no extra chars
}
});
// For debugging: log the exact header value
console.log('Auth header:', `Bearer ${token}`);
// Verify it starts with exactly 'Bearer ' (7 chars) before the tokenThe Bearer authentication scheme requires the exact string 'Bearer ' (capital B, single space) before the token. Most servers are strict about this format. When debugging a 401, log the complete Authorization header value from your code and compare it character by character with a working Postman request. Invisible characters, double spaces, or a missing prefix are common sources of mismatch that are hard to spot visually.
Using a stored JWT without checking expiry causes 401 errors when the token has expired.
JWT expiry check and refresh token flow
❌ Wrong
// Wrong — uses stored token without checking expiry
async function getUser() {
const token = localStorage.getItem('access_token');
// Token may be expired — server will return 401
const res = await fetch('/api/me', {
headers: { 'Authorization': `Bearer ${token}` }
});
return res.json();
}✅ Fixed
// Correct — check expiry and refresh before use
function isTokenExpired(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
// exp is in seconds, Date.now() is in milliseconds
return payload.exp * 1000 < Date.now();
} catch { return true; }
}
async function getUser() {
let token = localStorage.getItem('access_token');
if (!token || isTokenExpired(token)) {
token = await refreshAccessToken(); // Use refresh_token to get new access_token
localStorage.setItem('access_token', token);
}
const res = await fetch('/api/me', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.status === 401) {
// Token refreshed but still 401 — force re-login
return redirectToLogin();
}
return res.json();
}JWT access tokens have an expiry claim (exp) in the payload — the number of seconds since Unix epoch when the token expires. Decode the payload with atob(token.split('.')[1]) and check exp * 1000 against Date.now(). Refresh proactively before the token expires rather than waiting for a 401. Always handle the case where even a freshly refreshed token returns 401 — this catches revoked refresh tokens or account suspension scenarios.
What a 401 actually means at the HTTP level
The HTTP specification name '401 Unauthorized' is notoriously misleading. Despite the name, this status code means the request lacks valid authentication credentials — not that the authenticated user lacks authorization to perform the action. Authorization (permission) is what 403 Forbidden is for. This distinction matters because the fix for 401 is always about authentication: providing credentials, fixing the format, or refreshing an expired token.
When a server returns 401, it must also include a WWW-Authenticate response header that specifies what authentication scheme is required. A typical Bearer response looks like 'WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The token has expired"'. The error field is especially useful — 'invalid_token' means the token is malformed or expired, 'missing_token' means no Authorization header was sent. Read this header before anything else when debugging a 401.
The Authorization request header format follows the scheme-credentials pattern: 'Authorization: Scheme credentials'. For Bearer tokens (JWT and opaque tokens), the format is 'Authorization: Bearer eyJ...' — capital B in Bearer, a single space, then the token. For Basic authentication (API keys encoded as base64), the format is 'Authorization: Basic base64(username:password)'. Some APIs use custom header names like X-API-Key instead of Authorization — always check the API documentation for the expected header name.
JWT tokens are the most common authentication mechanism for modern APIs and the most common source of 401 errors. A JWT has three base64url-encoded parts separated by dots: header, payload, and signature. The payload contains the exp (expiration) claim in Unix epoch seconds. After the token expires, every request with that token returns 401 until a new token is obtained using the refresh token flow. Many 401 errors in production are simply expired tokens that the application is not refreshing.
The difference between development and production environments causes a large proportion of 401 errors in deployment scenarios. Development API keys may have different permissions, different expiry times, or be scoped to different environments than production keys. When an application returns 401 immediately after deployment to a new environment, the first thing to check is whether the environment variables containing API keys and secrets have been properly configured for the new environment.
Diagnosing 401s from the response headers
Start every 401 investigation by examining the complete response, not just the status code. Open the browser DevTools Network tab, find the failing request, and look at the Response Headers panel. The WWW-Authenticate header tells you what the server expects. The response body often contains a specific error message — 'token expired', 'invalid signature', 'missing credentials', or 'invalid api key'. This message is more specific than the 401 status code alone.
If the request works in Postman or curl but returns 401 from your application code, the most likely cause is a header format difference. In your application code, log the exact value of the Authorization header you are sending: console.log('Authorization header:', request.headers.get('Authorization')). Compare this character by character with the working Postman request. Common differences include a missing 'Bearer ' prefix, extra whitespace, the token being wrapped in quotes, or a newline character at the end from environment variable parsing.
For JWT tokens, decode the payload to inspect the claims before sending the request. The payload is the second segment of the token (split by dot), base64url-decoded and JSON-parsed. Check the exp claim — multiply by 1000 to convert to milliseconds and compare with Date.now(). Check the aud (audience) claim — if the API validates that the token was issued for a specific audience and yours has the wrong value, you will get 401. Check the iss (issuer) — if you are using a token from the wrong auth server (development vs production), the issuer will not match.
For environment variable issues, verify that the variable is being read correctly in your runtime. In Node.js, console.log('API key prefix:', process.env.API_KEY?.substring(0, 8)) logs the first 8 characters of the key, enough to confirm it is the right key without exposing the full secret. In Next.js, any environment variable prefixed with NEXT_PUBLIC_ is embedded in the browser bundle and should never contain private API keys or secrets. Server-side routes use process.env directly and do not require the prefix. A common mistake is adding NEXT_PUBLIC_ to a variable that contains a private key, causing it to be sent to the browser where it can be extracted.
Use /tools/http-request-builder to send the authenticated request from outside your application. This isolates whether the issue is in the API endpoint (401 returned for everyone) or in your application's request construction (401 only when your code sends the request). If the request builder succeeds with the same token, the issue is in how your application constructs the Authorization header.
Fix patterns for each 401 scenario
For a missing Authorization header, the fix is to add it to every request that requires authentication. The most reliable pattern is an HTTP client wrapper or interceptor that adds the header automatically rather than relying on every call site to include it. In axios, use an interceptor: axios.interceptors.request.use(config => { config.headers.Authorization = 'Bearer ' + getToken(); return config; }). In native fetch, create a wrapper function that merges the Authorization header into every options object.
For expired JWT tokens, implement a proactive refresh strategy. Before each request, check the token's exp claim and refresh if it expires within the next 60 seconds. This prevents the race condition where the token is valid when you check but expires before the request arrives at the server. Implement a single-flight refresh: if multiple requests need a refresh simultaneously, share a single refresh Promise rather than sending multiple refresh requests. In Node.js, store the refresh Promise in a module-level variable and reuse it if it exists.
For the OAuth 2.0 refresh token flow in single-page applications, use the authorization_code grant with PKCE (Proof Key for Code Exchange). PKCE prevents authorization code interception attacks in public clients where a client secret cannot be kept confidential. The refresh flow uses the refresh_token (which has a longer expiry) to obtain a new access_token. Store the refresh token in an httpOnly cookie — not localStorage — because httpOnly cookies are inaccessible to JavaScript and therefore safe from XSS attacks.
For server-to-server authentication where no user is involved, use the client_credentials OAuth 2.0 grant. Your server authenticates with the API provider using a client_id and client_secret to obtain an access token with a defined expiry. Cache the token and reuse it for all requests until it expires, then request a new one. This eliminates the per-request authentication overhead and keeps your client_secret server-side where it cannot be exposed.
For API key authentication, verify you are using the correct key for the environment. Keep separate API keys for development, staging, and production in your secrets manager. Never hard-code API keys in source code — not even in configuration files that are in .gitignore. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, or at minimum environment variables injected at deployment) and rotate keys on a regular schedule. After a key rotation, verify the old key is removed from all deployment environments to prevent continued use of revoked credentials.
Edge cases that make 401 harder to reproduce
Clock skew between client and server causes intermittent 401 errors with JWTs. JWT validation checks the exp claim against the server's current time. If the client's clock is ahead of the server's clock, the client may compute that a token is still valid while the server considers it expired. Most JWT implementations include a leeway parameter (commonly 30 to 60 seconds) to accommodate clock skew. If you are seeing intermittent 401 errors with JWTs, check the system time on both client and server using an NTP time source.
HTTP/2 connection reuse can cause 401 errors to appear in unexpected requests. HTTP/2 multiplexes multiple requests over a single connection. If the connection was authenticated with a token that expires mid-connection, subsequent requests on the same connection may receive 401 even though the client sends a valid new token. This is because some servers associate the authentication state with the connection rather than the individual request. Adding Connection: close forces a new connection per request — inefficient but useful for debugging this class of issue.
Proxy servers that strip or modify Authorization headers are a subtle source of 401 errors that are difficult to trace. If your requests pass through a corporate proxy, CDN, or API gateway, verify that the Authorization header is forwarded unchanged. Some proxies normalize header names, strip 'sensitive' headers, or require their own authentication before forwarding your request. Log the headers your application sends and compare with the headers received at the API server using the API server's request logging.
PKCE (Proof Key for Code Exchange) failures in SPAs produce 401 or 400 errors during the OAuth callback that look like authentication failures. PKCE requires the code_verifier sent in the token exchange to match the code_challenge sent in the authorization request. If the user completes authorization in a different browser tab or session than the one that started the flow, the code_verifier stored in session storage will not match. Implement PKCE state validation carefully and ensure the code_verifier is associated with the correct OAuth state parameter.
Authentication mistakes that are hard to see in code review
Storing API keys in environment variables prefixed with NEXT_PUBLIC_ is a critical security mistake that also causes 401 errors in confusing ways. The NEXT_PUBLIC_ prefix embeds the variable value in the browser JavaScript bundle. Private API keys exposed to the browser can be extracted by anyone inspecting the bundle. More confusingly, some APIs detect requests from browser contexts and reject them with 401 even when the key itself is valid, because the key is only authorized for server-side use. Always use private keys in server-only code and remove NEXT_PUBLIC_ from any variable that contains a secret.
Not handling 401 as a distinct case in client-side error handling means authentication failures look identical to other errors. Many applications have a generic 'something went wrong' handler that catches all non-2xx responses. A 401 should specifically redirect to the login page or trigger a token refresh, not show a generic error. Distinguish between 401 (authenticate) and 403 (different permissions needed or contact support) and 4xx in general (client error) in your error handling hierarchy.
Reusing tokens across environments is particularly insidious because the token may be syntactically valid but rejected by the production API server. A development JWT signed with a development secret will not validate against the production server's secret. A Stripe test-mode key will return 401 when used against production Stripe endpoints and vice versa. Prefix your environment variable names with the environment (DEV_STRIPE_KEY, PROD_STRIPE_KEY) and add runtime assertions that verify the expected environment based on token prefix or API endpoint URL.
Not implementing an axios or fetch interceptor for token refresh means refresh logic gets copy-pasted to every API call site or simply omitted. When a 401 occurs mid-session because a token expired, every API call that does not have its own retry logic will fail permanently until the user manually refreshes the page. A single response interceptor that catches 401, refreshes the token once, and retries the original request is the correct centralized solution. Store the retry attempt count to prevent infinite loops when the refresh itself fails.
Authentication patterns that are robust in production
Build authentication into a centralized HTTP client module rather than handling it at individual call sites. This module should automatically attach the Authorization header to every request, check token expiry before sending, refresh the token when it is about to expire, and handle 401 responses by refreshing and retrying once before propagating the error. This pattern ensures consistent authentication behavior across the entire application and makes auth logic testable in isolation.
Use short-lived access tokens (15 to 60 minutes) paired with long-lived refresh tokens stored in httpOnly cookies. Short access token expiry limits the window of exposure if a token is leaked. httpOnly cookies cannot be accessed by JavaScript, providing protection against XSS attacks that steal tokens from localStorage. The refresh token flow is automatic and invisible to the user — their session persists as long as they use the application within the refresh token's expiry window.
For server-to-server API clients, cache access tokens and share them across requests rather than requesting a new token for every API call. The token request itself adds latency and counts against rate limits. Cache the token with its expiry time and request a new one only when the cached token is within 60 seconds of expiry. In distributed systems, store the cached token in Redis so all application instances share a single cached token rather than each fetching their own.
Rotate API keys and secrets on a regular schedule and immediately on suspected compromise. Configure your secrets manager to alert when keys are approaching their rotation deadline. Test the rotation process in a staging environment before rotating production keys to ensure your application handles the transition without downtime. Keep the previous key valid for 24 to 48 hours after rotation to allow in-flight requests to complete and catch any configuration that still uses the old key.
Use /tools/http-request-builder to verify authentication configuration after every environment change or key rotation. Send a test authenticated request to a simple endpoint (like a profile or me endpoint) and confirm the 200 response. This is a faster sanity check than running a full deployment and waiting for production errors. Use /tools/cors-tester to verify that your API endpoint correctly includes WWW-Authenticate in its 401 responses — some APIs omit this header, making client-side debugging significantly harder.
401 Unauthorized fix checklist
- ✓Check the WWW-Authenticate response header for the expected auth scheme and error details
- ✓Verify the Authorization header format: 'Bearer ' (capital B, single space) then the token
- ✓Decode the JWT payload and check the exp claim — is the token expired?
- ✓Confirm you are using the correct API key for the target environment (dev vs prod)
- ✓For Next.js: ensure private keys do not have the NEXT_PUBLIC_ prefix
- ✓If token expired: implement refresh_token flow and proactive expiry check before requests
- ✓Compare the exact Authorization header value between Postman (works) and your code (401)
- ✓Use HTTP Request Builder to verify the endpoint accepts your credentials directly
Related guides
Frequently asked questions
What is the difference between HTTP 401 and HTTP 403?
HTTP 401 means the request lacks valid authentication credentials — the server does not know who you are. HTTP 403 means the server knows who you are but your account does not have permission for the action. Fix 401 by providing or fixing your credentials. Fix 403 by requesting the required permissions or checking your account's role. Treating a 403 as a 401 and redirecting to login is a common mistake.
Why does my API key work in Postman but return 401 from my code?
The most common cause is a header format difference. Log the exact Authorization header value your code sends and compare it character by character with what Postman sends. Check for a missing 'Bearer ' prefix, extra whitespace, the token wrapped in quotes, or a newline character at the end of the value. Postman's console shows the exact headers sent — use it as the source of truth for the correct format.
How do I decode a JWT to check its expiry?
The JWT payload is the second segment between dots, base64url-encoded. Decode it in JavaScript with JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))). The exp field is Unix epoch seconds. Multiply by 1000 and compare with Date.now() — if exp * 1000 is less than Date.now(), the token is expired. Never use this decode for security checks — only for expiry time display. Always verify the signature server-side.
What is the OAuth 2.0 refresh token flow?
When an access token expires, send a POST request to the authorization server's token endpoint with grant_type=refresh_token and your refresh_token value. The server returns a new access_token and optionally a new refresh_token. Store the new tokens and use the new access_token for subsequent requests. If the refresh request returns 401, the refresh token has been revoked and the user must log in again.
Is it safe to store JWT tokens in localStorage?
localStorage is accessible to any JavaScript on the page, making tokens stored there vulnerable to XSS attacks. A safer alternative is httpOnly cookies, which are set by the server and cannot be read by JavaScript. For SPAs that must store tokens in the browser, use short access token expiry and refresh frequently. Never store long-lived refresh tokens in localStorage — use httpOnly cookies for those.
What does WWW-Authenticate: Bearer error='invalid_token' mean?
This response header means the server received a Bearer token but it failed validation. Common reasons: the token signature is invalid (wrong secret), the token is expired (exp claim is in the past), the token was issued for a different audience (wrong aud claim), or the token has been revoked. Decode the token payload to check the exp and aud claims, and verify the token was signed with the correct secret for this environment.
Why does my Next.js app return 401 even though the environment variable is set?
Check whether the environment variable is prefixed with NEXT_PUBLIC_. Variables with this prefix are embedded in the browser bundle and may be treated differently by APIs. Server-side API routes use process.env directly without the prefix. If you added NEXT_PUBLIC_ to a private API key variable, remove the prefix and ensure the key is only accessed in server-side code (API routes, getServerSideProps, server components).
What is PKCE and when is it required for OAuth?
PKCE (Proof Key for Code Exchange) prevents authorization code interception in public clients — SPAs and mobile apps where a client_secret cannot be kept confidential. Generate a random code_verifier, hash it to create a code_challenge, send the challenge with the authorization request, and send the verifier with the token exchange. Most OAuth 2.0 providers require PKCE for public clients. Omitting PKCE when required causes 401 or 400 errors during token exchange.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.