【问题标题】:Web API - Handling HEAD requests and specify custom Content-LengthWeb API - 处理 HEAD 请求并指定自定义 Content-Length
【发布时间】:2018-08-23 20:10:35
【问题描述】:

我有通过 GET 请求提供文件的 API 控制器。我正在使用 PushStreamContentResponse 并且效果很好。

我还可以在响应对象上设置 Content-Length-Header。

现在我也想支持 HEAD-Requests。我试过http://www.strathweb.com/2013/03/adding-http-head-support-to-asp-net-web-api/,虽然这可能有效,但我需要一个不需要实际处理请求的解决方案:检索文件并流式传输它很昂贵,但获取元数据(长度等)实际上是无操作。

但是,当我尝试设置 Content-Length 标头时,它将被 0 覆盖。

我添加了请求跟踪,我看到我的处理程序返回的消息显示了正确的 URL、Content-Disposition 和 Content-Length。

我也尝试过使用自定义 HttpResponse 并实现 TryComputeLength。虽然确实调用了此方法,但结果在管道中的某个点被丢弃。

有没有办法使用 Web API 来支持这一点?

【问题讨论】:

    标签: asp.net asp.net-web-api


    【解决方案1】:

    虽然这在 2015 年可能是个问题,但今天(2017 年以后),您可以这样做

    [RoutePrefix("api/webhooks")]
    public class WebhooksController : ApiController
    {
        [HttpHead]
        [Route("survey-monkey")]
        public IHttpActionResult Head()
        {
            return Ok();
        }
    
        [HttpPost]
        [Route("survey-monkey")]
        public IHttpActionResult Post(object data)
        {
            return Ok();
        }
    }
    

    HEAD api/webhooks/survey-monkeyPOST api/webhooks/survey-monkey 都可以正常工作。 (这是我刚刚为实现 SurveyMonkey 的 webhook 所做的存根)

    【讨论】:

    • 你如何设置Content-LengthContent-Type等,用这个策略?
    • @binki :您需要创建HttpResponseMessage 以及您需要的任何内容标题。您可以通过即时创建它们或创建一个为您执行此操作的class HeadOk : IHttpActionResult(您可以像return HeadOk(contentLength: xx, contentType: "application/json"); 一样使用它)来做到这一点。前者快速而肮脏,而后者更适合生产代码和维护。我个人更喜欢后者
    • @obi-onuorah :不确定。如果不允许 HEAD,您的服务器配置可能会阻止该代码工作!
    • @gfache 我目前的方法是将HttpResponseMessage.Content 设置为与GET 请求相同的值。我做的唯一特别的事情是在请求方法为HEAD 时使用没有操作CopyToAsync()Read()ReadAsync()Stream,否则ASP.NET 会假脱机整个Stream。这样,我在服务器上使用的资源更少,也避免了手动复制 Content-LengthContent-Type 等。我在这里记录了这一点:github.com/aspnet/AspNetWebStack/issues/189
    • @binki :好吧,您可以避免深入,并按照@henning-krause 在其回答中所说的那样做所有的样板。只需创建一个EmptyHttpContent,它将包含您需要的所有标头并发送 1 个硬编码字节以触发请求发送。这样,您实际上会使用更少的服务器资源(内存和 cpu),因为您正在更高级别解决问题。
    【解决方案2】:

    最后,真的很简单。

    1. 为 HEAD 请求创建处理程序
    2. 返回至少一个字节内容的Body,将响应的Content-Length-Header设置为所需的长度。使用长度为零的 Body 是行不通的。
    3. 这是关键部分:禁用响应的输出缓冲。

    默认情况下,WebAPI 将禁用 StreamContent 和 PushStreamContent 的输出缓冲。但是,可以通过 Application_Startup 替换 WebHostBufferPolicySelector 来覆盖此行为:

    GlobalConfiguration.Configuration.Services.Replace(typeof (IHostBufferPolicySelector), new BufferlessHostBufferPolicySelector());
    

    【讨论】:

      【解决方案3】:

      另一种解决方案是创建一个自定义HttpContent 来为您完成这项工作。如果您想遵守指南,还需要自定义 IHttpActionResult

      假设您有一个控制器,它为这样的给定资源返回 HEAD 操作:

      [RoutePrefix("resources")]
      public class ResourcesController : ApiController
      {
          [HttpHead]
          [Route("{resource}")]
          public IHttpActionResult Head(string resource)
          {
              //  Get resource info here
      
              var resourceType = "application/json";
              var resourceLength = 1024;
      
              return Head(resourceType , resourceLength);
          }
      }
      

      我想出的解决方案如下:

      头部处理程序

      internal abstract class HeadBase : IHttpActionResult
      {
          protected HttpStatusCode Code { get; set; } = HttpStatusCode.OK;
      
          public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
          {
              HttpResponseMessage response = null;
      
              try
              {
                  response = new HttpResponseMessage(Code)
                  {
                      Content = new EmptyContent()
                  };
                  FillContentHeaders(response.Content.Headers);
                  return Task.FromResult(response);
              }
              catch (Exception)
              {
                  response?.Dispose();
                  //  Good place to log here
                  throw;
              }
          }
      
          protected abstract void FillContentHeaders(HttpContentHeaders contentHeaders);
      }
      
      // For current need
      internal class Head : HeadBase
      {
          public Head(string mediaType, long contentLength)
          {
              FakeLength = contentLength;
              MediaType = string.IsNullOrWhiteSpace(mediaType) ? "application/octet-stream" : mediaType;
          }
      
          protected long FakeLength { get; }
      
          protected string MediaType { get; }
      
          protected override void FillContentHeaders(HttpContentHeaders contentHeaders)
          {
              contentHeaders.ContentLength = FakeLength;
              contentHeaders.ContentType = new MediaTypeHeaderValue(MediaType);
          }
      }
      

      空内容

      internal sealed class EmptyContent : HttpContent
      {
          public EmptyContent() : this(null, null)
          {
          }
      
          public EmptyContent(string mediaType, long? fakeContentLength)
          {
              if (string.IsNullOrWhiteSpace(mediaType)) mediaType = Constant.HttpMediaType.octetStream;
              if (fakeContentLength != null) Headers.ContentLength = fakeContentLength.Value;
      
              Headers.ContentType = new MediaTypeHeaderValue(mediaType);
          }
      
          protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
          {
              //  Necessary to force send
              stream?.WriteByte(0);
              return Task.FromResult<object>(null);
          }
      
          protected override bool TryComputeLength(out long length)
          {
              length = Headers.ContentLength.HasValue ? Headers.ContentLength.Value : -1;
              return Headers.ContentLength.HasValue;
          }
      }
      

      缓冲策略选择器

      internal class HostBufferPolicySelector : IHostBufferPolicySelector
      {
          public bool UseBufferedInputStream(object hostContext)
          {
              if (hostContext == null) throw new ArgumentNullException(nameof(hostContext));
      
              return true;
          }
      
          public bool UseBufferedOutputStream(HttpResponseMessage response)
          {
              if (response == null) throw new ArgumentNullException(nameof(response));
      
              if (StringComparer.OrdinalIgnoreCase.Equals(response.RequestMessage.Method.Method, HttpMethod.Head.Method)) return false;
      
              var content = response.Content;
              if (content == null) return false;
      
              // If the content knows, then buffering is very likely
              var contentLength = content.Headers.ContentLength;
              if (contentLength.HasValue && contentLength.Value >= 0) return false;
      
              var buffering = !(content is StreamContent ||
                                  content is PushStreamContent ||
                                  content is EmptyContent);
      
              return buffering;
          }
      }
      

      缓冲策略应该设置在Application_Start()中调用的public static void Register(HttpConfiguration config)方法中, 像这样:

      config.Services.Replace(typeof(IHostBufferPolicySelector), new HostBufferPolicySelector());
      

      另外,检查服务器是否配置为接受HEAD


      这个解决方案有几个优点:

      • 可扩展:通过工厂和继承
      • 适应性强:头处理程序是在控制器操作内创建的,您可以在其中拥有响应请求所需的所有信息。
      • 资源消耗低,速度快:因为HEAD是通过WebAPI API处理的
      • 易于理解/维护:遵循 WebAPI 处理管道
      • 关注点分离

      我通过类似的机制创建了一个支持HEAD 的 Web API 2 文件存储控制器。

      感谢Henning Krause 提出的问题,感谢answer 引导我到达那里。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-04-20
        • 2017-05-31
        • 2020-09-21
        • 2023-03-14
        • 1970-01-01
        • 1970-01-01
        • 2012-04-30
        • 1970-01-01
        相关资源
        最近更新 更多