0

I am trying to implement Exception Handling in FastAPI.

  1. I have Try along with Exception block in which I am trying to implement Generic Exception if any other exception occurs than HTTPExceptions mentioned in Try Block. In generic exception, I would like to print details of actual exception in format { "details": "Actual Exception Details" } However, in below implementation, even though any exception in Try is raised it goes to Except block and prints that exception.

  2. I Try block specific exceptions, I am trying to have a CustomException class that can print the details of custom exception provded name of exception.

@app.get("/school/studentclass")
async def get_student_class(id: int, stream: str, rank: int = 1):
    try:      
        student = Student(id=id,stream=stream,rank=1)
        if (student.id != 0):
            if (student.stream is not None or  student.stream != ''):
                if(student.rank!= 0):
                   // Student Class is used to represent API Output : Student_Class_Name and Student_Class_Room and Student_Class_Teacher
                    studentClass = StudentClass()
                    // Processing Logic to get Student Class 
                                        
                    return JSONResponse(content=studentClass)
                else:
                    raise HTTPException(status_code = 422, detail = "Student Rank is not right", headers={"X-Error": "Student Rank is not right"})
            else:
                raise HTTPException(status_code = 422, detail="Student Stream is not right", headers={"X-Error": "Student Stream is not right"})
        else:
            raise HTTPException(status_code = 422, detail="Student Id is not right", headers={"X-Error": "Student Id is not right"})
except Exception as e:
        raise HTTPException(status_code = 418, detail=str(e))

Custom Exception Class

class CustomException(Exception):
    def __init__(self, name: str):
        self.name = name

For the second question, I tried to implement as below but it isn't working this way

else:
    raise HTTPException(status_code = 422, detail = CustomException(name = "Invalid Student Rank"), headers={"Error": "Invalid Student Rank"})

The Error Handling is done as follows:

@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
    print(f"HTTP Error: {repr(exc)}")
    return await http_exception_handler(request, exc)
    
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    print(f"Invalid Data: {exc}")
    return await request_validation_exception_handler(request, exc)
  • So if I understand correctly, you don't want to catch `HTTPException` in your `try`/`except` clause? Why don't you just put the `try`/`except` around the part that can raise exceptions? Which I'm guessing is the block inside `if student.rank != 0:` – M.O. May 15 '23 at 10:45
  • You might find [this answer](https://stackoverflow.com/a/71682274/17865804) helpful – Chris May 15 '23 at 15:13
  • @M.O. In case if there is any other exception other than HttpException, I would like to catch that also. – user14806829 May 15 '23 at 17:06

1 Answers1

1

You can use conint and constr from Pydantic to limit the ranges of allowed values for your parameters, which will also supply the necessary information if included in documentation. They will generate 422 errors automagically that includes descriptive information about values being too low.

I also strongly suggest following the "return early" rule (even if you're using exceptions here). However, in this case we can make pydantic do all the validation for us, and automagically have FastAPI return a 422 error if the submitted values doesn't conform:

from pydantic import constr, conint

@app.get("/school/studentclass")
async def get_student_class(id: conint(gt=0), stream: constr(min_length=1), rank: conint(gt=0) = 1):
    student = Student(id=id, stream=stream, rank=rank)

    # Student Class is used to represent API Output : Student_Class_Name and Student_Class_Room and Student_Class_Teacher
    
    student_class = StudentClass()
    # Processing Logic to get Student Class 
                                        
    return {"content": student_class)  # I'd also recommend avoiding "content" here as it conveys no meaning at all

I have no idea what your final except is meant to catch (i.e. what exception can be thrown inside Student), but you shouldn't have to handle that yourself (if a custom exception is thrown, use FastAPI's built-in exception handling to handle it).

If you want to take it a bit step further, where this is a common thing you do, move generating the Student object to a separate dependency instead:

from pydantic import constr, conint
from typing import Annotated


async def student_from_url_id(id: conint(gt=0), stream: constr(min_length=1), rank: conint(gt=0) = 1):
    return Student(id=id, stream=stream, rank=rank)


@app.get("/school/studentclass")
async def student_class_endpoint(student: Annotated[Student, Depends(student_from_url_id)])
    student_class = StudentClass()
    # Processing Logic to get Student Class 
                                        
    return {"content": student_class)

That way your dependency can be re-used everywhere you need to fetch a student from a URL-argument for example.

{
  detail: [
    {
      loc: [
        "query",
        "rank"
      ],
      msg: "ensure this value is greater than 0",
      type: "value_error.number.not_gt",
      ctx: {
        limit_value: 0
      }
    }
  ]
}
MatsLindh
  • 49,529
  • 4
  • 53
  • 84
  • the conint works well to enforce a vaule to be greater than 0 but in case a value that exceeds integer range is provided it allows it :O – user14806829 May 15 '23 at 17:17
  • Also the Depends Keyword is imported from which package? – user14806829 May 15 '23 at 17:34
  • @user14806829 There is no such thing as integer range in Python - integers can be as large as you want. If you want to enforce a 32-bit integer, you can use `lt=2**32` together with `gt`. Depends is a core part of FastAPI, so `from fastapi import Depends`. However, if `id` refers to a Student id that you want to fetch from the database, just look it up instead of enforcing a database field range, and raise a HTTPException with status_code 404 or 400 if the student isn't found. – MatsLindh May 15 '23 at 21:12
  • Thanks for the valuable suggestion. I was able to improve the code. The question that I have now is if input param values exception is being handled by FastAPI with the changes then if any exception occurs during processsing of logic to get Student Class in such case, we need to have custom exceptions right or FastAPI will send generic exceptions ? – user14806829 May 16 '23 at 18:33
  • FastAPI will return a general 500 error in that case; you can use a custom exception and the built-in error handling if you want to handle something in a specific way: https://fastapi.tiangolo.com/tutorial/handling-errors/ - generally I'd recommend using a dependency and raising a 404 or something similar (i would also recommend using a path variable like `/students/{student_id}` to match the common REST path scheme instead of a GET variable, but you might have reasons for doing what you're doing. – MatsLindh May 16 '23 at 18:37