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

.NET中ClientInterceptorContext生命周期及Header重复添加问题

问题背景

我有一个基于.NET 4.6.2、使用Grpc.Core库的gRPC客户端,已按照官方建议将gRPC通道注册为单例复用。

创建通道实例的代码如下:

public static InvokerAndChannel<T> CreateGrpcCallInvokerAndChannel<T>(
    string endpoint, ChannelCredentials credentials, IServiceProvider serviceProvider)
    where T : class
{
    var licenseCode = serviceProvider.GetRequiredService<ILicenseCodeResolver>().GetLicenseCode();

    var channel = new Channel(endpoint, credentials, new ChannelOption(ChannelOptions.MaxSendMessageLength, "1mb").Yield());
    var callInvoker = channel
        .Intercept(
            new ClientTracingInterceptor(
                new ClientTracingInterceptorOptions { RecordMessageEvents = false }))
        .Intercept(new StaticMetadataInterceptor(licenseCode));

    return new InvokerAndChannel<T>(callInvoker, channel);
}

调用管道包含StaticMetadataInterceptor,它负责为调用添加许可证Header,且该Interceptor是单例。

在对同一gRPC服务的多个方法进行并行调用时,发现StaticMetadataInterceptor接收到的是同一个ClientInterceptorContext,至少context.Options.Headers在不断增长,多次调用后会包含多个相同的Header,这是为什么?

以下是该Interceptor的代码:

internal sealed class StaticMetadataInterceptor : Interceptor
{
    private readonly string _licenseCode;

    public StaticMetadataInterceptor(string licenseCode)
    {
        _licenseCode = licenseCode;
    }

    private void ExtendMetadata<TRequest, TResponse>(ref ClientInterceptorContext<TRequest, TResponse> context)
            where TRequest : class
            where TResponse : class
    {
        var metadata = context.Options.Headers;
        if (metadata == null)
        {
            metadata = new Metadata();
            context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context.Host, context.Options.WithHeaders(metadata));
        }

        metadata.Add(GrpcStandardMetadataKeys.License, _licenseCode);
    }

    public override TResponse BlockingUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, BlockingUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(request, context);
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(request, context);
    }

    public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(context);
    }

    public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(request, context);
    }

    public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        ExtendMetadata(ref context);

        return continuation(context);
    }
}
原因分析

核心问题在于**ClientInterceptorContext的默认选项(包括Headers)是复用的实例**:

  • 当你创建单例的CallInvoker和Interceptor时,gRPC底层会复用同一个默认的CallOptions实例(不带自定义Headers的场景)给多个并行调用。
  • 你的Interceptor直接在这个共享的Metadata实例上添加Header,而非创建新副本。并行调用时,多个线程同时操作同一个Metadata集合,就会导致重复添加相同Header,最终Headers不断累积增长。
解决方案

修改ExtendMetadata方法,始终创建新的Metadata副本,避免修改共享实例:

private void ExtendMetadata<TRequest, TResponse>(ref ClientInterceptorContext<TRequest, TResponse> context)
        where TRequest : class
        where TResponse : class
{
    // 复制原有Headers到新实例,再添加许可证Header
    var newMetadata = context.Options.Headers != null ? new Metadata(context.Options.Headers) : new Metadata();
    newMetadata.Add(GrpcStandardMetadataKeys.License, _licenseCode);
    
    // 更新context的Options为新的Headers实例
    context = new ClientInterceptorContext<TRequest, TResponse>(
        context.Method, 
        context.Host, 
        context.Options.WithHeaders(newMetadata));
}

这样每个调用都会获得独立的Metadata实例,不会出现并行调用时Header重复累积的问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 21:55:55