Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
541 views
in Technique[技术] by (71.8m points)

iis - How to override all standard error pages in WebAPI

My beautiful REST webservice works great. Except if I visit pages like ~/, which returns the default IIS 403 Forbidden page (even using Fiddler and specifying only Accept: application/json). I want nothing but JSON or XML errors. Is there a way to override ALL exceptions with a custom exception handler? or a default controller to handle all unknown requests? What's the simplest, and the most correct (if different), way to handle this so that clients need only parse REST API-friendly XML datagrams or JSON blobs?

Example Request:

GET http://localhost:7414/ HTTP/1.1
User-Agent: Fiddler
Host: localhost:7414
Accept: application/json, text/json, text/xml

Response: (that I don't like, notice that text/html wasn't one of the accepted response types)

HTTP/1.1 403 Forbidden
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/8.0
X-SourceFiles: =?UTF-8?B?QzpcaWNhcm9sXENoYXJpdHlMb2dpYy5pQ2Fyb2wuQXBp?=
X-Powered-By: ASP.NET
Date: Fri, 25 Jan 2013 21:06:21 GMT
Content-Length: 5396

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head> 
<title>IIS 8.0 Detailed Error - 403.14 - Forbidden</title> 
<style type="text/css"> 
<!-- 
...

Response (that I would prefer):

HTTP/1.1 403 Forbidden
Cache-Control: private
Content-Type: application/json; charset=utf-8
Date: ...
Content-Length: ....

{
  "error":"forbidden",
  "status":403,
  "error_description":"Directory listing not allowed."
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Edit 1/26/14: Microsoft just added "Global Error Handling" to the latest WebAPI 2.1 update.


Ok, I think I've got it. There's a few parts to it.

First: Create a controller for your errors. I named my actions according to the HTTP error codes.

public class ErrorController : ApiController {
    [AllowAnonymous]
    [ActionName("Get")]
    public HttpResponseMessage Get() {
        return Request.CreateErrorInfoResponse(HttpStatusCode.InternalServerError, title: "Unknown Error");
    }

    [AllowAnonymous]
    [ActionName("404")]
    [HttpGet]
    public HttpResponseMessage Status404() {
        return Request.CreateErrorInfoResponse(HttpStatusCode.NotFound, description: "No resource matches the URL specified.");
    }

    [AllowAnonymous]
    [ActionName("400")]
    [HttpGet]
    public HttpResponseMessage Status400() {
        return Request.CreateErrorInfoResponse(HttpStatusCode.BadRequest);
    }

    [AllowAnonymous]
    [ActionName("500")]
    [HttpGet]
    public HttpResponseMessage Status500() {
        return Request.CreateErrorInfoResponse(HttpStatusCode.InternalServerError);
    }
}

Next, I created a GenericExceptionFilterAttribute that checks to see if the HttpActionExecutedContext.Exception is populated and if the response is still empty. If both cases are true, then it generates a response.

public class GenericExceptionFilterAttribute : ExceptionFilterAttribute {
    public GenericExceptionFilterAttribute()
        : base() {
        DefaultHandler = (context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.InternalServerError, "Internal Server Error", "An unepected error occoured on the server.", exception: ex);
    }

    readonly Dictionary<Type, Func<HttpActionExecutedContext, Exception, HttpResponseMessage>> exceptionHandlers = new Dictionary<Type, Func<HttpActionExecutedContext, Exception, HttpResponseMessage>>();

    public Func<HttpActionExecutedContext, Exception, HttpResponseMessage> DefaultHandler { get; set; }

    public void AddExceptionHandler<T>(Func<HttpActionExecutedContext, Exception, HttpResponseMessage> handler) where T : Exception {
        exceptionHandlers.Add(typeof(T), handler);
    }

    public override void OnException(HttpActionExecutedContext context) {
        if (context.Exception == null) return;

        try {
            var exType = context.Exception.GetType();
            if (exceptionHandlers.ContainsKey(exType))
                context.Response = exceptionHandlers[exType](context, context.Exception);

            if(context.Response == null && DefaultHandler != null)
                context.Response = DefaultHandler(context, context.Exception);
        }
        catch (Exception ex) {
            context.Response = context.Request.CreateErrorInfoResponse(HttpStatusCode.InternalServerError, description: "Error while building the exception response.", exception: ex);
        }
    }
}

In my case, I went with a single generic handler that I could register support for each of the main exception types and map each of those exception types to specific HTTP response codes. Now register your exception types and handlers this filter globally in your global.asax.cs:

// These filters override the default ASP.NET exception handling to create REST-Friendly error responses.
var exceptionFormatter = new GenericExceptionFilterAttribute();
exceptionFormatter.AddExceptionHandler<NotImplementedException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.InternalServerError, "Not Implemented", "This method has not yet been implemented. Please try your request again at a later date.", exception: ex));
exceptionFormatter.AddExceptionHandler<ArgumentException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, exception: ex));
exceptionFormatter.AddExceptionHandler<ArgumentNullException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, exception: ex));
exceptionFormatter.AddExceptionHandler<ArgumentOutOfRangeException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, exception: ex));
exceptionFormatter.AddExceptionHandler<FormatException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, exception: ex));
exceptionFormatter.AddExceptionHandler<NotSupportedException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, "Not Supported", exception: ex));
exceptionFormatter.AddExceptionHandler<InvalidOperationException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, "Invalid Operation", exception: ex));
GlobalConfiguration.Filters.Add(exceptionFormatter)

Next, create a catchall route to send all unknown requests to your new Error handler:

config.Routes.MapHttpRoute(
    name: "DefaultCatchall",
    routeTemplate: "{*url}",
    defaults: new {
        controller = "Error",
        action = "404"
    }
);

And, to wrap it all up, let IIS process all requests through ASP.NET by adding this to your web.config:

<configuration>
    <system.webServer>
        <modules runAllManagedModulesForAllRequests="true" />
    </system.webServer>
</configuration>

Optionally, you could also use the customErrors section of the web.config to redirect all errors to your new error handler.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...