Error handling¶
django-modern-rest has 3 layers where errors might be handled.
It provides flexible error handling logic
on Endpoint,
Controller,
and global levels.
All error handling functions always accept 3 arguments:
Endpointwhere error happenedControllerwhere error happenedException that happened
Here’s how it works:
We first try to call
error_handlerthat was passed into the endpoint definition viamodify()orvalidate()If it returns
django.http.HttpResponse, return it to the userIf it raises, call
handle_error()for sync controllers andhandle_async_error()for async controllersIf controller’s handler returns
HttpResponse, return it to the userIf it raises, call configured global error handler, by default it is
global_error_handler()(it is always sync)
Warning
There are two things to keep in mind:
Async endpoints will require async
error_handlerparameter, Sync endpoints will require syncerror_handlerparameter. This is validated on endpoint creationWe don’t allow to define sync
handle_errorhandlers for async controllers. We also don’t allow asynchandle_async_errorhandlers for sync controllers.
Note
APIError does not follow any of these
rules and has a default handler, which will convert an instance
of APIError to HttpResponse via
to_error() call.
You don’t need to catch APIError in any way,
unless you know what you are doing.
Customizing endpoint error handler¶
Let’s pass custom error handling to a single endpoint:
1from http import HTTPStatus
2
3import pydantic
4from django.http import HttpResponse
5
6from dmr import Body, Controller, modify
7from dmr.endpoint import Endpoint
8from dmr.plugins.pydantic import PydanticSerializer
9from dmr.serializer import BaseSerializer
10
11
12class TwoNumbers(pydantic.BaseModel):
13 left: int
14 right: int
15
16
17def division_error( # <- we define an error handler
18 endpoint: Endpoint,
19 controller: Controller[BaseSerializer],
20 exc: Exception,
21) -> HttpResponse:
22 if isinstance(exc, ZeroDivisionError):
23 # This response's schema was automatically added by `Body`:
24 return controller.to_error(
25 controller.format_error(str(exc)),
26 status_code=HTTPStatus.BAD_REQUEST,
27 )
28 # Reraise unfamiliar errors to let someone
29 # else to handle them further.
30 raise exc from None
31
32
33class MathController(Controller[PydanticSerializer]):
34 @modify(error_handler=division_error) # <- and we pass the handler
35 def patch(
36 self,
37 parsed_body: Body[TwoNumbers],
38 ) -> float: # <- has custom error handling
39 return parsed_body.left / parsed_body.right
40
41 def post(
42 self,
43 parsed_body: Body[TwoNumbers],
44 ) -> float: # <- has only default error handling
45 return parsed_body.left / parsed_body.right
46
Run result
$ curl http://127.0.0.1:8000/api/math/ -D - -X PATCH -d '{"left": 1, "right": 0}' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
date: Sun, 26 Apr 2026 21:11:26 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 39
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"division by zero"}]}
$ curl http://127.0.0.1:8000/api/math/ -D - -X POST -d '{"left": 1, "right": 0}' -H 'Content-Type: application/json'
HTTP/1.1 500 Internal Server Error
date: Sun, 26 Apr 2026 21:11:27 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 68
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Internal server error","type":"internal_error"}]}
In this example we add error handling defined as division_error
to patch endpoint (which serves as a division operation),
while keeping post endpoint (which serves as a multiply operation)
without a custom error handler.
Because ZeroDivisionError can’t happen in post.
Per-endpoint’s error handling has a priority over per-controller and global handlers.
You can also define endpoint error handlers as controller methods
and pass them wrapped with wrap_handler()
as handlers. Like so:
1from http import HTTPStatus
2
3import pydantic
4from django.http import HttpResponse
5
6from dmr import Body, Controller, modify
7from dmr.endpoint import Endpoint
8from dmr.errors import wrap_handler
9from dmr.plugins.pydantic import PydanticSerializer
10from dmr.serializer import BaseSerializer
11
12
13class TwoNumbers(pydantic.BaseModel):
14 left: int
15 right: int
16
17
18class MathController(Controller[PydanticSerializer]):
19 def division_error( # <- we define an error handler
20 self,
21 endpoint: Endpoint,
22 controller: Controller[BaseSerializer],
23 exc: Exception,
24 ) -> HttpResponse:
25 if isinstance(exc, ZeroDivisionError):
26 # This response's schema was automatically added by `Body`:
27 return controller.to_error(
28 controller.format_error(str(exc)),
29 status_code=HTTPStatus.BAD_REQUEST,
30 )
31 # Reraise unfamiliar errors to let someone
32 # else to handle them further.
33 raise exc from None
34
35 @modify(
36 error_handler=wrap_handler( # <- and we pass the handler
37 division_error,
38 ),
39 )
40 def patch(
41 self,
42 parsed_body: Body[TwoNumbers],
43 ) -> float: # <- has custom error handling
44 return parsed_body.left / parsed_body.right
45
46 def post(
47 self,
48 parsed_body: Body[TwoNumbers],
49 ) -> float: # <- has only default error handling
50 return parsed_body.left * parsed_body.right
51
Run result
$ curl http://127.0.0.1:8000/api/math/ -D - -X PATCH -d '{"left": 1, "right": 0}' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
date: Sun, 26 Apr 2026 21:11:27 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 39
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"division by zero"}]}
Customizing controller error handler¶
Let’s create custom error handling for the whole controller:
1from http import HTTPStatus
2
3import zapros
4from django.http import HttpResponse
5from typing_extensions import override
6
7from dmr import Controller, ResponseSpec
8from dmr.endpoint import Endpoint
9from dmr.plugins.pydantic import PydanticSerializer
10
11
12class ProxyController(Controller[PydanticSerializer]):
13 responses = (
14 # Custom schema that we can return when `HTTPError` happens:
15 ResponseSpec(str, status_code=HTTPStatus.FAILED_DEPENDENCY),
16 )
17
18 async def get(self) -> None:
19 async with self._client() as client:
20 # Simulate some real work:
21 await client.get('https://example.com')
22
23 async def post(self) -> None:
24 async with self._client() as client:
25 # Simulate some real work:
26 await client.post('https://example.com', json={})
27
28 @override
29 async def handle_async_error(
30 self,
31 endpoint: Endpoint,
32 controller: Controller[PydanticSerializer],
33 exc: Exception,
34 ) -> HttpResponse:
35 # Will handle errors in all endpoints.
36 if isinstance(exc, zapros.ZaprosError):
37 return self.to_error(
38 'Request to example.com failed',
39 status_code=HTTPStatus.FAILED_DEPENDENCY,
40 )
41 # Handle errors from super:
42 return await super().handle_async_error(
43 endpoint,
44 controller,
45 exc,
46 )
47
48 def _client(self) -> zapros.AsyncClient:
49 return zapros.AsyncClient()
In this example we are using zapros
HTTP client to proxy an HTTP GET and POST
requests to some other API service.
If we fail to send a request and raise a specific HTTP client error,
we return an error with 424 error code.
Going further¶
Now you can understand how you can create:
Endpoints with custom error handlers
Controllers with custom error handlers
ResponseSpecobjects for new error response schemas
You can dive even deeper and:
Subclass
Controllerand provide default error handling for this specific subclassRedefine
endpoint_clsand change how one specific endpoint behaves on a deep level, seehandle_error()andhandle_async_error()
Error handling diagram¶
The same error handling logic can be represented as a diagram:
---
config:
theme: forest
---
graph TB
Start[Request] --> Error{Error?};
Error -->|Yes| Endpoint[Endpoint-level handler];
Endpoint --> EndpointHandler{Raises or returns response?};
EndpointHandler -->|response| Failure[Error response];
EndpointHandler -->|raises| Controller[Controller-level handler];
Controller --> ControllerHandler{Raises or returns response?};
ControllerHandler -->|response| Failure[Error response];
ControllerHandler -->|raises| Global[Global handler];
Global --> GlobalHandler{Raises or returns response?};
GlobalHandler -->|response| Failure[Error response];
GlobalHandler -->|raises| Reraises[Reraises error];
Error ---->|No| Success[Successful response];
Error handling logic¶
Note
If Handling 500 errors is configured, it will catch all unhandled errors
in the provided scope and return 500 errors with the correct payload.
Customizing error messages¶
All error messages, including pre-defined ones, can be easily customized on a per-controller basis.
To do so, you would need to change:
error_modelattribute for all controllers that will be using this error message schemaformat_error()method to provide custom runtime error formatting
1from http import HTTPStatus
2
3from typing_extensions import TypedDict, override
4
5from dmr import APIError, Body, Controller, ResponseSpec, modify
6from dmr.errors import ErrorType, format_error
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class CustomErrorDetail(TypedDict):
11 message: str
12
13
14class CustomErrorModel(TypedDict):
15 errors: list[CustomErrorDetail]
16
17
18class ApiController(Controller[PydanticSerializer]):
19 error_model = CustomErrorModel
20
21 @override
22 def format_error(
23 self,
24 error: str | Exception,
25 *,
26 loc: str | list[str | int] | None = None,
27 error_type: str | ErrorType | None = None,
28 ) -> CustomErrorModel:
29 default = format_error(
30 error,
31 loc=loc,
32 error_type=error_type,
33 )
34 return {
35 'errors': [
36 {'message': detail['msg']} for detail in default['detail']
37 ],
38 }
39
40 @modify(
41 extra_responses=[
42 ResponseSpec(
43 return_type=CustomErrorModel,
44 status_code=HTTPStatus.PAYMENT_REQUIRED,
45 ),
46 ],
47 )
48 def post(self, parsed_body: Body[dict[str, str]]) -> str:
49 raise APIError(
50 self.format_error('test msg'),
51 status_code=HTTPStatus.PAYMENT_REQUIRED,
52 )
53
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{}' -H 'Content-Type: application/json'
{"errors":[{"message":"test msg"}]}
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '[]' -H 'Content-Type: application/json'
{"errors":[{"message":"Input should be a valid dictionary"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"CustomErrorDetail": {
"properties": {
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"message"
],
"title": "CustomErrorDetail",
"type": "object"
},
"CustomErrorModel": {
"properties": {
"errors": {
"items": {
"$ref": "#/components/schemas/CustomErrorDetail"
},
"title": "Errors",
"type": "array"
}
},
"required": [
"errors"
],
"title": "CustomErrorModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"post": {
"deprecated": false,
"operationId": "postApicontrollerApiApicontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"402": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomErrorModel"
}
}
},
"description": "Payment Required"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
This will also change the OpenAPI schema for the affected controller.
See ErrorModel
for the default error model schema.
And format_error()
for the default error formatting.
See content negotiation docs about how to use different error models for different content types.
Problem Details¶
See also
django-modern-rest supports customizing of all error message
inside the framework, including builtin ones.
ProblemDetailsError
is a great example of how it can be done.
It is a regular subclass of APIError,
which does not have any special handling inside our framework.
This is done on purpose, so we can be sure that users also can
to customize their exceptions any way they need.
We support two main use-cases for Problem Details.
Always raising Problem Details¶
To always use ProblemDetailsError
inside your controller you would need to:
Define
error_modelattribute asProblemDetailsModelRaise an exception itself, pass all the required fields
Convert other message to the Problem Details format using
format_error()method
1from http import HTTPStatus
2from typing import Any
3
4import pydantic
5from typing_extensions import override
6
7from dmr import Controller, Query, ResponseSpec
8from dmr.errors import ErrorType
9from dmr.plugins.pydantic import PydanticSerializer
10from dmr.problem_details import ProblemDetailsError, ProblemDetailsModel
11
12
13class _QueryModel(pydantic.BaseModel):
14 number: int = 0
15
16
17class ProblemDetailsController(Controller[PydanticSerializer]):
18 error_model = ProblemDetailsModel
19
20 responses = (
21 ResponseSpec(error_model, status_code=HTTPStatus.PAYMENT_REQUIRED),
22 )
23
24 async def get(self, parsed_query: Query[_QueryModel]) -> str:
25 raise ProblemDetailsError(
26 (
27 f'Your current balance is {parsed_query.number}, '
28 'but the price is 15'
29 ),
30 status_code=HTTPStatus.PAYMENT_REQUIRED,
31 type='https://example.com/probs/out-of-credit',
32 title='Not enough funds',
33 instance='/account/users/1/',
34 extra={'balance': parsed_query.number, 'price': 15},
35 )
36
37 @override
38 def format_error(
39 self,
40 error: str | Exception,
41 *,
42 loc: str | list[str | int] | None = None,
43 error_type: str | ErrorType | None = None,
44 ) -> Any:
45 return ProblemDetailsError.format_error(
46 error,
47 loc=loc,
48 error_type=error_type,
49 title='From format_error',
50 )
51
Run result
$ curl http://127.0.0.1:8000/api/balance/ -X GET
{"detail":"Your current balance is 0, but the price is 15","status":402,"type":"https://example.com/probs/out-of-credit","title":"Not enough funds","instance":"/account/users/1/","balance":0,"price":15}
$ curl 'http://127.0.0.1:8000/api/balance/?number=a' -X GET
{"detail":"Input should be a valid integer, unable to parse string as an integer","status":400,"type":"value_error","title":"From format_error"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ProblemDetailsModel": {
"description": "Error payload model for Problem Details.\n\nSee https://datatracker.ietf.org/doc/html/rfc9457\nfor the detailed description of each field.",
"properties": {
"detail": {
"title": "Detail",
"type": "string"
},
"instance": {
"title": "Instance",
"type": "string"
},
"status": {
"title": "Status",
"type": "integer"
},
"title": {
"title": "Title",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"title": "ProblemDetailsModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/problemdetailscontroller/": {
"get": {
"deprecated": false,
"operationId": "getProblemdetailscontrollerApiProblemdetailscontroller",
"parameters": [
{
"deprecated": false,
"in": "query",
"name": "number",
"schema": {
"default": 0,
"title": "Number",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"402": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsModel"
}
}
},
"description": "Payment Required"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Conditionally raising Problem Details¶
Another way is to negotiate the error response format. How does it work?
When user sends a request with
Acceptheader withapplication/problem+jsoncontent type, we will return Problem Details errorsWhen
application/jsonor any other content type is sent, we return regularErrorModelerror payloads
To do so, you would need a slightly more difficult setup:
Define
error_modelattribute as the result oferror_model()method call. It will add conditional schema types to your error responsesDefine several
Renderertypes, including the one which will handleapplication/problem+jsonRaise a conditional exception: use
conditional_error()to only raise Problem Details when the correct accepted type is passedConvert other message to the Problem Details format using
format_error()method when the correct accepted type is passed
1from http import HTTPStatus
2from typing import Any
3
4from typing_extensions import override
5
6from dmr import Controller, ResponseSpec
7from dmr.errors import ErrorModel, ErrorType
8from dmr.negotiation import ContentType, accepts
9from dmr.plugins.pydantic import PydanticSerializer
10from dmr.problem_details import ProblemDetailsError
11from dmr.renderers import JsonRenderer
12
13
14class ProblemDetailsController(Controller[PydanticSerializer]):
15 error_model = ProblemDetailsError.error_model({
16 ContentType.json: ErrorModel,
17 })
18
19 renderers = (
20 JsonRenderer(ContentType.json),
21 JsonRenderer(ContentType.json_problem_details),
22 )
23
24 responses = (
25 ResponseSpec(error_model, status_code=HTTPStatus.PAYMENT_REQUIRED),
26 )
27
28 async def get(self) -> str:
29 raise ProblemDetailsError.conditional_error(
30 'Your current balance is 0, but the price is 15',
31 status_code=HTTPStatus.PAYMENT_REQUIRED,
32 type='https://example.com/probs/out-of-credit',
33 title='Not enough funds',
34 instance='/account/users/1/',
35 extra={'balance': 0, 'price': 15},
36 controller=self,
37 )
38
39 @override
40 def format_error(
41 self,
42 error: str | Exception,
43 *,
44 loc: str | list[str | int] | None = None,
45 error_type: str | ErrorType | None = None,
46 ) -> Any:
47 if accepts(self.request, ContentType.json_problem_details):
48 return ProblemDetailsError.format_error(
49 error,
50 loc=loc,
51 error_type=error_type,
52 title='From format_error',
53 )
54 return super().format_error(error, loc=loc, error_type=error_type)
55
Run result
$ curl http://127.0.0.1:8000/api/balance/ -X GET
{"detail":[{"msg":"Your current balance is 0, but the price is 15","type":"https://example.com/probs/out-of-credit"}]}
$ curl http://127.0.0.1:8000/api/balance/ -X GET -H 'Accept: application/problem+json'
{"detail":"Your current balance is 0, but the price is 15","status":402,"type":"https://example.com/probs/out-of-credit","title":"Not enough funds","instance":"/account/users/1/","balance":0,"price":15}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"ProblemDetailsModel": {
"description": "Error payload model for Problem Details.\n\nSee https://datatracker.ietf.org/doc/html/rfc9457\nfor the detailed description of each field.",
"properties": {
"detail": {
"title": "Detail",
"type": "string"
},
"instance": {
"title": "Instance",
"type": "string"
},
"status": {
"title": "Status",
"type": "integer"
},
"title": {
"title": "Title",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"title": "ProblemDetailsModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/problemdetailscontroller/": {
"get": {
"deprecated": false,
"operationId": "getProblemdetailscontrollerApiProblemdetailscontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
},
"application/problem+json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"402": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsModel"
}
}
},
"description": "Payment Required"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetailsModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Tip
You can still make application/problem+json the default
and when application/json (or any other type) is explicitly requested
return the ErrorModel errors.
Handling validation errors from models¶
When creating models with, for example, pydantic.BaseModel,
your validation can fail. This error will not be handled by design.
Why? Because catching all specific validation errors for a specific serializer that can happen in your application will do more harm than good.
This is the default behavior:
1from typing import Literal
2
3import pydantic
4
5from dmr import Controller
6from dmr.plugins.pydantic import PydanticSerializer
7
8
9class Pong(pydantic.BaseModel):
10 message: Literal['pong']
11
12
13class PongController(Controller[PydanticSerializer]):
14 def get(self) -> Pong:
15 # This will trigger `pydantic.ValidationError`,
16 # because `message` must be `'pong'`, not `'wrong'`:
17 return Pong(message='wrong')
18
Run result
$ curl http://127.0.0.1:8000/api/ping/ -D - -X GET
HTTP/1.1 500 Internal Server Error
date: Sun, 26 Apr 2026 21:11:31 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 68
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Internal server error","type":"internal_error"}]}
If you want to catch this error in a specific place and attach a specific behavior, use an error handler at a proper level.
For example, here we would handle it on a controller level:
1from http import HTTPStatus
2from typing import Literal
3
4import pydantic
5from django.http import HttpResponse
6from typing_extensions import override
7
8from dmr import Controller, ResponseSpec
9from dmr.endpoint import Endpoint
10from dmr.plugins.pydantic import PydanticSerializer
11
12
13class Pong(pydantic.BaseModel):
14 message: Literal['pong']
15
16
17class PongController(Controller[PydanticSerializer]):
18 responses = (
19 ResponseSpec(
20 Controller.error_model,
21 status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
22 ),
23 )
24
25 def get(self) -> Pong:
26 # This will trigger `pydantic.ValidationError`,
27 # because `message` must be `'pong'`, not `'wrong'`:
28 return Pong(message='wrong')
29
30 @override
31 def handle_error(
32 self,
33 endpoint: Endpoint,
34 controller: Controller[PydanticSerializer],
35 exc: Exception,
36 ) -> HttpResponse:
37 if isinstance(exc, pydantic.ValidationError):
38 # Now, handle the error, but do not show what actually happened
39 # to the outside world, it might contain sensitive data:
40 return self.to_response(
41 self.format_error('Validation error'),
42 status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
43 )
44 return super().handle_error(endpoint, controller, exc)
45
Run result
$ curl http://127.0.0.1:8000/api/ping/ -D - -X GET
HTTP/1.1 500 Internal Server Error
date: Sun, 26 Apr 2026 21:11:32 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 39
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Validation error"}]}
Now, the error is handled: we modified its error text and status code. Remember not to dump all the error information out to users, since they might contain sensitive data.
See also
See Handling 500 errors if you want to change the 500 error rendering.
API Reference¶
- dmr.errors.global_error_handler(endpoint: Endpoint, controller: Controller[BaseSerializer], exc: Exception) HttpResponse[source]¶
Global error handler for all cases.
It is the last item in the chain that we try:
Per endpoint configuration via
handle_error()andhandle_async_error()methodsPer controller handlers
This global handler, specified via the configuration
If some exception cannot be handled, it is just reraised.
- Parameters:
endpoint – Endpoint where error happened.
controller – Controller instance that endpoint belongs to.
exc – Exception instance that happened.
- Returns:
HttpResponsewith proper response for this error. Or raise exc back.
Here’s an example that will produce
{'detail': [{'msg': 'inf', 'type': 'user_msg'}]}for anyZeroDivisionErrorin your application:>>> from http import HTTPStatus >>> from django.http import HttpResponse >>> from dmr.controller import Controller >>> from dmr.endpoint import Endpoint >>> from dmr.errors import global_error_handler, ErrorType >>> def custom_error_handler( ... controller: Controller, ... endpoint: Endpoint, ... exc: Exception, ... ) -> HttpResponse: ... if isinstance(exc, ZeroDivisionError): ... return controller.to_error( ... controller.format_error( ... 'inf', ... error_type=ErrorType.user_msg, ... ), ... status_code=HTTPStatus.NOT_IMPLEMENTED, ... ) ... # Call the original handler to handle default errors: ... return global_error_handler(controller, endpoint, exc) >>> # And then in your settings file: >>> DMR_SETTINGS = { ... # Object `custom_error_handler` will also work: ... 'global_error_handler': 'path.to.custom_error_handler', ... }
Warning
Make sure you always call original
global_error_handlerin the very end. Unless, you want to disable original error handling.
- dmr.errors.wrap_handler(method: _MethodSyncHandler) SyncErrorHandler[source]¶
- dmr.errors.wrap_handler(method: _MethodAsyncHandler) AsyncErrorHandler
Utility function to wrap controller methods.
It is used to wrap an existing controller method and pass it as
error_handler=argument to an endpoint.
- final class dmr.errors.ErrorType(*values)[source]¶
Collection of all possible error types that we use in DMR.
- value_error¶
Raised when we can’t parse something.
- internal_error¶
Raised when internal error happens.
- not_allowed¶
Raised when using unsupported http method. 405 alias.
- security¶
Raised when security related error happens.
- ratelimit¶
Raised when ratelimit related error happens.
- user_msg¶
Raised for custom errors from users.
- not_found¶
Raised when we can’t find controller.
- streaming¶
Happens when we stream events.
- class dmr.errors.ErrorModel[source]¶
Default error response schema.
Can be customized. See Customizing error messages for more details.
- dmr.errors.format_error(error: str | Exception, *, loc: str | list[str | int] | None = None, error_type: str | ErrorType | None = None) ErrorModel[source]¶
Convert error to the common format.
Default implementation.
- Parameters:
error – A serialization exception like a validation error.
loc – Location where this error happened. Like
"headers", or"field_name", or["parsed_headers", "header_name"].error_type – Optional type of the error for extra metadata.
- Returns:
Simple python object - exception converted to a common format.
Problem Details API¶
- class dmr.problem_details.ProblemDetailsError(detail: str, *, status_code: HTTPStatus, type: str | None = None, title: str | None = None, instance: str | None = None, extra: Mapping[str, Any] | None = None, headers: Mapping[str, str] | None = None, cookies: Mapping[str, NewCookie] | None = None, show_status: bool = True, show_detail: bool = True)[source]¶
Bases:
APIError[ProblemDetailsModel]Problem Details exception.
It is a subclass of
dmr.response.APIError, so you can raise it anywhere in the REST part of your app.There are two major use-cases that we support for this class:
Direct usage: you raise an error and get what you raise, no changes
Conditional usage: you call
conditional_errormethod and if the client acceptsapplication/problem+json, we will return the proper Problem Details description. But, if it is not requested directly, we will return our regulardmr.errors.ErrorModel
Both use-cases are independent. You can decide what to use per controller.
- classmethod conditional_error(detail: str, *, status_code: HTTPStatus, controller: Controller[BaseSerializer], type: str | None = None, title: str | None = None, instance: str | None = None, extra: Mapping[str, Any] | None = None, headers: Mapping[str, str] | None = None, cookies: Mapping[str, NewCookie] | None = None, show_status: bool = True, show_detail: bool = True) APIError[Any][source]¶
Create conditional error.
If request accepts
application/problem+jsonthen return a Problem Details exception. If not, return regulardmr.response.APIErrorinstance.Otherwise, returns regular error from controller using its
format_error()method for formatting.
- classmethod error_model(existing_errors: Mapping[str, Any], content_type: str | None = None) Any[source]¶
Builds an error model for conditional responses.
Only use this method when you use
conditional_error. If you are using regular exceptions, useProblemDetailsModeldirectly.
- classmethod format_error(error: str | Exception, *, loc: str | list[str | int] | None = None, error_type: str | ErrorType | None = None, status_code: HTTPStatus | None = None, title: str | None = None, instance: str | None = None, extra: Mapping[str, Any] | None = None) ErrorModel | ProblemDetailsModel[source]¶
Format other errors to be in format of Problem Details.
- class dmr.problem_details.ProblemDetailsModel[source]¶
Error payload model for Problem Details.
See https://datatracker.ietf.org/doc/html/rfc9457 for the detailed description of each field.