跳至内容

自定义请求和 APIRoute 类

在某些情况下,您可能希望覆盖 RequestAPIRoute 类使用的逻辑。

特别是,这可能是中间件中逻辑的一个很好的替代方案。

例如,如果您想在应用程序处理请求体之前读取或操作它。

危险

这是一个“高级”功能。

如果您刚开始使用 FastAPI,您可能希望跳过此部分。

用例

一些用例包括

  • 将非 JSON 请求体转换为 JSON(例如 msgpack)。
  • 解压缩 gzip 压缩的请求体。
  • 自动记录所有请求体。

处理自定义请求体编码

让我们看看如何使用自定义 Request 子类来解压缩 gzip 请求。

以及一个 APIRoute 子类来使用该自定义请求类。

创建自定义 GzipRequest

提示

这是一个玩具示例,用于演示其工作原理,如果您需要 Gzip 支持,您可以使用提供的 GzipMiddleware

首先,我们创建一个 GzipRequest 类,它将覆盖 Request.body() 方法,以便在存在适当 Header 的情况下解压缩主体。

如果 Header 中没有 gzip,它将不会尝试解压缩主体。

这样,相同的路由类可以处理 gzip 压缩或未压缩的请求。

import gzip
from typing import Callable, List

from fastapi import Body, FastAPI, Request, Response
from fastapi.routing import APIRoute


class GzipRequest(Request):
    async def body(self) -> bytes:
        if not hasattr(self, "_body"):
            body = await super().body()
            if "gzip" in self.headers.getlist("Content-Encoding"):
                body = gzip.decompress(body)
            self._body = body
        return self._body


class GzipRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            request = GzipRequest(request.scope, request.receive)
            return await original_route_handler(request)

        return custom_route_handler


app = FastAPI()
app.router.route_class = GzipRoute


@app.post("/sum")
async def sum_numbers(numbers: List[int] = Body()):
    return {"sum": sum(numbers)}

创建自定义 GzipRoute

接下来,我们创建 fastapi.routing.APIRoute 的自定义子类,它将使用 GzipRequest

这次,它将覆盖 APIRoute.get_route_handler() 方法。

此方法返回一个函数。该函数将接收请求并返回响应。

在这里,我们使用它从原始请求创建 GzipRequest

import gzip
from typing import Callable, List

from fastapi import Body, FastAPI, Request, Response
from fastapi.routing import APIRoute


class GzipRequest(Request):
    async def body(self) -> bytes:
        if not hasattr(self, "_body"):
            body = await super().body()
            if "gzip" in self.headers.getlist("Content-Encoding"):
                body = gzip.decompress(body)
            self._body = body
        return self._body


class GzipRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            request = GzipRequest(request.scope, request.receive)
            return await original_route_handler(request)

        return custom_route_handler


app = FastAPI()
app.router.route_class = GzipRoute


@app.post("/sum")
async def sum_numbers(numbers: List[int] = Body()):
    return {"sum": sum(numbers)}

“技术细节”

Request 具有 request.scope 属性,它只是一个 Python dict,包含与请求相关的元数据。

Request 还具有 request.receive,它是一个用于“接收”请求体面的函数。

scope dictreceive 函数都是 ASGI 规范的一部分。

并且这两件事,scopereceive,是创建新 Request 实例所需的东西。

要了解有关 Request 的更多信息,请查看 Starlette 关于请求的文档

GzipRequest.get_route_handler 返回的函数唯一不同之处在于它将 Request 转换为 GzipRequest

这样做,我们的 GzipRequest 将在将数据传递给我们的路径操作之前(如果需要)处理数据解压缩。

之后,所有处理逻辑都相同。

但由于我们在 GzipRequest.body 中的更改,请求体将在 FastAPI 需要时自动解压缩。

在异常处理程序中访问请求体

提示

为了解决同样的问题,使用 RequestValidationError 的自定义处理程序中的 body 可能更容易得多(处理错误)。

但此示例仍然有效,并且它显示了如何与内部组件交互。

我们也可以使用相同的方法在异常处理程序中访问请求体。

我们只需要在 try/except 块中处理请求

from typing import Callable, List

from fastapi import Body, FastAPI, HTTPException, Request, Response
from fastapi.exceptions import RequestValidationError
from fastapi.routing import APIRoute


class ValidationErrorLoggingRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            try:
                return await original_route_handler(request)
            except RequestValidationError as exc:
                body = await request.body()
                detail = {"errors": exc.errors(), "body": body.decode()}
                raise HTTPException(status_code=422, detail=detail)

        return custom_route_handler


app = FastAPI()
app.router.route_class = ValidationErrorLoggingRoute


@app.post("/")
async def sum_numbers(numbers: List[int] = Body()):
    return sum(numbers)

如果发生异常,Request 实例仍然在作用域内,因此我们可以在处理错误时读取和使用请求体。

from typing import Callable, List

from fastapi import Body, FastAPI, HTTPException, Request, Response
from fastapi.exceptions import RequestValidationError
from fastapi.routing import APIRoute


class ValidationErrorLoggingRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            try:
                return await original_route_handler(request)
            except RequestValidationError as exc:
                body = await request.body()
                detail = {"errors": exc.errors(), "body": body.decode()}
                raise HTTPException(status_code=422, detail=detail)

        return custom_route_handler


app = FastAPI()
app.router.route_class = ValidationErrorLoggingRoute


@app.post("/")
async def sum_numbers(numbers: List[int] = Body()):
    return sum(numbers)

路由器中的自定义APIRoute

您还可以设置APIRouterroute_class 参数。

import time
from typing import Callable

from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute


class TimedRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            before = time.time()
            response: Response = await original_route_handler(request)
            duration = time.time() - before
            response.headers["X-Response-Time"] = str(duration)
            print(f"route duration: {duration}")
            print(f"route response: {response}")
            print(f"route response headers: {response.headers}")
            return response

        return custom_route_handler


app = FastAPI()
router = APIRouter(route_class=TimedRoute)


@app.get("/")
async def not_timed():
    return {"message": "Not timed"}


@router.get("/timed")
async def timed():
    return {"message": "It's the time of my life"}


app.include_router(router)

在此示例中,router 下的路径操作将使用自定义的TimedRoute 类,并在响应中添加一个额外的X-Response-Time 头,其中包含生成响应所花费的时间。

import time
from typing import Callable

from fastapi import APIRouter, FastAPI, Request, Response
from fastapi.routing import APIRoute


class TimedRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            before = time.time()
            response: Response = await original_route_handler(request)
            duration = time.time() - before
            response.headers["X-Response-Time"] = str(duration)
            print(f"route duration: {duration}")
            print(f"route response: {response}")
            print(f"route response headers: {response.headers}")
            return response

        return custom_route_handler


app = FastAPI()
router = APIRouter(route_class=TimedRoute)


@app.get("/")
async def not_timed():
    return {"message": "Not timed"}


@router.get("/timed")
async def timed():
    return {"message": "It's the time of my life"}


app.include_router(router)