当传输过程中请求流被关闭时如何获取HTTP响应

14

TL;DR版本:

当向请求流写入数据时发生传输错误,即使服务器已发送响应,我也无法访问响应。


完整版本:

我有一个.NET应用程序,使用HttpWebRequest将文件上传到Tomcat服务器。在某些情况下,服务器会提前关闭请求流(因为出于某种原因拒绝文件,例如无效的文件名),并发送一个400响应以及自定义头以指示错误原因。

问题在于,如果上传的文件很大,请求流在我完成请求正文的写入之前就被关闭了,并且我会收到一个IOException

Message: Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host.
InnerException: SocketException: An existing connection was forcibly closed by the remote host

尽管服务器已经发送了响应(通过WireShark检查),但我无法获取响应。因此我永远也得不到响应。虽然我可以捕获这个异常,但是当我调用GetResponse时,我会得到一个WebException,它的内部异常是之前的IOException,而且Response属性为空。因此,我不知道实际的问题是什么。从我的应用程序角度来看,它看起来像是连接被中断了,所以我将其视为与网络相关的错误并重试上传...当然,这又失败了。

如何解决这个问题并从服务器检索实际响应?这可能吗?对我来说,当前的行为看起来像是HttpWebRequest中的一个错误或至少是一个严重的设计问题...


以下是我用于复现问题的代码:

var request = HttpWebRequest.CreateHttp(uri);
request.Method = "POST";
string filename = "foo\u00A0bar.dat"; // Invalid characters in filename, the server will refuse it
request.Headers["Content-Disposition"] = string.Format("attachment; filename*=utf-8''{0}", Uri.EscapeDataString(filename));
request.AllowWriteStreamBuffering = false;
request.ContentType = "application/octet-stream";
request.ContentLength = 100 * 1024 * 1024;

// Upload the "file" (just random data in this case)
try
{
    using (var stream = request.GetRequestStream())
    {
        byte[] buffer = new byte[1024 * 1024];
        new Random().NextBytes(buffer);
        for (int i = 0; i < 100; i++)
        {
            stream.Write(buffer, 0, buffer.Length);
        }
    }
}
catch(Exception ex)
{
    // here I get an IOException; InnerException is a SocketException
    Console.WriteLine("Error writing to stream: {0}", ex);
}

// Now try to read the response
try
{
    using (var response = (HttpWebResponse)request.GetResponse())
    {
        Console.WriteLine("{0} - {1}", (int)response.StatusCode, response.StatusDescription);
    }
}
catch(Exception ex)
{
    // here I get a WebException; InnerException is the IOException from the previous catch
    Console.WriteLine("Error getting the response: {0}", ex);
    var webEx = ex as WebException;
    if (webEx != null)
    {
        Console.WriteLine(webEx.Status); // SendFailure
        var response = (HttpWebResponse)webEx.Response;
        if (response != null)
        {
            Console.WriteLine("{0} - {1}", (int)response.StatusCode, response.StatusDescription);
        }
        else
        {
            Console.WriteLine("No response");
        }
    }
}

附加说明:

如果我正确理解了100 Continue状态的作用,那么如果服务器要拒绝文件,它不应该向我发送此状态。然而,似乎这个状态由Tomcat直接控制,并且无法被应用程序控制。理想情况下,我希望服务器在这种情况下不要向我发送100 Continue,但是据我负责后端的同事说,目前没有简单的方法可以实现。因此,我现在正在寻找客户端解决方案;但是如果你碰巧知道如何在服务器端解决这个问题,那也会很感激。

遇到这个问题的应用程序针对.NET 4.0,但我也使用4.5重现了这个问题。

我没有超时。异常在超时之前就被抛出。

我尝试了异步请求。结果并没有改变什么。

我尝试将请求协议版本设置为HTTP 1.0,但结果相同。


已经有其他人在Connect上报告了这个问题:https://connect.microsoft.com/VisualStudio/feedback/details/779622/unable-to-get-servers-error-response-when-uploading-file-with-httpwebrequest


嗯,我仍在努力弄清楚这里出了什么问题。然而,我能想到的服务器端解决方案是在Tomcat服务器中实现一个自定义阀门来处理这种情况。我个人还没有尝试过阀门,但只是有一种感觉,在这里它将是一个不那么困难的服务器端解决方案。 - jester
@LeandroTaset,服务器使用HTTP 1.1。禁用Expect100Continue没有任何效果。 - Thomas Levesque
@ThomasLevesque,你有没有找到这个问题的客户端解决方法? - Dmitry Korolev
@DmitryKorolev 不是很好的方法。客户端通过请求发送一个ID,如果请求失败,则询问服务器此请求出了什么问题。虽然这样做有些丑陋,但它确实起作用... - Thomas Levesque
当然,很长时间过去了,我的建议是,尽管它很丑陋,但切换库或创建一些代理客户端与另一个库进行实际上传,即使不是 .net 的库... - ilansch
显示剩余8条评论
6个回答

5

我对于如何解决你的问题没有客户端方案的想法了。但我仍然认为使用自定义tomcat阀门的服务器端解决方案可以在此处提供帮助。目前我没有tomcat设置来测试这个,但我认为服务器端的解决方案如下:

RFC 8.2.3节明确规定: HTTP / 1.1起源服务器的要求:

  - Upon receiving a request which includes an Expect request-header
    field with the "100-continue" expectation, an origin server MUST
    either respond with 100 (Continue) status and continue to read
    from the input stream, or respond with a final status code. The
    origin server MUST NOT wait for the request body before sending
    the 100 (Continue) response. If it responds with a final status
    code, it MAY close the transport connection or it MAY continue
    to read and discard the rest of the request.  It MUST NOT
    perform the requested method if it returns a final status code.

假设Tomcat符合RFC标准,在自定义的阀门中,您将收到HTTP请求头,但请求正文尚未发送,因为控制权尚未在读取主体的servlet中。因此,您可以实现一个类似于以下内容的自定义阀门:
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ErrorReportValve;

public class CustomUploadHandlerValve extends ValveBase {

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
         HttpServletRequest httpRequest = (HttpServletRequest) request;
         String fileName = httpRequest.getHeader("Filename");  // get the filename or whatever other parameters required as per your code
         bool validationSuccess = Validate(); // perform filename check or anyother validation here
         if(!validationSuccess)
         {
             response = CreateResponse(); //create your custom 400 response here
             request.SetResponse(response);
             // return the response here
         }
         else
         {
             getNext().invoke(request, response); // to pass to the next valve/ servlet in the chain
         }
    }
    ...
}

免责声明:我并没有尝试过这个方法是否成功,需要一些时间和Tomcat设置才能尝试;但我认为这可能是你的一个起点。


谢谢,我的服务器端同事正在评估这个想法。我会告诉你进展如何。 - Thomas Levesque
我的同事基于阀门提出了一个POC,但它采用了不同的方法:它用包装器替换请求和响应,并控制何时调用sendAcknowledgement。它可以工作,但是包装器必须适应每个Tomcat版本,因此很难维护... - Thomas Levesque
最终,我的后端同事们决定不包含这个阀门,因为它引入了维护和部署问题。解决方案是首先发出一个请求来检查文件是否可以被接受,然后再上传。虽然不太优雅,但很实用...无论如何,这是我收到的最佳答案,所以你得到了赏金。谢谢! - Thomas Levesque
谢谢Thomas :) 很抱歉我不能给你一个更好的答案,但很高兴我能帮到你。 - jester

3

我遇到了同样的问题。在尝试进行异步请求时,服务器会在请求正文传输完成之前发送响应。在一系列的实验中,我找到了一个解决方法。 当接收到请求流后,我使用反射检查HttpWebRequest的私有字段_CoreResponse。如果它是CoreResponseData类的对象,我使用反射来获取它的私有字段:m_StatusCodem_StatusDescriptionm_ResponseHeadersm_ContentLength。他们包含了关于服务器响应的信息! 在大多数情况下,这个技巧是有效的!


1
只是让你知道,你可以使用 反引号 符号添加代码格式化 :-) - Mathieu VIALES
1
有时也称为“反引号”。 :-) - Nisse Engström

1

请仔细阅读问题:我提到在“WebException”中,“Response”为空。否则,我就不会首先发布问题了;)。只有在响应中存在错误状态代码引起异常时,“WebException.Response”才会被设置,但在这种情况下,响应被丢弃。 - Thomas Levesque
抱歉错过了,但状态码也是空的吗? - Aman B
状态码应该是这些之一:http://msdn.microsoft.com/zh-cn/library/aa383887.aspx - Aman B
如果我要实现它,我会先调用服务器进行文件创建,然后再发送文件内容。这样你就不需要等待很长时间才发现文件创建出现问题。 - Aman B
抱歉,我说的是WebException.Status。正如我之前所说,我没有响应,所以我没有状态码... - Thomas Levesque
是的,我可能最终会先发送请求来检查是否可以上传,然后再进行实际上传。 - Thomas Levesque

1
您没有明确说明您使用的是Tomcat 7的哪个版本...
用WireShark检查过了吗?
您实际上看到WireShark中的什么?
您是否看到响应的状态行?
您是否看到完整的状态行,直到其末尾的CR-LF字符?
Tomcat是否要求身份验证凭据(401),还是出于某种其他原因拒绝文件上传(首先以100确认,但在传输过程中中止)?
问题在于,如果上传的文件很大,则在我完成写入请求正文之前,请求流会关闭,然后我会收到IOException:
如果您不希望连接关闭但所有数据都在服务器端传输并被吞掉,在Tomcat 7.0.55及更高版本上可以配置HTTP连接器上的maxSwallowSize属性,例如maxSwallowSize="-1"。

http://tomcat.apache.org/tomcat-7.0-doc/config/http.html

如果您想讨论Tomcat连接处理方面的问题,最好在Tomcat用户邮件列表上提问。

http://tomcat.apache.org/lists.html#tomcat-users

在 .Net 方面:

  1. 是否可以同时从不同的线程执行 stream.Write() 和 request.GetResponse()?

  2. 在实际上传文件之前,是否可以在客户端执行一些检查操作?


我没有说它是Tomcat 7 ;)(但我相信它是;必须与我的同事确认)。在Wireshark中,我看到服务器发送了一个有效和完整的响应,包括状态行(错误400)、头部和正文。该文件由于应用程序特定原因被拒绝(在我的测试中,我指定了一个带有无效字符的文件名)。我已经通过身份验证。 - Thomas Levesque
我不希望Tomcat吞噬我发送的数据(因为我会白白发送它)。我想要以下两种解决方案之一:1.即使上传被中断,也能够读取服务器发送的响应(客户端解决方案);2.如果服务器将拒绝文件,则不接收100-Continue状态(服务器端解决方案)。 - Thomas Levesque
关于你最后两个问题:1. 不,这没有意义;在上传完成(或中断)之前,服务器无法给我一个有意义的响应。2. 是的,这是一个可能的解决方案,我已经考虑过了,但除非没有其他选择,否则我宁愿避免使用它。 - Thomas Levesque

0

嗯...我不明白 - 这正是为什么在许多实际场景中,大文件会被分块上传(而不是作为单个大文件)的原因

顺便说一下:许多互联网服务器都有大小限制。例如,在Tomcat中,这由maxPostSize表示(如此链接所示:http://tomcat.apache.org/tomcat-5.5-doc/config/http.html

因此,调整服务器配置似乎是简单的方法,但我认为正确的方法是将文件拆分为几个请求

编辑:使用HttpServerUtility.UrlEncode替换Uri.EscapeDataString

 Uri.EscapeDataString(filename) // a problematic .net implementation
 HttpServerUtility.UrlEncode(filename) // the proper way to do it

这并没有回答我的问题...将文件分成块也无法解决我现在遇到的问题。 - Thomas Levesque
你尝试检查服务器上的 maxPostSize 了吗?它限制了以字节为单位的 POST 请求大小。 - ymz
我得到了某些东西,它涉及到 Uri.EscapeDataString,如此展示的:http://blogs.msdn.com/b/yangxind/archive/2006/11/09/don-t-use-net-system-uri-unescapedatastring-in-url-decoding.aspx。本文中的表格显示了标准编码和 .net 实现之间的差异。也许你可以尝试使用 HttpServerUtility.UrlEncode 代替? - ymz
嗯...如果你尝试这样读取响应:var stream = response.GetResponseStream(),你会立即收到错误吗? - ymz
1
我无法调用response.GetResponseStream(),因为我根本没有响应... 这就是我的问题,正如我在问题中所述。 - Thomas Levesque
显示剩余2条评论

0

我目前也遇到了一个类似的问题,与Tomcat和Java客户端有关。Tomcat REST服务在读取整个请求体之前发送带有响应体的HTTP返回码。然而,客户端却因为IOException而失败。我在客户端上插入了一个HTTP代理来嗅探协议,实际上HTTP响应最终被发送到了客户端。很可能是Tomcat在发送响应之前关闭了请求输入流。

一种解决方案是使用不会出现这个问题的不同HTTP服务器,比如Jetty。另一种解决方案是在Tomcat前面添加一个带有AJP的Apache HTTP服务器。Apache HTTP服务器对流的处理方式不同,因此问题就消失了。


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