Web API - Handling HEAD requests and specify custo

2019-07-17 20:47发布

I haven API-Controller serving files via GET-Requests. I'm using the PushStreamContentResponse and that works well.

I can also set the Content-Length-Header on the response object.

Now I also want to support HEAD-Requests. I've tried http://www.strathweb.com/2013/03/adding-http-head-support-to-asp-net-web-api/ and while that may work, I need a solution where I don't need to actually process the request: Retrieving the file and streaming it is expensive, but getting the meta data (length, etc) is practically a no-op.

However, when I try to set the Content-Length header, it will be overwritten with 0.

I have added request tracing and I see that the message returned by my handler is displayed with the correct URL, Content-Disposition and Content-Length.

I have also tried using a custom HttpResponse and implement the TryComputeLength. And while this method is indeed called, the result is discarded at some point in the pipeline.

Is there any way to support this using Web API?

3条回答
神经病院院长
2楼-- · 2019-07-17 21:14

While this might have been an issue in 2015, today (2017 onwards), you can just do this

[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();
    }
}

both HEAD api/webhooks/survey-monkey and POST api/webhooks/survey-monkey work just fine. (this is the stub I've just done for implementing SurveyMonkey's webhooks)

查看更多
Anthone
3楼-- · 2019-07-17 21:21

Another solution would be to create a custom HttpContent that will does this job for you. Also a customised IHttpActionResult is needed if you want to stick to the guidelines.

Let's say you have a controller that return a HEAD action for a given resource like that:

[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);
    }
}

The solution I came up with is as follow:

The head handler

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);
    }
}

The empty content

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;
    }
}

The buffering policy selector

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;
    }
}

The buffering policy should be set into the public static void Register(HttpConfiguration config) method called in Application_Start(), like that:

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

Also, check if the server is configured to accept HEAD!


This solution has few advantages:

  • Extendable: through factory and inheritance
  • Adaptable: the head handler is created inside the controller action where you have all needed information to response the request.
  • Low resource consumption and fast: Because the HEAD is handled through the WebAPI API
  • Simple to understand/maintain: follow the WebAPI processing pipeline
  • Separation of Concern

I created a Web API 2 file store controller that supports HEAD through a similar mechanism.

Thanks to Henning Krause for its question and answer that led me there.

查看更多
男人必须洒脱
4楼-- · 2019-07-17 21:28

In the end, it was really simple.

  1. Create a handler for the HEAD request
  2. Return a Body with at least one byte content, set the Content-Length-Header of the response to the desired length. Using a Body with zero length won't work.
  3. This is the critical part: Disable Outputbuffering for the response.

The WebAPI will, by default, disable output buffering for StreamContent and PushStreamContent. However, this behavior can be overridden by replacing the WebHostBufferPolicySelector via Application_Startup:

GlobalConfiguration.Configuration.Services.Replace(typeof (IHostBufferPolicySelector), new BufferlessHostBufferPolicySelector());
查看更多
登录 后发表回答