Oversized request body: 413 with a short warning instead of 500 and an ERROR traceback

Found on the live server (3 MB JSON to /api/auth/login answered 500). Django raises
RequestDataTooBig for bodies over DATA_UPLOAD_MAX_MEMORY_SIZE and would answer 400, but our DRF
exception handler turned every unknown exception into 500 + logger.exception, so each large request
also wrote a full ERROR traceback. Other SuspiciousOperation subclasses now get Django's 400 as well.

Tested: 2 tests (the 413 one fails without the change). Verified on the server after deploy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 14:48:03 +03:30
parent ff370112c0
commit 1d7eb829e2
3 changed files with 46 additions and 1 deletions

View File

@@ -7,6 +7,7 @@ JSON. این هندلر اون حالت رو با traceback کامل لاگ می
"""
import logging
from django.core.exceptions import RequestDataTooBig, SuspiciousOperation
from rest_framework.response import Response
from rest_framework.views import exception_handler as drf_exception_handler
@@ -19,11 +20,23 @@ def custom_exception_handler(exc, context):
return response
request = context.get('request')
path = getattr(request, 'path', '')
# بدنه‌ی بزرگ‌تر از DATA_UPLOAD_MAX_MEMORY_SIZE (≈ ۲٫۵ مگابایت): Django خودش این را ۴۰۰ می‌کند، ولی
# چون DRF آن را نمی‌شناسد پیش‌تر اینجا «۵۰۰ + ERRORِ لاگ با traceback» می‌شد و هر درخواستِ بزرگ لاگ را پر
# می‌کرد (۲۰۲۶-۱۰-۰۷ روی سرورِ زنده دیده شد). باقیِ SuspiciousOperationها هم همان ۴۰۰ِ Django را می‌گیرند.
if isinstance(exc, RequestDataTooBig):
logger.warning('request_too_large path=%s', path)
return Response({'detail': 'حجمِ درخواست بیش از حدِ مجاز است.'}, status=413)
if isinstance(exc, SuspiciousOperation):
logger.warning('suspicious_operation path=%s type=%s', path, type(exc).__name__)
return Response({'detail': 'درخواستِ نامعتبر.'}, status=400)
user = getattr(request, 'user', None)
user_id = user.id if getattr(user, 'is_authenticated', False) else None
logger.exception(
'unhandled_exception path=%s user_id=%s',
getattr(request, 'path', ''), user_id,
path, user_id,
)
return Response(
{'detail': 'خطای غیرمنتظره‌ای رخ داد؛ لطفاً بعداً دوباره تلاش کنید.'},

View File

@@ -0,0 +1,31 @@
"""
بدنه‌ی درخواستِ بزرگ ۵۰۰ و ERRORِ لاگ نمی‌دهد (config/exceptions.py). روی سرورِ زنده، JSONِ ۳ مگابایتی به
/api/auth/login ۵۰۰ داده بود؛ ۲۰۰ کیلوبایت درست ۴۰۰ می‌داد.
"""
import json
from django.core.cache import cache
from django.test import TestCase, override_settings
from rest_framework.test import APIClient
@override_settings(DATA_UPLOAD_MAX_MEMORY_SIZE=2_621_440)
class OversizedBodyTests(TestCase):
def setUp(self):
cache.clear()
def _post(self, path, size):
body = json.dumps({'phone': '09120000000', 'otp': '1' * size})
return APIClient().post(path, data=body, content_type='application/json')
def test_a_body_over_the_limit_is_413_with_a_persian_message(self):
for path in ('/api/auth/login', '/api/auth/otp/request'):
with self.assertNoLogs('config.exceptions', 'ERROR'):
response = self._post(path, 3_000_000)
self.assertEqual(response.status_code, 413, path)
self.assertIn('حجم', response.json()['detail'])
def test_a_large_but_allowed_body_still_gets_the_normal_validation_error(self):
response = self._post('/api/auth/login', 200_000)
self.assertEqual(response.status_code, 400)
self.assertIn('otp', response.json())