I wasn't expecting that I'll be writing a post about POST Tunneling in 2017 (almost 2018), I thought it's a thing of the past.

Recently a friend of mine reached out to me for advice. His company has delivered a new ASP.NET Core based service to a client. The service was exposing a Web API which (among others) relied on PATCH requests. After the deployment it turned out that one of older applications which were supposed to integrate with the new service wasn't able to issue PATCH requests due to technical limitations. I suggested they check if that old application can issue custom HTTP headers which would allow them to solve the problem with POST Tunneling.

What is POST Tunneling

POST Tunneling is a quite old technic. I've encountered it for the first time in 2012. Back then the issue was very common. A lot of HTTP clients (including XMLHttpRequest in some browsers) weren't providing support for all HTTP methods. Also many corporate networks infrastructures were blocking certain methods. The solution was to tunnel such method through POST request with help of custom header (I believe that X-HTTP-Method-Override was the most frequently used one). The server would examine the incoming POST request and if the header was present its value would be treated as the actual method.

Middleware implementation

The middleware should allow for configuring two things: the name of the custom header and list of methods which can be tunneled.

public class PostTunellingOptions
{
    public string HeaderName { get; set; }

    public IEnumerable<string> AllowedMethods { get; set; }
}

The implementation is very similar to SSL Acceleration Middleware I've done in the past. The heart is IHttpRequestFeature with its Method property. Changing value of that property will trick all the later steps of pipeline to use the new value.

public class PostTunnelingMiddleware
{
    private readonly RequestDelegate _next;

    private readonly string _headerName;
    private readonly HashSet<string> _allowedMethods;

    public PostTunnelingMiddleware(RequestDelegate next, IOptions options)
    {
        // Null checks removed for brevity

        _headerName = options.Value.HeaderName;

        _allowedMethods = new HashSet<string>();
        if (options.Value.AllowedMethods != null)
        {
            foreach (string allowedMethod in options.Value.AllowedMethods)
            {
                _allowedMethods.Add(allowedMethod.ToUpper());
            }
        }
    }

    public Task Invoke(HttpContext context)
    {
        if (HttpMethods.IsPost(context.Request.Method))
        {
            if (context.Request.Headers.ContainsKey(_headerName))
            {
                string tunelledMethod = context.Request.Headers[_headerName];
                if (_allowedMethods.Contains(tunelledMethod))
                {
                    IHttpRequestFeature httpRequestFeature = context.Features.Get<IHttpRequestFeature>();
                    httpRequestFeature.Method = tunelledMethod;
                }
            }
        }

        return _next(context);
    }
}

In order to add POST Tunneling to the application it's enough to register the middleware at the desired position in the pipeline.

public class Startup
{
    ...

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...

        app.UseMiddleware<PostTunnelingMiddleware>(Options.Create(new PostTunnelingOptions
        {
            HeaderName = "X-HTTP-Method-Override",
            AllowedMethods = new[] { HttpMethods.Patch }
        }));

        app.UseMvc();

        ...
    }
}

I've made the middleware (with some helper extensions) available as a Gist so if any of you ever end up with similar problem, it's out there ready to use.

If you frequently log your requests you might have noticed a presence of Save-Data header (especially if you have a significant amount of traffic from mobile devices). This is not a common header, I've noticed it for the first time when I was playing with Opera in Opera Turbo mode and I've been intrigued by it. It turns out that beside Opera Turbo it's being send by both Chrome and Opera when Data Saver/Data savings option on Android versions of those browsers is enabled. The intent of this header is to hint the server that client would like to reduce data usage. This immediately gave me couple of interesting ideas.

First things first - reading the header from request

Before I could do anything useful with the header I had to get it from the request. The header definition says that its value can consist of multiple tokens, while only one (on) is currently defined. I've decided to represent this with following class.

public class SaveDataHeaderValue
{
    private bool? _on = null;

    public bool On
    {
        get
        {
            if (!_on.HasValue)
            {
                _on = Tokens.Contains("on", StringComparer.InvariantCultureIgnoreCase);
            }

            return _on.Value;
        }
    }

    public IReadOnlyCollection<string> Tokens { get; }

    public SaveDataHeaderValue(IReadOnlyCollection<string> tokens)
    {
        Tokens = tokens ?? throw new ArgumentNullException(nameof(tokens));
    }
}

Now I could create a simple extension method which would grab the raw header value from request, split it, remove any optional white spaces and instantiate the SaveDataHeaderValue.

public static class HttpRequestHeadersExtensions
{
    public static SaveDataHeaderValue GetSaveData(this HttpRequest request)
    {
        if (!request.HttpContext.Items.ContainsKey("SaveDataHeaderValue"))
        {
            StringValues headerValue = request.Headers["Save-Data"];
            if (!StringValues.IsNullOrEmpty(headerValue) && (headerValue.Count == 1))
            {
                string[] tokens = ((string)headerValue).Split(';');
                for (int i = 0; i < tokens.Length; i++)
                {
                    tokens[i] = tokens[i].Trim();
                }

                request.HttpContext.Items["SaveDataHeaderValue"] = new SaveDataHeaderValue(tokens);
            }
        }

        return request.HttpContext.Items["SaveDataHeaderValue"] as SaveDataHeaderValue;
    }
}

I'm also caching the SaveDataHeaderValue instance in HttpContext.Items so parsing happens only once per request.

Dedicated images URLs

My first idea was to be able to define different images sources depending on presence of the hint. I wanted something similar to what link and script Tag Helpers provide in form of asp-fallback-href/asp-fallback-src - an attribute which would contain alternative source. The framework provides a UrlResolutionTagHelper class which can be used as base in order to take care of the URL processing. What left for me was to check if the hint has been sent along the request and if yes replace the original value of src attribute with value from the new attribute (which I've named asp-savedata-src). I've also targeted my Tag Helper only at img elements that have both attributes.

[HtmlTargetElement("img", Attributes = "src,asp-savedata-src",
    TagStructure = TagStructure.WithoutEndTag)]
public class ImageTagHelper : UrlResolutionTagHelper
{
    [HtmlAttributeName("asp-savedata-src")]
    public string SaveDataSrc { get; set; }

    public ImageTagHelper(IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder)
        : base(urlHelperFactory, htmlEncoder)
    { }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // Validations skipped for brevity
        ...

        output.CopyHtmlAttribute("src", context);
        if (ViewContext.HttpContext.Request.GetSaveData()?.On ?? false)
        {
            output.Attributes.SetAttribute("src", SaveDataSrc);
        }
        ProcessUrlAttribute("src", output);

        output.Attributes.RemoveAll("asp-savedata-src");
    }
}

This Tag Helper can be used like this.

<img src="~/images/highres.png" asp-savedata-src="~/images/lowres.png" />

Which is exactly what I wanted and I believe looks very elegant. The approach can easily be extended on other media (for example video).

Conditional markup

The second idea was conditional markup generation. There are often areas of a page which doesn't provide important information and serve more decorative purposes. Those areas could be skipped if client has opted for reduced data usage. For this purpose a simple HtmlHelper extension should be enough.

public static class HtmlHelperSaveDataExtensions
{
    public static bool ShouldSaveData(this IHtmlHelper htmlHelper)
    {
        if (htmlHelper == null)
        {
            throw new ArgumentNullException(nameof(htmlHelper));
        }

        return htmlHelper.ViewContext.HttpContext.Request.GetSaveData()?.On ?? false;
    }
}

With this extension such noncrucial areas of the page can be wrapped in an if block.

@if (!Html.ShouldSaveData())
{
    ...
}

This allows for more fine-tuned markup delivery strategy, but this idea can be taken further.

Dedicated actions

Having conditional sections is great but having dedicated views might be better in some cases. The Save-Data header can easily become a part of action selection process. All that is needed is an attribute which implements IActionConstraint interface, which boils down to implementing the Accept method. The Accept method should return true if action is valid for the request.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class SaveDataAttribute : Attribute, IActionConstraint
{
    private bool _on;

    public int Order { get; set; }

    public SaveDataAttribute(bool on)
    {
        _on = on;
    }

    public bool Accept(ActionConstraintContext context)
    {
        return (context.RouteContext.HttpContext.Request.GetSaveData()?.On ?? false) == _on;
    }
}

Applying attribute to actions having the same action name allows for clean separation between regular and reduced data flow.

public class DemoController : Controller
{
    [SaveData(false)]
    public IActionResult Index()
    {
        return View();
    }

    [ActionName(nameof(Index))]
    [SaveData(true)]
    public IActionResult IndexSavedData()
    {
        return View(nameof(IndexSavedData));
    }
}

This shows the power hiding behind this header. It opens a number of ways to optimize the application for clients which desire it and the samples above are just the simplest usages I could come up with. There is probably a lot more interesting usages that I haven't think of.

Couple more words about broader context

The Save-Data header is part of Client Hints proposal which aims at addressing a need to deliver optimized content for each device. The proposal contains more headers which provide information mostly about display capabilities of client. It also defines a mechanism for advertising supported hints through Accept-CH and Accept-CH-Lifetime headers. As I was going through the specification I've created a simple middleware capable of setting those headers. I'm not aware of any browser supporting those headers, so this is more like a learning example although it has one real-life use. In addition to advertising client hints support it also interacts with Vary header. It's important if the response which can be optimized is also cacheable. In such case the cache needs to know that the hint headers needs to be taken into consideration when choosing response. The middleware will add all the hint headers which has been configured to be supported to the Vary header.

I've put the projects containing the middleware and helpers build around Save-Data header up on GitHub.

This is another post resulting from my work on sample ASP.NET Core MVC powered Web API. This time I'm going to focus on conditional requests. Conditional requests have three main use cases: cache revalidation, concurrency control and range requests. Range requests are primarily used for media like video or audio and I'm not going to write about them here (I did in the past in context of ASP.NET MVC), but the other two are very useful for a Web API.

Adding metadata to the response

Before client can perform a conditional request some metadata should be provided which can be used as validators. The standard defines two types of such metadata: modification dates (delivered by Last-Modified header) and entity tags (delivered by ETag header). Below interface represents those metadata.

interface IConditionalRequestMetadata
{
    string EntityTag { get; }

    DateTime? LastModified { get; }
}

The modification date is simple, it should represent a date and time of the last change to the resource which is being returned. Entity tag is a little bit more complicated. In general entity tag should be unique per representation. This means that entity tag should change not only due to changes over time but also as a result of content negotiation. That second aspect is problematic because it forces entity tag generation to happen very late which can make them impractical. Fortunately standard leaves a gate in a form of weak entity tags. A weak entity tag indicates that the two representations are semantically equivalent, which for Web API usages should be good enough (this approach will break the standard at some point but more about this later). This allows for implementing IConditionalRequestMetadata as part of the demo application model.

public class Character: IConditionalRequestMetadata
{
    private string _entityTag;

    public string Id { get; protected set; }

    ...

    public DateTime LastUpdatedDate { get; protected set; }

    public string EntityTag
    {
        get
        {
            if (String.IsNullOrEmpty(_entityTag))
            {
                _entityTag = "\"" + Id + "-"
                    + LastUpdatedDate.Ticks.ToString(CultureInfo.InvariantCulture) + "\"";
            }

            return _entityTag;
        }
    }

    public DateTime? LastModified { get { return LastUpdatedDate; } }
}

What is missing is some generic mechanism for setting the headers on the response. Result filter is an interesting option for this task. The OnResultExecuting method can be used to inspect if the result from the action is ObjectResult which value is an implementation of IConditionalRequestMetadata. If those conditions are met the headers can be set.

internal class ConditionalRequestFilter : IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    { }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        IConditionalRequestMetadata metadata = (context.Result as ObjectResult)?.Value
            as IConditionalRequestMetadata;

        if (metadata != null)
        {
            SetConditionalMetadataHeaders(context, metadata);
        }
    }

    private static void SetConditionalMetadataHeaders(ResultExecutingContext context,
        IConditionalRequestMetadata metadata)
    {
        ResponseHeaders responseHeaders = context.HttpContext.Response.GetTypedHeaders();

        if (!String.IsNullOrWhiteSpace(metadata.EntityTag))
        {
            responseHeaders.ETag = new EntityTagHeaderValue(metadata.EntityTag, true);
        }

        if (metadata.LastModified.HasValue)
        {
            responseHeaders.LastModified = metadata.LastModified.Value;
        }
    }
}

After registering the filter the headers will be available on every response for which the underlying model provides the metadata.

Cache revalidation

Typically client will use one of two headers as part of GET or HEAD request in order to perform cache revalidation: If-None-Match or If-Modified-Since. Simple extension method can be used to extract both from request.

internal class HttpRequestConditions
{
    public IEnumerable<string> IfNoneMatch { get; set; }

    public DateTimeOffset? IfModifiedSince { get; set; }
}
internal static class HttpRequestExtensions
{
    internal static HttpRequestConditions GetRequestConditions(this HttpRequest request)
    {
        HttpRequestConditions requestConditions = new HttpRequestConditions();

        RequestHeaders requestHeaders = request.GetTypedHeaders();

        if (HttpMethods.IsGet(request.Method) || HttpMethods.IsHead(request.Method))
        {
            requestConditions.IfNoneMatch = requestHeaders.IfNoneMatch?.Select(v => v.Tag.ToString());
            requestConditions.IfModifiedSince = requestHeaders.IfModifiedSince;
        }

        return requestConditions;
    }
}

The If-None-Match is considered to be a more accurate (if both are present in the request only If-None-Match should be evaluated). It can contain one or more entity tags which represent versions of the resource cached by the client. If the current entity tag of the resource is on that list the server should respond with 304 Not Modified (with no body) instead of normal response.

The If-Modified-Since works similarly. It contains last modification date of the resource known by client. If that modification date is equal (or potentially later) to the modification date of the resource the server should also respond with 304 Not Modified.

It's hard to optimize for cache revalidation on the server side unless the metadata of the resource are cheaply accessible (for example a static file). For typical scenarios which involve some kind of database as store it usually results in multiple queries or a complex one. Because of that it's often good enough to retrieve the resource from store and validate later. The ConditionalRequestFilter can be extended to do that.

internal class ConditionalRequestFilter : IResultFilter
{
    ...

    public void OnResultExecuting(ResultExecutingContext context)
    {
        IConditionalRequestMetadata metadata = (context.Result as ObjectResult)?.Value
            as IConditionalRequestMetadata;

        if (metadata != null)
        {
            if (CheckModified(context, metadata))
            {
                SetConditionalMetadataHeaders(context, metadata);
            }
        }
    }

    private static bool CheckModified(ResultExecutingContext context,
        IConditionalRequestMetadata metadata)
    {
        bool modified = true;

        HttpRequestConditions requestConditions = context.HttpContext.Request.GetRequestConditions();

        if ((requestConditions.IfNoneMatch != null) && requestConditions.IfNoneMatch.Any())
        {
            if (!String.IsNullOrWhiteSpace(metadata.EntityTag)
                && requestConditions.IfNoneMatch.Contains(metadata.EntityTag))
            {
                modified = false;
                context.Result = new StatusCodeResult(StatusCodes.Status304NotModified);
            }
        }
        else if (requestConditions.IfModifiedSince.HasValue && metadata.LastModified.HasValue)
        {
            DateTimeOffset lastModified = metadata.LastModified.Value.AddTicks(
                -(metadata.LastModified.Value.Ticks % TimeSpan.TicksPerSecond));

            if (lastModified <= requestConditions.IfModifiedSince.Value)
            {
                modified = false;
                context.Result = new StatusCodeResult(StatusCodes.Status304NotModified);
            }
        }

        return modified;
    }

    ...
}

This way cache revalidation is being handled automatically for resources which support it.

Concurrency control

The concurrency control can be considered an opposite mechanism to cache revalidation. Its goal is to prevent the change of a resource (usually in result of PUT or PATCH request) if it has been already modified by another user (the Lost Update problem). The headers used to achieve this goal are counterparts of those used in cache revalidation: If-Match and If-Unmodified-Since. The previously created extension method can extract those as well.

internal class HttpRequestConditions
{
    ...

    public IEnumerable<string> IfMatch { get; set; }

    public DateTimeOffset? IfUnmodifiedSince { get; set; }
}
internal static class HttpRequestExtensions
{
    internal static HttpRequestConditions GetRequestConditions(this HttpRequest request)
    {
        ...

        if (HttpMethods.IsGet(request.Method) || HttpMethods.IsHead(request.Method))
        {
            ...
        }
        else if (HttpMethods.IsPut(request.Method) || HttpMethods.IsPatch(request.Method))
        {
            requestConditions.IfMatch = requestHeaders.IfMatch?.Select(v => v.Tag.ToString());
            requestConditions.IfUnmodifiedSince = requestHeaders.IfUnmodifiedSince;
        }

        return requestConditions;
    }
}

The If-Unmodified-Since is an exact opposite of If-Modified-Since which means that last modification date of the resource can't be later than the one provided. If it is, the operation shouldn't be performed and the response should be 412 Precondition Failed.

The If-Match is a little bit more tricky. Similar to If-None-Match it provides a list of entity tags and the current entity tag of the resource is required to be present on that list, but the standard disallows usage of weak entity tags here. This guarantees safety if different representations are stored separately, but for modern Web APIs this is often not a case. Different representations are a result of transforming the source resource which is stored only once. Because of that I believe that not following standard in this case is acceptable. One more thing is handling * value (which I've skipped for If-None-Match) - it means that resource should have at least one current representation. If the considered methods are only PUT and PATCH this condition should always evaluate to true (the absence of resource should be checked earlier and result in 404 Not Found).

All those rules can be encapsulated within a single method.

private bool CheckPreconditionFailed(HttpRequestConditions requestConditions,
    IConditionalRequestMetadata metadata)
{
    bool preconditionFailed = false;

    if ((requestConditions.IfMatch) != null && requestConditions.IfMatch.Any())
    {
        if ((requestConditions.IfMatch.Count() > 2) || (requestConditions.IfMatch.First() != "*"))
        {
            if (!requestConditions.IfMatch.Contains(metadata.EntityTag))
            {
                preconditionFailed = true;
            }
        }
    }
    else if (requestConditions.IfUnmodifiedSince.HasValue)
    {
        DateTimeOffset lastModified = metadata.LastModified.Value.AddTicks(
            -(metadata.LastModified.Value.Ticks % TimeSpan.TicksPerSecond));

        if (lastModified > requestConditions.IfUnmodifiedSince.Value)
        {
            preconditionFailed = true;
        }
    }

    return preconditionFailed;
}

This method should be used as part of an action flow. This can be done more or less generic depending on the application architecture (for example CQRS opens much more options). In simplest case it can be called directly by every action which needs it.

One last thing

Described here are most typical usages of If-Match, If-None-Match, If-Modified-Since and If-Unmodified-Since which doesn't exhaust the subject. The headers can be used with other methods than mentioned or have special usages (like If-None-Match with value of *). As always, when in doubt the standard is your friend.

My Server-Sent Events Middleware seems to be a mine of interesting issues. The latest one was about events being delivered with delay (in general one behind) under specific conditions.

The nature of the issue

Initially there was no hint at what are the conditions required for issue to manifest itself. The demo application was working correctly while the one on which the person who reported the issue was working didn't. Luckily the reporter was extremely helpful in diagnosing the issue and devoted some time to find the difference between his and mine code. The difference between working and not working scenario was presence of Response Compression Middleware which I've added while working on previous issue.

My first thought was that Response Compression Middleware must be writing to the response stream differently then my code (when Response Compression Middleware is present it wraps the original response stream). I've gone through the source code of BodyWrapperStream and found nothing. I went deeper and analyzed DeflateStream also without finding anything specific.

At this point I've decided to change approach and use Fiddler to see what was happening on the wire. To my surprise the first thing I've noticed is that the response was still gziped. That really baffled me so I've double checked that the Response Compression Middleware was removed. It was, so it must have been something external to my application. The only external component I was able to identify was IIS Express, so I quickly changed the launch drop down option to run on Kestrel only. That was it, without IIS in front everything was working as expected which meant that IIS (serving as reverse proxy) was compressing the response on its own which resulted in delayed delivery of events.

Preventing IIS from compressing the response

The first obvious option was changing the IIS configuration. This would certainly work but I would have to put the details into documentation and leave it as a trap for others using the same deployment scenario. I wanted to avoid that so I've started researching for other solution. The general conclusion from materials that I've found was that IIS will compress the response if it doesn't detect the Content-Encoding header. That gave me an idea. One of valid values for Content-Encoding is identity which indicates no compression/modification, it might be enough to prevent IIS from adding compression. I've added the code for setting the header to the middleware.

public class ServerSentEventsMiddleware
{
    ...

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Headers[Constants.ACCEPT_HTTP_HEADER] == Constants.SSE_CONTENT_TYPE)
        {
            DisableResponseBuffering(context);

            context.Response.Headers.Append("Content-Encoding", "identity");

            ...
        }
        else
        {
            await _next(context);
        }
    }

    ...
}

Running the demo application without Response Compression Middleware and behind IIS has confirmed that the solution was working. Now I had to make sure that I haven't broken anything else.

Maintaining compatibility with Response Compression Middleware

As the middleware is now setting the Content-Encoding it could somehow interfere with Response Compression Middleware. I've re-enabled it and run the test again. Screenshot bellow shows the result.

Chrome Developer Tools Network Tab - Multiple Content-Encoding

The response contains two Content-Encoding headers. The reason for this is that Response Compression Middleware is also blindly using IHeaderDictionary.Append. Unfortunately, the fact that header is present twice confuses the browser. The response is coming compressed but the browser treats it as not compressed. I couldn't change how Response Compression Middleware works so I had to be smarter when setting the header. Simply checking if the header is already present didn't work because Response Compression Middleware sets it upon first attempt to write. I was saved by HttpResponse.OnStarting which allows for interacting with the response just before sending the headers. I've replaced my header setting code with following method.

private void HandleContentEncoding(HttpContext context)
{
    context.Response.OnStarting(() =>
    {
        if (!context.Response.Headers.ContainsKey("Content-Encoding"))
        {
            context.Response.Headers.Append("Content-Encoding", "identity");
        }

        return _completedTask;
    });
}

This fixed the problem with two headers and allowed me to close the issue. The approach is universal and can be used in other scenarios with same requirement.

In last couple weeks I've been playing with ASP.NET Core MVC powered Web API. One of things I wanted to dig deeper into is support for HEAD method. The specification says that "The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response. The metainformation contained in the HTTP headers in response to a HEAD request SHOULD be identical to the information sent in response to a GET request.". In practice the HEAD method is often being used for performing "exists" requests.

How ASP.NET Core is handling HEAD at the server level

Before looking at higher layers it is worth to understand what is the behavior of underlying server in case of HEAD request. The sample Web API mentioned in the beginning has a following end-middleware for handling cases when none of routes has been hit, it will be perfect for this task.

public class Startup
{
    ...

    public void Configure(IApplicationBuilder app)
    {
        ...

        app.Run(async (context) =>
        {
            context.Response.ContentLength = 34;
            await context.Response.WriteAsync("-- Demo.AspNetCore.Mvc.CosmosDB --");
        });
    }
}

First testing environment will be Kestrel. Response to a GET request (which will be used as baseline) looks like below.

HTTP/1.1 200 OK
Content-Length: 34
Date: Mon, 02 Oct 2017 19:22:38 GMT
Server: Kestrel

-- Demo.AspNetCore.Mvc.CosmosDB --

Switching the method to HEAD (without any changes to the code) results in following.

HTTP/1.1 200 OK
Content-Length: 34
Date: Mon, 02 Oct 2017 19:22:38 GMT
Server: Kestrel

This shows that Kestrel is handling HEAD request quite nicely out of the box. All the headers are there and the write to the response body has been ignored. This is the exact behavior one should expect.

With this positive outcome application can be switched to the second testing environment which will be HTTP.sys server. Here the response to HEAD request is different.

HTTP/1.1 200 OK
Content-Length: 34
Date: Mon, 02 Oct 2017 19:25:43 GMT
Server: Microsoft-HTTPAPI/2.0

-- Demo.AspNetCore.Mvc.CosmosDB --

Unfortunately this is a malformed response as it contains body, which is incorrect from specification perspective and also removes the performance gain which HEAD request offers. This is something that should be addressed, but before that let's take a look at more complex scenario.

Adding ASP.NET Core MVC on top

Knowing how the servers are handling the HEAD method the scenario can be extended by adding MVC to the mix. For this purpose a simple GET action which takes an identifier as parameter can be used. The important part is that the action should return 404 Not Found for identifier which doesn't exist.

[Route("api/[controller]")]
public class CharactersController : Controller
{
    private readonly IMediator _mediator;

    public CharactersController(IMediator mediator)
    {
        _mediator = mediator;
    }

    ...

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(string id)
    {
        Character character = await _mediator.Send(new GetSingleRequest<Character>(id));
        if (character == null)
        {
            return NotFound();
        }

        return new ObjectResult(character);
    }

    ...
}

In context of previous discoveries testing environments can be limited to Kestrel only. Making a GET request with valid identifier results in response with JSON body.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 02 Oct 2017 19:40:25 GMT
Server: Kestrel
Transfer-Encoding: chunked

{"id":"1ba6271109d445c8972542985b2d3e96","createdDate":"2017-09-24T21:08:50.9990689Z","lastUpdatedDate":"2017-09-24T21:08:50.9990693Z","name":"Leia Organa","gender":"Female","height":150,"weight":49,"birthYear":"19BBY","skinColor":"Light","hairColor":"Brown","eyeColor":"Brown"}

Switching to HEAD produces a response which might be a little surprising.

HTTP/1.1 200 OK
Content-Length: 34
Date: Mon, 02 Oct 2017 19:42:10 GMT
Server: Kestrel

The presence of Content-Length and absence of Content-Type suggest this is not the response from the intended endpoint. In fact it looks like a response from the end-middleware. A request with invalid identifier returns exactly same response instead of expected 404. Taking one more look at the code should reveal why this shouldn't be a surprise. The action is decorated with HttpGetAttribute which makes it unreachable by HEAD request, in result application has indeed defaulted to the end-middleware. Adding HttpHeadAttribute should solve the problem.

[Route("api/[controller]")]
public class CharactersController : Controller
{
    ...

    [HttpGet("{id}")]
    [HttpHead("{id}")]
    public async Task<IActionResult> Get(string id)
    {
        ...
    }

    ...
}

After this change both HEAD requests (with valid and invalid identifier) return expected responses.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 02 Oct 2017 19:44:23 GMT
Server: Kestrel
HTTP/1.1 404 Not Found
Date: Mon, 02 Oct 2017 19:48:07 GMT
Server: Kestrel

This means that an action needs to be decorated with two attributes. Separating between GET and HEAD makes perfect sense when it's possible to optimize the HEAD request handling on server side but for simple scenario like this one it seems unnecessary. One possible improvement is custom HttpMethodAttribute which would allow both methods.

public class HttpGetOrHeadAttribute : HttpMethodAttribute
{
    private static readonly IEnumerable<string> _supportedMethods = new[] { "GET", "HEAD" };

    public HttpGetOrHeadAttribute()
        : base(_supportedMethods)
    { }

    public HttpGetOrHeadAttribute(string template)
        : base(_supportedMethods, template)
    {
        if (template == null)
        {
            throw new ArgumentNullException(nameof(template));
        }
    }
}

Still anybody who will be working on the project in future will have to know that a custom attribute must be used. It might be preferred to have a solution which can be applied once, especially keeping in mind that there is also HTTP.Sys issue to be solved.

Solving the problems in one place

In context of ASP.NET Core "one place" typically ends up being some kind of middleware. In this case a middleware could be used to perform an old trick of switching incoming HEAD request to GET. The switch should be only temporary, otherwise the Kestrel integrity checks might fail due to Content-Length being different from actual number of bytes written. There is also one important thing to remember. After switching the method Kestrel will stop ignoring writes to the body. The easiest solution to this is to change body stream to Stream.Null (this will also fix the problem observed in case of HTTP.Sys server).

public class HeadMethodMiddleware
{
    private readonly RequestDelegate _next;

    public HeadMethodMiddleware(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task Invoke(HttpContext context)
    {
        bool methodSwitched = false;

        if (HttpMethods.IsHead(context.Request.Method))
        {
            methodSwitched = true;

            context.Request.Method = HttpMethods.Get;
            context.Response.Body = Stream.Null;
        }

        await _next(context);

        if (methodSwitched)
        {
            context.Request.Method = HttpMethods.Head;
        }
    }
}

This middleware should be applied with caution. Some middlewares (for example StaticFiles) can have their own optimized handling of HEAD method. It is also possible that in case of some middlewares switching method can result in undesired side effects.

Older Posts