Runtime: HttpRequestException 中的状态码

创建于 2017-09-26  ·  35评论  ·  资料来源: dotnet/runtime

当 $ HttpResponseMessage中的EnsureSuccessStatusCode方法抛出HttpRequestException异常时,无法确定状态码。

将名为StatusCode的新属性添加到HttpRequestException类,该类由HttpResponseMessage类的EnsureSuccessStatusCode方法设置。

基本原理和用法

允许您确定触发异常的状态代码。

try
{
    DoSomeHttp();
}
catch (HttpRequestException e)
{
    Log($"Request failed with status code: {e.StatusCode}.");
}

private void DoSomeHttp()
{
    // ...
    message.EnsureSuccessStatusCode();
}

提议的 API

public class HttpRequestException : Exception
{
    // ...
    public HttpRequestException(string message, StatusCode statusCode);
    public HttpRequestException(string message, Exception inner, StatusCode statusCode);
    // ...
    public StatusCode StatusCode { get; }
    // ...
}

开放式问题

属性应该有一个setter吗?
异常是否应该有一个设置属性的构造函数?
属性应该可以为空吗?

api-suggestion area-System.Net.Http

最有用的评论

我来到这里只是为了评论这似乎是 API 中的一个基本漏洞。 这意味着如果我们想抛出一个表明 http 状态失败的异常,我们必须烘焙自己的异常类型。 在我看来,这样一个基本的事情——在异常情况下抛出异常不应该包括异常的原因,这似乎很疯狂。 如果你在网上寻找这个,你会看到到处都是建议_PARSING THE ERROR MESSAGE_ 来推断状态码的人! 这是一个可怕的想法。 但是在没有像样的 API 的情况下,这就是我们所剩下的。

所有35条评论

HttpRequestException 仅在无法从服务器获取 HTTP 响应时引发。 即服务器已关闭或其他错误。 因此,根本没有收到 HTTP 状态代码,因为在线上没有发送过 HTTP 响应。

这与旧的 HttpWebRequest API 不同,旧的 HttpWebRequest API 会为不成功的状态码返回(即不是 2xx)抛出 WebException。 如果异常是由于非 2xx 状态代码引起的,该 API 在 WebException 中有一个 WebResponse。 对于其他错误,WebException 中没有 WebResponse。

因此,我不确定您将StatusCode添加到 HttpRequestException 的请求的目标。 你能澄清一下吗?

抄送: @stephentoub

IsSuccessStatusCodefalse时, @davidsh HttpRequestExceptionHttpResponseMessage类中的EnsureSuccessStatusCode方法抛出。

https://github.com/dotnet/corefx/blob/6b5ef121ebea45b14f489a177e2e3f27fce86781/src/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs#L148 -L165

当 IsSuccessStatusCode 为 false 时,HttpResponseMessage 类中的 EnsureSuccessStatusCode 方法会抛出@davidsh HttpRequestException。

是的,这是真的。 但是,我不太明白将StatusCode属性添加到HttpRequestException的价值。 在大多数情况下,此属性将为空。 事实上,它必须是一个可为空的属性,因为 0 不能使用,因为它(理论上)可能是一个状态码。

在 HttpRequestException 对象上需要此属性的场景是什么?

在处理HttpClient时,我只从EnsureSuccessStatusCode遇到 $# HttpRequestException 0$#$ 所以我不知道在大多数情况下它会为空,如果是这样,那么也许他们应该抛出不同的异常。

在我的场景中,我调用了一个使用HttpClient的方法,当从该方法抛出异常时,我可以捕获它,如果我在异常中有可用的状态代码,那么我可以记录它,显示一些相关的消息在用户界面中或以有意义的方式处理它,例如提供身份验证选项。

@vanillajonathan你能用 api-proposal 更新原始问题吗?

@Priya91完成。 :)

该属性应该只是一个getter,否则接收到异常的代码可以更改状态码,但异常上的其他值将无效。

您应该添加一个接受异常状态代码的构造函数,并仅在属性上公开 getter ..

@Priya91好的,我编辑了帖子并将StatusCode属性声明为仅设置器,没有获取器。
我还添加了构造函数。 该类有3个构造函数。 不确定需要添加多少个,也许是 3 个?

@vanillajonathan我刚刚在我们的 System.Net.Http 代码库中搜索了 HttpRequestException,事实上在很多情况下,我们在没有像@davidsh注释这样的状态码的情况下使用此异常。 只有在您在代码中指出我们有一个状态码的情况下,它才会作为异常消息的一部分给出。 在这个异常上添加一个 StatusCode 是没有意义的,因为不是每个 httprequestexception 都有一个状态码,它不是类型行为的属性。

对于您的场景,您可以尝试捕获 httprequestexception,并解析字符串消息以获取状态码并从您的应用程序中抛出不同的自定义异常。

在大多数情况下,此属性将为空。 事实上,它必须是一个可为空的属性,因为 0 不能使用,因为它(理论上)可能是一个状态码。

它可以保持为-1

它可以保持为-1

当然。 但是在大多数情况下添加此属性似乎没有用,它将是-1。

提议的 API 在大多数情况下都不能很好地工作,并且为特定的狭窄情况添加新的可用性 API 似乎不值得付出代价。 关闭。

我来到这里只是为了评论这似乎是 API 中的一个基本漏洞。 这意味着如果我们想抛出一个表明 http 状态失败的异常,我们必须烘焙自己的异常类型。 在我看来,这样一个基本的事情——在异常情况下抛出异常不应该包括异常的原因,这似乎很疯狂。 如果你在网上寻找这个,你会看到到处都是建议_PARSING THE ERROR MESSAGE_ 来推断状态码的人! 这是一个可怕的想法。 但是在没有像样的 API 的情况下,这就是我们所剩下的。

虽然我同意当前的 API 存在缺陷,但让我提出一个替代方案。

这里真正的问题是 EnsureSuccessStatusCode 方法。 通过它我们告诉框架我们希望依赖异常作为控制流的机制。 哪个,我们应该避免,对吧?

从远程服务器获取 4xx 或 5xx 并不例外——这是一个有效的响应。 但是,无法访问远程服务器是一个例外。

还要考虑一下,将 StatusCode 添加到异常只是一个开始。 在 400 的情况下,我们需要了解完整的请求 - 这包括我们发送的不明确的内容,例如 Content-Type 标头。

这样我们就可以解析响应并避免大量样板代码。 在这里,我会考虑使用代理来发送请求并拦截响应。 如果我真的需要例外,我会扔在这里,我可以完全控制。

我的要求是弃用 EnsureSuccessStatusCode - 它充其量只是一个拐杖。

HttpRequestException 仅在无法从服务器获取 HTTP 响应时引发。 即服务器已关闭或其他错误。 因此,根本没有收到 HTTP 状态代码,因为在线上没有发送过 HTTP 响应。

一个简单的测试证明这不是真的。 我有一个返回 UnauthorizedResult() 的 Web 服务 IActionResult 端点。 如果我使用 HttpClient 访问此端点并调用 EnsureSuccessStatusCode,它会引发 HttpRequestException。

但是,我看不到将 StatusCode 属性添加到 HttpRequestException 的价值。 在大多数情况下,此属性将为空。

这对我来说似乎很可疑。 我看不出有什么原因导致 HttpClient 调用比服务器返回错误代码更可能由于连接问题而失败。 在多个服务器相互调用的架构中,对一个服务器的调用中的传输错误几乎肯定会导致多个正在进行的上游调用返回错误代码。

在这个异常上添加一个 StatusCode 是没有意义的,因为不是每个 httprequestexception 都有一个状态码,它不是类型行为的属性。

并非每个异常都有 InnerException、HelpLink 或 Data 字典值,但这并不意味着这些属性不是 Exception 类接口的有用元素。

从外到内看问题,HttpClient 的消费者希望能够处理请求失败的情况(即无法访问服务器或 StatusCode >= 400),而不必用“if”语句来混乱每个 HttpClient 调用位置和手动抛出自定义异常类型。

这似乎完全合理,而且很容易实现,而这里的反击让我觉得证据不足,而且有点防御性。

我刚刚在我们的 System.Net.Http 代码库中搜索了 HttpRequestException,事实上在很多情况下我们使用这个异常而没有像@davidsh注释这样的状态码。

组件的内部实现需求不应该决定组件呈现给用户的外部接口的性质。 它被称为从外到内的设计,是 OOP 的一个非常基本的方面。 事实上,在这个线程中,该代码库的所有者建议解析错误消息字符串并抛出自定义异常,这表明有人忽略了基础知识。

我希望这不会被视为过度贬损,我不是故意的。 只是一些坦率诚实的批评。

对于您的场景,您可以尝试捕获 httprequestexception,并解析字符串消息以获取状态码并从您的应用程序中抛出不同的自定义异常。

这听起来像是雷德蒙德员工的笑话。
如果用户输入验证以 400 失败,为什么我应该在 21 世纪 _parse the string message_? 我如何发现它确实是由于用户输入或不是内部 500 或冲突 409 而导致的错误? 我应该为此解析字符串消息吗? 逆天。

绝对同意,如果EnsureSuccessStatusCode抛出HttpRequestException ,那么至少StatusCode将是一个逻辑可选字段(该值甚至可以存储在 HResult 字段中!) . 然而,接收具有非 2xx 状态代码的有效 HTTP 响应是否实际上是HttpRequestException是值得怀疑的 - 毕竟,异常不是 HttpRequest 对象本身,它完成了它的工作 - 完整的消息有已经成功构建并传输到服务器,但我们得到的结果却不是我们所希望的。 事实上,如果后端服务器出现问题(顺便说一句,在这种情况下会引发什么异常?),它可能根本不是一个有效的 HTTP 响应。 无论哪种方式,如果它带有状态代码的有效 HTTP 响应,则HttpRequestException本身是不够的 - 我需要知道它是 4xx 错误还是 5xx 错误(因为前者意味着调用者做错了什么,后者意味着服务器做错了什么,或者我可以重试的暂时错误)。 但事实上,我需要的不仅仅是状态码——在许多情况下,我们几乎肯定也需要实际的响应正文(许多 API 返回,例如状态码 400,但随附的 JSON 正文给出了应用程序级别的错误代码/详细信息)。 所以我实际上更愿意EnsureSuccessStatusCode抛出一个引用完整HttpResponse的异常,就像旧的WebException一样)
我实际上会说在其当前的实现中EnsureSuccessStatusCode永远不应该在生产代码中使用。

也许您可以添加一个继承自HttpRequestStatusExceptionHttpRequestStatusException $ 并添加此 StatusCode 属性?

除了在服务器响应不可用的情况下也使用此异常之外, WebException对于完整的 HttpResponse 和状态代码更有用。

提议的 API(修订版)

公开“ResponseMessage”( HttpResponseMessage )属性而不是“StatusCode”( int )属性。
```cs
公共类 HttpRequestException :异常
{
// ...
公共 HttpRequestException(字符串消息,HttpResponseMessage 消息);
public HttpRequestException(字符串消息,异常内部,HttpResponseMessage 消息);
// ...
公共 HttpResponseMessage? 响应消息 { 获取; }
// ...
}

公开“ResponseMessage”(HttpResponseMessage)属性而不是“StatusCode”(int)属性。

感谢您对此进行更多思考。

但是,添加 HttpResponseMessage 并不是我们想要做的事情。 它将保持 TCP 连接打开,因为 HttpResponseMessage.Content 表示 HTTP 响应流。 这意味着我们必须改变处理 HttpRequestException 对象的模式并显式地处理内部的 HttpResponseMessage。 否则我们最终会保持连接保持打开状态

通常,如果对象具有 IDisposable 的嵌套对象,则意味着 HttpRequestException 也必须是 IDisposable。

这是一个公平的观点 - 所以可能需要一种方法来指定“抛出一个异常,其中处理程序需要提供完整的响应主体”,这将触发将剩余的响应读取到内存支持的流中,然后关闭连接。
如果在读取剩余响应时还有进一步的错误,那就有点棘手了! 实际上,对于 HttpClient,如果在读取完整响应之前出现网络异常,是否有办法检索到那时为止已读取的任何内容? 还是从套接字读取响应数据总是延迟到应用程序尝试从 HttpResponseMessage.Content 读取?

HttpException用于不止一件事(连接错误和协议错误;传输层和应用层的问题)导致适用于应用层的异常,并引入状态码当异常源于传输层上的问题时,将无关紧要。

这应该是两个不同的例外, SocketExceptionHttpException 。 HttpException 扩展了应用层信息,例如状态码,或者可能是整个HttpResponse

引用@davidsh

HttpRequestException 仅在无法从服务器获取 HTTP 响应时引发。 即服务器已关闭或其他错误。

这应该抛出 SocketException 而不是 HttpException。

这将允许如下代码:

try
{
    DoSomeHttp();
}
catch (SocketException e)
{
    Log("Failed to establish connection.");
}
catch (HttpRequestException e)
{
    Log($"Request failed with status code: {e.StatusCode}.");
}

如果添加StatusCode整数字段确实不可行(无论出于何种意识形态原因),那么添加它的“污染较少”的方法是使用已经内置的异常Data字典. 它或多或少是为这些场景设计的。

HttpResponseMessage.EnsureSuccessStatusCode()可以使用键“StatusCode”将整数状态代码添加到Exception.Data字典。 然后,您可以使用HttpRequestException上的扩展方法轻松地将其拉回。

这是对HttpResponseMessage.EnsureSuccessStatusCode()的一两行更改。

https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs

当前实施:

public HttpResponseMessage EnsureSuccessStatusCode()
{
    if (!IsSuccessStatusCode)
    {
        throw new HttpRequestException(SR.Format(
        System.Globalization.CultureInfo.InvariantCulture,
        SR.net_http_message_not_success_statuscode,
        (int)_statusCode,
        ReasonPhrase));
    }

        return this;
}

更新的实现:

public HttpResponseMessage EnsureSuccessStatusCode()
{
    if (!IsSuccessStatusCode)
    {
        var exception = new HttpRequestException(SR.Format(
        System.Globalization.CultureInfo.InvariantCulture,
        SR.net_http_message_not_success_statuscode,
        (int)_statusCode,
        ReasonPhrase));

        exception.Data["StatusCode"] = _statusCode;

        throw exception;
    }

    return this;
}

扩展方法可能如下所示:

public static HttpStatusCode? GetHttpStatusCode(this HttpRequestException httpRequestException) {
    if(httpRequestException.Data["StatusCode"] is HttpStatusCode statusCode) {
        return statusCode;
    }
    else {
        return null;
    }
}

您可能还想将“StatusCode”魔术字符串移动到某个地方的internal static readonly string StatusCodeKey变量中。

这样, HttpRequestException的 API 表面并没有改变, HttpResponseMessage.EnsureSuccessStatusCode()的行为变化不太可能破坏任何东西。 如果您不使用HttpResponseMessage.EnsureSuccessStatusCode() ,您可以简单地不导入扩展方法的命名空间。

编辑

这是在扩展方法中添加状态代码的当前解决方法:

public static class HttpResponseMessageExtensions
{
    public static HttpResponseMessage EnsureSuccessStatusCodeWithStatus(this HttpResponseMessage httpResponseMessage)
    {
        try
        {
            return httpResponseMessage.EnsureSuccessStatusCode();
        }
        catch (HttpRequestException ex)
        {
            ex.Data["StatusCode"] = httpResponseMessage.StatusCode;
            throw;
        }
    }
}

它肯定比当前版本好 - 有许多 Web 服务类型调用,其中唯一重要的是状态代码,无需从响应正文中提取任何扩展的错误信息。

您_可以_选择性地从响应中读取字符串结果,并将其读取到异常Data字典上的另一个键中。 但是,它变得有点混乱。 例如,如果在读取过程中发生错误会发生什么? 或者如果你想使用异步怎么办? 如果这是用例,最好只创建自己的扩展方法或手动进行数据处理。 状态码基本上是免费的。

我发现令人惊讶的是,在框架中没有提出任何解决方法的情况下,它被关闭并被驳回。 在某些情况下,拥有状态代码是绝对必要的,现在我们需要使用自定义代码来将该信息添加到其中。

对于您的场景,您可以尝试捕获 httprequestexception,并解析字符串消息以获取状态码并从您的应用程序中抛出不同的自定义异常。

你知道异常是本地化的吗? 您是否建议为每种语言使用单独的解析器?

我不得不同意之前的说法,即真正的问题似乎是 EnsureSuccessStatusCode 的用处。

当您考虑以其他方式处理此问题时,它会更有意义。 当发生 2xx 以外的任何事情时抛出异常? 该方法本身正在推广使用异常作为逻辑流,我们都知道不应该这样做。
再加上将返回的其他状态类型是非常好的响应这一事实。 消息已完成,您收到回复。 处理它!

如果由于从未收到响应而引发异常,则这是一种异常情况,需要处理。

在我看来,如果这种方法根本不存在,那么问题实际上会更少。

@dpuckett那么,什么是编写执行请求并将其JSON有效负载反序列化为对象的函数的合适方法,这似乎是一个非常常见的场景。 或者是做两件事(获取和反序列化),因此违反了单一责任原则? 如果请求不成功,也许只是返回null

就像您想从 HTTP 端点获取用户一样。

private async Task<User> FetchUserAsync(string username)
{
    var response = await _client.GetAsync($"/api/users/{username}");
    if (!response.IsSuccessStatusCode)
    {
        return null;
    }
    var user = JsonSerializer.Deserialize<User>(response.Content);

    return user;
}

因此,我以 HttpClient Wrapper 的粗略摘录为例。

public async Task PostAsync<T>(T item, string url)
{            
     var json = JsonConvert.SerializeObject(item);
     var content = new StringContent(json, Encoding.UTF8, "application/json");

     HttpResponseMessage response = await Client.PostAsync(new Uri(url, UriKind.Relative), content);
     if (response.IsSuccessStatusCode)
     {
         return;
     }

     var ex = new HttpRequestException(response.ReasonPhrase);
     if(response != null)
     {
         ex.Data.Add("StatusCode", response.StatusCode);
     } 
     throw ex;
}

因此,与其使用 EnsureSuccessStatusCode 自动抛出一个没有状态代码的相当无用的异常,我仍在检查是否成功,如果没有抛出我自己的异常并将 StatusCode 作为数据条目添加到它上面。

这样,在我的服务层/应用层中,我可以将方法调用包装在 try/catch 中,然后在请求失败时进行适当处理。

当然,如果 EnsureSuccessStatusCode 为我们执行此操作会很好,但事实仍然是,如果该方法运行时根本没有响应,这有点没用。 这样我们处理这两种情况。

我们实际上在 PR #32455 for .NET 5 中实现了这一点。

#911 的副本

此页面是否有帮助?
0 / 5 - 0 等级