Carles Roch Arnau
Carles Roch Arnau

Reputation: 41

Return FastAPI HTTPException to frontend from langchain runnable

My backend is running langchain routes with certain runnables. I want to be able to return an http exception like "404 Not found" to the frontend, but when I raise it with with raise HTTPException(status_code=404, detail="Item not found") a CancelledException is created and the frontend recieves "internal server error". How can I pull this off?

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/my-code-path/.venv/lib/python3.10/site-packages/sse_starlette/sse.py", line 269, in __call__
    await wrap(partial(self.listen_for_disconnect, receive))
  File "/my-code-path/.venv/lib/python3.10/site-packages/sse_starlette/sse.py", line 258, in wrap
    await func()
  File "/my-code-path/.venv/lib/python3.10/site-packages/sse_starlette/sse.py", line 215, in listen_for_disconnect
    message = await receive()
  File "/my-code-path/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/httptools_impl.py", line 596, in receive
    await self.message_event.wait()
  File "/usr/lib/python3.10/asyncio/locks.py", line 214, in wait
    await fut
asyncio.exceptions.CancelledError: Cancelled by cancel scope 7fba499a8490

During handling of the above exception, another exception occurred:

  + Exception Group Traceback (most recent call last):
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/httptools_impl.py", line 435, in run_asgi
  |     result = await app(  # type: ignore[func-returns-value]
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
  |     return await self.app(scope, receive, send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/fastapi/applications.py", line 276, in __call__
  |     await super().__call__(scope, receive, send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__
  |     await self.middleware_stack(scope, receive, send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
  |     raise exc
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
  |     await self.app(scope, receive, _send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/middleware/cors.py", line 91, in __call__
  |     await self.simple_response(scope, receive, send, request_headers=headers)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/middleware/cors.py", line 146, in simple_response
  |     await self.app(scope, receive, send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
  |     raise exc
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
  |     await self.app(scope, receive, sender)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
  |     raise e
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
  |     await self.app(scope, receive, send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__
  |     await route.handle(scope, receive, send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
  |     await self.app(scope, receive, send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/starlette/routing.py", line 69, in app
  |     await response(scope, receive, send)
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/sse_starlette/sse.py", line 255, in __call__
  |     async with anyio.create_task_group() as task_group:
  |   File "/my-code-path/.venv/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 680, in __aexit__
  |     raise BaseExceptionGroup(
  | exceptiongroup.ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/sse_starlette/sse.py", line 258, in wrap
    |     await func()
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/sse_starlette/sse.py", line 245, in stream_response
    |     async for data in self.body_iterator:
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langserve/api_handler.py", line 1121, in _stream
    |     async for chunk in self._runnable.astream(
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 4745, in astream
    |     async for item in self.bound.astream(
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 2898, in astream
    |     async for chunk in self.atransform(input_aiter(), config, **kwargs):
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 2881, in atransform
    |     async for chunk in self._atransform_stream_with_config(
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 1972, in _atransform_stream_with_config
    |     chunk = cast(Output, await py_anext(iterator))
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 2851, in _atransform
    |     async for output in final_pipeline:
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 4178, in atransform
    |     async for output in self._atransform_stream_with_config(
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 1972, in _atransform_stream_with_config
    |     chunk = cast(Output, await py_anext(iterator))
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 4147, in _atransform
    |     output = await acall_func_with_variable_args(
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 4122, in f
    |     return await run_in_executor(config, func, *args, **kwargs)
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/config.py", line 547, in run_in_executor
    |     return await asyncio.get_running_loop().run_in_executor(
    |   File "/usr/lib/python3.10/concurrent/futures/thread.py", line 58, in run
    |     result = self.fn(*self.args, **self.kwargs)
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/base.py", line 4116, in func
    |     return call_func_with_variable_args(
    |   File "/my-code-path/.venv/lib/python3.10/site-packages/langchain_core/runnables/config.py", line 380, in call_func_with_variable_args
    |     return func(input, **kwargs)  # type: ignore[call-arg]
    |   File "/my-code-path/src/chains/forms/refine_chain.py", line 65, in get_refined_runnable_map
    |     raise HTTPException(status_code=404, detail="Item not found")
    | fastapi.exceptions.HTTPException
    +------------------------------------

I tried searching for a way to somehow catch that exception but the runnable is directly inserted into the "add_routes" function so I can't figure out how to do it, since any exception inside the runnable raises the "CancelledException".

refine_chain = RefineChain(custom_app, tool_name)
add_routes(
  app,
  refine_chain.get_runnable().with_config({"tags": [custom_app]}),
  enabled_endpoints=["stream"],
  path=f"/{custom_app}/{tool_name}/refine",
  config_keys=["configurable"],
  dependencies=[Depends(get_current_active_username)],
)   

Upvotes: 1

Views: 353

Answers (2)

Sergio García
Sergio García

Reputation: 1

The current implementation of LangServe has a hardcoded error handling for the stream method:

https://github.com/langchain-ai/langserve/blob/v0.3.0/langserve/api_handler.py#L1210-L1219

There is an open issue in the LangServe repository to address this limitation: https://github.com/langchain-ai/langserve/issues/481

So, the client will always receive the same "Internal Server Error" message.

Upvotes: 0

Olsi Hoxha
Olsi Hoxha

Reputation: 414

I usually do manage the exceptions through the exception_handler-s, so it can scale more when the project grows, here is how I would implement handling the asyncio.exceptions.CancelledError that your code is raising:

@app.exception_handler(asyncio.exceptions.CancelledError)
    async def http_exception_handler(req: Request, exc: RequestValidationError) -> Response:
        error_message = 'Bad Request'
        return JSONResponse(
            status_code=400,
            content={"message": error_message},
        )

Note: app is the app = FastAPI(...)

You can customise the JSONResponse however you like it, put 404 and whatever message you want.

Upvotes: 0

Related Questions