5

I have an ApiController which responds to a POST request by redirecting it via HTTP's status code 307. It does so using only information from the header, so the body of the request is not needed by this action. This action is equivalent to:

public HttpResponseMessage Post() {
    var url;
    // Some logic to construct the URL
    var response = new HttpResponseMessage(HttpStatusCode.TemporaryRedirect);
    response.Headers.Location = new System.Uri(url);
    return response;
}

This is straightforward enough, but there is one improvement I would like to make. The request body could potentially contain a large amount of data, so I would like to leverage the HTTP status code 100 to make this request more efficient. With the controller as it is now, a conversation might look like this:

> POST /api/test HTTP/1.1
> Expect: 100-continue
> ...

< HTTP/1.1 100 Continue

> (request body is sent)

< HTTP/1.1 307 Temporary Redirect
< Location: (the URL)
< ...

Since the request body is not needed by the redirection action, I would like to be able to shorten the conversation to:

> POST /api/controller HTTP/1.1
> Expect: 100-continue
> ...

< HTTP/1.1 307 Temporary Redirect
< Location: (the URL)
< ...

I have spent the better part of a day researching how to accomplish this, and I have not been able to come up with a solution. In my research, I have learned:

  • When the ApiController's action executes, the 100 Continue has already been sent.
  • When the ApiController is constructed, the 100 Continue has already been sent.
  • When the HttpApplication's PreRequestHandlerExecute event is triggered, the 100 Continue response has not been sent.
  • When a DelegatingHandler executes, the 100 Continue has already been sent.

Based on this, the best solution I have come up with so far is to create an HttpModule which uses the RouteData on the RequestContext to override the response when the ApiController in question is the recipient of the request. This is far from an ideal solution, however, for several reasons (code separation, not taking advantage of Web API's parameter binding, and bypassing additional logic in an AuthorizeAttribute on the ApiController).

It seems as if there must be a better solution to this, but I have found very little information on how to properly handle the Expect: 100-continue header in a Web API application. What would be the simplest way to implement this ApiController to properly handle the Expect: 100-continue header?

K. Alan Bates
  • 2,914
  • 3
  • 25
  • 53
  • Are you using IIS 7? – K. Alan Bates Oct 24 '14 at 01:20
  • WebAPI can't handle this header, as it is transparently processed at the kernel level without WebAPI's knowledge, in IIS. – Claies Oct 24 '14 at 02:01
  • @AndrewCounts ...that's what I said. – K. Alan Bates Oct 24 '14 at 02:10
  • @K.AlanBates yes, I was just agreeing with you. :) – Claies Oct 24 '14 at 02:13
  • @AndrewCounts gotcha – K. Alan Bates Oct 24 '14 at 02:19
  • @K.AlanBates This application is running on IIS 7.5. – Jordan Higgins Oct 24 '14 at 13:24
  • @JordanHiggins You aren't going to be able to solve this with WebApi configuration options. In my response below, I detailed why that is the case. "100 Continues" is handled by HTTP.sys, so there's nothing ASP.NET can do about it. If you want to intervene, you'll have to write an ISAPI filter for IIS. Brush off your C++ and STL and get to searching on altavista ;) – K. Alan Bates Oct 24 '14 at 13:34
  • I've spoken around this in several of my responses but haven't stated it outright: this is an implementation detail of IIS, not a quirk with ASP.NET or WebApi. It has more to do with IIS' handling of the Expect Header than anything to do with the actual response type. Apache had somewhat similar issues (Bug47087)...it took 5 years for it to get fixed. – K. Alan Bates Oct 24 '14 at 13:38

2 Answers2

1

...Are you sure that you need this optimization?

If you're using IIS 6, you're looking at kicking down into IIS 5 Compatibility Mode and writing a ReadRawData/SendRawData ISAPI Filter. An ISAPI Extension is out of the question for reasons given further down in my response. (If you're using IIS 5 or below, may God have mercy on your soul)

If you're using IIS 7+, you might be able to get away with writing an IIS Native Module.

You are correct in your thinking that by the time the controller becomes involved, the response has already been sent because Web API lives inside ASP.NET; this response is being handled by the IIS Core.

Some light reading material

HTTP.SYS IIS and the 100 Continue

David Wang

A "100 continue", like a "400 Bad Request" or a Kernel Response Cache Hit, is special in that HTTP.SYS transparently handles it in kernel mode without notifying user mode of anything.In addition, ISAPI Extensions cannot interact with any response output - they can only generate response output, not see results of response output. Thus, an ISAPI Extension will never be able to interact with requests that generate "100 continue" nor "100 continue" responses themselves to suppress them.

On IIS6, the only way to inject user mode processing into these transparent request handlings of HTTP.SYS is to run in IIS5 Compatibility Mode and use an ReadRawData/SendRawData ISAPI Filter. ReadRawData forces HTTP.SYS to hand the raw data off the network into user mode for filtering PRIOR to parsing that user mode output into HTTP requests to place into queues.

Of course, this method completely defeats the purpose of running IIS6 with Application Pools and process isolation (a single failure in this filtering user mode process halts the entire server)... but such is the server-side compromise when the client is buggy...

FYI: This approach will not work on Vista Server/IIS7. HTTP.SYS will no longer hand raw data off the network into user mode for filtering prior to parsing, so it will be impossible for user mode code to know that a request which triggers the automatic "100 continue" happened.


Edit

Haacked Http Web Request Expect 100 Continue

K. Alan Bates
  • 2,914
  • 3
  • 25
  • 53
  • The request body could be several MB and is being sent over a cellular connection, so this optimization is somewhat important. If the redirect response is sent from the `HttpApplication`'s `PreRequestHandlerExecute` event, then the exchange *behaves as intended*--at least on the IIS8 Express server I'm developing on. This leads me to believe that while the 100-continue may be sent by IIS, ASP.NET is actually able to send a response without triggering the 100-continue. – Jordan Higgins Oct 24 '14 at 13:54
  • You may want to do an upgrade request to WebSockets and then create your own client-server handshake protocol for servicing this request. The point of an Http Continuation is that the initial check is just to say "hey, server. Are you serving?" Server responds with "yup. Continue" and the client immediately sends its request body. – K. Alan Bates Oct 24 '14 at 14:12
  • Section 8.2.3 of [RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html) says that the server may respond with either a `100 Continue` and read the request body *or* a final status code. My interpretation of the spec is that responding with a 307 status code is valid. – Jordan Higgins Oct 24 '14 at 14:26
  • RFC 2616 is dead. You should refer to RFC7231.6.2.1 for any future references to the 100 Continue. Respectfully, I believe your interpretation of the spec is incorrect. The server can respond with a 100 Continue, indicating "client, you may proceed" or the server will send a final status code to indicate that the connection has been closed. RFC7231.5.1.1 provided additional explanation and instructions for the Expect header. – K. Alan Bates Oct 24 '14 at 14:30
  • 1
    According to [RFC 7231, section 5.1.1](http://tools.ietf.org/html/rfc7231#section-5.1.1): "A 100-continue expectation informs recipients that the client is about to send a (presumably large) message body in this request and wishes to receive a 100 (Continue) interim response if the request-line and header fields are not sufficient to cause an immediate success, **redirect**, or error response." (emphasis mine) – Jordan Higgins Oct 24 '14 at 14:41
  • ...so to draw this back to the problem at hand rather than discussing this at the RFC level, why are you sending a POST again if you're not wanting to send the body on a 100-Continue? You could get your Redirect from a GET request and then the amount of bandwidth being used is under control of the service and you avoid this quirk of IIS' implementation of Expect 100 Continue altogether. – K. Alan Bates Oct 24 '14 at 14:54
  • The link you added appears to be related to initiating a request using `HttpWebRequest` without sending an `Expect: 100-continue` header. The client in this case is not written in C#, and I *do* want it to send the `Expect: 100-continue` header. – Jordan Higgins Oct 24 '14 at 14:59
  • The URL that the request is being redirected to *does* need the request body. I'm just trying to avoid sending it to the Web API action that is only going to redirect the request. Making one request to get the URL and then another separate request to that URL is a viable solution, although it requires a little extra code on the client, which is fine. – Jordan Higgins Oct 24 '14 at 15:01
  • @JordanHiggins, The solution is writing an ISAPI Filter or a Handler for IIS. You can't modify this behavior with your WebAPI config. – K. Alan Bates Oct 24 '14 at 15:02
  • @JordanHiggins (To your last response) So you're trying to use ASP.NET to perform a URL Redirect? (That's rhetorical...that's what it looks like you're doing) You want to perform the redirect *before* ASP.NET becomes involved. – K. Alan Bates Oct 24 '14 at 15:08
0

I assume that you are redirecting the browser to another controller in the same solution. Instead of the redirect, you could process the request directly in the original url-address. This way the client would need to send the request only once without being redirected at all.

One way to implement this would be to write your own IHttpControllerSelector which would assign the request to the correct controller based on the request headers.

You might want to check the following SO-question if you want to assign the custom selector to some specific route only: ASP.NET Web API custom IHttpControllerSelector for a single route

Community
  • 1
  • 1
artokai
  • 395
  • 2
  • 8