-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
P3
-
Affects Version/s: 25, 26
-
Component/s: core-libs
-
generic
ADDITIONAL SYSTEM INFORMATION :
Affected (first broken):
openjdk version "21.0.9" 2025-10-14 LTS
OpenJDK Runtime Environment Temurin-21.0.9+10 (build 21.0.9+10-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.9+10 (build 21.0.9+10-LTS, mixed mode, sharing)
Last working:
openjdk version "21.0.8" 2025-07-15 LTS
OpenJDK Runtime Environment Temurin-21.0.8+9 (build 21.0.8+9-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.8+9 (build 21.0.8+9-LTS, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
After the fix forJDK-7036144 (and its backports), GZIPInputStream.readTrailer() unconditionally calls readHeader(in, false) to check for concatenated GZIP members. readHeader with failOnEOF=false performs in.read() on the underlying stream to detect the next GZIP magic bytes.
When the underlying stream is a socket InputStream with SO_TIMEOUT configured, this read() call blocks for the full timeout duration when there are no concatenated members (the common case for single-member GZIP streams). Previously, readTrailer() checked this.in.available() > 0 before attempting to read, which returned immediately on sockets with no buffered data.
Note:JDK-8374644 addresses EOFException construction overhead in readHeader() but does not fix the socket blocking issue -- in.read() still blocks for SO_TIMEOUT before reaching the EOF handling path.
Impact:
This is a silent performance regression for any application that wraps a socket InputStream in GZIPInputStream over a persistent connection -- a common pattern in message queue consumers, custom RPC protocols, and data streaming systems. There are no errors or warnings; the only symptom is degraded throughput.
REGRESSION : Last worked in version 21.0.8
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run to reproduce via docker (source code below)
# JDK 21.0.8 -- no blocking (17ms)
docker run --rm -v "$(pwd)/GzipSocketTest.java:/app/GzipSocketTest.java" -w /app \
eclipse-temurin:21.0.8_9-jdk bash -c "javac GzipSocketTest.java && java GzipSocketTest"
# JDK 21.0.9 -- blocks for full SO_TIMEOUT (5021ms)
docker run --rm -v "$(pwd)/GzipSocketTest.java:/app/GzipSocketTest.java" -w /app \
eclipse-temurin:21.0.9_10-jdk bash -c "javac GzipSocketTest.java && java GzipSocketTest"
---------- BEGIN SOURCE ----------
import java.io.*;
import java.net.*;
import java.util.zip.*;
public class GzipSocketTest
{
static final int SO_TIMEOUT_MS = 5_000;
public static void main(String[] args) throws Exception
{
System.out.println("Java version: " + System.getProperty("java.version"));
System.out.println("Java runtime: " + System.getProperty("java.runtime.version"));
System.out.println("SO_TIMEOUT: " + SO_TIMEOUT_MS + "ms");
System.out.println();
ServerSocket serverSocket = new ServerSocket(0);
int port = serverSocket.getLocalPort();
// Server: send a single GZIP member over a persistent connection
Thread serverThread = new Thread(() -> {
try
{
Socket server = serverSocket.accept();
GZIPOutputStream gzOut = new GZIPOutputStream(server.getOutputStream());
gzOut.write("line1\nline2\nline3\nline4\nline5\n".getBytes("UTF-8"));
gzOut.finish();
gzOut.flush();
System.out.println("[server] Sent 5 lines of GZIP data, keeping connection open...");
// Keep connection open (persistent connection, e.g. message queue dispatcher)
Thread.sleep(SO_TIMEOUT_MS + 5_000);
server.close();
}
catch (Exception e) { /* ignore */ }
});
serverThread.setDaemon(true);
serverThread.start();
// Client: read from socket with SO_TIMEOUT
Socket client = new Socket("localhost", port);
client.setSoTimeout(SO_TIMEOUT_MS);
GZIPInputStream gzIn = new GZIPInputStream(client.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(gzIn, "UTF-8"));
System.out.println("[client] Reading lines...");
long start = System.currentTimeMillis();
int lineCount = 0;
String line;
while ((line = reader.readLine()) != null)
{
lineCount++;
System.out.println("[client] Line " + lineCount
+ " (" + (System.currentTimeMillis() - start) + "ms): " + line);
}
long elapsed = System.currentTimeMillis() - start;
System.out.println();
System.out.println("Read " + lineCount + " lines in " + elapsed + "ms");
if (elapsed > SO_TIMEOUT_MS - 500)
System.out.println("BUG: blocked for ~SO_TIMEOUT (" + SO_TIMEOUT_MS + "ms)");
else
System.out.println("OK: completed quickly");
client.close();
serverSocket.close();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Wrap the socket InputStream in a BoundedInputStream (e.g., from Apache Commons IO) limited to the known compressed payload size before passing to GZIPInputStream. This causes readTrailer() to see immediate EOF from the bounded stream instead of blocking on the socket.
FREQUENCY :
ALWAYS
Affected (first broken):
openjdk version "21.0.9" 2025-10-14 LTS
OpenJDK Runtime Environment Temurin-21.0.9+10 (build 21.0.9+10-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.9+10 (build 21.0.9+10-LTS, mixed mode, sharing)
Last working:
openjdk version "21.0.8" 2025-07-15 LTS
OpenJDK Runtime Environment Temurin-21.0.8+9 (build 21.0.8+9-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.8+9 (build 21.0.8+9-LTS, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
After the fix for
When the underlying stream is a socket InputStream with SO_TIMEOUT configured, this read() call blocks for the full timeout duration when there are no concatenated members (the common case for single-member GZIP streams). Previously, readTrailer() checked this.in.available() > 0 before attempting to read, which returned immediately on sockets with no buffered data.
Note:
Impact:
This is a silent performance regression for any application that wraps a socket InputStream in GZIPInputStream over a persistent connection -- a common pattern in message queue consumers, custom RPC protocols, and data streaming systems. There are no errors or warnings; the only symptom is degraded throughput.
REGRESSION : Last worked in version 21.0.8
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run to reproduce via docker (source code below)
# JDK 21.0.8 -- no blocking (17ms)
docker run --rm -v "$(pwd)/GzipSocketTest.java:/app/GzipSocketTest.java" -w /app \
eclipse-temurin:21.0.8_9-jdk bash -c "javac GzipSocketTest.java && java GzipSocketTest"
# JDK 21.0.9 -- blocks for full SO_TIMEOUT (5021ms)
docker run --rm -v "$(pwd)/GzipSocketTest.java:/app/GzipSocketTest.java" -w /app \
eclipse-temurin:21.0.9_10-jdk bash -c "javac GzipSocketTest.java && java GzipSocketTest"
---------- BEGIN SOURCE ----------
import java.io.*;
import java.net.*;
import java.util.zip.*;
public class GzipSocketTest
{
static final int SO_TIMEOUT_MS = 5_000;
public static void main(String[] args) throws Exception
{
System.out.println("Java version: " + System.getProperty("java.version"));
System.out.println("Java runtime: " + System.getProperty("java.runtime.version"));
System.out.println("SO_TIMEOUT: " + SO_TIMEOUT_MS + "ms");
System.out.println();
ServerSocket serverSocket = new ServerSocket(0);
int port = serverSocket.getLocalPort();
// Server: send a single GZIP member over a persistent connection
Thread serverThread = new Thread(() -> {
try
{
Socket server = serverSocket.accept();
GZIPOutputStream gzOut = new GZIPOutputStream(server.getOutputStream());
gzOut.write("line1\nline2\nline3\nline4\nline5\n".getBytes("UTF-8"));
gzOut.finish();
gzOut.flush();
System.out.println("[server] Sent 5 lines of GZIP data, keeping connection open...");
// Keep connection open (persistent connection, e.g. message queue dispatcher)
Thread.sleep(SO_TIMEOUT_MS + 5_000);
server.close();
}
catch (Exception e) { /* ignore */ }
});
serverThread.setDaemon(true);
serverThread.start();
// Client: read from socket with SO_TIMEOUT
Socket client = new Socket("localhost", port);
client.setSoTimeout(SO_TIMEOUT_MS);
GZIPInputStream gzIn = new GZIPInputStream(client.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(gzIn, "UTF-8"));
System.out.println("[client] Reading lines...");
long start = System.currentTimeMillis();
int lineCount = 0;
String line;
while ((line = reader.readLine()) != null)
{
lineCount++;
System.out.println("[client] Line " + lineCount
+ " (" + (System.currentTimeMillis() - start) + "ms): " + line);
}
long elapsed = System.currentTimeMillis() - start;
System.out.println();
System.out.println("Read " + lineCount + " lines in " + elapsed + "ms");
if (elapsed > SO_TIMEOUT_MS - 500)
System.out.println("BUG: blocked for ~SO_TIMEOUT (" + SO_TIMEOUT_MS + "ms)");
else
System.out.println("OK: completed quickly");
client.close();
serverSocket.close();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Wrap the socket InputStream in a BoundedInputStream (e.g., from Apache Commons IO) limited to the known compressed payload size before passing to GZIPInputStream. This causes readTrailer() to see immediate EOF from the bounded stream instead of blocking on the socket.
FREQUENCY :
ALWAYS
- caused by
-
JDK-7036144 GZIPInputStream readTrailer uses faulty available() test for end-of-stream
-
- Closed
-