JDK-8326420 : http2 upgrade "IOException: HTTP/1.1 header parser received no bytes"
  • Type: Enhancement
  • Component: core-libs
  • Sub-Component: java.net
  • Affected Version: 21
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2024-02-21
  • Updated: 2024-02-21
Related Reports
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
Reproduced on a range of platform:

Mac OS (latest) and JDK 21 latest
RedHat Linux (REHL) 9 and JDK 17.0.8+7-LTS

A DESCRIPTION OF THE PROBLEM :

Additional comments on the following existing issue:
https://bugs.openjdk.org/browse/JDK-8239117

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
This code always fails when the targetted server isn't able to handle the "Upgrade: h2c" header automatically sent by this request. Indeed, the server I've found crashes, and breaks the TCP connection right away (confirmed by testing with curl).

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Ideally, a retry with http 1.1 instead.
Else, raise a IOException or InterruptedException
ACTUAL -
Exception in thread "main" java.io.IOException: HTTP/1.1 header parser received no bytes
	at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:964)
	at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:133)
	at Test.main(test.java:21)
Caused by: java.io.IOException: HTTP/1.1 header parser received no bytes
	at java.net.http/jdk.internal.net.http.common.Utils.wrapWithExtraDetail(Utils.java:388)
	at java.net.http/jdk.internal.net.http.Http1Response$HeadersReader.onReadError(Http1Response.java:590)
	at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.checkForErrors(Http1AsyncReceiver.java:302)
	at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.flush(Http1AsyncReceiver.java:268)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.io.EOFException: EOF reached while reading
	at java.net.http/jdk.internal.net.http.Http1AsyncReceiver$Http1TubeSubscriber.onComplete(Http1AsyncReceiver.java:601)
	at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$ReadSubscription.signalCompletion(SocketTube.java:648)
	at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$InternalReadSubscription.read(SocketTube.java:853)
	at java.net.http/jdk.internal.net.http.SocketTube$SocketFlowTask.run(SocketTube.java:181)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:280)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler.runOrSchedule(SequentialScheduler.java:233)
	at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$InternalReadSubscription.signalReadable(SocketTube.java:782)
	at java.net.http/jdk.internal.net.http.SocketTube$InternalReadPublisher$ReadEvent.signalEvent(SocketTube.java:965)
	at java.net.http/jdk.internal.net.http.SocketTube$SocketFlowEvent.handle(SocketTube.java:253)
	at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.handleEvent(HttpClientImpl.java:1467)
	at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.lambda$run$3(HttpClientImpl.java:1412)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.net.http/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:1412)

---------- BEGIN SOURCE ----------
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.io.IOException;

public class Test {
  public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
    final var request = HttpRequest.newBuilder()
        .uri(new URI("http://url_to_weird_server"))
        .GET()
        .timeout(Duration.ofSeconds(10))
        .build();

    var httpClient = HttpClient.newBuilder().build();

    var r = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
  }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
The workaround is to set `.version(HttpClient.Version.HTTP_1_1)` after line 12 in this particular case.

(setting the version to HttpClient.Version.HTTP_2, always leads to the issue, as expected snce this is the default behaviour)

FREQUENCY : always



Comments
Retrying with HTTP/1.1 does not make much sense as an upgrade request is already HTTP/1.1. If the server doesn't want to upgrade it is supposed to ignore the upgrade headers and just reply with HTTP/1.1. As noted in the description one workaround is to set the request version to HTTP/1.1. Another workaround would be to use https, as versions would then be negotiated during the handshake using ALPN. The issue here is that we have no idea why the server closed the connection. And the current retry mechanism would retry with the upgrade again.
21-02-2024

Moving it to JDK for further evaluation.
21-02-2024