Returning redirects¶
We support returning redirects from API endpoints with
APIRedirectError exception:
views.py¶
1from http import HTTPStatus
2from typing import Final
3
4from django.http import HttpResponse
5
6from dmr import APIRedirectError, Controller, HeaderSpec, modify, validate
7from dmr.metadata import ResponseSpec
8from dmr.plugins.pydantic import PydanticSerializer
9
10_RedirectSpec: Final = ResponseSpec(
11 None,
12 status_code=HTTPStatus.FOUND,
13 headers={'Location': HeaderSpec()},
14)
15
16
17class UserController(Controller[PydanticSerializer]):
18 @validate(_RedirectSpec)
19 def get(self) -> HttpResponse:
20 raise APIRedirectError('https://example.com/api/new/user/list')
21
22 @modify(extra_responses=[_RedirectSpec])
23 def post(self) -> dict[str, str]:
24 raise APIRedirectError('https://example.com/api/new/user/create')
25
Run result
$ curl http://127.0.0.1:8000/api/user/ -D - -X GET
HTTP/1.1 302 Found
date: Thu, 09 Apr 2026 14:42:21 GMT
server: uvicorn
Location: https://example.com/api/new/user/list
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 0
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST
HTTP/1.1 302 Found
date: Thu, 09 Apr 2026 14:42:21 GMT
server: uvicorn
Location: https://example.com/api/new/user/create
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 0
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"get": {
"deprecated": false,
"operationId": "getUsercontrollerApiUsercontroller",
"responses": {
"302": {
"content": {
"application/json": {
"schema": {
"type": "null"
}
}
},
"description": "Found",
"headers": {
"Location": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
}
},
"description": "Created"
},
"302": {
"content": {
"application/json": {
"schema": {
"type": "null"
}
}
},
"description": "Found",
"headers": {
"Location": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Note
APIError does not support 3xx status codes.
Redirects are different from regular errors.
The second way is to use
default Django’s django.http.HttpResponseRedirect:
views.py¶
1from http import HTTPStatus
2
3from django.http import HttpResponse, HttpResponseRedirect
4
5from dmr import Controller, HeaderSpec, validate
6from dmr.metadata import ResponseSpec
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class UserController(Controller[PydanticSerializer]):
11 @validate(
12 ResponseSpec(
13 None,
14 status_code=HTTPStatus.FOUND,
15 headers={'Location': HeaderSpec()},
16 ),
17 )
18 def get(self) -> HttpResponse:
19 return HttpResponseRedirect(
20 'https://example.com/api/new/user/list',
21 content_type='application/json',
22 )
23
Run result
$ curl http://127.0.0.1:8000/api/user/ -D - -X GET
HTTP/1.1 302 Found
date: Thu, 09 Apr 2026 14:42:22 GMT
server: uvicorn
Content-Type: application/json
Location: https://example.com/api/new/user/list
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 0
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"get": {
"deprecated": false,
"operationId": "getUsercontrollerApiUsercontroller",
"responses": {
"302": {
"content": {
"application/json": {
"schema": {
"type": "null"
}
}
},
"description": "Found",
"headers": {
"Location": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Note that in both cases you would need to document Location header
in a response spec.
API Reference¶
- exception dmr.response.APIRedirectError(redirect_to: _StrOrPromise, *, status_code: HTTPStatus = HTTPStatus.FOUND, headers: dict[str, str] | None = None)[source]¶
Special class to fast return redirects from API.
We model this class closely to match
django.http.HttpResponseRedirect.Usage:
>>> from http import HTTPStatus >>> from dmr import ( ... APIRedirectError, ... Controller, ... ResponseSpec, ... modify, ... HeaderSpec, ... ) >>> from dmr.errors import ErrorType >>> from dmr.plugins.pydantic import PydanticSerializer >>> class UserController(Controller[PydanticSerializer]): ... @modify( ... extra_responses=[ ... ResponseSpec( ... None, ... status_code=HTTPStatus.FOUND, ... headers={'Location': HeaderSpec()}, ... ), ... ], ... ) ... def get(self) -> str: ... # This API endpoint is deprecated, use new one: ... raise APIRedirectError( ... '/api/new/users/', ... status_code=HTTPStatus.FOUND, ... )