Skip to content

limit_to_content_types on global ResponseSpec raises ResponseSchemaError (422) when error handler returns different content type than negotiated renderer. #701

@sh1rokovs

Description

@sh1rokovs

Summary

When limit_to_content_types is set on a ResponseSpec registered in DMR_SETTINGS[Settings.responses] (global error responses), and the global error handler returns an HttpResponse whose Content-Type differs from the request's negotiated renderer content type, dmr raises a ResponseSchemaError and returns a 422 instead of the intended error response.

When limit_to_content_types is set application/problem+json
Image
Image

When limit_to_content_types is set None
Image
Image

Environment

  • dmr version: 0.2.0
  • Python: 3.12
  • Django: 5.2

Steps to reproduce

# settings.py
  DMR_SETTINGS = {
      Settings.responses: [
          ResponseSpec(
              ProblemDetail,
              status_code=HTTPStatus.UNAUTHORIZED,
              description="Unauthorized",
              limit_to_content_types=frozenset({"application/problem+json"}),
          ),
      ],
      Settings.global_error_handler: "myapp.error_handler.dmr_global_error_handler",
  }
# error_handler.py
  def dmr_global_error_handler(endpoint, controller, exc):
      if isinstance(exc, NotAuthenticatedError):
          return JsonResponse(
              {"type": "about:blank", "title": "Unauthorized",
  "status": 401},
              status=401,
              content_type="application/problem+json",
          )
      raise
  1. Send a request without authentication to any protected endpoint.
  2. Client sends Accept: application/json (or Accept: */* header (ex.: Postman)).

Expected behavior

The global error handler returns a 401 response with Content-Type: application/problem+json. Since the ResponseSpec for 401 allows application/problem+json, validation should pass and the client should receive the 401

Actual behavior

Client receives 422 Unprocessable Entity with Content-Type: application/json:

  {
    "detail": [
      {
        "msg": "Response 401 is not allowed for
  'application/json', only for ['application/problem+json']",
        "type": "value_error"
      }
    ]
  }

Setting the parameter:
Settings.renderers: [JsonRenderer(), ProblemJsonRenderer()]

class ProblemJsonRenderer(JsonRenderer):
    """Renderer for RFC 7807 Problem Details responses."""

    content_type = "application/problem+json"

    @property
    @override
    def validation_parser(self) -> JsonParser:
        return JsonParser()

does not solve the problem.

Setting the parameter:
Settings.renderers: [ProblemJsonRenderer(), JsonRenderer()]
may solve the problem, but other endpoints start returning Content-Type:application/problem+json as the default header.

In my opinion, the root cause might be the following (though I could be wrong):
In dmr/validation/response.py, method _maybe_validate_body (line ~185):

content_type=getattr(renderer, 'content_type', parser.content_type)

Here, renderer is determined from the request's Accept header
(e.g. application/json), rather than from the actual
HttpResponse.headers['Content-Type']. As a result,
_validate_body compares limit_to_content_types against the
negotiated content type of the request, not the one that the
error handler actually set in the response.

When _validate_body raises ResponseSchemaError, it gets caught
by _make_http_response in endpoint.py (line ~444) — but at
that point there is nowhere to delegate handling further, so
controller.to_error(...) is called directly, which produces a
422 with the serializer's default content type (application/json).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions