web-dev-qa-db-fra.com

Android HttpUrlConnection EOFException

J'aimerais savoir s'il existe des problèmes connus sous Android avec les requêtes HttpUrlConnection et POST. Nous rencontrons intermittent EOFExceptions lors de l’enregistrement de POST demandes depuis un client Android. Réessayer la même demande finira par fonctionner. Voici un exemple de trace de pile:

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)

Il y a beaucoup de rapports de bogues et de messages similaires à empiler, mais je ne peux pas comprendre s'il y a vraiment un problème et, dans l'affirmative, quelles versions d'Android sont affectées et quel est le correctif/travail proposé.

Voici quelques-uns des rapports similaires auxquels je me réfère:

Voici un correctif potentiel du cadre Android

Je sais qu'il y a eu un problème avec des connexions empoisonnées dans le pool de connexions dans les versions antérieures à Froyo, mais ces problèmes se produisent exclusivement sur les nouveaux périphériques ICS +. S'il y avait un problème sur les appareils plus récents, j'attendrais une sorte de documentation officielle d'Android sur le problème.

33
Matt Accola

Notre conclusion est qu'il y a un problème dans la plateforme Android. Notre solution de contournement consistait à intercepter l'erreur EOFException et à réessayer la requête N plusieurs fois. Ci-dessous le pseudo code:

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
}
12
Matt Accola

La bibliothèque HttpURLConnection gère en interne un pool de connexions. Ainsi, chaque fois qu'une demande est envoyée, il vérifie d'abord s'il existe une connexion existante dans le pool, sur la base de laquelle il décide de créer une nouvelle. 

Ces connexions ne sont que des sockets, et cette bibliothèque par défaut ne ferme pas ces sockets. Il peut parfois arriver qu'une connexion (socket) non utilisée actuellement et présente dans le pool ne soit plus utilisable, car le serveur peut choisir de mettre fin à la connexion après un certain temps. Maintenant, étant donné que la connexion est fermée par le serveur, la bibliothèque n’en sait rien et suppose que la connexion/socket est toujours connectée. Ainsi, il envoie la nouvelle requête en utilisant cette connexion obsolète et nous obtenons donc EOFException. 

La meilleure façon de gérer cela consiste à vérifier les en-têtes de réponse après chaque demande que vous envoyez. Le serveur envoie toujours une "Connexion: Fermer" avant de mettre fin à une connexion (HTTP 1.1). Vous pouvez donc utiliser getHeaderField () et vérifier le champ "Connexion". Une autre chose à noter est que le serveur envoie UNIQUEMENT ce champ de connexion lorsqu'il est sur le point de mettre fin à la connexion. Donc, vous devez coder autour de cela avec la possibilité d'obtenir un "null" dans le cas normal (lorsque le serveur ne ferme pas la connexion)

7
AllThatICode

Cette solution de contournement a tendance à être fiable et performante:

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();
    }
}

Cette implémentation repose sur le fait que le nombre par défaut de connexions pouvant être ouvertes avec un serveur est 5 (Froyo - KitKat). Cela signifie qu'il peut exister jusqu'à 5 connexions obsolètes, chacune d'entre elles devant être fermée.

Après chaque tentative infructueuse, la propriété de demande Connection:close amène le moteur HTTP sous-jacent à fermer le socket lorsque connection.disconnect() est appelé. En réessayant jusqu'à 6 fois (nombre maximal de connexions + 1), nous nous assurons que la dernière tentative se verra toujours attribuer un nouveau socket.

La demande peut connaître une latence supplémentaire si aucune connexion n'est vivante, mais cela vaut certainement mieux qu'une EOFException. Dans ce cas, la dernière tentative d’envoi ne fermera pas immédiatement la connexion récemment ouverte. C'est la seule optimisation pratique qui puisse être faite.

Au lieu de vous fier à la valeur magique par défaut de 5, vous pourrez peut-être configurer vous-même la propriété système. Gardez à l'esprit que cette propriété est accessible par un bloc d'initialiseur statique dans ConnectionPool.Java de KitKat, et que cela fonctionne également dans les versions antérieures d'Android. Par conséquent, la propriété peut être utilisée avant que vous puissiez la définir.

static final int MAX_CONNECTIONS = 5;

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

Oui. Il y a un problème dans la plate-forme Android, en particulier dans Android libcore avec la version 4.1-4.3. 

Le problème est introduit dans ce commit: https://Android.googlesource.com/platform/libcore/+/b2b02ac6cd42a69463fd172531aa1f9b988b887a8

Android 4.4 a commuté http lib sur "okhttp" qui n'a pas ce problème.

Problème expliqué comme suit:

Sous Android 4.1-4.3, lorsque vous utilisez URLConnection/HttpURLConnection sur POST avec "ChunkedStreamingMode" ou "FixedLengthStreamingMode" set, URLConnection/HttpURLConnection ne fera pas ne pas réessayer en silence si la connexion est en retour vicié. Vous devez réessayer POST au plus "http.maxConnections + 1" fois dans votre code, comme le suggéraient les réponses précédentes.

4
solrex

J'imagine que c'est le serveur qui est en cause ici, et la connexion HttpURLConnection n'est pas aussi tolérante que d'autres implémentations. C'était la cause de mon EOFException. Je soupçonne que dans mon cas, cela ne serait pas intermittent (corrigé avant de tester la solution de contournement de N), aussi les réponses ci-dessus concernent-elles d'autres problèmes et constituent-elles une solution correcte dans ces cas.

Mon serveur utilisait python SimpleHTTPServer et je supposais à tort que tout ce que je devais faire pour indiquer que le succès était le suivant était le suivant:

self.send_response(200)

Cela envoie la ligne d'en-tête de réponse initiale, un serveur et un en-tête de date, mais laisse le flux dans l'état dans lequel vous pouvez également envoyer des en-têtes supplémentaires. HTTP nécessite une nouvelle ligne supplémentaire après les en-têtes pour indiquer qu'ils sont terminés. Il semble que si cette nouvelle ligne n’est pas présente lorsque vous tentez d’obtenir le corps de résultat InputStream ou le code de réponse, etc. avec HttpURLConnection, une exception EOFException (ce qui est en fait raisonnable, y réfléchit). Certains clients HTTP ont accepté la réponse courte et ont signalé le code de résultat de réussite qui m'a amené à pointer peut-être injustement du doigt vers HttpURLConnection.

J'ai changé mon serveur pour faire ceci à la place:

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

Pas plus EOFException avec ce code.

1
tangobravo

Cela a fonctionné pour moi.

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;
}
0
Rajdeo Das