11

We are using JDK11 java.net.http HTTP client to get data from an API. After we receive the response the connections remain opened in our server with TCP state CLOSE_WAIT, which means the client must close the connection.

From RFC 793 terminology:

CLOSE-WAIT - represents waiting for a connection termination request from the local user.

This is our client code which runs on WildFly 16 running on Java 12 as a stateless REST API. We don't understand why this is happening.

import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;

public class SocketSandbox {

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        try (var listener = new ServerSocket(59090)) {
            System.out.println("Server is running...");
            while (true) {
                try (var socket = listener.accept()) {
                    HttpRequest request = HttpRequest
                            .newBuilder(URI.create("<remote_URL>"))
                            .header("content-type", "application/json").build();
                    HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
                    var out = new PrintWriter(socket.getOutputStream(), true);
                    out.println(String.format("Response HTTP status: %s", response.statusCode()));
                }
            }
        }
    }

}

We get the "status code" meaning the http response was processed.

When using the same code to call other endpoints connections are fine. It seems to be a particular issue with the remote API we are calling, but still we don't understand why the Java HTTP client is keeping connections opened.

We tried both Windows and Linux machines, and even standalone outside of WildfFly, but the same result happens. After each request, even doing it from our stateless client and receiving the response, each one is left as CLOSE_WAIT and never close.

Connections will disappear if we shutdown the Java process.

enter image description here

Headers that are sent by the HTTP client:

connection: 'Upgrade, HTTP2-Settings','content-length': '0',
host: 'localhost:3000', 'http2-settings': 'AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA',
upgrade: 'h2c',
user-agent': 'Java-http-client/12'

Server returns response with header: Connection: close

Update (1)

We tried to fine-tune the pool parameters in implementation class jdk.internal.net.http.ConnectionPool.

It did not solve the problem.

System.setProperty("jdk.httpclient.keepalive.timeout", "5"); // seconds
System.setProperty("jdk.httpclient.connectionPoolSize", "1");

Update (2)

With Apache HTTP the connections l getting left in CLOSE_WAIT state for about 90 seconds, but it is able to the connections after that time.

Calling method HttpGet.releaseConnection() force the connection close immediately.

HttpClient client = HttpClients.createDefault();
HttpGet get = new HttpGet("https://<placeholderdomain>/api/incidents/list");
get.addHeader("content-type", "application/json");
HttpResponse response = client.execute(get);

// This right here did the trick
get.releaseConnection();

return response.getStatusLine().getStatusCode();

And with OkHttp client it worked as expected out of the box, no connections stuck.

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://<placeholderdomain>/grb/sb/incidentes/listar")
        .header("content-type", "application/json").build();
Response response = client.newCall(request).execute();
return response.body().string();

We are still trying to find how to make it work in java-http-client so that we don't have to rewrite the code.

Community
  • 1
  • 1
Evandro Pomatti
  • 13,341
  • 16
  • 97
  • 165
  • I believe you are expected to close the response input stream – user207421 Mar 21 '19 at 22:12
  • 2
    @user207421 I did not find an API method to do so. Can you demonstrate how it can be done with this API? – Evandro Pomatti Mar 22 '19 at 12:30
  • I'm having the same problem here. Even on OpenJDK 11.0.10 it still happens. Were you able to fix it? – lucasdclopes Apr 23 '21 at 22:34
  • @lucasdclopes have you tried later Java versions? Go for https://bugreport.java.com/bugreport/ if you really think that is a bug. – Evandro Pomatti Apr 23 '21 at 22:42
  • As of jdk11.0.15_9 this issue is still not fixed - we still see hundreds of connections remaining in CLOSE_WAIT which eventually exhaust available file handles. We switched all our applications to Apache HttpClient which doesn't have this issue. – David Hedley May 06 '22 at 09:26
  • @DavidHedley if that is the case you should report a bug here: https://bugreport.java.com/bugreport/. It was supposed to be fixed on [11.0.6](https://bugs.openjdk.java.net/browse/JDK-8221395). – Evandro Pomatti May 06 '22 at 14:48
  • Yes I know, however I don't have the time to put together a suitable test case to file a report. Also my experience of the built-in HttpClient is that it is broken in other ways and just seems to be a poor implementation. I would recommend just using the Apache HttpClient instead. – David Hedley May 09 '22 at 06:44
  • @DavidHedley if you help me isolate the problem I can post that for you. Can you make this trigger the leak? I tried but couldn't. https://github.com/epomatti/java-httpclient-closewait-leak – Evandro Pomatti May 09 '22 at 19:27
  • I got a warning message about leaks if I didn't add a response.close() in the OkHttp code. – user1162020 Mar 09 '23 at 01:18

2 Answers2

5

Submitted and confirmed as a bug in the implementation.

https://bugs.openjdk.java.net/browse/JDK-8221395

Update

Check the JIRA issue, it's fixed in JDK 13, and backported to 11.0.6. (Not sure about 12)

Evandro Pomatti
  • 13,341
  • 16
  • 97
  • 165
2

I wouldn't recommend creating a new client for every new request. This is defeating the purpose of HTTP/2 which allows multiplexing requests on a single connection.

The second thing is that the two properties:

System.setProperty("jdk.httpclient.keepalive.timeout", "5"); // seconds
System.setProperty("jdk.httpclient.connectionPoolSize", "1");

only apply to HTTP/1.1 connections, not HTTP/2. Also take care that these properties are only read once at class loading time. So setting them after the java.net.http classes have been loaded will have no effect.

Finally it may take some time after an HttpClient is released before all kept alive connections are closed - the internal mechanism to do so is basically relying on GC - and this is not very friendly with short lived HttpClients.

daniel
  • 2,665
  • 1
  • 8
  • 18
  • The production code uses a single client, I shortened the code to make in simple for this thread. Also connections are getting stuck forever. About this API behavior, that's another thing I find annoying because I also found out about this behavior but I had to dig into the code. Is there any documentation about all of it? I was not able to find it. Also how can I force HTTP/1.1 to see what happens? – Evandro Pomatti Mar 22 '19 at 12:50
  • Ok I found how to set HTTP/1.1 with `HttpClient.version(Version.HTTP_1_1)` and set the properties BEFORE any `java.net.http` loading, and it did not work. I am running only those lines of code in a clean environment. Also notified GC and it should be cleaning it right away. Nothing is happening. – Evandro Pomatti Mar 22 '19 at 13:00
  • BTW, the remote server does NOT support HTTP/2, so I was using HTTP/1.1 all along because of the implementation automatic downgrade. – Evandro Pomatti Mar 22 '19 at 13:10
  • "Connections are getting stuck forever" can you elaborate on this? Do you mean the response is never delivered? If so the connection will remain open. Only idle connections get closed after the keepalive timeout expires. – daniel Mar 22 '19 at 14:31
  • 1
    There are a couple of fixes like this one https://bugs.openjdk.java.net/browse/JDK-8217094 that have not made it to 12 GA. – daniel Mar 22 '19 at 14:38
  • we get the response from the server, and the Thread completes, but the connection used for the HTTP request stays in CLOSE_WAIT until the Java process is terminated. – Evandro Pomatti Mar 22 '19 at 15:41
  • OK - I'm not sure what's going on. Connection should get closed right after the last response byte is received when the server replies with "Connection: close". I see the code is there. Hmmm... these connection you're showing have port 443. The `HttpClient` might close its side of the connection abruptly without going through the graceful SSL shutdown. Could that be an issue on the server side that might not handle this situation gracefully? – daniel Mar 22 '19 at 16:28
  • I suspect too that the server is the trigger. But, if I switch to Apache HTTP Client or OkHttp the connections in CLOSE_WAIT state are cleared after a couple of seconds. It tells me that the JDK implementation is not handling this particular scenario in the pool, which I consider to be a bug. I also turned on `-Djdk.internal.httpclient.debug=true` and found an `java.io.EOFException: EOF reached while reading`. I've submitted a but to Oracle and I'm waiting for it's review. – Evandro Pomatti Mar 22 '19 at 16:37
  • 1
    The debug logs are very verbose and there could be cases where the EOF/EOFException might be expected. Anyway - thanks for submitting an issue. Note that the connection pool is probably not involved here. One thing that could be happenning is that the `HttpClient` may be closing the underlying `SocketChannel` - without going through the SSL graceful shutdown. – daniel Mar 22 '19 at 17:00
  • Indeed the code indicates that the EOF is not related to the issue (`Http1AsyncReceiver$Http1TubeSubscriber.onComplete(Http1AsyncReceiver.java:591)`). I've sent the actual QAS server name and IP in the sample code with credentials to help simulate the issue, hope it helps. I couldn't reproduce the server behavior as it is a 3rd party legacy (doesn't even support HTTP 2). – Evandro Pomatti Mar 22 '19 at 17:36