[JAVA] Trace mystérieuse de pile de HttpUrlConnection

Aperçu

Je ne comprends pas si ʻIOException se produit lors de l'appel de la méthode getInputStream` de HttpUrlConnection Les traces de pile d'exceptions peuvent être renvoyées.

TL;DR

Si HttpUrlConnection # getInputStream déclenche une ʻIOException, l 'ʻIOException qui s'est produite dans cette instance dans le passé est imbriquée.

Environnement de vérification

un événement

Supposons que vous ayez un code comme celui-ci:

--Préparez un serveur HTTP simple qui renvoie 400 demandes incorrectes de manière fixe --Envoyer une requête à un serveur HTTP simple en utilisant HttpUrLConnection --Appelez HttpUrLConnection # getInputStream pour lire la réponse

code

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;

public class HttpURLConnectionTest {

    public static void main(String[] args) throws IOException {
        // HTTP Server
        HttpServer server = HttpServer.create(new InetSocketAddress(8087), 0);
        server.createContext("/", exchange -> {
            Headers headers = exchange.getResponseHeaders();
            headers.add("Content-type", "text/plain");
            String body = "error!";
            byte[] bytes = body.getBytes();
            exchange.sendResponseHeaders(400, bytes.length);
            try (OutputStream out = exchange.getResponseBody()) {
                out.write(bytes);
            }
        });
        server.start();
        System.out.println("start server.");

        // HTTP Client
        HttpURLConnection conn = null;
        try {
            URL url = new URL("http://localhost:8087/");
            conn = (HttpURLConnection) url.openConnection();
            conn.connect();
            int responseCode = conn.getResponseCode();
            System.out.println("responseCode = " + responseCode); // 400

            try (InputStream in = conn.getInputStream()) {  //Une exception se produit ici
                throw new AssertionError("Ne devrait pas venir ici");
            }
        } catch (IOException e) {
            e.printStackTrace();  //J'obtiens une trace de pile que je ne comprends pas
        } finally {
            if (conn != null) conn.disconnect();
            server.stop(0);
        }
    }
}

Une fois exécuté, la trace de pile suivante sera affichée.

Trace de la pile

Ce qui suit est affiché sur la console.

start server.
responseCode = 400
java.io.IOException: Server returned HTTP response code: 400 for URL: http://localhost:8087/
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at sun.net.www.protocol.http.HttpURLConnection$10.run(HttpURLConnection.java:1950)
	at sun.net.www.protocol.http.HttpURLConnection$10.run(HttpURLConnection.java:1945)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.net.www.protocol.http.HttpURLConnection.getChainedException(HttpURLConnection.java:1944)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1514)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498)
	at HttpURLConnectionTest.main(HttpURLConnectionTest.java:38)
Caused by: java.io.IOException: Server returned HTTP response code: 400 for URL: http://localhost:8087/
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1900)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498)
	at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480)
	at HttpURLConnectionTest.main(HttpURLConnectionTest.java:35)

Process finished with exit code 0

Considération

supposition

HttpURLConnection lance ʻIOException ou FileNotFoundExceptionsigetInputStream` est appelé lorsque le code d'état de la réponse est 400 ou supérieur.

                if (respCode >= 400) {
                    if (respCode == 404 || respCode == 410) {
                        throw new FileNotFoundException(url.toString());
                    } else {
                        throw new java.io.IOException("Server returned HTTP" +
                              " response code: " + respCode + " for URL: " +
                              url.toString());
                    }
                }

Dans ce cas, le corps de la réponse ne peut pas être lu, sauf si vous utilisez la méthode getErrorStream au lieu de getInputStream.

C'est une API incroyable, mais cela fonctionne en fait de cette façon.

Prise en compte de la trace de pile

En regardant le sujet principal, la trace de pile, il y a des exceptions imbriquées.

En regardant la première exception, l'appel à getInputStream a déclenché une ʻIOException`, qui est une trace de pile qui est cohérente avec les hypothèses ci-dessus.

Le problème est l'exception imbriquée.

En regardant les exceptions imbriquées, il semble que l'exception soit déclenchée dans l'appel précédent à getResponseCode au lieu de getInputStream. Cependant, en réalité, getResponseCode ne lève pas d'exception et renvoie le code d'état 400 comme valeur de retour.

La trace de pile de l'appel de méthode exécuté deux lignes avant que getInputStream ne soit imbriquée dans getInputStream, qui est une trace de pile qui n'est pas vue dans la programmation Java normale.

Pourquoi une telle trace de pile se produit-elle?

Comportement lors de l'appel de getResponseCode

--getResponseCode appelle en interne getInputStream (pour lire le code d'état) --getInputStream lance ʻIOException lorsque le code d'état est supérieur ou égal à 400 --À ce moment-là, enregistrez l'exception qui s'est produite dans le champ d'instance. --getResponseCode ne lance pas l''IOException générée et renvoie l'état comme valeur de retour.

Comportement lors de l'appel de getInputStream après cela

--Lorsque ʻIOException se produit dans getInputStream`, s'il y a une exception qui s'est produite auparavant, imbriquez-la dans cette exception.

Il est décrit comme ceci dans le code source.

    /* Remembered Exception, we will throw it again if somebody
       calls getInputStream after disconnect */
    private Exception rememberedException = null;

Il dit: "Si getInputStream est appelé après disconnect, souvenez-vous de renvoyer l'exception."

Si une exception précédente s'est produite, imbriquez l'exception précédente dans la nouvelle exception (utilisez Throwable # initCause).

    private synchronized InputStream getInputStream0() throws IOException {
        //Omission

        if (rememberedException != null) {
            if (rememberedException instanceof RuntimeException)
                throw new RuntimeException(rememberedException);
            else {
                throw getChainedException((IOException)rememberedException);
            }
        }
        try {
            final Object[] args = { rememberedException.getMessage() };
            IOException chainedException = //...

            //Omission
                    
            //Imbriquez les exceptions précédentes ici
            chainedException.initCause(rememberedException);
            return chainedException;
        } catch (Exception ignored) {
            return rememberedException;
        }
    }

Pour cette raison, l 'ʻIOException qui s'est produite en interne lorsque le getResponseCodea été démarré avant est sauvegardé, puis apparaît comme une exception imbriquée lorsque legetInputStream` est démarré.

Du point de vue de l'utilisateur de l'API, getResponseCode semble réussir, il ne semble donc pas pertinent que les exceptions imbriquées suivantes dans getInputStream incluent la trace de pile du dernier getResponseCode réussi.

Parce que je ne savais pas que je devais utiliser getInputStream et getErrorStream correctement Quand j'ai regardé cette trace de pile, cela n'avait aucun sens.

Résumé

Si HttpUrlConnection # getInputStream déclenche une ʻIOException, l 'ʻIOException qui s'est produite dans cette instance dans le passé est imbriquée.

Grâce à cette recherche, j'ai pensé que HttpUrlConnection avait les problèmes suivants.

  1. Le code d'état a une conception d'API non naturelle qui vous oblige à utiliser correctement getInputStream et getErrorStream.
  2. Comportement non naturel tel que l'imbrication d'une exception qui n'est pas exposée à l'utilisateur de l'API à une autre exception
  3. La documentation de l'API ne spécifie pas un tel comportement (en fonction de l'implémentation)
  4. Rien n'indique que les problèmes ci-dessus seront améliorés.

De nombreuses personnes ont remis en question la conception de l'API de 1.

De plus, si vous constatez que le comportement est le même dans Java 11 et 13, il est déduit que le comportement ne peut pas être modifié pour maintenir la compatibilité. C'est inévitable, mais au moins j'aimerais que la documentation de l'API s'améliore.

Ce n'est pas une API facile à utiliser à compléter, donc à moins que vous n'ayez une raison spécifique, telle que devoir utiliser une API standard, il semble préférable d'utiliser une autre bibliothèque.

Recommended Posts

Trace mystérieuse de pile de HttpUrlConnection
Journal de trace de pile Java NullPointerException
Obtenir des informations sur l'appelant à partir de la trace de la pile (Java)