tezvyn:

Starlette's Request Object: A Clean API for ASGI

AI-drafted, machine-checkedSource: starlette.ioadvanced

Starlette's Request object is a high-level wrapper around the raw ASGI scope, providing a clean API for request data. Use it in endpoints to read headers, query params, or parse the body. The footgun: the request body can only be read once.

WHY IT EXISTS ASGI is a low-level specification defining communication between a server and an application using a dictionary called scope and receive/send channels. Directly working with these is verbose and error-prone. Starlette's Request object was created to provide a more intuitive, object-oriented interface on top of this raw data.

THE MENTAL MODEL The Request object is a friendly facade over the raw ASGI connection details. Instead of digging into the scope dictionary like scope['headers'], you use a clean property like request.headers. It organizes all the disparate pieces of an HTTP request—URL, headers, cookies, body—into a single, easy-to-use Python object.

HOW IT WORKS When a request comes in, Starlette instantiates a Request object using the ASGI scope and receive channel. Simple attributes like request.method and request.url are read directly from the scope. Accessing the body, however, involves await-ing methods that consume the receive channel. For example, await request.json() reads the incoming byte stream, ensures it's fully received, and then parses it as JSON. This body consumption can only happen once.

WHEN TO USE IT Use the Request object in every Starlette or FastAPI path operation function to handle incoming data. It's the standard, idiomatic way to access query parameters (request.query_params), path parameters (request.path_params), headers (request.headers), cookies (request.cookies), and the client's address (request.client). It's also essential for handling different body types, like JSON, form data, or file uploads.

WHEN NOT TO USE IT There's rarely a reason to avoid the Request object in application code. However, if you are writing low-level ASGI middleware that needs to inspect or modify the raw scope before the application sees it, you might work with the scope dictionary directly. For most application-level logic, sticking to the Request object is best practice.

ONE CANONICAL EXAMPLE A common footgun is trying to read the request body twice, as it's a stream that gets consumed on the first read. The following code will fail because .body() is called before .json(): async def my_endpoint(request: Request): body_bytes = await request.body() # First read, consumes the stream body_json = await request.json() # Second read, will raise an error

The correct approach is to read it once into a variable and reuse that variable: async def my_endpoint(request: Request): body_json = await request.json() # Now use the body_json variable for all subsequent logic

Read the original → starlette.io

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.