[JAVA] Geheimnisvolle Stapelspur von HttpUrlConnection

Überblick

Ich verstehe nicht, ob beim Aufrufen der Methode "getInputStream" von HttpUrlConnection eine "IOException" auftritt. Ausnahmestapelspuren können zurückgegeben werden.

TL;DR

Wenn in "HttpUrlConnection # getInputStream" eine "IOException" auftritt, wird die "IOException", die in dieser Instanz in der Vergangenheit aufgetreten ist, verschachtelt.

Überprüfungsumgebung

Veranstaltung

Angenommen, Sie haben folgenden Code:

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()) {  //Hier tritt eine Ausnahme auf
                throw new AssertionError("Sollte nicht hierher kommen");
            }
        } catch (IOException e) {
            e.printStackTrace();  //Ich erhalte eine Stapelverfolgung, die ich nicht verstehe
        } finally {
            if (conn != null) conn.disconnect();
            server.stop(0);
        }
    }
}

Bei der Ausführung wird die folgende Stapelverfolgung angezeigt.

Stapelspur

Folgendes wird auf der Konsole angezeigt.

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

Erwägung

Annahme

HttpURLConnection löst eine "IOException" oder "FileNotFoundException" aus, wenn "getInputStream" aufgerufen wird, wenn der Antwortstatuscode 400 oder höher ist.

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

In diesem Fall kann der Antworttext nur gelesen werden, wenn Sie die Methode "getErrorStream" anstelle von "getInputStream" verwenden.

Es ist eine unglaubliche API, aber es funktioniert tatsächlich so.

Berücksichtigung der Stapelverfolgung

Beim Hauptthema, der Stapelverfolgung, gibt es verschachtelte Ausnahmen.

Mit Blick auf die erste Ausnahme hat der Aufruf von "getInputStream" eine "IOException" ausgelöst, bei der es sich um eine Stapelverfolgung handelt, die mit den obigen Annahmen übereinstimmt.

Das Problem ist die verschachtelte Ausnahme.

Bei den verschachtelten Ausnahmen sieht es so aus, als ob die Ausnahme beim vorherigen Aufruf von "getResponseCode" anstelle von "getInputStream" ausgelöst wurde. In der Realität löst getResponseCode jedoch keine Ausnahme aus und gibt den Statuscode 400 als Rückgabewert zurück.

Der Stack-Trace des Methodenaufrufs, der zwei Zeilen vor getInputStream ausgeführt wurde, ist dann in getInputStream verschachtelt. Dies ist ein Stack-Trace, der in der normalen Java-Programmierung nicht angezeigt wird.

Warum tritt eine solche Stapelverfolgung auf?

Verhalten beim Aufruf von getResponseCode

--getResponseCode ruft intern getInputStream auf (um den Statuscode zu lesen) --getInputStream löst eine IOException aus, wenn der Statuscode 400 oder höher ist

Verhalten beim anschließenden Aufruf von getInputStream

Es wird im Quellcode so beschrieben.

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

Es heißt: "Wenn getInputStream nach dem Trennen aufgerufen wird, denken Sie daran, die Ausnahme erneut auszulösen."

Wenn eine vorherige Ausnahme aufgetreten ist, verschachteln Sie die vorherige Ausnahme in der neuen Ausnahme (verwenden Sie Throwable # initCause).

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

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

            //Unterlassung
                    
            //Verschachteln Sie frühere Ausnahmen hier
            chainedException.initCause(rememberedException);
            return chainedException;
        } catch (Exception ignored) {
            return rememberedException;
        }
    }

Aus diesem Grund wird die "IOException", die intern beim Starten des "getResponseCode" aufgetreten ist, gespeichert und beim Start des "getInputStream" als verschachtelte Ausnahme angezeigt.

Aus Sicht des API-Benutzers scheint "getResponseCode" erfolgreich zu sein, so dass es irrelevant erscheint, dass die nachfolgenden verschachtelten Ausnahmen in "getInputStream" den Stack-Trace des zuvor erfolgreichen "getResponseCode" enthalten.

Weil ich nicht wusste, dass ich getInputStream und getErrorStream richtig verwenden musste Als ich mir diese Stapelspur ansah, ergab das keinen Sinn.

Zusammenfassung

Wenn in "HttpUrlConnection # getInputStream" eine "IOException" auftritt, wird die "IOException", die in dieser Instanz in der Vergangenheit aufgetreten ist, verschachtelt.

Durch diese Forschung dachte ich, dass HttpUrlConnection die folgenden Probleme hat.

  1. Der Statuscode hat ein unnatürliches API-Design, das Sie dazu zwingt, "getInputStream" und "getErrorStream" ordnungsgemäß zu verwenden.
  2. Unnatürliches Verhalten, z. B. Verschachteln einer Ausnahme, die dem API-Benutzer nicht ausgesetzt ist, mit einer anderen Ausnahme
  3. In der API-Dokumentation wird ein solches Verhalten nicht angegeben (implementierungsabhängig).
  4. Es gibt keine Anzeichen dafür, dass sich die oben genannten Probleme verbessern werden.

Viele Leute haben das API-Design von 1 in Frage gestellt.

Wenn Sie feststellen, dass das Verhalten in Java 11 und 13 identisch ist, wird davon ausgegangen, dass das Verhalten nicht geändert werden kann, um die Kompatibilität aufrechtzuerhalten. Das ist unvermeidlich, aber zumindest möchte ich, dass die API-Dokumentation verbessert wird.

Es ist keine einfach zu verwendende API, die ein Kompliment macht. Wenn Sie also keinen bestimmten Grund haben, z. B. die Verwendung einer Standard-API, scheint es besser, eine andere Bibliothek zu verwenden.

Recommended Posts

Geheimnisvolle Stapelspur von HttpUrlConnection
Protokollieren Sie den Java NullPointerException-Stack-Trace
Abrufen von Anruferinformationen aus dem Stack-Trace (Java)