HTTP 401 is Unauthorized, and despite the name it means unauthenticated: the request lacks valid credentials for this resource. RFC 9110 section 15.5.2 requires the server to send a WWW-Authenticate header with at least one challenge saying how to authenticate. If the request did carry credentials, the 401 says they were refused; the client can get new ones and try again.
The exchange
GET /api/orders HTTP/1.1
Authorization: Bearer eyJhbGciOi...
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"
RFC 6750 defines the Bearer errors and their codes: invalid_request is 400, invalid_token is 401 and insufficient_scope is 403. A client that sees invalid_token should refresh its token once and retry; a second 401 means signing in again.
401 or 403
no token, expired token, wrong password 401 sign in, then retry
signed in, but not allowed to do this 403 the same credentials won't help
Django REST Framework shows how fine the line is: for the same missing login it answers 401 when the view’s first authentication class can write a WWW-Authenticate header (token or Basic auth) and 403 when it can’t (session auth). Search 401 403 in the tool below to compare the two cards.
Gotchas
Basicopens a login box. Browsers show their own username and password dialog forWWW-Authenticate: Basic. A single-page app with its own sign-in form should challenge withBearerinstead.- Browsers usually get a redirect. Web apps send a signed-out visitor a 302 to the login page, as Django’s
@login_requireddoes; APIs answer 401. - Proxies use 407. A proxy asking for credentials answers 407 Proxy Authentication Required, with
Proxy-Authenticate. - Caching. 401 isn’t heuristically cacheable. And a shared cache won’t reuse a response to a request that carried
Authorizationunless the response sayspublic,s-maxageormust-revalidate(RFC 9111 section 3.5). - Search. Google doesn’t index pages that answer 401, and asks sites not to use 401 or 403 to slow its crawler.
In your stack
nginx’s auth_basic sends the 401 and the Basic challenge for you:
location /admin/ {
auth_basic "Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
}
res.set('WWW-Authenticate', 'Bearer realm="api"').status(401).json({ error: 'invalid_token' }); // Express
raise HTTPException(status_code=401, detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}) # FastAPI
w.Header().Set("WWW-Authenticate", `Basic realm="admin"`)
http.Error(w, "unauthorized", http.StatusUnauthorized)