You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在LangChain Runnable中向前端返回FastAPI HTTPException?

LangChain Runnable路由抛出HTTPException异常时触发CancelledException的问题解决

后端基于LangChain Runnable配置FastAPI路由,期望向前端返回404 Not found这类HTTPException,但直接在Runnable逻辑中抛出raise HTTPException(status_code=404, detail="Item not found")时,会触发CancelledException,最终前端收到"内部服务器错误"的500响应。由于Runnable直接传入add_routes函数,无法直接在路由层捕获异常,需要特殊处理。

报错栈

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
    +------------------------------------

当前代码示例

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)],
)   

解决方案

方案1:用RunnableLambda包装异常捕获逻辑

将抛出HTTPException的逻辑包裹在RunnableLambda中,捕获异常并转换为可识别的标记,再通过FastAPI中间件处理:

from langchain_core.runnables import RunnableLambda
from fastapi import HTTPException

def wrap_runnable_with_exception_handler(runnable):
    async def handle_exception(input_data):
        try:
            return await runnable.ainvoke(input_data)
        except HTTPException as e:
            # 用特殊标记封装异常信息
            raise RuntimeError(f"HTTP_EXCEPTION:{e.status_code}:{e.detail}") from e
        except Exception as e:
            raise e
    
    return RunnableLambda(handle_exception)

# 使用包装后的runnable
refine_chain = RefineChain(custom_app, tool_name)
wrapped_runnable = wrap_runnable_with_exception_handler(refine_chain.get_runnable())

add_routes(
  app,
  wrapped_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)],
)

添加FastAPI异常处理中间件:

from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(RuntimeError)
async def handle_http_exception_from_runnable(request: Request, exc: RuntimeError):
    if exc.args[0].startswith("HTTP_EXCEPTION:"):
        _, status_code, detail = exc.args[0].split(":", 2)
        return JSONResponse(
            status_code=int(status_code),
            content={"detail": detail}
        )
    # 其他异常按默认逻辑返回
    return JSONResponse(
        status_code=500,
        content={"detail": "Internal Server Error"}
    )

方案2:直接捕获ExceptionGroup中的HTTPException

针对LangServe抛出的ExceptionGroup,提取其中的HTTPException并返回正确响应:

from exceptiongroup import BaseExceptionGroup
from fastapi import HTTPException
from fastapi.responses import JSONResponse

@app.exception_handler(BaseExceptionGroup)
async def handle_exception_group(request: Request, exc: BaseExceptionGroup):
    # 遍历异常组查找HTTPException
    for sub_exc in exc.exceptions:
        if isinstance(sub_exc.__cause__, HTTPException):
            http_exc = sub_exc.__cause__
            return JSONResponse(
                status_code=http_exc.status_code,
                content={"detail": http_exc.detail}
            )
    # 无匹配异常返回默认500
    return JSONResponse(
        status_code=500,
        content={"detail": "Internal Server Error"}
    )

方案3:在Runnable中返回错误格式而非抛出异常

如果允许修改返回结构,直接在Runnable中返回错误信息,由前端自行处理状态:

# 修改RefineChain的get_refined_runnable_map方法
def get_refined_runnable_map(self):
    def process_input(input_data):
        if not self._check_item_exists(input_data):
            # 返回错误结构
            return {"error": True, "status_code": 404, "detail": "Item not found"}
        # 正常业务逻辑
        return self._process_refine(input_data)
    
    return RunnableLambda(process_input)

内容的提问来源于stack exchange,提问作者Carles Roch Arnau

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 03:28:10