ASP.NET Core中等价于ASP.NET MVC 5的HttpException

83
在ASP.NET MVC 5中,您可以通过抛出HTTP异常代码的方式来设置响应,如下所示:

HttpException

throw new HttpException((int)HttpStatusCode.BadRequest, "Bad Request.");

HttpException在ASP.NET Core中不存在。它的等效代码是什么?

7个回答

56

我实现了自己的HttpException和支持中间件,用于捕获所有的HttpException并将其转换为相应的错误响应。以下是一段简短的摘录。您也可以使用Boxed.AspNetCore Nuget包。

在Startup.cs中的使用示例

public void Configure(IApplicationBuilder application)
{
    application.UseIISPlatformHandler();

    application.UseStatusCodePagesWithReExecute("/error/{0}");
    application.UseHttpException();

    application.UseMvc();
}

扩展方法

public static class ApplicationBuilderExtensions
{
    public static IApplicationBuilder UseHttpException(this IApplicationBuilder application)
    {
        return application.UseMiddleware<HttpExceptionMiddleware>();
    }
}

中间件

internal class HttpExceptionMiddleware
{
    private readonly RequestDelegate next;

    public HttpExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await this.next.Invoke(context);
        }
        catch (HttpException httpException)
        {
            context.Response.StatusCode = httpException.StatusCode;
            var responseFeature = context.Features.Get<IHttpResponseFeature>();
            responseFeature.ReasonPhrase = httpException.Message;
        }
    }
}

HttpException

public class HttpException : Exception
{
    private readonly int httpStatusCode;

    public HttpException(int httpStatusCode)
    {
        this.httpStatusCode = httpStatusCode;
    }

    public HttpException(HttpStatusCode httpStatusCode)
    {
        this.httpStatusCode = (int)httpStatusCode;
    }

    public HttpException(int httpStatusCode, string message) : base(message)
    {
        this.httpStatusCode = httpStatusCode;
    }

    public HttpException(HttpStatusCode httpStatusCode, string message) : base(message)
    {
        this.httpStatusCode = (int)httpStatusCode;
    }

    public HttpException(int httpStatusCode, string message, Exception inner) : base(message, inner)
    {
        this.httpStatusCode = httpStatusCode;
    }

    public HttpException(HttpStatusCode httpStatusCode, string message, Exception inner) : base(message, inner)
    {
        this.httpStatusCode = (int)httpStatusCode;
    }

    public int StatusCode { get { return this.httpStatusCode; } }
}

从长远来看,我建议不要使用异常来返回错误。异常比仅从方法返回错误慢。


很遗憾,消息没有传递到客户端。正文返回为空 :( (内容长度=0) - StackOverflower
@StackOverflower 已更新 - Muhammad Rehan Saeed
另外对我有用的是使用Response.WriteAsync来设置响应体。谢谢。 - StackOverflower
如上所述,我已将catch块的主体替换为:`context.Response.StatusCode = httpException.StatusCode; await context.Response.WriteAsync(httpException.Message);` - ToDevAndBeyond
1
将所有异常包装并转换为错误响应可能不是一个好主意。 异常不会作为异常冒泡到Azure Application Insights中,而Application Insights将无法为错误提供详细的堆栈跟踪。 - Rosdi Kasim
显示剩余2条评论

27

在与@davidfowl的简短交谈后,似乎ASP.NET 5没有HttpExceptionHttpResponseException这样的概念,可以“神奇地”转换为响应消息。

您可以通过MiddleWare 连接到ASP.NET 5管道,并创建一个处理异常的中间件。

以下是他们错误处理程序中间件的源代码示例,它将在管道中出现异常时将响应状态代码设置为500:

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ErrorHandlerOptions _options;
    private readonly ILogger _logger;

    public ErrorHandlerMiddleware(RequestDelegate next, 
                                  ILoggerFactory loggerFactory,
                                  ErrorHandlerOptions options)
    {
        _next = next;
        _options = options;
        _logger = loggerFactory.CreateLogger<ErrorHandlerMiddleware>();
        if (_options.ErrorHandler == null)
        {
            _options.ErrorHandler = _next;
        }
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError("An unhandled exception has occurred: " + ex.Message, ex);

            if (context.Response.HasStarted)
            {
                _logger.LogWarning("The response has already started, 
                                    the error handler will not be executed.");
                throw;
            }

            PathString originalPath = context.Request.Path;
            if (_options.ErrorHandlingPath.HasValue)
            {
                context.Request.Path = _options.ErrorHandlingPath;
            }
            try
            {
                var errorHandlerFeature = new ErrorHandlerFeature()
                {
                    Error = ex,
                };
                context.SetFeature<IErrorHandlerFeature>(errorHandlerFeature);
                context.Response.StatusCode = 500;
                context.Response.Headers.Clear();

                await _options.ErrorHandler(context);
                return;
            }
            catch (Exception ex2)
            {
                _logger.LogError("An exception was thrown attempting
                                  to execute the error handler.", ex2);
            }
            finally
            {
                context.Request.Path = originalPath;
            }

            throw; // Re-throw the original if we couldn't handle it
        }
    }
}

你需要在StartUp.cs中注册它:

public class Startup
{
    public void Configure(IApplicationBuilder app, 
                          IHostingEnvironment env, 
                          ILoggerFactory loggerfactory)
    {
       app.UseMiddleWare<ExceptionHandlerMiddleware>();
    }
}

1
@RehanSaeed 我同意,那是非常有用的功能,我也经常使用它。ASP.NET 5的好处是开源的,如果我们想要,我们可以自己应用这个功能。 - Yuval Itzchakov
1
我将把你的代码添加到ASP.NET MVC样板,并默认启用它。感谢你的帮助。 - Muhammad Rehan Saeed
1
@davidfowl 哪个扩展方法? - Yuval Itzchakov
1
@YuvalItzchakov 我使用空的ASP.NET 5 Web API,并意识到漂亮的500错误页面可能来自于这个诊断中间件。如何替换此中间件或在生产环境中关闭它?框架是否在调用Startup.Configure之前预先注册它? - CallMeLaNN
1
如何向响应添加自定义正文?例如结构化的JSON错误响应模型? - kspearrin
显示剩余6条评论

13

或者,如果您只想返回任意状态代码,并且不关心基于异常的方法,可以使用

return new HttpStatusCodeResult(400);

更新:截至.NET Core RC 2,已删除Http前缀。现在是:

return new StatusCodeResult(400);

9
在某些情况下,直接在一个操作中执行肯定是最好的方法。但有时候,该操作会调用一个私有方法来完成特定的工作(返回ActionResult以外的其他东西),有时候你希望该方法可以抛出异常以产生响应(主要是像400错误请求、403禁止等错误)。这种情况下,异常是一个好的选择。 - gentiane
我曾经在一个地方工作,那里明确禁止Controller中使用new关键字(可能是出于依赖注入的可测试性原因,尽管我们都知道这样做是可行的)。那么你会怎么做呢? - PRMan
如果你想要反传统,不使用 new 关键字,我猜 (StatusCodeResult) Activator.CreateInstance(typeof(StatusCodeResult), 400); 可以满足需求。或者你可以创建一个 StatusCodeFactory 类来生成新的状态码。 - Jeremy

6
Microsoft.AspNet.Mvc.Controller是一个基类,提供了一个重载的HttpBadRequest(string)方法,该方法接收一个错误消息并返回给客户端。因此,在控制器操作中可以调用以下代码:
return HttpBadRequest("Bad Request.");

我的看法是,在控制器操作中调用的任何私有方法都应该完全具备 http 上下文感知能力并返回一个 IActionResult,或者执行一些与其处于 http 管道内部无关的小任务。尽管这是我的个人意见,但是执行某些业务逻辑的类不应该返回 HTTP 状态码,而是应该抛出自己的异常,这些异常可以在控制器/操作级别进行捕获和转换。


1
同意,但有些人可能有使用 HttpException 的旧 MVC 5 应用程序。如果可以选择性地提供相同的概念,则移植将变得更加容易。 - Muhammad Rehan Saeed
1
当然,我认为实际上有一个可用的 shim。我来到这个问题是在寻找“ASP.NET 5的做法”,但是不得不在其他地方找到它 - 你的问题问“等价代码是什么”,这在我看来与“新的做法是什么”相一致 - 这就是我想要的。所以我想为下一个人添加这个答案。 - lc.
我正在进行核心开发,并在我的中间件中捕获404错误以重定向到404页面,但遇到了一个问题,我想要抛出这个错误而不是500。这个答案让我找到了正确的方向,当发生服务器错误时,我使用return NotFound();作为操作结果。 - Vasily Hall

6
ASP.NET Core本身没有相应的功能。正如其他人所说,实现此功能的方法是使用中间件和自己的异常处理方式。 Opw.HttpExceptions.AspNetCore NuGet包正是这样做的。
引用:
用于在HTTP上返回异常的中间件和扩展程序,例如作为ASP.NET Core问题详细信息。问题详细信息是基于https://www.rfc-editor.org/rfc/rfc7807的一种机器可读格式,用于指定HTTP API响应中的错误。但您不仅限于将异常结果返回为问题详细信息,而且可以创建自己的映射器以获取自己的自定义格式。
它是可配置的并且有很好的文档。
以下是提供的开箱即用的异常列表:

4xx

  • 400 BadRequestException(错误请求异常)
  • 400 InvalidModelException(无效模型异常)
  • 400 ValidationErrorException<T>(验证错误异常<T>)
  • 400 InvalidFileException(无效文件异常)
  • 401 UnauthorizedException(未授权异常)
  • 403 ForbiddenException(禁止访问异常)
  • 404 NotFoundException(未找到异常)
  • 404 NotFoundException<T>(未找到异常<T>)
  • 409 ConflictException(冲突异常)
  • 409 ProtectedException(受保护异常)
  • 415 UnsupportedMediaTypeException(不支持的媒体类型异常)

5xx

  • 500 InternalServerErrorException(内部服务器错误异常)
  • 500 DbErrorException(数据库错误异常)
  • 500 SerializationErrorException(序列化错误异常)
  • 503 ServiceUnavailableException(服务不可用异常)

3
这是 @muhammad-rehan-saeed 回答的扩展版本。它会有条件地记录异常并禁用 http 缓存。
如果你使用此方法和 UseDeveloperExceptionPage,则应该在此之前调用 UseDeveloperExceptionPage。
Startup.cs:
app.UseMiddleware<HttpExceptionMiddleware>();

HttpExceptionMiddleware.cs

/**
 * Error handling: throw HTTPException(s) in business logic, generate correct response with correct httpStatusCode + short error messages.
 * If the exception is a server error (status 5XX), this exception is logged.
 */
internal class HttpExceptionMiddleware
{
    private readonly RequestDelegate next;

    public HttpExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await this.next.Invoke(context);
        }
        catch (HttpException e)
        {
            var response = context.Response;
            if (response.HasStarted)
            {
                throw;
            }

            int statusCode = (int) e.StatusCode;
            if (statusCode >= 500 && statusCode <= 599)
            {
                logger.LogError(e, "Server exception");
            }
            response.Clear();
            response.StatusCode = statusCode;
            response.ContentType = "application/json; charset=utf-8";
            response.Headers[HeaderNames.CacheControl] = "no-cache";
            response.Headers[HeaderNames.Pragma] = "no-cache";
            response.Headers[HeaderNames.Expires] = "-1";
            response.Headers.Remove(HeaderNames.ETag);

            var bodyObj = new {
                Message = e.BaseMessage,
                Status = e.StatusCode.ToString()
            };
            var body = JsonSerializer.Serialize(bodyObj);
            await context.Response.WriteAsync(body);
        }
    }
}

HTTPException.cs

public class HttpException : Exception
{
    public HttpStatusCode StatusCode { get; }

    public HttpException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpException(int httpStatusCode)
        : this((HttpStatusCode) httpStatusCode)
    {
    }

    public HttpException(HttpStatusCode statusCode, string message)
        : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpException(int httpStatusCode, string message)
        : this((HttpStatusCode) httpStatusCode, message)
    {
    }

    public HttpException(HttpStatusCode statusCode, string message, Exception inner)
        : base(message, inner)
    {
    }

    public HttpException(int httpStatusCode, string message, Exception inner)
        : this((HttpStatusCode) httpStatusCode, message, inner)
    {
    }
}

相比于以下两种方式,我使用这段代码取得了更好的结果:

  • UseExceptionHandler:
    • 自动记录“正常”异常(例如404)。
    • 在开发模式下被禁用(当调用app.UseDeveloperExceptionPage时)。
    • 不能仅捕获特定的异常。
  • Opw.HttpExceptions.AspNetCore:即使一切正常也记录异常

另请参阅ASP.NET Core Web API异常处理


2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接