NEW: Fenrir v4.1.2 — 56 Bug Fixes, Performance Optimizations, 1331 Tests, 5-Framework Benchmark! Read the changelog
request-response.md
docs request-response.md

Request & Response

Request

The Request object provides access to all incoming HTTP request data. It is available as a context-local singleton via from fenrir import request.

Request Properties

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from fenrir import request

@app.get("/info")
async def get_info():
    # Core properties
    scope = request.scope          # Raw ASGI scope dict
    method = request.method        # HTTP method (GET, POST, etc.)
    path = request.path            # URL path (e.g., "/info")
    query_string = request.query_string  # Raw query string as bytes

    # Parsed query parameters
    args = request.args            # Dict[str, str] – first value per key
    args_list = request.args_list  # Dict[str, List[str]] – all values per key

    # Headers (lowercased keys)
    headers = request.headers      # Dict[str, str]
    user_agent = headers.get("user-agent")

    # Cookies
    cookies = request.cookies      # Dict[str, str]
    session_id = cookies.get("session_id")

    # Body (requires body to be read first)
    raw_body = request.body        # bytes
    json_data = request.json       # Parsed JSON or None

    # Session (if session middleware is active)
    session = request.session      # Session object or None

    # Host (with TRUSTED_HOSTS validation)
    host = request.host            # str

    # Falcon-compatible context dict
    request.context["user"] = {"id": 1}  # Dict[str, Any]

    return {"method": method, "path": path}

Query Parameters

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from fenrir import request

@app.get("/search")
async def search():
    # Single value (first occurrence)
    q = request.args.get("q", "")

    # All values for multi-value params
    tags = request.args_list.get("tags", [])
    # e.g., /search?tags=python&tags=web → ["python", "web"]

    return {"query": q, "tags": tags}

Request Body

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from fenrir import request

@app.post("/data")
async def receive_data():
    # Read raw body (bytes)
    raw = request.body

    # Parse JSON body (auto-decoded from body)
    data = request.json  # Returns parsed dict/list or None

    # Parse form data (multipart or urlencoded)
    form = await request.form()
    # Returns Dict[str, Any] – fields are strings, files are UploadFile

    return {"received": True}

Streaming Request Body

Process large uploads efficiently without buffering the entire body into memory:

1
2
3
4
5
6
7
8
from fenrir.request import Request

@app.post("/upload")
async def upload(raw_request: Request):
    total_bytes = 0
    async for chunk in raw_request.stream_body(chunk_size=65536):
        total_bytes += len(chunk)
    return {"bytes_received": total_bytes}

stream_body(chunk_size=65536) is an async generator yielding bytes chunks. If the body has already been fully read, it yields from the internal buffer. Otherwise it streams directly from the ASGI receive channel (the body is not buffered, so request.body / request.json will not be available afterward).

Async Body Helpers

1
2
3
4
5
6
7
8
9
from fenrir import request

@app.post("/async-data")
async def async_data():
    # FastAPI-style async accessors
    raw = await request.body_async()   # Returns request.body
    data = await request.json_async()  # Returns request.json

    return {"data": data}

Host Validation (TRUSTED_HOSTS)

The host property validates against TRUSTED_HOSTS configured on the app. Wildcard matching is supported:

1
2
3
4
5
6
7
8
9
from fenrir import Fenrir

app = Fenrir()
app.config["TRUSTED_HOSTS"] = ["example.com", "*.example.com"]

@app.get("/safe")
async def safe_endpoint():
    host = request.host  # Raises HTTPBadRequest if not trusted
    return {"host": host}

Wildcard rules:

  • *.example.com matches api.example.com and example.com
  • Exact match is always checked

Falcon-Compatible Request Helpers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from fenrir import request

@app.get("/falcon-style")
async def falcon_style():
    # get_header – case-insensitive header lookup
    content_type = request.get_header("Content-Type")

    # get_param – query parameter with optional required/default
    page = request.get_param("page")
    page_int = request.get_param_as_int("page", required=True, default=1)

    # context – per-request dict (Falcon-style)
    request.context["user"] = {"id": 1}

    return {"content_type": content_type, "page": page}

Request API Reference

Constructor:

  • Request(scope: Dict[str, Any]) — initialized from the ASGI scope

Properties:

Property Type Description
scope Dict[str, Any] Raw ASGI scope
method str HTTP method (uppercased)
path str URL path
query_string bytes Raw query string
args Dict[str, str] First query value per key
args_list Dict[str, List[str]] All query values per key
headers Dict[str, str] Lowercased request headers
cookies Dict[str, str] Parsed cookies
session Session \| None Session object (if middleware active)
body bytes Raw request body
json Any \| None Parsed JSON body
host str Host header (validated against TRUSTED_HOSTS)
context Dict[str, Any] Falcon-compatible per-request dict

Methods:

Method Returns Description
stream_body(chunk_size=65536) AsyncIterator[bytes] Stream body in chunks
form() Dict[str, Any] Parse multipart/urlencoded form data
body_async() bytes Async wrapper for body
json_async() Any Async wrapper for json
get_header(name, default=None) str \| None Case-insensitive header lookup
get_param(name, required=False, default=None) str \| None Query parameter with validation
get_param_as_int(name, required=False, default=None) int \| None Query parameter cast to int

Response

Response Base Class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from fenrir import Response

@app.get("/custom")
async def custom_response():
    resp = Response(
        body="Hello World",
        status=200,
        headers={"X-Custom-Header": "value"},
        content_type="text/plain",
    )
    resp.set_header("X-Another", "value")
    resp.set_cookie("theme", "dark", max_age=86400)
    return resp

Constructor:

  • Response(body=b"", status=200, headers=None, content_type="text/html; charset=utf-8")
  • body accepts str or bytes (strings are UTF-8 encoded)

Properties:

Property Type Description
status int HTTP status code (getter/setter)
body bytes Raw response body (getter/setter, str auto-encoded)
text str \| None UTF-8 decoded body (getter/setter)
headers Dict[str, str] Response headers
cookies SimpleCookie Response cookies
media Any Get/set body as Python object (auto-serialized to/from JSON)

Methods:

Method Description
set_header(name, value) Set a response header (lowercased key)
unset_header(name) Remove a response header
set_cookie(key, value, max_age, expires, path, domain, secure, httponly, samesite) Set a cookie
delete_cookie(key, path, domain) Mark a cookie for deletion
get_asgi_headers() Returns headers as List[Tuple[bytes, bytes]] for ASGI transport
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
resp.set_cookie(
    key="session",
    value="abc123",
    max_age=3600,          # Seconds until expiry
    expires="Wed, 01 Jan 2025 00:00:00 GMT",  # Or int (seconds from now)
    path="/",
    domain=".example.com",
    secure=True,
    httponly=True,
    samesite="lax",
)

Media Property (Falcon-Compat)

1
2
3
resp = Response()
resp.media = {"key": "value"}  # Auto-serializes to JSON, sets content-type
data = resp.media              # Parses body as JSON

Response Subclasses

JSONResponse

Auto-serializes Python objects to JSON with application/json content type.

1
2
3
4
5
6
7
8
9
from fenrir import JSONResponse

@app.get("/data")
async def get_data():
    return JSONResponse({"key": "value"}, status=200)

@app.get("/list")
async def get_list():
    return JSONResponse([1, 2, 3])

Constructor: JSONResponse(content, status=200, headers=None)

HTMLResponse

Returns HTML content with text/html; charset=utf-8 content type.

1
2
3
4
5
from fenrir import HTMLResponse

@app.get("/page")
async def page():
    return HTMLResponse("<h1>Hello World</h1>")

Constructor: HTMLResponse(content, status=200, headers=None)

TextResponse / PlainTextResponse

Returns plain text with text/plain; charset=utf-8 content type. PlainTextResponse is an alias for TextResponse.

1
2
3
4
5
6
7
8
9
from fenrir import TextResponse, PlainTextResponse

@app.get("/text")
async def text():
    return TextResponse("Plain text content")

@app.get("/plain")
async def plain():
    return PlainTextResponse("Also plain text")

Constructor: TextResponse(content, status=200, headers=None)

RedirectResponse

Returns a redirect with Location header. Default status is 307 (Temporary Redirect).

1
2
3
4
5
6
7
8
9
from fenrir import RedirectResponse

@app.get("/old-path")
async def redirect_old():
    return RedirectResponse(url="/new-path")  # 307

@app.get("/permanent")
async def redirect_permanent():
    return RedirectResponse(url="/new", status=301)  # 301

Constructor: RedirectResponse(url, status=307, headers=None)

StreamingResponse

Streams an async/sync generator or iterable. Use media_type (or content_type) to set the content type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from fenrir import StreamingResponse

@app.get("/stream")
async def stream():
    async def generate():
        for i in range(100):
            yield f"data: {i}\n\n"
    return StreamingResponse(generate(), media_type="text/event-stream")

@app.get("/sync-stream")
async def sync_stream():
    def generate():
        for i in range(10):
            yield f"line {i}\n"
    return StreamingResponse(generate(), content_type="text/plain")

Constructor:

1
2
3
4
5
6
7
StreamingResponse(
    content,          # AsyncGenerator, Generator, AsyncIterable, Iterable, or Callable
    status=200,
    headers=None,
    media_type="text/plain; charset=utf-8",
    content_type=None, # Alias for media_type (Sanic/Bottle compat)
)

Attributes:

  • streaming = True — marks this as a streaming response for the ASGI dispatcher
  • stream_body() — async generator yielding body chunks as bytes

FileResponse

Serves a file from the filesystem with automatic MIME type detection.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from fenrir import FileResponse

@app.get("/download")
async def download():
    return FileResponse("path/to/file.pdf")

@app.get("/named")
async def named_download():
    return FileResponse(
        "path/to/report.pdf",
        filename="annual-report.pdf",
        content_disposition_type="attachment",
    )

@app.get("/inline")
async def inline_view():
    return FileResponse(
        "image.png",
        media_type="image/png",
        filename="image.png",
        content_disposition_type="inline",
    )

Constructor:

1
2
3
4
5
6
7
8
FileResponse(
    path,                              # File path string
    status=200,
    headers=None,
    media_type=None,                   # Auto-detected via mimetypes if omitted
    filename=None,                     # Defaults to basename of path
    content_disposition_type="attachment",  # "attachment" or "inline"
)

Attributes:

  • streaming = True
  • stream_body() — async generator yielding file contents in 64 KB chunks
  • Automatically sets Content-Length, Content-Type, and Content-Disposition headers
Edit on GitHub Last Updated: Oct 20, 2018
© 2026 Fenrir Project.
main*
v4.1.2
Ln 1, Col 1
UTF-8
Prettier
Light Mode
Markdown