From 8d8b841051c2ca0fbcdce4a558b8d4a04749e2de Mon Sep 17 00:00:00 2001 From: Renn F Date: Fri, 25 Sep 2026 21:23:18 +0200 Subject: [PATCH 1/2] fix(adapters): set explicit text/plain content type on blocked responses --- djangoapi_guard/adapters.py | 4 +- tests/test_adapters.py | 1 + tests/test_block_response_content_type.py | 71 +++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 tests/test_block_response_content_type.py diff --git a/djangoapi_guard/adapters.py b/djangoapi_guard/adapters.py index 7e72778..3e74a48 100644 --- a/djangoapi_guard/adapters.py +++ b/djangoapi_guard/adapters.py @@ -123,7 +123,9 @@ def read_body_prefix(self, max_bytes: int) -> bytes: class DjangoResponseFactory: def create_response(self, content: str, status_code: int) -> DjangoGuardResponse: - return DjangoGuardResponse(HttpResponse(content, status=status_code)) + return DjangoGuardResponse( + HttpResponse(content, status=status_code, content_type="text/plain; charset=utf-8") + ) def create_redirect_response( self, url: str, status_code: int diff --git a/tests/test_adapters.py b/tests/test_adapters.py index e16106a..91efc25 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -136,6 +136,7 @@ def test_django_response_factory_create() -> None: factory = DjangoResponseFactory() guard_resp = factory.create_response("error", 403) assert guard_resp.status_code == 403 + assert guard_resp.headers["Content-Type"] == "text/plain; charset=utf-8" def test_django_response_factory_redirect() -> None: diff --git a/tests/test_block_response_content_type.py b/tests/test_block_response_content_type.py new file mode 100644 index 0000000..93a4479 --- /dev/null +++ b/tests/test_block_response_content_type.py @@ -0,0 +1,71 @@ +from django.http import HttpRequest, HttpResponse +from django.test import RequestFactory +from guard_core.models import SecurityConfig +from guard_core.protocols.response_protocol import GuardResponse + +from djangoapi_guard.middleware import DjangoAPIGuard + +BLOCKED_IP = "203.0.113.10" + + +def _config(**overrides: object) -> SecurityConfig: + return SecurityConfig( + blacklist=[BLOCKED_IP], + enable_rate_limiting=False, + enable_redis=False, + enable_agent=False, + enable_penetration_detection=False, + **overrides, # type: ignore[arg-type] + ) + + +def _make_middleware(config: SecurityConfig) -> DjangoAPIGuard: + import django.conf + + django.conf.settings.GUARD_SECURITY_CONFIG = config + + def get_response(request: HttpRequest) -> HttpResponse: + return HttpResponse("OK", status=200) + + return DjangoAPIGuard(get_response) + + +def _blocked_response_headers(config: SecurityConfig) -> tuple[int, str, str]: + middleware = _make_middleware(config) + factory = RequestFactory() + request = factory.get("/ping") + request.META["REMOTE_ADDR"] = BLOCKED_IP + response = middleware(request) + return ( + response.status_code, + response.get("Content-Type", ""), + response.get("X-Content-Type-Options", ""), + ) + + +def test_block_response_is_plain_text() -> None: + assert _blocked_response_headers(_config()) == ( + 403, + "text/plain; charset=utf-8", + "nosniff", + ) + + +def test_custom_error_message_is_plain_text() -> None: + config = _config(custom_error_responses={403: "Access denied"}) + + status_code, content_type, _nosniff = _blocked_response_headers(config) + + assert (status_code, content_type) == (403, "text/plain; charset=utf-8") + + +def test_response_modifier_can_still_set_its_own_content_type() -> None: + def problem_json(response: GuardResponse) -> GuardResponse: + response.headers["Content-Type"] = "application/problem+json" + return response + + config = _config(custom_response_modifier=problem_json) + + status_code, content_type, _nosniff = _blocked_response_headers(config) + + assert (status_code, content_type) == (403, "application/problem+json") From 0029d0f70dca437498b6f3f0a9758bbc347800d5 Mon Sep 17 00:00:00 2001 From: Renn F Date: Fri, 25 Sep 2026 21:36:24 +0200 Subject: [PATCH 2/2] style: ruff format adapters --- djangoapi_guard/adapters.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/djangoapi_guard/adapters.py b/djangoapi_guard/adapters.py index 3e74a48..688c83e 100644 --- a/djangoapi_guard/adapters.py +++ b/djangoapi_guard/adapters.py @@ -124,7 +124,9 @@ def read_body_prefix(self, max_bytes: int) -> bytes: class DjangoResponseFactory: def create_response(self, content: str, status_code: int) -> DjangoGuardResponse: return DjangoGuardResponse( - HttpResponse(content, status=status_code, content_type="text/plain; charset=utf-8") + HttpResponse( + content, status=status_code, content_type="text/plain; charset=utf-8" + ) ) def create_redirect_response(