Custom Request and APIRoute class

Warning

The current page still doesn’t have a translation for this language.

But you can help translating it: Contributing.

In some cases, you may want to override the logic used by the Request and APIRoute classes.

In particular, this may be a good alternative to logic in a middleware.

For example, if you want to read or manipulate the request body before it is processed by your application.

Danger

This is an “advanced” feature.

If you are just starting with FastAPI you might want to skip this section.

Use cases

Some use cases include:

  • Converting non-JSON request bodies to JSON (e.g. msgpack).
  • Decompressing gzip-compressed request bodies.
  • Automatically logging all request bodies.

Handling custom request body encodings

Let’s see how to make use of a custom Request subclass to decompress gzip requests.

And an APIRoute subclass to use that custom request class.

Create a custom GzipRequest class

Tip

This is a toy example to demonstrate how it works, if you need Gzip support, you can use the provided GzipMiddleware.

First, we create a GzipRequest class, which will overwrite the Request.body() method to decompress the body in the presence of an appropriate header.

If there’s no gzip in the header, it will not try to decompress the body.

That way, the same route class can handle gzip compressed or uncompressed requests.

  1. import gzip
  2. from typing import Callable, List
  3. from fastapi import Body, FastAPI, Request, Response
  4. from fastapi.routing import APIRoute
  5. class GzipRequest(Request):
  6. async def body(self) -> bytes:
  7. if not hasattr(self, "_body"):
  8. body = await super().body()
  9. if "gzip" in self.headers.getlist("Content-Encoding"):
  10. body = gzip.decompress(body)
  11. self._body = body
  12. return self._body
  13. class GzipRoute(APIRoute):
  14. def get_route_handler(self) -> Callable:
  15. original_route_handler = super().get_route_handler()
  16. async def custom_route_handler(request: Request) -> Response:
  17. request = GzipRequest(request.scope, request.receive)
  18. return await original_route_handler(request)
  19. return custom_route_handler
  20. app = FastAPI()
  21. app.router.route_class = GzipRoute
  22. @app.post("/sum")
  23. async def sum_numbers(numbers: List[int] = Body(...)):
  24. return {"sum": sum(numbers)}

Create a custom GzipRoute class

Next, we create a custom subclass of fastapi.routing.APIRoute that will make use of the GzipRequest.

This time, it will overwrite the method APIRoute.get_route_handler().

This method returns a function. And that function is what will receive a request and return a response.

Here we use it to create a GzipRequest from the original request.

  1. import gzip
  2. from typing import Callable, List
  3. from fastapi import Body, FastAPI, Request, Response
  4. from fastapi.routing import APIRoute
  5. class GzipRequest(Request):
  6. async def body(self) -> bytes:
  7. if not hasattr(self, "_body"):
  8. body = await super().body()
  9. if "gzip" in self.headers.getlist("Content-Encoding"):
  10. body = gzip.decompress(body)
  11. self._body = body
  12. return self._body
  13. class GzipRoute(APIRoute):
  14. def get_route_handler(self) -> Callable:
  15. original_route_handler = super().get_route_handler()
  16. async def custom_route_handler(request: Request) -> Response:
  17. request = GzipRequest(request.scope, request.receive)
  18. return await original_route_handler(request)
  19. return custom_route_handler
  20. app = FastAPI()
  21. app.router.route_class = GzipRoute
  22. @app.post("/sum")
  23. async def sum_numbers(numbers: List[int] = Body(...)):
  24. return {"sum": sum(numbers)}

Technical Details

A Request has a request.scope attribute, that’s just a Python dict containing the metadata related to the request.

A Request also has a request.receive, that’s a function to “receive” the body of the request.

The scope dict and receive function are both part of the ASGI specification.

And those two things, scope and receive, are what is needed to create a new Request instance.

To learn more about the Request check Starlette’s docs about Requests.

The only thing the function returned by GzipRequest.get_route_handler does differently is convert the Request to a GzipRequest.

Doing this, our GzipRequest will take care of decompressing the data (if necessary) before passing it to our path operations.

After that, all of the processing logic is the same.

But because of our changes in GzipRequest.body, the request body will be automatically decompressed when it is loaded by FastAPI when needed.

Accessing the request body in an exception handler

Tip

To solve this same problem, it’s probably a lot easier to use the body in a custom handler for RequestValidationError (Handling Errors).

But this example is still valid and it shows how to interact with the internal components.

We can also use this same approach to access the request body in an exception handler.

All we need to do is handle the request inside a try/except block:

  1. from typing import Callable, List
  2. from fastapi import Body, FastAPI, HTTPException, Request, Response
  3. from fastapi.exceptions import RequestValidationError
  4. from fastapi.routing import APIRoute
  5. class ValidationErrorLoggingRoute(APIRoute):
  6. def get_route_handler(self) -> Callable:
  7. original_route_handler = super().get_route_handler()
  8. async def custom_route_handler(request: Request) -> Response:
  9. try:
  10. return await original_route_handler(request)
  11. except RequestValidationError as exc:
  12. body = await request.body()
  13. detail = {"errors": exc.errors(), "body": body.decode()}
  14. raise HTTPException(status_code=422, detail=detail)
  15. return custom_route_handler
  16. app = FastAPI()
  17. app.router.route_class = ValidationErrorLoggingRoute
  18. @app.post("/")
  19. async def sum_numbers(numbers: List[int] = Body(...)):
  20. return sum(numbers)

If an exception occurs, theRequest instance will still be in scope, so we can read and make use of the request body when handling the error:

  1. from typing import Callable, List
  2. from fastapi import Body, FastAPI, HTTPException, Request, Response
  3. from fastapi.exceptions import RequestValidationError
  4. from fastapi.routing import APIRoute
  5. class ValidationErrorLoggingRoute(APIRoute):
  6. def get_route_handler(self) -> Callable:
  7. original_route_handler = super().get_route_handler()
  8. async def custom_route_handler(request: Request) -> Response:
  9. try:
  10. return await original_route_handler(request)
  11. except RequestValidationError as exc:
  12. body = await request.body()
  13. detail = {"errors": exc.errors(), "body": body.decode()}
  14. raise HTTPException(status_code=422, detail=detail)
  15. return custom_route_handler
  16. app = FastAPI()
  17. app.router.route_class = ValidationErrorLoggingRoute
  18. @app.post("/")
  19. async def sum_numbers(numbers: List[int] = Body(...)):
  20. return sum(numbers)

Custom APIRoute class in a router

You can also set the route_class parameter of an APIRouter:

  1. import time
  2. from typing import Callable
  3. from fastapi import APIRouter, FastAPI, Request, Response
  4. from fastapi.routing import APIRoute
  5. class TimedRoute(APIRoute):
  6. def get_route_handler(self) -> Callable:
  7. original_route_handler = super().get_route_handler()
  8. async def custom_route_handler(request: Request) -> Response:
  9. before = time.time()
  10. response: Response = await original_route_handler(request)
  11. duration = time.time() - before
  12. response.headers["X-Response-Time"] = str(duration)
  13. print(f"route duration: {duration}")
  14. print(f"route response: {response}")
  15. print(f"route response headers: {response.headers}")
  16. return response
  17. return custom_route_handler
  18. app = FastAPI()
  19. router = APIRouter(route_class=TimedRoute)
  20. @app.get("/")
  21. async def not_timed():
  22. return {"message": "Not timed"}
  23. @router.get("/timed")
  24. async def timed():
  25. return {"message": "It's the time of my life"}
  26. app.include_router(router)

In this example, the path operations under the router will use the custom TimedRoute class, and will have an extra X-Response-Time header in the response with the time it took to generate the response:

  1. import time
  2. from typing import Callable
  3. from fastapi import APIRouter, FastAPI, Request, Response
  4. from fastapi.routing import APIRoute
  5. class TimedRoute(APIRoute):
  6. def get_route_handler(self) -> Callable:
  7. original_route_handler = super().get_route_handler()
  8. async def custom_route_handler(request: Request) -> Response:
  9. before = time.time()
  10. response: Response = await original_route_handler(request)
  11. duration = time.time() - before
  12. response.headers["X-Response-Time"] = str(duration)
  13. print(f"route duration: {duration}")
  14. print(f"route response: {response}")
  15. print(f"route response headers: {response.headers}")
  16. return response
  17. return custom_route_handler
  18. app = FastAPI()
  19. router = APIRouter(route_class=TimedRoute)
  20. @app.get("/")
  21. async def not_timed():
  22. return {"message": "Not timed"}
  23. @router.get("/timed")
  24. async def timed():
  25. return {"message": "It's the time of my life"}
  26. app.include_router(router)