Reputation: 5422
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?
Upvotes: 3
Views: 6744
Reputation: 616
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:
HEAD
is handled through the WebAPI APII 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.
Upvotes: 1
Reputation: 3089
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)
Upvotes: 6
Reputation: 5422
In the end, it was really simple.
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());
Upvotes: 3