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.
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
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.
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
HttpURLConnection throws ʻIOException or
FileNotFoundExceptionif
getInputStream` 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.
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?
--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.
--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 the
getInputStream` 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.
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.
getInputStream
and getErrorStream
properly.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.