HTTP Status CodesRunbook 0/4 done
403

HTTP Status Codes Error 403: Forbidden

Error✕ Not retryable · fix the payloadHTTP 403 ForbiddenAPI Gateway
TL;DR · 10-second read

The server knows who you are but refuses the action: missing role, scope, IP allowlist or WAF block. Re-authenticating will not help; grant the permission or change the request.

Identify: does your log match?

What you see · exact log / response
HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error":"forbidden","message":"Missing scope: repo:write"}
  1. High

    Missing role or OAuth scope

    The token is valid but was issued without the scope (e.g. repo:write) or the user lacks the role the endpoint requires.

  2. Medium

    WAF, CDN or IP allowlist block

    Cloudflare, AWS WAF or nginx allow/deny rules reject the client IP or user agent before the app runs.

  3. Medium

    CSRF or origin check failed

    Frameworks return 403 when the CSRF token or Origin header does not match for state-changing requests.

Fix

Return 401 vs 403 deliberately

✕ Broken · Node.js
app.delete('/projects/:id', async (req, res) => {
  if (!req.user || req.user.role !== 'admin') return res.sendStatus(401);
});
✓ Fixed · Node.js
app.delete('/projects/:id', async (req, res) => {
  if (!req.user) return res.status(401).set('WWW-Authenticate', 'Bearer').end();
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'forbidden', required: 'admin' });
  }
  // ...delete
});

Clients refresh tokens on 401 and show "no permission" on 403. Mixing them causes refresh loops.

Verify: reproduce it on purpose

Reproduce locally · curl
$ curl -i https://httpbin.org/status/403

Run it against a dev environment to trigger 403 on purpose and test your error handler.

Prevent

  • Include the missing permission in the 403 body.
  • Return 404 instead of 403 when existence itself is secret.
  • Log the policy decision with user id and resource.

FAQ

Is 403 retryable?

No. The same request will be rejected again until permissions or the request change.

Why does S3 return 403 for a missing object?

Without s3:ListBucket, S3 hides whether a key exists and returns AccessDenied (403) instead of NoSuchKey (404).

Should I return 403 or 404 for another tenant's resource?

404 avoids leaking that the id exists. GitHub does this for private repos.

Related errors

Last verified 2026-10-21 · Source: IANA HTTP Status Code Registry (RFC 9110) · Official docs ↗ · ~49,500 searches/mo