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

使用OpenCensus追踪FastAPI失败请求时的响应对象问题

解决FastAPI OpenCensus中间件捕获5xx异常后返回有效响应的问题

你的核心问题是:当请求处理逻辑抛出未处理异常时,中间件捕获后返回None,违反了Starlette/FastAPI对HTTP中间件必须返回合法Response对象的要求,导致后续出现"None不可调用"的错误。

解决方案步骤

  1. 从FastAPI导入Response和status模块,用于构造标准的500错误响应
  2. 在异常捕获块中,替换返回None为构造一个包含500状态码的Response对象
  3. 确保追踪Span正确结束,保证数据能正常导出到AppInsights

修改后的中间件代码

import logging

from fastapi import Request, Response, status  # 新增导入
from opencensus.trace import (
    attributes_helper,
    execution_context,
    samplers,
)
from opencensus.ext.azure.trace_exporter import AzureExporter
from opencensus.trace import span as span_module
from opencensus.trace import tracer as tracer_module
from opencensus.trace import utils
from opencensus.trace.propagation import trace_context_http_header_format
from opencensus.ext.azure.log_exporter import AzureLogHandler
from starlette.types import ASGIApp

from src.settings import settings

HTTP_HOST = attributes_helper.COMMON_ATTRIBUTES["HTTP_HOST"]
HTTP_METHOD = attributes_helper.COMMON_ATTRIBUTES["HTTP_METHOD"]
HTTP_PATH = attributes_helper.COMMON_ATTRIBUTES["HTTP_PATH"]
HTTP_ROUTE = attributes_helper.COMMON_ATTRIBUTES["HTTP_ROUTE"]
HTTP_URL = attributes_helper.COMMON_ATTRIBUTES["HTTP_URL"]
HTTP_STATUS_CODE = attributes_helper.COMMON_ATTRIBUTES["HTTP_STATUS_CODE"]

module_logger = logging.getLogger(__name__)
module_logger.addHandler(AzureLogHandler(
    connection_string=settings.appinsights_connection_string
))

class AppInsightsMiddleware:
    """
    Middleware class to handle tracing of fastapi requests and exporting the data to AppInsights. 
    
    Most of the code here is copied from a github issue: https://github.com/census-instrumentation/opencensus-python/issues/1020
    """
    def __init__(
        self,
        app: ASGIApp,
        excludelist_paths=None,
        excludelist_hostnames=None,
        sampler=None,
        exporter=None,
        propagator=None,
    ) -> None:
        self.app = app
        self.excludelist_paths = excludelist_paths
        self.excludelist_hostnames = excludelist_hostnames
        self.sampler = sampler or samplers.AlwaysOnSampler()
        self.propagator = (
            propagator or trace_context_http_header_format.TraceContextPropagator()
        )
        self.exporter = exporter or AzureExporter(
            connection_string=settings.appinsights_connection_string
        )

    async def __call__(self, request: Request, call_next):

        # Do not trace if the url is in the exclude list
        if utils.disable_tracing_url(str(request.url), self.excludelist_paths):
            return await call_next(request)

        try:
            span_context = self.propagator.from_headers(request.headers)

            tracer = tracer_module.Tracer(
                span_context=span_context,
                sampler=self.sampler,
                exporter=self.exporter,
                propagator=self.propagator,
            )
        except Exception:
            module_logger.error("Failed to trace request", exc_info=True)
            return await call_next(request)

        try:
            span = tracer.start_span()
            span.span_kind = span_module.SpanKind.SERVER
            span.name = "[{}]{}".format(request.method, request.url)
            tracer.add_attribute_to_current_span(HTTP_HOST, request.url.hostname)
            tracer.add_attribute_to_current_span(HTTP_METHOD, request.method)
            tracer.add_attribute_to_current_span(HTTP_PATH, request.url.path)
            tracer.add_attribute_to_current_span(HTTP_URL, str(request.url))
            execution_context.set_opencensus_attr(
                "excludelist_hostnames", self.excludelist_hostnames
            )
        except Exception:  # pragma: NO COVER
            module_logger.error("Failed to trace request", exc_info=True)

        try:
            response = await call_next(request)
            tracer.add_attribute_to_current_span(HTTP_STATUS_CODE, response.status_code)
            tracer.end_span()
            return response
        # Explicitly handle any internal exception here, and set status code to 500 
        except Exception as exception:
            module_logger.exception(exception)
            tracer.add_attribute_to_current_span(HTTP_STATUS_CODE, 500)
            tracer.end_span()
            # 构造合法的500响应对象返回,替换原来的return None
            return Response(
                content="Internal Server Error",
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                media_type="text/plain"
            )

关键说明

  • 必须返回Response实例:Starlette的HTTP中间件要求__call__方法必须返回一个实现ASGI接口的响应对象,None不符合要求,因此会触发错误
  • 自定义响应内容:你可以根据需求修改content和media_type,比如返回JSON格式的错误信息:
    return Response(
        content='{"detail": "Internal Server Error"}',
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        media_type="application/json"
    )
    
  • 确保追踪数据导出:在返回响应前已经调用tracer.end_span(),保证异常请求的追踪数据能正常发送到AppInsights

内容的提问来源于stack exchange,提问作者mchristos

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 22:24:11