Beginner 5 min HTTP 405

405 Method Not Allowed — Wrong HTTP Verb

الأعراض

- An API endpoint returns 405 Method Not Allowed with no further explanation
- The response includes an `Allow` header listing which methods are accepted (e.g., `Allow: GET, HEAD, OPTIONS`) — your request used a different method
- The same URL works when accessed with a different HTTP method, confirming the route exists but the verb is wrong
- A redirect (301 or 302) changes a POST to a GET internally, and the destination only accepts POST, causing an unexpected 405
- A load balancer or WAF is blocking PUT, DELETE, or PATCH while allowing GET and POST — these methods never reach the application server

الأسباب الجذرية

  • Client sending POST to a GET-only endpoint or GET to a POST-only endpoint — a common mistake when reading API docs or copy-pasting curl commands
  • URL typo causing the request to hit a different route than intended, one that only accepts a different method
  • REST framework auto-generating method restrictions based on the view class — a `ListAPIView` only accepts GET; `CreateAPIView` only accepts POST
  • A 301/302 redirect from the original URL changes the method from POST to GET per RFC 7231 (browsers follow this; curl follows with `-L`)
  • Load balancer, WAF, or API gateway blocking specific HTTP methods like PUT, DELETE, or PATCH at the network layer before they reach the application

التشخيص

**Step 1 — Read the Allow header to see what the endpoint accepts**

```bash
curl -v -X POST https://api.example.com/resource/123
# Look for:
# < HTTP/1.1 405 Method Not Allowed
# < Allow: GET, HEAD, OPTIONS
# Means this endpoint only accepts GET. Use GET instead.
```

**Step 2 — Send an OPTIONS request to discover allowed methods**

```bash
curl -v -X OPTIONS https://api.example.com/resource/123
# Django REST Framework / FastAPI return Allow header on OPTIONS:
# < Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
# < HTTP/1.1 200 OK
```

**Step 3 — Check for redirect-induced method change**

```bash
# Use -v to see each hop and its method
curl -v -L -X POST https://api.example.com/resource
# If you see:
# < HTTP/1.1 301 Moved Permanently
# < Location: https://api.example.com/resource/
# > POST (changes to GET after redirect)
# < HTTP/1.1 405 Method Not Allowed
# → The trailing slash redirect changed POST to GET
```

**Step 4 — Verify your WAF/load balancer allows the method**

```bash
# Test if the method is blocked at the network layer by bypassing it:
curl -v -X DELETE http://direct-backend-ip/resource/123 \
-H 'Host: api.example.com'
# If this works but the public URL doesn't → WAF is blocking DELETE
```

**Step 5 — Check Django URL and view configuration**

```python
# In Django REST Framework:
class ResourceViewSet(viewsets.ModelViewSet):
# ModelViewSet allows GET, POST, PUT, PATCH, DELETE by default
# If you use ReadOnlyModelViewSet — only GET is allowed
pass

# In Django function-based views:
from django.views.decorators.http import require_http_methods

@require_http_methods(['GET', 'POST']) # Only these methods are allowed
def my_view(request):
pass
```

الحل

**Fix 1: Match the HTTP method to what the API specifies**

```bash
# If Allow: GET, read the resource with GET:
curl https://api.example.com/resource/123

# If Allow: POST, create with POST (include a body):
curl -X POST https://api.example.com/resource/ \
-H 'Content-Type: application/json' \
-d '{"name": "example"}'

# For updates, use PATCH (partial) or PUT (full replacement):
curl -X PATCH https://api.example.com/resource/123 \
-H 'Content-Type: application/json' \
-d '{"name": "updated"}'
```

**Fix 2: Handle the redirect method-change in HTTP client**

```python
import httpx

# httpx preserves the method on redirect by default for POST:
resp = httpx.post('https://api.example.com/resource', json=data)

# Or use follow_redirects=False and handle manually:
resp = httpx.post(
'https://api.example.com/resource',
json=data,
follow_redirects=False,
)
if resp.status_code == 301:
# POST to the final URL directly
resp = httpx.post(resp.headers['location'], json=data)
```

**Fix 3: Expose the correct methods in Django REST Framework**

```python
from rest_framework import viewsets
from rest_framework.decorators import action

class ItemViewSet(viewsets.ModelViewSet):
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']

@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""Custom action on POST /items/{pk}/publish/"""
...
```

**Fix 4: Configure WAF to allow required methods**

For AWS WAF or Cloudflare, add a rule that allows PUT/DELETE/PATCH for API paths like `/api/*` while keeping restrictions on other paths.

الوقاية

- **Document the HTTP method alongside every endpoint URL** in your API documentation — many 405 errors occur simply because the caller read `/api/resource/123` without noticing the required method
- **Return 405 with a helpful Allow header** from your application — Django REST Framework does this automatically, but custom views may not
- **Use POST to the exact destination URL** for mutation requests — avoid relying on redirect following for anything other than GET requests, since redirects change POST to GET per HTTP spec
- **Test all methods on every endpoint in your API test suite** — a simple check that `GET /resource` returns 200 and `POST /resource` returns 405 (or 201) catches method routing bugs before production

رموز الحالة ذات الصلة

المصطلحات ذات الصلة