DEVESSENTIALS

HTTP 401 Unauthorized

Despite the name, 401 is about authentication (who are you?), not authorization (what are you allowed to do?). The server is saying: I don't know who you are; send valid credentials and try again.

4xx · Client errorRFC 9110 §15.5.2

401 Unauthorized

The request lacks valid authentication credentials for the target resource.

Who fixes it: Client. The client has to send valid credentials. Server-side setup (CORS order, token validation, proxies) is often why valid credentials don't arrive.

What 401 means

RFC 9110 requires a 401 to include a WWW-Authenticate header with at least one challenge naming the scheme to use (Basic, Bearer, Digest...). If the request already had credentials, the 401 means they were rejected.

A browser that receives WWW-Authenticate: Basic shows its built-in username and password dialog. APIs using tokens should send WWW-Authenticate: Bearer, which doesn't trigger a dialog.

Source: RFC 9110 §15.5.2.

Common causes

  • The access token (often a JWT) has expired. Check its exp claim. Short-lived tokens without a refresh flow produce 401s after exactly N minutes.
  • A malformed header: Authorization: <token> without the Bearer prefix, Bearer Bearer ..., surrounding quotes, or a trailing newline copied from an environment variable.
  • Clock skew: nbf or iat is in the future for the server, or exp has passed on one machine but not another.
  • Wrong audience or issuer: a staging token sent to production, or an OpenID Connect ID token sent where an access token is expected.
  • CORS preflight: the browser's OPTIONS request never includes Authorization. If auth middleware runs before CORS handling, the preflight gets a 401 and the real request is never sent.
  • Credentials dropped on a redirect: fetch() removes Authorization when following a redirect to a different origin, and curl -L won't send it to another host either. A redirect from api.example.com to www.api.example.com silently turns an authenticated call into a 401.
  • Behind nginx, a custom auth header containing underscores (X_API_KEY) is dropped, because underscores_in_headers is off by default.

How to debug and fix a 401

Read the WWW-Authenticate header

For bearer tokens, the error parameter (RFC 6750) says whether the request was malformed (invalid_request) or the token was expired, revoked or invalid (invalid_token). Decode the token to check exp, aud and iss.

curl -i https://api.example.com/me \
  -H "Authorization: Bearer $TOKEN"

HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The access token expired"

Express: handle CORS before auth

The cors middleware answers preflight OPTIONS requests itself, so they never reach your auth check.

import cors from 'cors';

app.use(cors({ origin: 'https://app.example.com', credentials: true }));

app.use('/api', (req, res, next) => {
  const [scheme, token] = (req.get('Authorization') ?? '').split(' ');
  if (scheme !== 'Bearer' || !token) {
    res.set('WWW-Authenticate', 'Bearer realm="api"');
    return res.status(401).json({ error: 'missing_token' });
  }
  // verify the token, then:
  next();
});

Client: refresh once on 401

Retry exactly once with a fresh token. Looping on 401 can lock accounts or hammer the token endpoint.

async function apiFetch(url, options = {}) {
  const send = (token) =>
    fetch(url, {
      ...options,
      headers: { ...options.headers, Authorization: `Bearer ${token}` },
    });

  let res = await send(getAccessToken());
  if (res.status === 401) {
    res = await send(await refreshAccessToken());
  }
  return res;
}

Headers that matter with 401

WWW-Authenticate
Required in every 401. It names the scheme and, for bearer tokens, the reason (error="invalid_token").
Authorization
What the client sends: Authorization: Bearer <token> or Authorization: Basic <base64(user:pass)>.
Access-Control-Allow-Headers
For cross-origin calls, the preflight response must list Authorization, or the browser won't send it.

401 vs similar status codes

  • 403 Forbidden: The server knows who you are and still says no. New credentials won't help.
  • 407 Proxy Authentication Required: Like 401, but a proxy between you and the server wants credentials (Proxy-Authorization).
  • 400 Bad Request: The request itself is malformed, regardless of credentials.

SEO impact of 401

Pages behind authentication return 401 (or redirect to a login page) to Googlebot, so they aren't indexed, which is usually what you want. Protecting a staging site with HTTP auth is more reliable than robots.txt; just remember to remove it at launch.

Related status codes

Looking for another code? Search the full HTTP status codes list, or read which code your API should return in the REST API status codes guide.

Learn more

Browse all developer guides →

Frequently Asked Questions

What is the difference between 401 and 403?▾

401 means not authenticated: no credentials, or invalid ones, so log in and retry. 403 means authenticated but not allowed: the server knows who you are and the answer is still no, so retrying with the same credentials won't help.

Why do I get a 401 in the browser when curl works?▾

Usually CORS. The browser first sends an OPTIONS preflight without the Authorization header; if the server requires auth on OPTIONS, the preflight fails. Also check that Access-Control-Allow-Headers includes Authorization, and that cookie-based requests use credentials: 'include'.

Why does my token work and then suddenly return 401?▾

It expired. JWT access tokens typically live between 5 and 60 minutes. Decode the token to see its exp claim, and implement a refresh flow that gets a new access token when the API returns 401.