2

I'm trying to return custom error responses from web.api. Let it be simple string "Oops!" formatted as json. So I created simple delegating handler which replaces error responses like this:

public class ErrorMessageHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
       HttpRequestMessage request, CancellationToken cancellationToken)
    {
       var response = await base.SendAsync(request, cancellationToken);

       if (response.IsSuccessStatusCode)
           return response;

       var formatter = new JsonMediaTypeFormatter();
       var errorResponse = request.CreateResponse(response.StatusCode, "Oops!", formatter);
       return errorResponse;
    }
}

Next I make sure that this is the only one message handler in pipeline:

httpConfig.MessageHandlers.Clear();
httpConfig.MessageHandlers.Add(new ErrorMessageHandler());

// Only default route
httpConfig.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

app.UseWebApi(httpConfig); // OWIN self-hosting

Controller is also simplest one:

public class ValuesController : ApiController
{
   public IHttpActionResult Get(int id)
   {
      if (id == 42)
        return Ok("Answer to the Ultimate Question of Life, the Universe, and Everything");

      return NotFound();
   }
}

And here goes interesting:

  • /api/values/42 gives me 200 response with value string
  • /api/values/13 gives me 404 response with my custom "Oops!" string
  • /api/values/42/missing gives me empty 404 response

The last case is my problem. When I set a breakpoint on the last line of delegating handler I clearly see that errorResponse contains ObjectContent<string> with the correct value. But why this value is cleared away later?

Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
  • So you are sure that "/api/values/42/missing" hits your handler? I tested that in my environment (which is not the same as yours but still) and for me this handler is completely bypassed and response is served by IIS. – Evk Oct 31 '17 at 15:13
  • Perhaps a routing issue? I have to do some reading, but perhaps the `DelegatingHandler` is being executed, but a downstream event is squashing the response due to the unknown route? It does seem that `HttpRoutingDispatcher` runs after custom handler, as per [Asp.Net docs](https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/http-message-handlers). – claylong Oct 31 '17 at 15:14
  • @Evk yep, totally sure. Handler gets response with `HttpError` that states `"No route data was found for this request."`. Then this response is replaced with new `errorResponse` and returned to the pipeline (I suppose it should go to `HttpServer`). Maybe it's owin issue? – Sergey Berezovskiy Oct 31 '17 at 15:25
  • @claylong I had same thoughts, but quick check for asp.net sources and [web.api message lifecycle](https://www.asp.net/media/4071077/aspnet-web-api-poster.pdf) poster convinced me that dispatcher goes after all handlers – Sergey Berezovskiy Oct 31 '17 at 15:54

1 Answers1

2

The reason is this code in HttpMessageHandlerAdapter.InvokeCore (so basically in UseWebApi middleware):

response = await _messageInvoker.SendAsync(request, cancellationToken);
// ...
if (IsSoftNotFound(request, response)) {
       callNext = true;
}
else { 
    // ...
}

Where IsSoftNotFound is:

private static bool IsSoftNotFound(HttpRequestMessage request, HttpResponseMessage response)
{
    if (response.StatusCode == HttpStatusCode.NotFound)
    {
        bool routingFailure;
        if (request.Properties.TryGetValue<bool>(HttpPropertyKeys.NoRouteMatched, out routingFailure)
            && routingFailure)
        {
            return true;
        }
    }
    return false;
}

So basically in case of "soft" 404, where "soft" means no route matched (and is indicated by property with specific key in request.Properties) - middleware will call some next component. Otherwise - will just send response.

This IsSoftDelete is true for your case (because indeed no route is matched) and that next component (didn't have time to figure out what that is really) clears your response content.

To "fix" this issue - remove property with that key from request after request was handled by previous handler:

public class ErrorMessageHandler : DelegatingHandler {
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken) {
        var response = await base.SendAsync(request, cancellationToken);

        if (response.IsSuccessStatusCode)
            return response;

        // here, can also check if 404
        request.Properties.Remove(HttpPropertyKeys.NoRouteMatched);
        var formatter = new JsonMediaTypeFormatter();
        var errorResponse = request.CreateResponse(response.StatusCode, "Oops!", formatter);
        return errorResponse;
    }
}
Evk
  • 98,527
  • 8
  • 141
  • 191
  • Oh my god! I checked everything including HttpServer, but the problem indeed was on owin side. I'll investigate more why they implemented this strange 'soft not found' behavior. Thanks a lot! – Sergey Berezovskiy Oct 31 '17 at 23:13
  • @SergeyBerezovskiy if you find out what's that soft not found is about - please comment here or add new answer maybe, since I'd be interested to know too. – Evk Nov 01 '17 at 11:15