某些情况下,需要向客户端返回错误提示。

这里所谓的客户端包括前端浏览器、其他应用程序、物联网设备等。

需要向客户端返回错误提示的场景主要如下:

  • 客户端没有执行操作的权限
  • 客户端没有访问资源的权限
  • 客户端要访问的项目不存在
  • 等等 …

遇到这些情况时,通常要返回 4XX(400 至 499)HTTP 状态码。

4XX 状态码与表示请求成功的 2XX(200 至 299) HTTP 状态码类似。

只不过,4XX 状态码表示客户端发生的错误。

大家都知道「404 Not Found」错误,还有调侃这个错误的笑话吧?

使用 HTTPException

向客户端返回 HTTP 错误响应,可以使用 HTTPException。

导入 HTTPException

  1. from fastapi import FastAPI, HTTPException
  2. app = FastAPI()
  3. items = {"foo": "The Foo Wrestlers"}
  4. @app.get("/items/{item_id}")
  5. async def read_item(item_id: str):
  6. if item_id not in items:
  7. raise HTTPException(status_code=404, detail="Item not found")
  8. return {"item": items[item_id]}

触发 HTTPException

HTTPException 是额外包含了和 API 有关数据的常规 Python 异常。

因为是 Python 异常,所以不能 return,只能 raise。

如在调用路径操作函数里的工具函数时,触发了 HTTPException,FastAPI 就不再继续执行路径操作函数中的后续代码,而是立即终止请求,并把 HTTPException 的 HTTP 错误发送至客户端。

在介绍依赖项与安全的章节中,您可以了解更多用 raise 异常代替 return 值的优势。

本例中,客户端用 ID 请求的 item 不存在时,触发状态码为 404 的异常:

  1. from fastapi import FastAPI, HTTPException
  2. app = FastAPI()
  3. items = {"foo": "The Foo Wrestlers"}
  4. @app.get("/items/{item_id}")
  5. async def read_item(item_id: str):
  6. if item_id not in items:
  7. raise HTTPException(status_code=404, detail="Item not found") return {"item": items[item_id]}

响应结果

请求为 http://example.com/items/foo(item_id 为 「foo」)时,客户端会接收到 HTTP 状态码 - 200 及如下 JSON 响应结果:

{ "item": "The Foo Wrestlers" }

但如果客户端请求 http://example.com/items/bar(item_id 「bar」 不存在时),则会接收到 HTTP 状态码 - 404(「未找到」错误)及如下 JSON 响应结果:

{ "detail": "Item not found" }

提示

触发 HTTPException 时,可以用参数 detail 传递任何能转换为 JSON 的值,不仅限于 str。

还支持传递 dict、list 等数据结构。

FastAPI 能自动处理这些数据,并将之转换为 JSON。

添加自定义响应头

有些场景下要为 HTTP 错误添加自定义响应头。例如,出于某些方面的安全需要。

一般情况下可能不会需要在代码中直接使用响应头。

但对于某些高级应用场景,还是需要添加自定义响应头:

  1. from fastapi import FastAPI, HTTPException
  2. app = FastAPI()
  3. items = {"foo": "The Foo Wrestlers"}
  4. @app.get("/items-header/{item_id}")
  5. async def read_item_header(item_id: str):
  6. if item_id not in items:
  7. raise HTTPException(
  8. status_code=404,
  9. detail="Item not found",
  10. headers={"X-Error": "There goes my error"}, )
  11. return {"item": items[item_id]}

安装自定义异常处理器

添加自定义处理器,要使用 Starlette 的异常工具。

假设要触发的自定义异常叫作 UnicornException。

且需要 FastAPI 实现全局处理该异常。

此时,可以用 @app.exception_handler() 添加自定义异常控制器:

  1. from fastapi import FastAPI, Request
  2. from fastapi.responses import JSONResponse
  3. class UnicornException(Exception):
  4. def __init__(self, name: str): self.name = name
  5. app = FastAPI()
  6. @app.exception_handler(UnicornException) async def unicorn_exception_handler(request: Request, exc: UnicornException):
  7. return JSONResponse( status_code=418, content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."}, )
  8. @app.get("/unicorns/{name}")
  9. async def read_unicorn(name: str):
  10. if name == "yolo":
  11. raise UnicornException(name=name) return {"unicorn_name": name}

请求 /unicorns/yolo 时,路径操作会触发 UnicornException。

但该异常将会被 unicorn_exception_handler 处理。

接收到的错误信息清晰明了,HTTP 状态码为 418,JSON 内容如下:

{"message": "Oops! yolo did something. There goes a rainbow..."}

技术细节

  1. from starlette.requests import Request
  2. ``` 和

from starlette.responses import JSONResponse `` 也可以用于导入Request和JSONResponse`。

FastAPI 提供了与 starlette.responses 相同的 fastapi.responses 作为快捷方式,但大部分响应操作都可以直接从 Starlette 导入。同理,Request 也是如此。

覆盖默认异常处理器

FastAPI 自带了一些默认异常处理器。

触发 HTTPException 或请求无效数据时,这些处理器返回默认的 JSON 响应结果。

不过,也可以使用自定义处理器覆盖默认异常处理器。

覆盖请求验证异常

请求中包含无效数据时,FastAPI 内部会触发 RequestValidationError。

该异常也内置了默认异常处理器。

覆盖默认异常处理器时需要导入 RequestValidationError,并用 @app.excption_handler(RequestValidationError) 装饰异常处理器。

这样,异常处理器就可以接收 Request 与异常。

  1. from fastapi import FastAPI, HTTPException
  2. from fastapi.exceptions import RequestValidationError from fastapi.responses import PlainTextResponse
  3. from starlette.exceptions import HTTPException as StarletteHTTPException
  4. app = FastAPI()
  5. @app.exception_handler(StarletteHTTPException)
  6. async def http_exception_handler(request, exc):
  7. return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
  8. @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc):
  9. return PlainTextResponse(str(exc), status_code=400)
  10. @app.get("/items/{item_id}")
  11. async def read_item(item_id: int):
  12. if item_id == 3:
  13. raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
  14. return {"item_id": item_id}

访问 /items/foo,可以看到默认的 JSON 错误信息:

{ "detail": [ { "loc": [ "path", "item_id" ], "msg": "value is not a valid integer", "type": "type_error.integer" } ] }

被替换为了以下文本格式的错误信息:

1 validation error path -> item_id value is not a valid integer (type=type_error.integer)

RequestValidationError vs ValidationError

警告

如果您觉得现在还用不到以下技术细节,可以先跳过下面的内容。

RequestValidationError 是 Pydantic 的 ValidationError 的子类。

FastAPI 调用的就是 RequestValidationError 类,因此,如果在 response_model 中使用 Pydantic 模型,且数据有错误时,在日志中就会看到这个错误。

但客户端或用户看不到这个错误。反之,客户端接收到的是 HTTP 状态码为 500 的「内部服务器错误」。

这是因为在响应或代码(不是在客户端的请求里)中出现的 Pydantic ValidationError 是代码的 bug。

修复错误时,客户端或用户不能访问错误的内部信息,否则会造成安全隐患。

覆盖 HTTPException 错误处理器

同理,也可以覆盖 HTTPException 处理器。

例如,只为错误返回纯文本响应,而不是返回 JSON 格式的内容:

  1. from fastapi import FastAPI, HTTPException
  2. from fastapi.exceptions import RequestValidationError
  3. from fastapi.responses import PlainTextResponse from starlette.exceptions import HTTPException as StarletteHTTPException
  4. app = FastAPI()
  5. @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request, exc):
  6. return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
  7. @app.exception_handler(RequestValidationError)
  8. async def validation_exception_handler(request, exc):
  9. return PlainTextResponse(str(exc), status_code=400)
  10. @app.get("/items/{item_id}")
  11. async def read_item(item_id: int):
  12. if item_id == 3:
  13. raise HTTPException(status_code=418, detail="Nope! I don't like 3.") return {"item_id": item_id}

技术细节

还可以使用 from starlette.responses import PlainTextResponse。

FastAPI 提供了与 starlette.responses 相同的 fastapi.responses 作为快捷方式,但大部分响应都可以直接从 Starlette 导入。

使用 RequestValidationError 的请求体

RequestValidationError 包含其接收到的无效数据请求的 body 。

开发时,可以用这个请求体生成日志、调试错误,并返回给用户。

  1. from fastapi import FastAPI, Request, status
  2. from fastapi.encoders import jsonable_encoder
  3. from fastapi.exceptions import RequestValidationError
  4. from fastapi.responses import JSONResponse
  5. from pydantic import BaseModel
  6. app = FastAPI()
  7. @app.exception_handler(RequestValidationError)
  8. async def validation_exception_handler(request: Request, exc: RequestValidationError):
  9. return JSONResponse(
  10. status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
  11. content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), )
  12. class Item(BaseModel):
  13. title: str
  14. size: int
  15. @app.post("/items/")
  16. async def create_item(item: Item):
  17. return item

现在试着发送一个无效的 item,例如:

{ "title": "towel", "size": "XL" }

收到的响应包含 body 信息,并说明数据是无效的:

{ "detail": [ { "loc": [ "body", "size" ], "msg": "value is not a valid integer", "type": "type_error.integer" } ], "body": { "title": "towel", "size": "XL" } }

FastAPI HTTPException vs Starlette HTTPException

FastAPI 也提供了自有的 HTTPException。

FastAPI 的 HTTPException 继承自 Starlette 的 HTTPException 错误类。

它们之间的唯一区别是,FastAPI 的 HTTPException 可以在响应中添加响应头。

OAuth 2.0 等安全工具需要在内部调用这些响应头。

因此你可以继续像平常一样在代码中触发 FastAPI 的 HTTPException 。

但注册异常处理器时,应该注册到来自 Starlette 的 HTTPException。

这样做是为了,当 Starlette 的内部代码、扩展或插件触发 Starlette HTTPException 时,处理程序能够捕获、并处理此异常。

注意,本例代码中同时使用了这两个 HTTPException,此时,要把 Starlette 的 HTTPException 命名为 StarletteHTTPException:

  1. from starlette.exceptions import HTTPException as StarletteHTTPException

复用 FastAPI 异常处理器

FastAPI 支持先对异常进行某些处理,然后再使用 FastAPI 中处理该异常的默认异常处理器。

从 fastapi.exception_handlers 中导入要复用的默认异常处理器:

  1. from fastapi import FastAPI, HTTPException
  2. from fastapi.exception_handlers import (
  3. http_exception_handler, request_validation_exception_handler, ) from fastapi.exceptions import RequestValidationError
  4. from starlette.exceptions import HTTPException as StarletteHTTPException
  5. app = FastAPI()
  6. @app.exception_handler(StarletteHTTPException)
  7. async def custom_http_exception_handler(request, exc):
  8. print(f"OMG! An HTTP error!: {repr(exc)}")
  9. return await http_exception_handler(request, exc)
  10. @app.exception_handler(RequestValidationError)
  11. async def validation_exception_handler(request, exc):
  12. print(f"OMG! The client sent invalid data!: {exc}")
  13. return await request_validation_exception_handler(request, exc)
  14. @app.get("/items/{item_id}")
  15. async def read_item(item_id: int):
  16. if item_id == 3:
  17. raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
  18. return {"item_id": item_id}

虽然,本例只是输出了夸大其词的错误信息。

但也足以说明,可以在处理异常之后再复用默认的异常处理器。