Подтвердить что ты не робот

Android HttpUrlConnection EOFException

Я хотел бы знать, есть ли известные проблемы на Android с HttpUrlConnection и POST-запросами. Мы испытываем прерывистый EOFExceptions при выполнении POST-запросов от клиента Android. Повторная попытка повторного запроса будет в конечном итоге работать. Вот пример трассировки стека:

java.io.EOFException
at libcore.io.Streams.readAsciiLine(Streams.java:203)
at libcore.net.http.HttpEngine.readResponseHeaders(HttpEngine.java:579)
at libcore.net.http.HttpEngine.readResponse(HttpEngine.java:827)
at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:283)
at libcore.net.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:497)
at libcore.net.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:134)

Есть много похожих сообщений об ошибках и сообщений для, но я не могу понять, действительно ли проблема, и если да, то какие версии Android затронуты и что предлагается для исправления/работы.

Вот некоторые из подобных отчетов, которые я имею в виду:

Ниже приведено потенциальное исправление для платформы Android.

Я знаю, что была проблема с отравленными соединениями в пуле соединений в pre-Froyo, но эти проблемы происходят только на новых устройствах ICS+. Если возникла проблема на более поздних устройствах, я бы ожидал какой-то официальной документации по этой проблеме Android.

4b9b3361

Ответ 1

Наш вывод о том, что на платформе Android есть проблема. Наше решение заключалось в том, чтобы поймать EOFException и повторить запрос N раз. Ниже приведен псевдокод:

private static final int MAX_RETRIES = 3;

private ResponseType fetchResult(RequestType request) {
    return fetchResult(request, 0);
}

private ResponseType fetchResult(RequestType request, int reentryCount) {
    try {
        // attempt to execute request
    } catch (EOFException e) {
        if (reentryCount < MAX_RETRIES) {
            fetchResult(request, reentryCount + 1);
        }
    }
    // continue processing response
}

Ответ 2

Библиотека HttpURLConnection внутренне поддерживает пул соединений. Таким образом, всякий раз, когда запрос отправляется, он сначала проверяет, существует ли уже существующее соединение в пуле, на основе которого оно решает создать новый.

Эти соединения - ничего, кроме сокетов, и эта библиотека по умолчанию не закрывает эти сокеты. Иногда может случиться так, что соединение (сокет), которое в настоящее время не используется и присутствует в пуле, больше не может использоваться, поскольку сервер может решить завершить соединение через некоторое время. Теперь, поскольку соединение, даже если оно закрыто сервером, библиотека не знает об этом и предполагает, что соединение/сокет все еще подключено. Таким образом, он отправляет новый запрос, используя это устаревшее соединение, и, следовательно, мы получаем EOFException.

Лучший способ справиться с этим - проверить заголовки ответов после каждого отправляемого вами запроса. Сервер завершает соединение (HTTP 1.1). Таким образом, вы можете использовать getHeaderField() и проверить поле "Соединение". Другое дело, что сервер ТОЛЬКО отправляет это поле подключения, когда он собирается завершить соединение. Итак, вам нужно закодировать это с возможностью получения "нулевого" в обычном случае (когда сервер не закрывает соединение)

Ответ 3

Это обходное решение имеет тенденцию быть надежным и эффективным:

static final int MAX_CONNECTIONS = 5;

T send(..., int failures) throws IOException {
    HttpURLConnection connection = null;

    try {
        // initialize connection...

        if (failures > 0 && failures <= MAX_CONNECTIONS) {
            connection.setRequestProperty("Connection", "close");
        }

        // return response (T) from connection...
    } catch (EOFException e) {
        if (failures <= MAX_CONNECTIONS) {
            disconnect(connection);
            connection = null;

            return send(..., failures + 1);
        }

        throw e;
    } finally {
        disconnect(connection);
    }
}

void disconnect(HttpURLConnection connection) {
    if (connection != null) {
        connection.disconnect();
    }
}

Эта реализация основана на том факте, что число подключений по умолчанию, которое может быть открыто с сервером, составляет 5 (Froyo - KitKat). Это означает, что может существовать до 5 устаревших соединений, каждый из которых должен быть закрыт.

После каждой неудачной попытки свойство запроса Connection:close приведет к тому, что базовый механизм HTTP закроет сокет при вызове connection.disconnect(). Повторяя до 6 раз (максимальные соединения + 1), мы гарантируем, что последней попытке всегда будет присвоен новый сокет.

Запрос может испытывать дополнительную задержку, если соединения не существуют, но это, безусловно, лучше, чем EOFException. В этом случае окончательная попытка отправки не будет немедленно закрывать только что открытое соединение. Это единственная практическая оптимизация, которую можно сделать.

Вместо того, чтобы полагаться на магическое значение по умолчанию 5, вы можете самостоятельно настроить системное свойство. Имейте в виду, что к этому свойству обращается статический блок инициализатора в KitKat ConnectionPool.java, и он также работает в старых версиях Android. В результате свойство может быть использовано до того, как вы сможете его установить.

static final int MAX_CONNECTIONS = 5;

static {
    System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
}

Ответ 4

Да. В Android-платформе есть проблема, в частности, в Android libcore с версией 4.1-4.3.

В этой фиксации задается проблема: https://android.googlesource.com/platform/libcore/+/b2b02ac6cd42a69463fd172531aa1f9b9bb887a8

Android 4.4 переключил http lib на "okhttp", у которого нет этой проблемы.

Проблема объясняется следующим образом:

В андроиде 4.1-4.3, когда вы используете URLConnection/HttpURLConnection для POST с помощью "ChunkedStreamingMode" или "FixedLengthStreamingMode" set, URLConnection/HttpURLConnection не будет делать тихие повторы если повторное соединение устарело. Вы должны повторить POST не более "http.maxConnections + 1" раз в вашем коде, как и предполагают предыдущие ответы.

Ответ 5

Я подозреваю, что здесь может быть и сервер, и HttpURLConnection не так прощает, как другие реализации. Это было причиной моего исключения EOF. Я подозреваю, что в моем случае это не было бы прерывистым (исправлено его перед тестированием обходного пути N), поэтому приведенные выше ответы касаются других проблем и будут правильным решением в этих случаях.

Мой сервер использовал python SimpleHTTPServer, и я ошибочно полагал, что все, что мне нужно было сделать, чтобы указать успех, было следующим:

self.send_response(200)

Это отправляет начальную строку заголовка ответа, сервер и заголовок даты, но оставляет поток в состоянии, в котором вы также можете отправлять дополнительные заголовки. HTTP требует дополнительной новой строки после заголовков, чтобы указать, что они завершены. Если эта новая строка отсутствует, когда вы пытаетесь получить тело ввода InputStream или код ответа и т.д. С HttpURLConnection, тогда он выдает исключение EOFException (что на самом деле разумно, думая об этом). Некоторые HTTP-клиенты действительно принимали короткий ответ и сообщали код результата успеха, который привел ко мне, возможно, несправедливо указывая пальцем на HttpURLConnection.

Вместо этого я изменил свой сервер:

self.send_response(200)
self.send_header("Content-Length", "0")
self.end_headers()

Больше EOFException с этим кодом.

Ответ 6

Это сработало для меня.

public ResponseObject sendPOST(String urlPrefix, JSONObject payload) throws JSONException {
    String line;
    StringBuffer jsonString = new StringBuffer();
    ResponseObject response = new ResponseObject();
    try {

        URL url = new URL(POST_URL);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Accept", "application/json");
        connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");

        OutputStream os = connection.getOutputStream();
        os.write(payload.toString().getBytes("UTF-8"));
        os.close();
        BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        while ((line = br.readLine()) != null) {
            jsonString.append(line);
        }
        response.setResponseMessage(connection.getResponseMessage());
        response.setResponseReturnCode(connection.getResponseCode());
        br.close();
        connection.disconnect();
    } catch (Exception e) {
        Log.w("Exception ",e);
        return response;
    }
    String json = jsonString.toString();
    response.setResponseJsonString(json);
    return response;
}

Ответ 7

connection.addRequestProperty( "Accept-Encoding", "gzip" );

- ответ