Returning redirects¶
We support returning redirects from API endpoints with
RedirectTo exception with modify():
1from http import HTTPStatus
2from typing import Final
3
4from django.http import HttpResponse
5
6from dmr import Controller, HeaderSpec, RedirectTo, 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 RedirectTo('https://example.com/api/new/user/list')
21
22 @modify(extra_responses=[_RedirectSpec])
23 def post(self) -> dict[str, str]:
24 raise RedirectTo('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: Tue, 26 May 2026 19:09: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: Tue, 26 May 2026 19:09: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"
}
}
}
}
}
}
We model RedirectTo as an exception, because you are not allowed
to return HttpResponse objects
from modify() endpoints.
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
together with validate():
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: Tue, 26 May 2026 19:09: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.RedirectTo(redirect_to: _StrOrPromise, *, status_code: HTTPStatus = HTTPStatus.FOUND, headers: dict[str, str] | None = None)[source]¶
Special class to redirect from
@modifystyled endpoints.It is modeled as an exception, because you can’t return
HttpResponsesubclasses frommodify()endpoints.We model this class closely to match
django.http.HttpResponseRedirect.Usage:
>>> from http import HTTPStatus >>> from dmr import ( ... RedirectTo, ... 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 RedirectTo( ... '/api/new/users/', ... status_code=HTTPStatus.FOUND, ... )