Как получить ответ HTTP, когда поток запроса был закрыт во время передачи

TL; версия DR

Когда при записи в поток запроса возникает ошибка передачи, я не могу получить доступ к ответу, даже если сервер отправляет его.


Полная версия

У меня есть приложение .NET, которое загружает файлы на сервер Tomcat, используя HttpWebRequest. В некоторых случаях сервер закрывает поток запросов досрочно (поскольку он отказывается от файла по той или иной причине, например, недопустимое имя файла) и отправляет ответ 400 с настраиваемым заголовком, чтобы указать причину ошибки.

Проблема заключается в том, что если загруженный файл большой, поток запросов закрывается, прежде чем закончить запись тела запроса, и я получаю IOException:

Сообщение: невозможно записать данные в транспортное соединение: существующее соединение было принудительно закрыто удаленным хостом.
InnerException: SocketException: существующее соединение было принудительно закрыто удаленным хостом

Я могу поймать это исключение, но затем, когда я вызываю GetResponse, я получаю WebException с предыдущим IOException как его внутреннее исключение и свойство null Response. Поэтому я не могу получить ответ, даже если сервер отправляет его (отмечен с помощью WireShark).

Поскольку я не могу получить ответ, я не знаю, какова фактическая проблема. С точки зрения моего приложения, похоже, что соединение было прервано, поэтому я рассматриваю его как ошибку, связанную с сетью, и повторю загрузку... что, конечно же, снова не работает.

Как я могу обойти эту проблему и получить реальный ответ от сервера? Возможно ли это? Для меня текущее поведение выглядит как ошибка в 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

Ответ 1

У меня нет идей относительно того, что может быть решением вашей проблемы на стороне клиента. Но я по-прежнему думаю, что решение на стороне сервера, использующее специальный клапан tomcat, может помочь здесь. У меня в настоящее время нет настройки tomcat, где я могу проверить это, но я думаю, что решение на стороне сервера будет состоять из следующих строк:

В разделе 8.2.3 RFC четко говорится: Требования к серверам происхождения 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-запроса, но тело запроса не будет отправлено, поскольку элемент управления еще не находится в сервлете, который читает тело.

Таким образом, вы, вероятно, можете использовать специальный клапан, что-то похожее на:

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, чтобы попробовать это;). Думал, что это может послужить отправной точкой для вас.

Ответ 2

У меня была та же проблема. Сервер отправляет ответ до конца клиентского запроса тела запроса, когда я пытаюсь выполнить асинхронный запрос. После серии экспериментов я нашел обходное решение. После того, как поток запроса получен, я использую отражение, чтобы проверить частное поле _CoreResponse HttpWebRequest. Если это объект класса CoreResponseData, я беру его частные поля (используя отражение): m_StatusCode, m_StatusDescription, m_ResponseHeaders, m_ContentLength. Они содержат информацию об ответе сервера! В большинстве случаев этот хак работает!

Ответ 3

Что вы получаете в коде состояния и ответе второго исключения, а не внутреннего исключения?

Если вызывается WebException, используйте свойства Response и Status для исключения, чтобы определить ответ с сервера.

http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.getresponse(v=vs.110).aspx

Ответ 4

Вы не говорите, какую именно версию Tomcat 7 вы используете...

отмечен с помощью WireShark

Что вы на самом деле видите с помощью WireShark?

Вы видите строку состояния ответа?

Вы видите полную строку состояния, вплоть до символов CR-LF в конце?

Рассматривается ли Tomcat учетные данные для аутентификации (401) или отказывается от загрузки файлов по какой-либо другой причине (сначала подтверждая это 100, но затем прерывая ее в середине полета)?

Проблема заключается в том, что если загруженный файл большой, поток запросов закрывается, прежде чем закончить запись тела запроса, и я получаю исключение IOException:

Если вы не хотите, чтобы соединение было закрыто, но все данные, переданные по кабелю и проглоченные на стороне сервера, на Tomcat 7.0.55 и более поздних версиях, можно настроить атрибут maxSwallowSize на соединителе HTTP, например. maxSwallowSize =. "- 1"

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

Если вы хотите обсудить обработку соединения с Tomcat, вам лучше спросить список рассылки пользователей Tomcat,

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

На .Net стороне:

  • Можно ли выполнять stream.Write() и request.GetResponse() одновременно из разных потоков?

  • Возможно ли выполнить некоторые проверки на стороне клиента перед фактической загрузкой файла?

Ответ 5

hmmm... я не понимаю - это ТОЧНО, почему во многих реальных сценариях большие файлы загружаются в куски (а не как один большой файл)

Кстати: у многих интернет-серверов есть ограничения по размеру. например, в tomcat, который представляет maxPostSize (как показано в этой ссылке: http://tomcat.apache.org/tomcat-5.5-doc/config/http.html)

поэтому настройка конфигураций сервера кажется простым способом, но я думаю, что правильный способ состоит в том, чтобы разделить файл на несколько запросов

EDIT: замените Uri.EscapeDataString на HttpServerUtility.UrlEncode

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

Ответ 6

В настоящее время я испытываю довольно похожую проблему с Tomcat и Java-клиентом. Служба Tomcat REST отправляет HTTP-код возврата с телом ответа перед чтением всего тела запроса. Клиент, однако, не работает с IOException. Я вставил HTTP-прокси на клиент, чтобы обнюхать протокол, и в конечном итоге HTTP-запрос отправляется клиенту. Наиболее вероятно, что Tomcat закрыл входной поток запроса перед отправкой ответа.

Одним из решений является использование другого HTTP-сервера, такого как Jetty, у которого нет этой проблемы. Другое решение - добавить HTTP-сервер Apache с AJP перед Tomcat. HTTP-сервер Apache имеет различную обработку потоков и с этим проблема уходит.