<div dir="ltr"><div>Hello,</div><div><br></div><div>I'm documenting some guidelines for using java.net.http.HttpClient defensively for my team. For example: "Always set a request timeout", "Don't assume HTTP response entities are small and/or will fit in memory", etc.</div><div><br></div><div>One guideline I'd like to document is "Set a maximum for HTTP response header size." However, I can't seem to find a way to set that limit, either in documentation or in OpenJDK code.</div><div><br></div><div>I tried my best to search the archives for this mailing list for any mentions, but came up empty.<br></div><div><br></div><div>To make sure my head is on straight and there isn't an undocumented limit set by default, I wrote the attached (very quick and dirty) client and server programs. LongResponseHeaderDemoServer opens a raw server socket and reads (what it assumes is) a well-formed HTTP request, and then prints an HTTP response which includes a response header of infinite length. LongResponseHeaderDemoHttpClient uses java.net.http.HttpClient to make a request and print the response body.<br></div><div><br></div><div>When I run LongResponseHeaderDemoServer in one terminal and make a curl request to the server in another terminal, this is what curl spits out:</div><div><br></div><div style="margin-left:40px"><span style="font-family:monospace">$ curl -vvv -D - <a href="http://localhost:3000">http://localhost:3000</a></span></div><div style="margin-left:40px"><span style="font-family:monospace">* Host localhost:3000 was resolved.<br>* IPv6: ::1<br>* IPv4: 127.0.0.1<br>*   Trying [::1]:3000...<br>* Connected to localhost (::1) port 3000<br>> GET / HTTP/1.1<br>> Host: localhost:3000<br>> User-Agent: curl/8.6.0<br>> Accept: */*<br>> <br>< HTTP/1.1 200 OK<br>HTTP/1.1 200 OK<br>< Content-Type: text/plain<br>Content-Type: text/plain<br>< Connection: close<br>Connection: close<br>< Content-Length: 3<br>Content-Length: 3<br>* Closing connection<br>curl: (100) A value or data field grew larger than allowed</span></div><div><br></div><div>So curl detects the long response header and bails out. Safe and sane.<br></div><div><br></div><div>However, when I run LongResponseHeaderDemoServer in one terminal and run LongResponseHeaderDemoHttpClient in another terminal, this is what happens:</div><div><br></div><div style="margin-left:40px"><span style="font-family:monospace">$ java LongResponseHeaderDemoHttpClient       </span><span style="font-family:monospace"><br>Exception in thread "main" java.io.IOException: Requested array size exceeds VM limit<br>   at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:966)<br>   at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:133)<br>       at </span><span style="font-family:monospace">LongResponseHeaderDemoHttpClient</span><span style="font-family:monospace">.main(</span><span style="font-family:monospace">LongResponseHeaderDemoHttpClient</span><span style="font-family:monospace">.java:13)<br>Caused by: java.lang.OutOfMemoryError: Requested array size exceeds VM limit<br> at java.base/java.util.Arrays.copyOf(Arrays.java:3541)<br>        at java.base/java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:242)<br>   at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:806)<br>   at java.base/java.lang.StringBuilder.append(StringBuilder.java:246)<br>   at java.net.http/jdk.internal.net.http.Http1HeaderParser.readResumeHeader(Http1HeaderParser.java:250)<br> at java.net.http/jdk.internal.net.http.Http1HeaderParser.parse(Http1HeaderParser.java:124)<br>    at java.net.http/jdk.internal.net.http.Http1Response$HeadersReader.handle(Http1Response.java:605)<br>     at java.net.http/jdk.internal.net.http.Http1Response$HeadersReader.handle(Http1Response.java:536)<br>     at java.net.http/jdk.internal.net.http.Http1Response$Receiver.accept(Http1Response.java:527)<br>  at java.net.http/jdk.internal.net.http.Http1Response$HeadersReader.tryAsyncReceive(Http1Response.java:583)<br>    at java.net.http/jdk.internal.net.http.Http1AsyncReceiver.flush(Http1AsyncReceiver.java:233)<br>  at java.net.http/jdk.internal.net.http.Http1AsyncReceiver$$Lambda/0x00000008010dbd50.run(Unknown Source)<br>      at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)<br>    at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)<br>   at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)<br>   at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)<br>  at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)<br>  at java.base/java.lang.Thread.runWith(Thread.java:1596)<br>       at java.base/java.lang.Thread.run(Thread.java:1583)</span></div><div><br></div><div>Ostensibly, HttpClient just keeps on reading the never-ending header until it OOMs. This seems to confirm that there is no default limit to header size. It also seems like A Very Bad Thing to me. This suggests that any time a program makes an HTTP request to an untrusted source using HttpClient, for example when crawling the web, they are at risk of an OOM.</div><div><br></div><div>For grins, I also wrote an application LongResponseHeaderDemoHttpURLConnection that does the same thing as LongResponseHeaderDemoHttpClient, just using HttpURLConnection instead of HttpClient. When I run LongResponseHeaderDemoServer in one terminal and LongResponseHeaderDemoHttpURLConnection in another terminal, this is what happens:</div><div><br></div><div style="margin-left:40px"><span style="font-family:monospace">$ java LongResponseHeaderDemoHttpURLConnection</span></div><div style="margin-left:40px"><span style="font-family:monospace">Exception in thread "main" java.lang.NegativeArraySizeException: -1610612736<br>      at java.base/sun.net.www.MessageHeader.mergeHeader(MessageHeader.java:526)<br>    at java.base/sun.net.www.MessageHeader.parseHeader(MessageHeader.java:481)<br>    at java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:804)<br> at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:726)<br>       at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1688)<br> at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1589)<br>  at java.base/java.net.URL.openStream(URL.java:1161)<br>   at </span><span style="font-family:monospace">LongResponseHeaderDemoHttpURLConnection</span><span style="font-family:monospace">.main(</span><span style="font-family:monospace">LongResponseHeaderDemoHttpURLConnection</span><span style="font-family:monospace">.java:12)</span><br></div><div><br></div><div>So HttpURLConnection doesn't handle things gracefully either, but at least it doesn't OOM. That seems like a bug, too, but perhaps less severe.<br></div><div><br></div><div>For reference, here's my java version:</div><div><br></div><div style="margin-left:40px"><span style="font-family:monospace">$ java -version<br>openjdk version "21.0.2" 2024-01-16 LTS<br>OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)<br>OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)</span></div><div><br></div><div>Can anyone check my work, and maybe reproduce? And ideally, can someone with more knowledge than me about java.net.http.HttpClient and/or java.net.HttpURLConnection please comment? Is this real, or have I made a mistake somewhere along the way? If it's real, what's next? A bug report?<br></div><div><br></div><div><div><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature"><div dir="ltr">Andy Boothe<div><b>Email</b>: <a href="mailto:andy.boothe@gmail.com" target="_blank">andy.boothe@gmail.com</a></div><div><b>Mobile</b>: (979) 574-1089<br></div></div></div></div></div></div>