Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ContextDoesNotExistError on FastAPI Custom Exception Handler #74

Closed
mancioshell opened this issue Sep 23, 2022 · 8 comments
Closed

ContextDoesNotExistError on FastAPI Custom Exception Handler #74

mancioshell opened this issue Sep 23, 2022 · 8 comments
Labels
enhancement New feature or request

Comments

@mancioshell
Copy link

mancioshell commented Sep 23, 2022

Hi,
i'm using Depend FastAPI mechaninsm to access Context on request-response cycle:

async def my_context_dependency(
    x_plt_session_id: str = Header(None),
    x_plt_correlation_id: str = Header(None),
    x_plt_user_id: str = Header(None),
    x_plt_event_id: str = Header(None),
    x_plt_solution_user: str = Header(None),
) -> Any:
    # When used a Depends(), this fucntion get the `X-Client_ID` header,
    # which will be documented as a required header by FastAPI.
    # use `x_client_id: str = Header(None)` for an optional header.

    data = {
        "session-id": x_plt_session_id,
        "correlation-id": x_plt_correlation_id,
        "user-id": x_plt_user_id,
        "event-id": x_plt_event_id,
        "solution-user": x_plt_solution_user,
    }
    with request_cycle_context(data):
        # yield allows it to pass along to the rest of the request
        yield

app = FastAPI(
        dependencies=[Depends(my_context_dependency)],
        title="Virtual Entity API",
        description="This is a very fancy project, with auto docs for the API and everything",
        version="0.0.1",
        openapi_url="/openapi.json"
    )

I'm also using custom exception handler in FastAPI:

from starlette_context import context

@app.exception_handler(AWSException)
async def unicorn_exception_handler(
    request: Request, exc: AWSException
) -> JSONResponse:

   user_id = context.get("user-id", default=None)
    
    print(user_id )

    return JSONResponse(
        status_code=400,
        content={"message": exc.message},
    )

But when i try to access context inside the custom exception handler i receive :

starlette_context.errors.ContextDoesNotExistError: You didn't use the required middleware or you're trying to access context object outside of the request-response cycle.

Any hints ?

@mancioshell mancioshell changed the title ContextDoesNotExistError on FastAPI Exception Handler ContextDoesNotExistError on FastAPI Custom Exception Handler Sep 23, 2022
@hhamana
Copy link
Collaborator

hhamana commented Sep 23, 2022

How exactly are you "try[ing] to access context inside the custom exception handler"?
Exceptions within a request cycle are still handled with the Depends, so in a regular situation, this should be ok. It will break if, as the message says, you are not actually within a request-response cycle.
Are you by any chance trying to test the exception handler alone without initiating a request?

@hhamana hhamana added the question Further information is requested label Sep 23, 2022
@mancioshell
Copy link
Author

mancioshell commented Sep 23, 2022

@hhamana thank you for your reply.

This is an example, which reproduce my issue.

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from typing import Dict, Any
from fastapi import Request
from starlette_context import context
from fastapi import FastAPI, Depends, Header
from starlette_context import request_cycle_context


async def my_context_dependency(x_plt_user_id: str = Header(None)) -> Any:

    data = {"user-id": x_plt_user_id}
    with request_cycle_context(data):
        # yield allows it to pass along to the rest of the request
        yield


app = FastAPI(
    dependencies=[Depends(my_context_dependency)],
    title="Test App",
)


@app.exception_handler(Exception)
async def unicorn_exception_handler(request: Request, exc: Exception) -> JSONResponse:

    user_id = context.get("user-id", default=None)

    print(user_id)

    return JSONResponse(
        status_code=400,
        content={"message": "Bad Request"},
    )


@app.get("/")
async def root() -> Dict[str, str]:
    raise Exception("Test")
    return {"message": "Hello World"}


if __name__ == "__main__":
    # Use this for debugging purposes only
    import uvicorn

    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8080,
        log_level="debug",
    )

If you run this server and try to do a HTTP GET request on localhost:8080 you should receive the following error:

$ poetry run python src/test.py
INFO: Started server process [13584]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette\middleware\errors.py", line 162, in call
await self.app(scope, receive, _send)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette\exceptions.py", line 93, in call
raise exc
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette\exceptions.py", line 82, in call
await self.app(scope, receive, sender)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 21, in call
raise e
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in call
await self.app(scope, receive, send)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette\routing.py", line 670, in call
await route.handle(scope, receive, send)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette\routing.py", line 266, in handle
await self.app(scope, receive, send)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette\routing.py", line 65, in app
response = await func(request)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\fastapi\routing.py", line 227, in app
raw_response = await run_endpoint_function(
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\fastapi\routing.py", line 160, in run_endpoint_function
return await dependant.call(**values)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity\src\pltm021\test.py", line 39, in root
raise Exception("Test")
Exception: Test

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette_context\ctx.py", line 33, in data
return _request_scope_context_storage.get()
LookupError: <ContextVar name='starlette_context' at 0x000002BBC3084CC0>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 404, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 78, in call
return await self.app(scope, receive, send)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\fastapi\applications.py", line 269, in call
await super().call(scope, receive, send)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette\applications.py", line 124, in call
await self.middleware_stack(scope, receive, send)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette\middleware\errors.py", line 174, in call
response = await self.handler(request, exc)
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity\src\pltm021\test.py", line 27, in unicorn_exception_handler
user_id = context.get("user-id", default=None)
File "C:\Python310\lib_collections_abc.py", line 819, in get
return self[key]
File "C:\Python310\lib\collections_init_.py", line 1102, in getitem
if key in self.data:
File "C:\Users\A409089\Desktop\Git\Room Core\dl00001\glin-pltm021-lan-tm021\repo_structure\src\docker_virtualentity.venv\lib\site-packages\starlette_context\ctx.py", line 35, in data
raise ContextDoesNotExistError
starlette_context.errors.ContextDoesNotExistError: You didn't use the required middleware or you're trying to access context object outside of the request-response cycle.
INFO: 127.0.0.1:50577 - "GET / HTTP/1.1" 500 Internal Server Error

Python version: 3.10

fastapi = "^0.80.0"
starlette-context = "^0.3.4"

@hhamana
Copy link
Collaborator

hhamana commented Sep 24, 2022

I tried it as well, and it looks like I overlooked the exception handling interaction with Depends indeed.
As it currently stands, the context is reset in a try/finally clause, as part of the yield, which causes it to be executed before exception handlers, leading your scenario of invalidated context once it gets there.
It looks like not running it as a try/finally, but resetting it after the yield pushes the execution after the exception handlers, and after background tasks as well, which sounds desirable.
I'll need to investigate a bit further to make sure that kind of fix doesn't introduce unwanted side-effects.
thank you for raising the issue

@hhamana hhamana added invalid This doesn't seem right enhancement New feature or request and removed question Further information is requested labels Sep 24, 2022
@tomwojcik
Copy link
Owner

tomwojcik commented Sep 24, 2022

I don't really have time to check it myself right now, but IIRC handlers for any custom exception and a generic Exception won't work the same. Please try to reproduce with another Exception type.

Refer to
previous ticket about a similar issue #9 (comment)
starlette-context docs https://starlette-context.readthedocs.io/en/latest/middleware.html#errors-and-middlewares-in-starlette
starlette docs https://www.starlette.io/exceptions/#errors-and-handled-exceptions
starlette source that might prevent you from achieving this https://github.com/encode/starlette/blob/040d8c86b09f34be49e8c253d97a588973bc7308/starlette/applications.py#L91-L99
related fastapi ticket tiangolo/fastapi#2750
related fastapi ticket tiangolo/fastapi#2683

I think it'd also make sense to first reproduce with Starlette instead of FastAPI, even though we want to support both. If you can't make it work using Starlette, it won't work with FastAPI.

Again, I assume the reason for missing context there is that this exception handler is run in the outermost middleware and it's not really possible to "make it work differently" (refer to Starlette source), so the previous middleware (starlette-context middleware) already cleaned up after itself. At least that's the case for 500 / Exception.

@mancioshell
Copy link
Author

mancioshell commented Sep 24, 2022

I have also tried with a custom exception, with the same error. I'll reproduce it in the afternoon.

Anyway i''m using starlette context to save specific header values in the context, because i have to log these informations.

My aim is to raise an exception anywhere in my code when something went wrong, and intercept it in fastapi customs exception handler, log the reason of the exception with the current headers informations and fail safe with an appropriate http response.
If are there any other methods to achieve this purpose, wuold be ok for me.
I wouldn't fill my code with a lot of try except and repetitive log methods.

@mancioshell
Copy link
Author

mancioshell commented Sep 24, 2022

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from typing import Dict, Any
from fastapi import Request
from starlette_context import context
from fastapi import FastAPI, Depends, Header
from starlette_context import request_cycle_context


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

    def __str__(self) -> str:
        return f'{self.message}'


async def my_context_dependency(x_plt_user_id: str = Header(None)) -> Any:

    data = {"user-id": x_plt_user_id}
    with request_cycle_context(data):
        # yield allows it to pass along to the rest of the request
        yield


app = FastAPI(
    dependencies=[Depends(my_context_dependency)],
    title="Test App",
)


@app.exception_handler(CustomException)
async def unicorn_exception_handler(request: Request, exc: CustomException) -> JSONResponse:

    user_id = context.get("user-id", default=None)

    print(user_id)

    return JSONResponse(
        status_code=400,
        content={"message": "Bad Request"},
    )


@app.get("/")
async def root() -> Dict[str, str]:
    raise CustomException("Test")
    return {"message": "Hello World"}


if __name__ == "__main__":
    # Use this for debugging purposes only
    import uvicorn

    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8080,
        log_level="debug",
    )

Same issue

@tomwojcik
Copy link
Owner

@mancioshell chances are @hhamana figured it out. Their fix has been released in 0.3.5. Could you please confirm the issue has been resolved?

@mancioshell
Copy link
Author

@tomwojcik sorry for the delayed reply. Yep i have just tested the issue is solved in 0.3.5.
Thank you so much! @hhamana

@tomwojcik tomwojcik removed the invalid This doesn't seem right label Feb 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants