Как получить ответ 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 стороне:
Ответ 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 имеет различную обработку потоков и с этим проблема уходит.