Skip to content

Request handlers

The previous pages describe that a request handler in BlackSheep is a function associated with a route, having the responsibility of handling web requests. This page describes request handlers in detail, covering the following:

  • Request handler normalization.
  • Using asynchronous and synchronous code.

Request handler normalization.

A normal request handler in BlackSheep is defined as an asynchronous function having the following signature:

from blacksheep import Request, Response

async def normal_handler(request: Request) -> Response:
    ...

To be a request handler, a function must be associated with a route:

from blacksheep import Application, Request, Response, get, text


app = Application()


@get("/")
async def normal_handler(request: Request) -> Response:
    return text("Example")

A request handler defined this way is called directly to generate a response when a web request matches the route associated with the function (in this case, HTTP GET at the root of the website "/").

However, to improve the developer's experience and development speed, BlackSheep implements automatic normalization of request handlers. For example, it is possible to define a request handler as a synchronous function, the framework automatically wraps the synchronous function into an asynchronous wrapper:

@get("/sync")
def sync_handler(request: Request) -> Response:
    return text("Example")

Avoid blocking code in synchronous methods!

When a request handler is defined as a synchronous method, BlackSheep assumes that the author of the code knows what they are doing and about asynchronous programming, and that the response should be returned immediately without I/O or CPU-intensive operations that would block the event loop. BlackSheep does nothing to prevent blocking the event loop, if you add blocking operations in your code.

Similarly, request handlers are normalized when their function signature is different than the normal one. For example, a request handler can be defined without arguments, and returning a plain str or an instance of an object (which gets serialized to JSON and configured as response content):

@get("/greetings")
def hello_there() -> str:
    return "Hello, There!"

In the example below, the response content is JSON {"id":1,"name":"Celine"}:

from dataclasses import dataclass


@dataclass
class Cat:
    id: int
    name: str


@get("/example-cat")
def get_example_cat() -> Cat:
    return Cat(1, "Celine")

Automatic binding of parameters

An important feature enabled by function normalization is the automatic binding of request parameters, as described in the Getting Started pages. Common scenarios are using route parameters and query string parameters:

# in this case, a route parameter is passed directly to the request handler
@get("/greetings/{name}")
def hello(name: str) -> str:
    return f"Hello, {name}!"


# in this case, query string parameters by name are read from the request and
# passed to the request handler
@get("/api/cats")
def get_cats(page: int = 1, page_size: int = 30, search: str = "") -> Response:
    ...

In the get_cats example above, function parameters are read automatically from the query string and parsed, if present, otherwise default values are used.

Explicit and implicit binding

All examples so far showed how to use implicit binding of request parameters. In the get_cats example above, all parameters are implicitly bound from the request query string. To enable more scenarios, BlackSheep also provides explicit bindings that allow specifying the source of the parameter (e.g. request headers, cookies, route, query, body, application services). In the example below, cat_input is read automatically from the request payload as JSON and deserialized automatically into an instance of the CreateCatInput class.

from dataclasses import dataclass

from blacksheep.server.bindings import FromJSON


@dataclass
class CreateCatInput:
    name: str
    ...


@post("/cat")
async def create_cat(
    cat_input: FromJSON[CreateCatInput]
):
    data = cat_input.value
    ...

More details about bindings are described in Binders.

Normalization and OpenAPI Documentation

Request handler normalization also enables a more accurate generation of OpenAPI Documentation, since the web framework knows that request handlers need input from query strings, routes, headers, cookies, etc.; and produce responses of a certain type.

Using asynchronous and synchronous code.

BlackSheep supports both asynchronous and synchronous request handlers. Request handlers don't need to be asynchronous in those scenarios when the response is well-known and can be produced without doing any I/O bound operation or any CPU-intensive operation. This is the case for example of redirects, and the previous "Hello, There!" example:

from blacksheep import Application, Request, Response, get, text, redirect


app = Application()


@get("/sync")
def sync_handler() -> str:
    return "Example Sync"

@get("/redirect-me")
def redirect_example() -> Response:
    return redirect("/sync")

Request handlers that do I/O bound operations or CPU-intensive operations should instead be async, to not hinder the performance of the web server. For example, if information is fetched from a database or a remote API when handling a web request handler, it is correct to use asynchronous code to reduce RAM consumption and not block the event loop of the web application.

Warning

If an operation is CPU-intensive (e.g. involving file operations, resizing a picture), the request handlers that initiate such operation should be async, and use a thread or process pool to not block the web app's event loop. Similarly, request handlers that initiate I/O bound operations (e.g. web requests to external APIs, connecting to a database) should also be async.

Next

The next pages describe requests and responses in detail.

Last modified on: 2023-12-18 17:52:09

EW
RV