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

C#健康检查中如何获取HttpContext?多端点场景异常排查

ASP.NET Core自定义健康检查中获取调用端点上下文的问题

核心问题

在ASP.NET Core中实现自定义健康检查时,已在实现IHealthCheck的类中注入IHttpContextAccessor,依赖解析正常,但访问其HttpContext属性始终为空。当前定义了多个健康检查端点,需要在检查逻辑中识别当前调用的是哪个端点,请问如何让HttpContext可访问,或通过其他方式获取调用端点的上下文?

补充说明

添加Debug.WriteLine()输出HttpContext中的路径后发现:

  • 访问/healthz端点时偶尔能输出路径,但也会出现空值;
  • 访问/healthz/live-probe或/healthz/readiness-probe端点时,路径始终为空,且输出时机与请求发起时间不匹配。

推测健康检查可能在后台循环执行,端点仅返回最新检查结果,但无法解释/healthz端点的偶发现象,除官方文档外,是否有其他文档可解释该行为?


相关代码实现

健康检查端点扩展方法

public static IApplicationBuilder UseHealthCheckEndpoint(this IApplicationBuilder app) =>
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHealthChecks("/healthz",
            new HealthCheckOptions
            {
                Predicate = _ => true,
                ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
                ResultStatusCodes = Statuses
            });
        endpoints.MapHealthChecks("/healthz/live-probe",
            new HealthCheckOptions
            {
                Predicate = check => check.Tags.Contains(LivenessTag),
                ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
                ResultStatusCodes = Statuses
            });
        endpoints.MapHealthChecks("/healthz/ready-probe",
            new HealthCheckOptions
            {
                Predicate = check => check.Tags.Contains(ReadinessTag),
                ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
                ResultStatusCodes = Statuses
            });
    });

微服务添加健康检查的扩展方法

public static IHealthChecksBuilder AddHealthCheckUrls(this IHealthChecksBuilder healthChecksBuilder,
    string name,
    HealthStatus? failureStatus = null,
    IEnumerable<string> tags = null,
    TimeSpan? timeout = null,
    HttpMethod httpMethod = null,
    params string[] urls)
{
    return healthChecksBuilder.Add(new HealthCheckRegistration(
        name: name,
        factory: s => new UrlHealthCheck(s.GetService<IHttpClientFactory>(), s.GetService<IHttpContextAccessor>(), httpMethod, failureStatus.HasValue, urls),
        failureStatus: failureStatus,
        tags: new List<string> { "urls" }.Concat(tags ?? Enumerable.Empty<string>()),
        timeout: timeout));
}

自定义健康检查实现类

internal class UrlHealthCheck : IHealthCheck
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly string[] _urls;
    private readonly HttpMethod _httpMethod = HttpMethod.Head;
    private readonly bool _hasUserFailureStatus;

    public UrlHealthCheck(bool hasUserFailureStatus) => _hasUserFailureStatus = hasUserFailureStatus;

    public UrlHealthCheck(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor, HttpMethod httpMethod, bool hasUserFailureStatus, params string[] urls)
    {
        if ((urls?.Length ?? 0) == 0)
            throw new ArgumentException("Urls cannot be empty", nameof(urls));
        _urls = urls;
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory), "IHttpClientFactory could not be resolved. Ensure you are calling 'services.AddHttpClient();' from your ConfigureServices method.");
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor), "IHttpContextAccessor could not be resolved. Ensure you are calling 'services.AddHttpContextAccessor();' from your ConfigureServices method.");
        _httpMethod = httpMethod ?? HttpMethod.Head;
        _hasUserFailureStatus = hasUserFailureStatus;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new())
    {
        var isLivenessProbe = _httpContextAccessor?.HttpContext?.Request?.Path.Value?.Contains("live-probe") ?? false;
        var isReadinessProbe = !isLivenessProbe && (_httpContextAccessor?.HttpContext?.Request?.Path.Value?.Contains("ready-probe") ?? false);
        var defaultFailureStatus = isReadinessProbe ? HealthStatus.Degraded : HealthStatus.Unhealthy;
        var failureStatus = _hasUserFailureStatus ? context.Registration.FailureStatus : defaultFailureStatus;

        try
        {
            // Prevent infinite dependency loops.
            var depthCount = 0;
            StringValues dependencyDepthCount = default;
            _httpContextAccessor?.HttpContext?.Request?.Query.TryGetValue(nameof(dependencyDepthCount), out dependencyDepthCount);
            if (dependencyDepthCount.Count > 0 && int.TryParse(dependencyDepthCount[0], out depthCount))
            {
                if (depthCount > 5)
                {
                    return new HealthCheckResult(HealthStatus.Healthy, "Dependency depth count exceeded");
                }
            }

            using var client = _httpClientFactory.CreateClient();

            var tasks = _urls.Select(url =>
            {
                var uriBuilder = new UriBuilder(url);
                var query = HttpUtility.ParseQueryString(uriBuilder.Query);
                query["dependencyDepthCount"] = (depthCount + 1).ToString();
                uriBuilder.Query = query.ToString();

                var request = new HttpRequestMessage(_httpMethod, uriBuilder.Uri);
                return client.SendAsync(request, cancellationToken);
            });

            var result = await Task.WhenAll(tasks);

            return result.All(x => x.IsSuccessStatusCode)
                ? new HealthCheckResult(HealthStatus.Healthy)
                : new HealthCheckResult(failureStatus, "One or more requests failed", data: new Dictionary<string, object>(result.Where(r => !r.IsSuccessStatusCode).Select(r => new KeyValuePair<string, object>(r.RequestMessage.RequestUri.AbsolutePath, r.ReasonPhrase))));
        }
        catch (Exception ex)
        {
            return new HealthCheckResult(failureStatus, exception: ex);
        }
    }
}

微服务注册健康检查代码

services.AddOurCompanyHealthChecks<UserServiceContext>(tags: new string[] { "User Microservice" })
    .AddHealthCheckUrls("Some dependency", tags: new string[] { HealthCheckBuilderExtensions.ReadinessTag }, urls: $"{Configuration["Services:UserService:BaseUrl"]}/healthz");

解决方案

1. 为什么HttpContext为空?

健康检查有两种执行模式:

  • 按需执行:用户访问健康检查端点时实时执行,此时HttpContext可用;
  • 后台定期执行:若配置了后台服务(如通过健康检查UI搭配定时任务),检查会在后台线程执行,无关联HttpContext,因此IHttpContextAccessor获取到空值。

/healthz偶尔能获取到HttpContext,是因为某次请求触发了按需执行,空值则是后台定期执行的检查结果被返回。

2. 正确识别调用端点的方案

不要依赖HttpContext,推荐以下两种方案:

方案一:利用健康检查标签直接判断

你已经给不同端点配置了对应标签的Predicate,当前执行的检查必然包含对应标签,可直接通过context.Registration.Tags判断:

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new())
{
    var isLivenessProbe = context.Registration.Tags.Contains(LivenessTag);
    var isReadinessProbe = context.Registration.Tags.Contains(ReadinessTag);
    var defaultFailureStatus = isReadinessProbe ? HealthStatus.Degraded : HealthStatus.Unhealthy;
    var failureStatus = _hasUserFailureStatus ? context.Registration.FailureStatus : defaultFailureStatus;

    // 后续逻辑保持不变
}

方案二:通过HealthCheckOptions传递自定义参数

修改端点配置,给每个端点添加标识参数:

endpoints.MapHealthChecks("/healthz/live-probe",
    new HealthCheckOptions
    {
        Predicate = check => check.Tags.Contains(LivenessTag),
        ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
        ResultStatusCodes = Statuses,
        AdditionalData = new Dictionary<string, object> { { "ProbeType", "Liveness" } }
    });

endpoints.MapHealthChecks("/healthz/ready-probe",
    new HealthCheckOptions
    {
        Predicate = check => check.Tags.Contains(ReadinessTag),
        ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
        ResultStatusCodes = Statuses,
        AdditionalData = new Dictionary<string, object> { { "ProbeType", "Readiness" } }
    });

然后在检查逻辑中获取参数:

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new())
{
    string probeType = null;
    if (context.Registration.Options is HealthCheckOptions options && options.AdditionalData.TryGetValue("ProbeType", out var value))
    {
        probeType = value.ToString();
    }

    var isLivenessProbe = probeType == "Liveness";
    var isReadinessProbe = probeType == "Readiness";
    // 后续逻辑保持不变
}

3. 禁用后台执行(可选)

如果不需要后台定期执行检查,可在注册时禁用:

services.AddHealthChecks()
    .AddCheck<UrlHealthCheck>("url-check", tags: new[] { "readiness" }, 
        options: new HealthCheckRegistrationOptions { DisableBackgroundExecution = true });

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 12:14:53