HTTP 403 Forbidden

Forbidden

HTTP 403 is Forbidden: the server understood the request and refuses to carry it out. RFC 9110 section 15.5.4 adds that if the request carried credentials, the server considers them insufficient, and the client shouldn’t repeat the request with the same ones. Unlike a 401, a 403 needs no WWW-Authenticate header, and signing in again won’t change the answer.

What sends it

  • Your app, for a signed-in user without the right role, or a token without the right scope; RFC 6750 pairs insufficient_scope with 403.
  • nginx, when a deny rule matches, when a directory has no index file and autoindex is off, and when its worker can’t read the file (Permission denied in the error log).
  • Django, when a POST fails the CSRF check.
  • S3, for a key that doesn’t exist, when the caller lacks s3:ListBucket. A static site on S3 behind CloudFront therefore shows 403 for pages that simply aren’t there.
  • AWS WAF, whose block action answers 403 by default, and API Gateway REST APIs, whose Missing Authentication Token 403 usually means the path or method doesn’t exist.

403, 401 or 404

not signed in, or the token expired        401
signed in, not allowed                     403
not allowed, and its existence is secret   404

RFC 9110 lets a server answer 404 instead of 403 to hide that a resource exists. GitHub’s API does this for private repositories, so an outsider can’t probe for their names. A 403 confirms the thing is there.

What the client should do

Not retry. Tell the user they lack access, offer a way to ask for it or to sign in as someone else. A 403 from a WAF or CDN often arrives as an HTML page even when you asked for JSON, so check Content-Type before parsing the body.

403 isn’t heuristically cacheable. Google drops pages that answer 403 from its index, as it does for every 4xx except 429, and asks sites not to use 401 or 403 to slow its crawler. To ask Googlebot to back off, answer 429 or 503 for a while.

In your stack

if (!req.user.roles.includes('admin')) return res.status(403).json({ error: 'forbidden' });   // Express
location /internal/ {
    allow 10.0.0.0/8;
    deny all;           # everyone else gets 403
}
raise PermissionDenied      # Django: renders 403.html
abort(403)                  # Flask
http.Error(w, "forbidden", http.StatusForbidden)

Try it in the HTTP Status Codes

403 Forbidden

  1. 403Forbidden4xx Client error

    The server understood the request and refuses to do it. The credentials, if there are any, are not the problem, and sending them again will not help.

    When to use it

    A signed-in user reaching something outside their role, a blocked address, or a resource the app will not serve to this caller.

    Common mistakes

    Using it for a missing login (that is 401) and for a resource that does not exist (that is 404; a 403 here tells the caller the resource is real).

    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.