HTTP 401 Unauthorized

Unauthorized

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

  • Basic opens a login box. Browsers show their own username and password dialog for WWW-Authenticate: Basic. A single-page app with its own sign-in form should challenge with Bearer instead.
  • Browsers usually get a redirect. Web apps send a signed-out visitor a 302 to the login page, as Django’s @login_required does; 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 Authorization unless the response says public, s-maxage or must-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)

Try it in the HTTP Status Codes

401 Unauthorized

  1. 401Unauthorized4xx Client error

    The request lacks valid credentials for this resource. Despite the name it means unauthenticated: sign in, then send the request again. The WWW-Authenticate header says how.

    When to use it

    A missing, expired or wrong token or session cookie, and the challenge a browser shows as a login prompt.

    Common mistakes

    Confusing it with 403 — being signed in and still not allowed is 403 — and forgetting WWW-Authenticate, which leaves a browser with nothing to do.

    Not cacheable by defaultRFC 9110

Every code, by class

Codes marked unofficial come from one server’s own convention — nginx, Cloudflare, cPanel, Microsoft — and are not in the IANA registry.

Runs in your browser. Nothing you enter is sent anywhere.