Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/docs/guides/input/header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Request Header

Django Ninja provides a convenient `Header()` parameter function to extract values from HTTP request headers in your API endpoints.

## Usage Patterns

There are two main ways to work with headers:

### 1. Custom Header Name

!!! note
Use the `alias` parameter to specify the exact HTTP header name (with hyphens)
Do NOT use underscores in the parameter name, as Python identifiers cannot contain hyphens.

- Python 3.6+ non-Annotated

```python hl_lines="5"
{!./src/tutorial/header/code01.py!}
```

- Python 3.9+ Annotated

```python hl_lines="5 9"
{!./src/tutorial/header/code02.py!}
```

This pattern is ideal for:
- Custom headers (e.g., `X-API-Key`, `X-Request-ID`)
- Non-standard header names
- When you want explicit control over header mapping

### 2. Well-Known Headers

For common HTTP headers, you can use snake_case parameter names without an alias:

```python hl_lines="5 11 19 23"
{!./src/tutorial/header/code03.py!}
```

Common implicit headers include:

- `user_agent` → `User-Agent`
- `content_length` → `Content-Length`
- `content_type` → `Content-Type`
- `authorization` → `Authorization`
- etc.
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ nav:
- guides/input/path-params.md
- guides/input/query-params.md
- guides/input/body.md
- guides/input/header.md
- guides/input/form-params.md
- guides/input/file-params.md
- guides/input/request-parsers.md
Expand Down
6 changes: 6 additions & 0 deletions docs/src/tutorial/header/code01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from ninja import Header


@api.get("/events")
def events(request, custom_header: str = Header(alias="X-Custom-Header")):
return {"received": custom_header}
10 changes: 10 additions & 0 deletions docs/src/tutorial/header/code02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import Annotated
from ninja import Header


CustomHeader = Annotated[str, Header(alias="X-Custom-Header")]


@api.get("/events")
def events(request, custom_header: CustomHeader):
return {"received": custom_header}
24 changes: 24 additions & 0 deletions docs/src/tutorial/header/code03.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from ninja import Header


@api.get("/events")
def events(request, user_agent: str | None = Header(None)):
return {"user_agent": user_agent}


# Using alias instead of argument name
@api.get("/events")
def events(request, ua: str | None = Header(None, alias="User-Agent")):
return {"user_agent": ua}


# Python 3.9+ Annotated
from typing import Annotated


UserAgent = Annotated[str | None, Header(None)]


@api.get("/events")
def events(request, user_agent: UserAgent):
return {"user_agent": user_agent}
8 changes: 4 additions & 4 deletions ninja/params/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def __getitem__(self, args: Any) -> Any:
Cookie = Annotated[T, param_functions.Cookie()]
File = Annotated[T, param_functions.File()]
Form = Annotated[T, param_functions.Form()]
Header = Annotated[T, param_functions.Header()]
Path = Annotated[T, param_functions.Path()]
Query = Annotated[T, param_functions.Query()]
# mypy does not like to extend already annotated params
Expand All @@ -52,15 +51,13 @@ def __getitem__(self, args: Any) -> Any:
from typing_extensions import Annotated as CookieEx
from typing_extensions import Annotated as FileEx
from typing_extensions import Annotated as FormEx
from typing_extensions import Annotated as HeaderEx
from typing_extensions import Annotated as PathEx
from typing_extensions import Annotated as QueryEx
else:
Body = ParamShortcut(param_functions.Body)
Cookie = ParamShortcut(param_functions.Cookie)
File = ParamShortcut(param_functions.File)
Form = ParamShortcut(param_functions.Form)
Header = ParamShortcut(param_functions.Header)
Path = ParamShortcut(param_functions.Path)
Query = ParamShortcut(param_functions.Query)
# mypy does not like to extend already annotated params
Expand All @@ -69,11 +66,14 @@ def __getitem__(self, args: Any) -> Any:
CookieEx = Cookie
FileEx = File
FormEx = Form
HeaderEx = Header
PathEx = Path
QueryEx = Query


Header = ParamShortcut(param_functions.Header)
HeaderEx = Header


def P(
*,
alias: Optional[str] = None,
Expand Down
12 changes: 12 additions & 0 deletions tests/test_request.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import Optional

import pytest
Expand Down Expand Up @@ -26,6 +27,16 @@ def headers1(request, user_agent: str = Header(...)):
return user_agent


annotated_available = sys.version_info >= (3, 9)

if annotated_available:
from typing import Annotated

@router.get("/headers1_annotated")
def headers1_annotated(request, user_agent: Annotated[str, Header(...)]):
return user_agent


@router.get("/headers2")
def headers2(request, ua: str = Header(..., alias="User-Agent")):
return ua
Expand Down Expand Up @@ -68,6 +79,7 @@ def schema(request, payload: ExtraForbidSchema = Body(...)):
"path,expected_status,expected_response",
[
("/headers1", 200, "Ninja"),
*([("/headers1_annotated", 200, "Ninja")] if annotated_available else []),
("/headers2", 200, "Ninja"),
("/headers3", 200, 10),
("/headers4", 200, 10),
Expand Down