< HTTP/1.1 403 ForbiddenHTTP 403 Forbidden: Access Denied
403 Forbidden means the server refuses the request. Find whether Cloudflare, nginx, Apache, S3 or your app sent it; fix permissions, WAF rules and role checks.
- Cacheable
- Only with explicit freshness
- Retry?
- No: different credentials may help, a retry will not
- Usually sent by
- Origin app, WAF or CDN
- Spec
- RFC 9110 §15.5.4
On this page
TL;DR: 403 Forbidden means the server refuses to let you access this URL. Find whether the response came from your app, web server or WAF, then check the matching role, scope, filesystem permission or blocking rule.
RFC 9110 section 15.5.4: the server “understood the request but refuses to fulfill it”. Unlike 401, re-authenticating normally does not help, though a different account might. A 403 can come from your application, the web server, a WAF, or the CDN, and the fix differs for each, so identify the sender first. The longer walkthrough is in the 403 Forbidden debug guide.
The messages and response bodies here are illustrative. Client messages use https://api.example.com/resource; reason phrases and URLs can vary.
HTTP/1.1 403 Forbidden
Server: cloudflare
CF-Ray: 8f1c2a9b7e0d1234-AMS
Content-Type: text/html
<title>Attention Required! | Cloudflare</title>
Sorry, you have been blocked
What you see in your client
- Axios:
AxiosError: Request failed with status code 403with the default status handling. Inspecterror.response.datafor the server’s explanation. Axios source. - fetch: the promise resolves with
response.status === 403andresponse.ok === false. Check the status before reading the body; HTTP errors do not entercatchautomatically. MDN fetch. - Python requests:
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://api.example.com/resourcewhen you callresponse.raise_for_status(). Requests source. - curl -f:
curl: (22) The requested URL returned error: 403. Usecurl -iwithout-fwhile inspecting the error body. curl source. - .NET:
HttpRequestExceptionwith the English messageResponse status code does not indicate success: 403 (Forbidden).afterEnsureSuccessStatusCode(). Runtime message template. - Spring WebClient:
WebClientResponseException$Forbidden: 403 Forbidden from POST https://api.example.com/resourcewithretrieve(). ReadgetResponseBodyAsString(). Spring 6.2 source. - Spring RestTemplate (6.2):
HttpClientErrorException$Forbidden: 403 Forbidden on POST request for "https://api.example.com/resource": [no body]with the default error handler and an empty response body. Read the exception body when present. Message builder, exception subclasses.
Who sent it?
| Evidence | Layer |
|---|---|
Server: cloudflare, CF-Ray, page titled “Attention Required! | Cloudflare” with a Ray ID | Cloudflare firewall, WAF managed rule, Bot Fight Mode or IP Access Rule. Look up the Ray ID under Security > Events |
Server: nginx, body 403 Forbidden; error log directory index of "/var/www/html/" is forbidden or open() "/var/www/x" failed (13: Permission denied) | nginx filesystem or deny rule |
Server: Apache, log AH01630: client denied by server configuration or AH00035: access to / denied ... because search permissions are missing on a directory | Apache Require/permissions |
Request blocked. We can't connect to the server for this app or website at this time with X-Cache: Error from cloudfront | CloudFront with WAF or geo restriction |
XML <Error><Code>AccessDenied</Code> | S3 bucket policy, Block Public Access, or missing s3:GetObject |
ModSecurity / Mod_Security in body, or This request has been blocked by the website's security rules | ModSecurity or a host WAF |
JSON {"error":"Forbidden"} | Your application’s authorization check |
Fix it, most common first
- Missing index file or directory listing off. A request for
/with noindex.htmlandautoindex offgives 403. Add the file or anindexdirective. - Filesystem permissions. The web server user (
www-data,nginx,apache) needsxon every parent directory andron the file. Check withnamei -l /var/www/site/index.html. SELinux:ls -Zandrestorecon -Rv /var/www. - A WAF or CDN rule. Find the event for your request (Cloudflare Security Events by Ray ID, AWS WAF sampled requests, ModSecurity audit log) and tune or exclude that rule rather than disabling the WAF.
- IP, Referer, User-Agent or country block. For a configured User-Agent rule, compare with
-A 'Mozilla/5.0'. Hotlink protection returns 403 whenRefereris foreign. - Authorization logic. The user is authenticated but lacks the role, scope, or ownership. Return the missing scope in the body, and use
WWW-Authenticate: Bearer error="insufficient_scope"for OAuth APIs. - CSRF or same-origin checks. Django’s
CSRF verification failed. Request aborted.returns 403; Rails mapsInvalidAuthenticityTokento 422. Pass the token or fixOriginhandling behind your proxy (CSRF_TRUSTED_ORIGINS,X-Forwarded-Proto). - Expired signed URL. S3 and CloudFront signed URLs return 403 after expiry or when the signature, region, or clock does not match.
- WordPress and plugins. Inspect
.htaccessafter a permalink or security-plugin change; compare it with WordPress’s documented rules and look for an unintended deny directive. WordPress Apache configuration.
Common causes by stack
- Django REST framework: an authenticated user failing a permission class gets 403;
SessionAuthenticationalso returns 403 for unauthenticated permission failures. Check the authentication class order andpermission_classes. DRF authentication. - Laravel: a denied gate or policy throws
AuthorizationException, converted to 403; inspect the policy method and resource ownership. CSRF failures instead map to 419. Laravel authorization, exception handler. - ASP.NET Core JWT bearer: an authenticated user failing an authorization policy gets 403; check required roles and claims rather than refreshing the same token. Microsoft JWT guidance.
- AWS ALB / API Gateway: a WAF web ACL can make ALB return 403; API Gateway’s
ACCESS_DENIED,INVALID_API_KEYandMISSING_AUTHENTICATION_TOKENdefault to 403. The last also covers an unsupported method or resource, so verify the route and stage URL. ALB troubleshooting, gateway responses. - Kubernetes ingress-nginx: an external
auth-urlservice returning 403 denies the request throughauth_request; check that service’s decision and forwarded credentials. Ingress annotations, nginx auth_request.
Fix by stack
nginx
server {
root /var/www/site;
index index.html; # missing index => "directory index ... is forbidden"
location /admin/ {
allow 203.0.113.0/24; # first match wins
deny all; # everyone else gets 403
}
}
Behind a proxy or CDN, check which address allow/deny evaluates. Configure set_real_ip_from for trusted proxies and real_ip_header X-Forwarded-For when restoring the client address. nginx realip.
Apache 2.4
<Directory /var/www/site>
# Grant access here; review inherited Require rules too
Require all granted
</Directory>
Express, Next.js, Django
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) return res.status(401).set('WWW-Authenticate', 'Bearer').end()
if (!roles.includes(req.user.role)) return res.status(403).json({ error: 'forbidden', required: roles })
next()
}
}
In Next.js, middleware that returns new NextResponse(null, { status: 403 }) is not the same as the framework forbidden() helper, which is experimental and needs experimental.authInterrupts enabled. In Django, PermissionDenied produces 403 and renders 403.html.
Cloudflare
Check Security > Events for the Ray ID. Allow a legitimate integration with a WAF custom rule using action Skip, scoped to a path plus a secret header or IP, not a blanket disable.
A token works, but the API still returns HTML
An OAuth token can pass your API’s checks while an edge rule rejects the request earlier. If an API call returns text/html with a block page, save the response headers and request ID before trying to parse JSON. For Cloudflare, match the Ray ID to the security event; for API Gateway, check the response type and authorizer decision. A 403 from a token endpoint needs that endpoint’s policy checked separately from the resource API. Cloudflare 403 guidance.
Named API failures
For MongoDB Atlas Administration API 403s, inspect the returned error code. ACCESS_FORBIDDEN identifies access denied to the current user; IP_ADDRESS_NOT_ON_WHITELIST identifies a rejected source IP. Check the API key or service account’s project access and the Administration API IP access list. A token can be created from an unlisted address yet fail when used for an API call. Atlas error codes, programmatic access setup.
For a Vertex AI Search widget reporting “configuration is not authorized,” check Integration → Widget: the page’s domain must be configured, including localhost during local testing. Confirm the selected authorization type and use the generated snippet’s configuration. JWT/OAuth mode needs a token supplied by your backend; Google documents the https://www.googleapis.com/auth/cloud-platform scope. Google widget setup.
Reproduce and verify
curl -sI https://example.com/admin/ | head -n 5 # status and Server header
curl -sI -A 'Mozilla/5.0' https://example.com/admin/ # UA-based block?
curl -sI -e https://example.com/ https://example.com/img/x.png # hotlink protection?
curl -s -o /dev/null -w '%{http_code} %{remote_ip}\n' https://example.com/
403 vs 404
Returning 404 for resources a user must not know exist is a legitimate choice (GitHub does it for private repositories), at the cost of harder debugging. Pick one policy and apply it everywhere. See also 401 vs 403, 401, 404, 451 for legally blocked content.
See also the comparison 403 vs 404: when to hide a resource behind a 404 instead of admitting it with a 403.
In practice
Express.js
// Role-based access control middleware
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).set('WWW-Authenticate', 'Bearer').json({ error: 'Authentication required' })
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Forbidden',
message: `Requires role: ${roles.join(' or ')}`
})
}
next()
}
}
app.delete('/api/users/:id', requireAuth, requireRole('admin'), async (req, res) => {
await db.users.delete(req.params.id)
res.status(204).end()
})Next.js App Router
// app/api/admin/users/route.ts
export async function DELETE(request: Request) {
const user = await getAuthUser(request)
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401, headers: { 'WWW-Authenticate': 'Bearer' } })
}
if (user.role !== 'admin') {
return Response.json(
{ error: 'Forbidden', message: 'Admin access required' },
{ status: 403 }
)
}
// Proceed with deletion...
return new Response(null, { status: 204 })
}Django
from django.http import JsonResponse, HttpResponse
from django.contrib.auth.decorators import login_required
def require_role(*roles):
def decorator(view_func):
@login_required # redirects anonymous users to the login URL (302), not 401
def wrapper(request, *args, **kwargs):
if request.user.role not in roles:
return JsonResponse(
{'error': 'Forbidden', 'message': f'Requires role: {roles}'},
status=403
)
return view_func(request, *args, **kwargs)
return wrapper
return decorator
@require_role('admin')
def admin_delete_user(request, user_id):
User.objects.filter(pk=user_id).delete()
return HttpResponse(status=204)Frequently asked questions
How can I tell whether a 403 is temporary?
The status alone has no expiry time. Check the rule or policy decision and its logs: a changed WAF rule, role assignment or signed URL may resolve it, but repeated requests with the same credentials should not be retried automatically.
Should an admin role check return 403 after successful authentication?
Yes. If the user is authenticated but lacks the required admin role, return 403. Check the assigned role or authorization policy; refreshing a token with the same permissions will not fix that denial.
What does 403 Forbidden mean?
The server understood the request and refuses to authorize it (RFC 9110 section 15.5.4). It applies whether or not credentials were sent, and clients should avoid automatically repeating the same request with the same credentials.
What is the difference between 401 and 403?
401 says authentication is missing or invalid and must include a WWW-Authenticate challenge; supplying valid credentials may help. 403 says the server refuses regardless, usually because the authenticated user lacks permission or a rule blocks the request.
Why do I get 403 Forbidden on my own nginx or Apache site?
Typically the web server user cannot read the file or traverse a parent directory, there is no index file and directory listing is off (nginx logs directory index of ... is forbidden), or an allow/deny rule matched. Apache 2.4 logs AH01630 when Require rules deny the request.
Why do I get 403 from Cloudflare?
A firewall custom rule, managed WAF rule, IP Access Rule, or Bot Fight Mode blocked the request. The block page shows a Ray ID you can search for under Security Events to see which rule matched.
Should I use 403 or 404 for resources a user cannot access?
Use 403 when revealing that the resource exists is acceptable. Use 404 when existence itself is sensitive; RFC 9110 explicitly allows origin servers to use 404 to hide a forbidden resource.
Sources
Related
401 vs 403: Authentication vs Authorization
Understand the difference between 401 Unauthorized and 403 Forbidden. Learn when each status code applies, common mistakes, and how to use them correctly in APIs.
403 vs 404: Forbidden or Not Found
403 Forbidden vs 404 Not Found: when to admit a resource exists, when to hide it, the GitHub private-repo example, and the debugging cost of masking 403 as 404.
419 Page Expired (Laravel CSRF Token Mismatch)
Laravel 419 Page Expired means the CSRF token did not match the session. Causes: expired session, missing @csrf, dropped cookie, cached forms. Fixes included.
HTTP 401 Unauthorized: Authentication Required
401 Unauthorized means missing or invalid credentials. Read WWW-Authenticate, check the Authorization header, token expiry and proxies, with fixes by stack.
407 Proxy Authentication Required: Fix It
407 means your proxy wants credentials before forwarding the request. Fix it in curl, npm, pip, git and browsers, and learn how Proxy-Authenticate works.
HTTP Authentication Methods and Best Practices
A comprehensive guide to HTTP authentication methods including Basic Auth, Bearer tokens, API keys, and OAuth 2.0.