HttpUrlConnection mysterious stack trace

Overview

I don't understand if ʻIOException occurs when calling the getInputStream` method of HttpUrlConnection Exception stack traces may be returned.

TL;DR

If HttpUrlConnection # getInputStream raises a ʻIOException, the ʻIOException that occurred on that instance in the past is nested.

Verification environment

Event

Suppose you have code like this:

--Prepare a simple HTTP server that returns 400 Bad Requests in a fixed manner --Send a request to a simple HTTP server using HttpUrLConnection --Call HttpUrLConnection # getInputStream to read the response

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()) {  //Exception occurs here
                throw new AssertionError("Should not come here");
            }
        } catch (IOException e) {
            e.printStackTrace();  //I get an unfamiliar stack trace
        } finally {
            if (conn != null) conn.disconnect();
            server.stop(0);
        }
    }
}

When you run it, you'll see a stack trace similar to the one below.

Stack trace

The following is displayed on the 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

Consideration

Premise

HttpURLConnection throws ʻIOException or FileNotFoundExceptionifgetInputStream` is called when the response status code is 400 or higher.

                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 this case, you must use the getErrorStream method instead of getInputStream to read the response body.

It's an unbelievable API, but it actually works that way.

Consideration of stack trace

Looking at the stack trace, which is the main subject, there are nested exceptions.

Looking at the first exception, I'm getting a ʻIOException in the call to getInputStream`, which is a stack trace that is consistent with the above assumptions.

The problem is the nested exception.

Looking at the nested exception, it looks like the exception is raised in the previous call to getResponseCode instead of getInputStream. However, in reality, getResponseCode does not throw an exception and returns status code 400 as a return value.

The stack trace of the method call executed two lines before getInputStream is then nested in getInputStream, which is a stack trace that you wouldn't see in normal Java programming.

Why does such a stack trace occur?

Behavior when calling getResponseCode

--getResponseCode internally calls getInputStream (to read the status code) --getInputStream throws ʻIOException when status code is 400 or higher --At that time, save the exception that occurred in the instance field. --getResponseCode does not throw the generated ʻIOException and returns the status as a return value.

Behavior when calling getInputStream after that

--When ʻIOException occurs in getInputStream`, if there is an exception that occurred before, nest it in this exception.

It is described like this in the source code.

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

It says, "If getInputStream is called after disconnect, remember it to throw the exception again."

If a previous exception occurred, nest the previous exception in the new exception (use 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
                    
            //Nest previous exceptions here
            chainedException.initCause(rememberedException);
            return chainedException;
        } catch (Exception ignored) {
            return rememberedException;
        }
    }

For this reason, the ʻIOException that occurred internally when the getResponseCodewas started before is saved, and then appears as a nested exception when thegetInputStream` is started.

From the API user's point of view, getResponseCode seems to be successful, so it seems irrelevant that the subsequent nested exceptions in getInputStream include the stack trace of the last successful getResponseCode.

Because I didn't know that I had to use getInputStream and getErrorStream properly When I looked at this stack trace, it didn't make any sense.

Summary

If HttpUrlConnection # getInputStream raises a ʻIOException, the ʻIOException that occurred on that instance in the past is nested.

Through this investigation, I thought that HttpUrlConnection has the following problems.

  1. The status code has an unnatural API design that forces you to use getInputStream and getErrorStream properly.
  2. Unnatural behavior such as nesting an exception that is not exposed to the API user to another exception
  3. API documentation does not specify such behavior (implementation dependent)
  4. There is no sign that the above problems will be improved.

Many people have questioned the API design of 1.

Also, if you see that the behavior is the same in Java 11 and 13, it is inferred that the behavior cannot be changed to maintain compatibility. That's unavoidable, but at least I would like the API documentation to improve.

It's not an easy-to-use API to compliment, so unless you have a specific reason, such as having to use a standard API, it seems better to use another library.

Recommended Posts

HttpUrlConnection mysterious stack trace
Log Java NullPointerException stack trace
Get caller information from stack trace (java)