Skip to content

Commit 7921cfe

Browse files
committed
doc: improve doc and add section regarding documenting problems
1 parent e054220 commit 7921cfe

File tree

1 file changed

+66
-22
lines changed

1 file changed

+66
-22
lines changed

README.md

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ This FastAPI plugin allow you to automatically format any errors as Problem deta
77
- [Changing default validation error status code and/or detail](#changing-default-validation-error-status-code-andor-detail)
88
- [HTTP errors handling](#http-errors-handling)
99
- [Unexisting routes error handling](#unexisting-routes-error-handling)
10-
- [Unhandled errors handling](#unhandled-errors-handling)
10+
- [Unexpected errors handling](#unexpected-errors-handling)
1111
- [Including exceptions type and stack traces](#including-exceptions-type-and-stack-traces)
1212
- [Custom errors handling](#custom-errors-handling)
1313
- [Returning HTTP errors as Problem Details](#returning-http-errors-as-problem-details)
1414
- [Keeping the code DRY](#keeping-the-code-dry)
1515
- [1. Inheritance](#1-inheritance)
1616
- [2. Custom error handlers](#2-custom-error-handlers)
17+
- [Wrapping up](#wrapping-up)
18+
- [Documenting your custom problems details](#documenting-your-custom-problems-details)
1719

1820
## Getting Started
1921

@@ -178,14 +180,12 @@ curl -X POST http://localhost:8000/not-exist
178180
}
179181
```
180182

181-
## Unhandled errors handling
183+
## Unexpected errors handling
182184

183-
Any unhandled errors raised during processing of a request will be automatically handled by the plugin which will returns an internal server error formatted as a Problem Details.
185+
Any unexpected errors raised during processing of a request will be automatically handled by the plugin which will returns an internal server error formatted as a Problem Details.
184186

185187
> Also note that the exception will be logged as well using logger named `fastapi_problem_details.error_handlers`
186188
187-
The message of the error `str(exception)` will be used as the `detail` property and will default to a more generic message when not defined.
188-
189189
```python
190190
from typing import Any
191191

@@ -195,18 +195,14 @@ import fastapi_problem_details as problem
195195

196196
app = FastAPI()
197197

198-
problem.init_app(app, include_exc_info_in_response=True)
198+
problem.init_app(app)
199199

200200

201201
class CustomError(Exception):
202202
pass
203203

204204

205205
@app.get("/")
206-
def raise_error() -> Any: # noqa: ANN401
207-
raise CustomError
208-
209-
@app.get("/with-details")
210206
def raise_error() -> Any: # noqa: ANN401
211207
raise CustomError("Something went wrong...")
212208
```
@@ -219,13 +215,6 @@ $ curl http://localhost:8000
219215
"status": 500,
220216
"detail": "Server got itself in trouble",
221217
}
222-
$ curl http://localhost:8000/with-details
223-
{
224-
"type": "about:blank",
225-
"title": "Internal Server Error",
226-
"status": 500,
227-
"detail": "Something went wrong...",
228-
}
229218
```
230219

231220
### Including exceptions type and stack traces
@@ -324,7 +313,6 @@ $ curl http://localhost:8000/users/1234
324313
"title":"User Not Found",
325314
"status":404,
326315
"detail":"There is no user with id '1234'",
327-
"instance":null,
328316
"user_id":"1234"
329317
}
330318
```
@@ -333,7 +321,7 @@ Note that in this example I've provided a custom `type` property but this might
333321

334322
> Likewise, truly generic problems -- i.e., conditions that might apply to any resource on the Web -- are usually better expressed as plain status codes. For example, a "write access disallowed" problem is probably unnecessary, since a 403 Forbidden status code in response to a PUT request is self-explanatory.
335323
336-
Also note that you can additional properties to the `ProblemResponse` object like `headers` or `instance`. Any extra properties will be added as-is in the returned Problem Details object (like the `user_id` in this example).
324+
Also note that you can include additional properties to the `ProblemResponse` object like `headers` or `instance`. Any extra properties will be added as-is in the returned Problem Details object (like the `user_id` in this example).
337325

338326
Last but not least, any `null` values are stripped from returned Problem Details object.
339327

@@ -390,7 +378,8 @@ curl http://localhost:8000 -v
390378
"type":"about:blank",
391379
"title":"Service Unavailable",
392380
"status":503,
393-
"detail":"One or several internal services are not working properly","service_1":"down",
381+
"detail":"One or several internal services are not working properly",
382+
"service_1":"down",
394383
"service_2":"up"
395384
}
396385
```
@@ -399,7 +388,7 @@ The `ProblemException` exception takes almost same arguments as a `ProblemRespon
399388

400389
### Keeping the code DRY
401390

402-
If you start having to raise almost the same `ProblemException` in several places of your code (for example when you validate a requester permissions) you have too ways to avoid copy-pasting the same object in many places of your code
391+
If you start having to raise almost the same `ProblemException` in several places of your code (for example when you validate a requester permissions) you have two ways to avoid copy-pasting the same object in many places of your code
403392

404393
#### 1. Inheritance
405394

@@ -431,7 +420,7 @@ def do_something_meaningful(user_id: str):
431420

432421
The advantage of this solution is that its rather simple and straightforward. You do not have anything else to do to properly returns Problem Details responses.
433422

434-
The main issue of this is that it can cause your code to cross boundaries. If you start to use `ProblemException` into your domain logic, you couple your core code with your primary adapter (See ports and adapters pattern), your API. If you decide to build a CLI and/or and event based application using the same core logic, you'll end up with uncomfortable problem exception and status code which has no meaning here.
423+
The main issue of this is that it can cause your code to cross boundaries. If you start to use `ProblemException` into your domain logic, you couple your core code with your HTTP API. If you decide to build a CLI and/or and event based application using the same core logic, you'll end up with uncomfortable problem exception and status code which has no meaning here.
435424

436425
#### 2. Custom error handlers
437426

@@ -484,3 +473,58 @@ def get_user_by_id(user_id: str):
484473
The biggest advantage of this solution is that you decouple your core code from your FastAPI app. You can define regular Python exceptions whatever you want and just do the conversion for your API in your custom error handler(s).
485474

486475
The disadvantage obviously is that it requires you to write more code. Its a question of balance.
476+
477+
#### Wrapping up
478+
479+
Considering the two previous mechanisms, the way which worked best for me is to do the following:
480+
481+
- When I raise errors in my core (domain code, business logic) I use dedicated exceptions, unrelated to HTTP nor APIs, and I add a custom error handler to my FastAPI app to handle and returns a ProblemResponse`.
482+
- When I want to raise an error directly in one of my API controller (i.e: a FastAPI route) I simply raise a `ProblemException`. If I'm raising same problem exception in several places I create a subclass of problem exception and put in my defaults and raise that error instead.
483+
484+
## Documenting your custom problems details
485+
486+
When registering problem details against your FastAPI app, it adds a `default` openapi response to all routes with the Problem Details schema. This might be enough in most cases but if you want to explicit additional problem details responses for specific status code or document additional properties you can register your Problem Details.
487+
488+
```python
489+
from typing import Any, Literal
490+
491+
from fastapi import FastAPI, Request, status
492+
493+
import fastapi_problem_details as problem
494+
from fastapi_problem_details import ProblemResponse
495+
496+
app = FastAPI()
497+
problem.init_app(app)
498+
499+
500+
class UserNotFoundProblem(problem.Problem):
501+
status: Literal[404]
502+
user_id: str
503+
504+
505+
class UserNotFoundError(Exception):
506+
def __init__(self, user_id: str) -> None:
507+
super().__init__(f"There is no user with id {user_id!r}")
508+
self.user_id = user_id
509+
510+
511+
@app.exception_handler(UserNotFoundError)
512+
async def handle_user_not_found_error(
513+
_: Request, exc: UserNotFoundError
514+
) -> ProblemResponse:
515+
return ProblemResponse.from_exception(
516+
exc,
517+
status=status.HTTP_404_NOT_FOUND,
518+
detail=f"User {exc.user_id} not found",
519+
user_id=exc.user_id,
520+
)
521+
522+
523+
@app.get("/users/{user_id}", responses={404: {"model": UserNotFoundProblem}})
524+
def get_user(user_id: str) -> Any: # noqa: ANN401
525+
raise UserNotFoundError(user_id)
526+
```
527+
528+
Note that this has limitation. Indeed, the `UserNotFoundProblem` class just act as a model schema for openapi documentation. You actually not instantiate this class and no validation is performed when returning the problem response. It means that the error handler can returns something which does not match a `UserNotFoundProblem`.
529+
530+
This is because of the way FastAPI manages errors. At the moment, there is no way to register error handler and its response schema in the same place and there is no mechanism to ensure both are synced.

0 commit comments

Comments
 (0)