首页 > 技术文章 > ServiceStack 错误处理

joyswings 2019-03-12 12:18 原文

抛出C#异常

在大多数情况下,您不需要关心ServiceStack的错误处理,因为它为抛出C#异常的正常用例提供本机支持,例如:

public object Post(User request) 
{
    if (string.IsNullOrEmpty(request.Name))
        throw new ArgumentNullException("Name");
}

HTTP错误C#异常的默认映射

默认C#例外:

  • ArgumentException使用HTTP StatusCode为400 BadRequest返回继承
  • NotImplementedException或者NotSupportedException 作为405 MethodNotAllowed返回
  • AuthenticationException401 Unauthorized身份返回
  • UnauthorizedAccessException403 Forbidden返回
  • OptimisticConcurrencyException返回409冲突
  • 其他正常的C#异常作为500 InternalServerError返回

可以使用用户定义的映射扩展此列表Config.MapExceptionToStatusCode

WebServiceException

所有异常都被注入到ResponseStatus响应DTO 属性中,属性被序列化到ServiceClient的首选内容类型中,使得错误处理变得透明,无论您的首选格式如何 - 即,相同的C#错误处理代码可用于所有ServiceClient。

try 
{
    var client = new JsonServiceClient(BaseUri);
    var response = client.Send<UserResponse>(new User());
} 
catch (WebServiceException webEx) 
{
    /*
      webEx.StatusCode        = 400
      webEx.StatusDescription = ArgumentNullException
      webEx.ErrorCode         = ArgumentNullException
      webEx.ErrorMessage      = Value cannot be null. Parameter name: Name
      webEx.StackTrace        = (your Server Exception StackTrace - in DebugMode)
      webEx.ResponseDto       = (your populated Response DTO)
      webEx.ResponseStatus    = (your populated Response Status DTO)
      webEx.GetFieldErrors()  = (individual errors for each field if any)
    */
}

 

其中StatusCodeStatusDescription是HTTP StatusCode和Description,显示所有HTTP客户端看到的顶级HTTP层详细信息。StatusDescription通常很短,用于指示返回的错误类型,默认情况下是抛出的异常类型。HTTP客户端通常会检查StatusCode以确定如何在客户端上处理错误。

所有服务客户端还可以访问错误响应DTO主体中返回的应用程序级错误详细信息,其中ErrorCode包含异常类型,客户端将检查以确定和处理异常类型,同时ErrorMessage保存服务器异常消息它提供了一个人性化的,更长和描述性的错误描述,可以显示给最终用户。DebugMode中StackTrace使用Server StackTrace填充,以帮助前端开发人员识别错误的原因和位置。

如果错误引用特定字段(如字段验证异常),则GetFieldErrors()保留每个具有错误的字段的错误信息。

可以通过以下各种选项更改这些默认值以提供进一步的自定义错误响应:

启用S​​tackTraces

默认情况下,在响应DTO中显示StackTraces仅在调试版本中启用,尽管此行为可以通过以下方式覆盖:

SetConfig(new HostConfig { DebugMode = true });

错误响应类型

抛出异常时返回的错误响应取决于是否存在常规命名的{RequestDto}ResponseDTO。

如果存在:

{RequestDto}Response返回,而不管服务方法的响应类型的。如果{RequestDto}ResponseDTO具有ResponseStatus属性,则会填充它,否则将不返回ResponseStatus(如果{ResponseDto}Response使用[DataContract]/[DataMember]属性修饰了类和属性,则还需要对ResponseStatus进行修饰以填充)。

否则:

通过ErrorResponse填充的ResponseStatus属性返回泛型

服务客户端透明地处理不同的错误响应类型,并为无模式格式,如JSON / JSV /等有返回之间没有实际明显的区别ResponseStatus自定义或通用的ErrorResponse-因为它们都输出电线上的同样的反应。

自定义例外

最终,所有ServiceStack WebServiceExceptions都只是Response DTO,其中包含一个填充的ResponseStatus,它返回HTTP错误状态。有多种不同的方法可以自定义异常的返回方式,包括:

自定义C#异常到HTTP错误状态的映射

您可以通过以下方式配置为不同的异常类型更改返回的HTTP错误状态:

 

SetConfig(new HostConfig { 
    MapExceptionToStatusCode = {
        { typeof(CustomInvalidRoleException), 403 },
        { typeof(CustomerNotFoundException), 404 },
    }
});

 

返回HttpError

如果你想要对你的HTTP错误进行更细粒度的控制,你可以抛出返回一个HttpError,让你自定义Http HeadersStatus Code和HTTP Response Body以便在线上获得你想要的内容:

public object Get(User request) 
{
    throw HttpError.NotFound("User {0} does not exist".Fmt(request.Name));
}

 

以上内容在线路上返回404 NotFound StatusCode,是以下方面的简写:

new HttpError(HttpStatusCode.NotFound, 
    "User {0} does not exist".Fmt(request.Name)); 

 

具有自定义响应DTO的HttpError

HttpError还可用于返回更结构化的错误响应:

var responseDto = new ErrorResponse { 
    ResponseStatus = new ResponseStatus {
        ErrorCode = typeof(ArgumentException).Name,
        Message = "Invalid Request",
        Errors = new List<ResponseError> {
            new ResponseError {
                ErrorCode = "NotEmpty",
                FieldName = "Company",
                Message = "'Company' should not be empty."
            }
        }
    }
};

throw new HttpError(HttpStatusCode.BadRequest, "ArgumentException") {
    Response = responseDto,
}; 

 

实现IResponseStatusConvertible

您还可以通过实现IResponseStatusConvertible接口来覆盖自定义异常的序列化,以返回您自己填充的ResponseStatus。这是ValidationException允许通过让ValidationException实现IResponseStatusConvertible接口来自定义Response DTO 方法

例如,这是一个自定义的Exception示例,它返回填充的字段错误:

public class CustomFieldException : Exception, IResponseStatusConvertible
{
  ...
    public string FieldErrorCode { get; set; }
    public string FieldName { get; set; }
    public string FieldMessage { get; set; }

    public ResponseStatus ToResponseStatus()
    {
        return new ResponseStatus {
            ErrorCode = GetType().Name,
            Message = Message,
            Errors = new List<ResponseError> {
                new ResponseError {
                    ErrorCode = FieldErrorCode,
                    FieldName = FieldName,
                    Message = FieldMessage
                }
            }
        }
    }    
}

 

实现IHasStatusCode

除了使用IResponseStatusConvertible自定义C#Exceptions的HTTP Response Body之外,您还可以通过实现IHasStatusCode以下内容来自定义HTTP状态代码

public class Custom401Exception : Exception, IHasStatusCode
{
    public int StatusCode 
    { 
        get { return 401; } 
    }
}

 

同样IHasStatusDescription可以用于自定义StatusDescriptionIHasErrorCode自定义ErrorCode返回的,而不是其异常类型。

覆盖AppHost中的OnExceptionTypeFilter

您还可以ResponseStatus通过覆盖OnExceptionTypeFilterAppHost 来捕获和修改返回的返回值,例如ServiceStack使用它来自定义返回的ResponseStatus以自动为ArgumentExceptions指定的字段添加自定义字段错误ParamName,例如:

public virtual void OnExceptionTypeFilter(
    Exception ex, ResponseStatus responseStatus)
{
    var argEx = ex as ArgumentException;
    var isValidationSummaryEx = argEx is ValidationException;
    if (argEx != null && !isValidationSummaryEx && argEx.ParamName != null)
    {
        var paramMsgIndex = argEx.Message.LastIndexOf("Parameter name:");
        var errorMsg = paramMsgIndex > 0
            ? argEx.Message.Substring(0, paramMsgIndex)
            : argEx.Message;

        responseStatus.Errors.Add(new ResponseError
        {
            ErrorCode = ex.GetType().Name,
            FieldName = argEx.ParamName,
            Message = errorMsg,
        });
    }
}

 

自定义HTTP错误

在任何请求或响应过滤器中,您可以通过发出自定义HTTP响应并结束请求来短路请求管道,例如:

this.PreRequestFilters.Add((req,res) => 
{
    if (req.PathInfo.StartsWith("/admin") && 
        !req.GetSession().HasRole("Admin")) 
    {
        res.StatusCode = (int)HttpStatusCode.Forbidden;
        res.StatusDescription = "Requires Admin Role";
        res.EndRequest();
    }
});

 

在自定义HttpHandler中结束请求使用 res.EndHttpHandlerRequest()

后备错误页面

使用IAppHost.GlobalHtmlErrorHttpHandler用于指定后备HttpHandler的所有错误状态代码,例如:

public override void Configure(Container container)
{
    this.GlobalHtmlErrorHttpHandler = new RazorHandler("/oops"),
}

 

要获得更细粒度的控制,请使用IAppHost.CustomErrorHttpHandlers指定自定义HttpHandler以与特定错误状态代码一起使用,例如:

public override void Configure(Container container)
{
    this.CustomErrorHttpHandlers[HttpStatusCode.NotFound] = 
        new RazorHandler("/notfound");
    this.CustomErrorHttpHandlers[HttpStatusCode.Unauthorized] = 
        new RazorHandler("/login");
}

 

注册处理服务异常的处理程序

ServiceStack及其API设计提供了一种拦截异常的灵活方法。如果你需要的所有服务异常的单一入口点,您可以将处理程序添加到AppHost.ServiceExceptionHandlerConfigure要处理服务之外发生的异常,您可以设置全局AppHost.UncaughtExceptionHandlers处理程序,例如:

public override void Configure(Container container)
{
    //Handle Exceptions occurring in Services:

    this.ServiceExceptionHandlers.Add((httpReq, request, exception) => {
        //log your exceptions here
        ...
        return null; //continue with default Error Handling

        //or return your own custom response
        //return DtoUtils.CreateErrorResponse(request, exception);
    });

    //Handle Unhandled Exceptions occurring outside of Services
    //E.g. Exceptions during Request binding or in filters:
    this.UncaughtExceptionHandlers.Add((req, res, operationName, ex) => {
         res.Write($"Error: {ex.GetType().Name}: {ex.Message}");
         res.EndRequest(skipHeaders: true);
    });
}

 

异步异常处理程序

如果您的处理程序需要进行任何异步调用,则可以使用Async版本:

this.ServiceExceptionHandlersAsync.Add(async (httpReq, request, ex) =>
{
    await LogServiceExceptionAsync(httpReq, request, ex);

    if (ex is UnhandledException)
        throw ex;

    if (request is IQueryDb)
        return DtoUtils.CreateErrorResponse(request, new ArgumentException("AutoQuery request failed"));

    return null;
});

this.UncaughtExceptionHandlersAsync.Add(async (req, res, operationName, ex) =>
{
    await res.WriteAsync($"UncaughtException '{ex.GetType().Name}' at '{req.PathInfo}'");
    res.EndRequest(skipHeaders: true);
});

 

使用自定义ServiceRunner进行错误处理

如果您想为不同的操作和服务提供不同的错误处理程序,您可以告诉ServiceStack在您自己的自定义IServiceRunner中运行您的服务,在AppHost中实现HandleExcepion事件挂钩:

public override IServiceRunner<TRequest> CreateServiceRunner<TRequest>(
    ActionContext actionContext)
{           
    return new MyServiceRunner<TRequest>(this, actionContext); 
}

 

其中MyServiceRunner就是实现你感兴趣的,如自定义挂钩的自定义类:

public class MyServiceRunner<T> : ServiceRunner<T> 
{
    public MyServiceRunner(IAppHost appHost, ActionContext actionContext) 
        : base(appHost, actionContext) {}

    public override object HandleException(IRequest request, 
        T request, Exception ex) {
      // Called whenever an exception is thrown in your Services Action
    }
}

 

 

推荐阅读