8326498: java.net.http.HttpClient connection leak using http/2

Reviewed-by: vyazici, djelinski, dfuchs
This commit is contained in:
Jaikiran Pai 2025-11-25 11:13:59 +00:00
parent 67ef81eb78
commit c19b12927d
10 changed files with 854 additions and 275 deletions

View File

@ -27,11 +27,9 @@ package jdk.internal.net.http;
import java.io.IOException;
import java.net.ProtocolException;
import java.net.http.HttpClient.Version;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@ -708,12 +706,10 @@ final class Exchange<T> {
if (s == null) {
// s can be null if an exception occurred
// asynchronously while sending the preface.
Throwable t = c.getRecordedCause();
final Http2TerminationCause tc = c.getTerminationCause();
IOException ioe;
if (t != null) {
if (!cached)
c.close();
ioe = new IOException("Can't get stream 1: " + t, t);
if (tc != null) {
ioe = new IOException("Can't get stream 1", tc.getCloseCause());
} else {
ioe = new IOException("Can't get stream 1");
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,7 +25,6 @@
package jdk.internal.net.http;
import java.io.EOFException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Base64;
@ -234,11 +233,8 @@ class Http2ClientImpl {
}
}
private EOFException STOPPED;
void stop() {
if (debug.on()) debug.log("stopping");
STOPPED = new EOFException("HTTP/2 client stopped");
STOPPED.setStackTrace(new StackTraceElement[0]);
connectionPoolLock.lock();
try {
stopping = true;
@ -253,10 +249,7 @@ class Http2ClientImpl {
private boolean close(Http2Connection h2c) {
// close all streams
try { h2c.closeAllStreams(); } catch (Throwable t) {}
// send GOAWAY
try { h2c.close(); } catch (Throwable t) {}
// attempt graceful shutdown
try { h2c.shutdown(STOPPED); } catch (Throwable t) {}
// double check and close any new streams
try { h2c.closeAllStreams(); } catch (Throwable t) {}
// Allows for use of removeIf in stop()

View File

@ -25,21 +25,20 @@
package jdk.internal.net.http;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpHeaders;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.net.http.HttpConnectTimeoutException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@ -49,6 +48,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@ -56,6 +56,7 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
@ -130,7 +131,7 @@ import static jdk.internal.net.http.frame.SettingsFrame.MAX_HEADER_LIST_SIZE;
* and incoming stream creation (Server push). Incoming frames destined for a
* stream are provided by calling Stream.incoming().
*/
class Http2Connection {
class Http2Connection implements Closeable {
final Logger debug = Utils.getDebugLogger(this::dbgString);
static final Logger DEBUG_LOGGER =
@ -143,6 +144,8 @@ class Http2Connection {
private static final int MAX_SERVER_STREAM_ID = Integer.MAX_VALUE - 1; // 2147483646
// may be null; must be accessed/updated with the stateLock held
private IdleConnectionTimeoutEvent idleConnectionTimeoutEvent;
private final AtomicBoolean goAwaySent = new AtomicBoolean();
private final AtomicBoolean goAwayRecvd = new AtomicBoolean();
/**
* Flag set when no more streams to be opened on this connection.
@ -215,7 +218,7 @@ class Http2Connection {
}
/**
* {@link #shutdown(Throwable) Shuts down} the connection, unless this event is
* {@link #close(Http2TerminationCause) Closes} the connection, unless this event is
* {@link #cancelled}
*/
@Override
@ -228,26 +231,20 @@ class Http2Connection {
try {
if (cancelled) {
if (debug.on()) {
debug.log("Not initiating idle connection shutdown");
}
return;
}
if (!markIdleShutdownInitiated()) {
if (debug.on()) {
debug.log("Unexpected state %s, skipping idle connection shutdown",
describeClosedState(closedState));
debug.log("Idle timeout event already cancelled, not initiating idle connection close");
}
return;
}
// the connection has been idle long enough, we now
// mark a state indicating that the connection is chosen
// for idle termination and should not be handed out (from the pool)
// for newer requests.
connTerminator.markForIdleTermination();
} finally {
stateLock.unlock();
}
if (debug.on()) {
debug.log("Initiating shutdown of HTTP connection which is idle for too long");
}
HttpConnectTimeoutException hte = new HttpConnectTimeoutException(
"HTTP connection idle, no active streams. Shutting down.");
shutdown(hte);
// terminate the connection due to being idle long enough
connTerminator.idleTimedOut();
}
/**
@ -256,7 +253,7 @@ class Http2Connection {
void cancel() {
assert stateLock.isHeldByCurrentThread() : "Current thread doesn't hold " + stateLock;
// mark as cancelled to prevent potentially already triggered event from actually
// doing the shutdown
// doing the close
this.cancelled = true;
// cancel the timer to prevent the event from being triggered (if it hasn't already)
client().cancelTimer(this);
@ -376,16 +373,7 @@ class Http2Connection {
}
private static final int HALF_CLOSED_LOCAL = 1;
private static final int HALF_CLOSED_REMOTE = 2;
private static final int SHUTDOWN_REQUESTED = 4;
// state when idle connection management initiates a shutdown of the connection, after
// which the connection will go into SHUTDOWN_REQUESTED state
private static final int IDLE_SHUTDOWN_INITIATED = 8;
private final ReentrantLock stateLock = new ReentrantLock();
private volatile int closedState;
//-------------------------------------
final HttpConnection connection;
private final Http2ClientImpl client2;
private final ConcurrentHashMap<Integer,Stream<?>> streams = new ConcurrentHashMap<>();
@ -403,7 +391,9 @@ class Http2Connection {
private final Decoder hpackIn;
final SettingsFrame clientSettings;
private volatile SettingsFrame serverSettings;
private record PushContinuationState(PushPromiseDecoder pushContDecoder, PushPromiseFrame pushContFrame) {}
private volatile PushContinuationState pushContinuationState;
private final String key; // for HttpClientImpl.connections map
private final FramesDecoder framesDecoder;
@ -418,7 +408,7 @@ class Http2Connection {
private final FramesController framesController = new FramesController();
private final Http2TubeSubscriber subscriber;
final ConnectionWindowUpdateSender windowUpdater;
private final AtomicReference<Throwable> cause = new AtomicReference<>();
private final Terminator connTerminator = new Terminator();
private volatile Supplier<ByteBuffer> initial;
private volatile Stream<?> initialStream;
@ -640,35 +630,31 @@ class Http2Connection {
}
void abandonStream() {
boolean shouldClose = false;
stateLock.lock();
try {
long reserved = --numReservedClientStreams;
assert reserved >= 0;
if (finalStream && reserved == 0 && streams.isEmpty()) {
shouldClose = true;
}
} catch (Throwable t) {
shutdown(t); // in case the assert fires...
close(Http2TerminationCause.forException(t)); // in case the assert fires...
} finally {
stateLock.unlock();
}
// We should close the connection here if
// it's not pooled. If it's not pooled it will
// be marked final stream, reserved will be 0
// after decrementing it by one, and there should
// be no active request-response streams.
if (shouldClose) {
shutdown(new IOException("HTTP/2 connection abandoned"));
// if the connection is eligible to be closed, we close it here
if (shouldClose()) {
close(Http2TerminationCause.noErrorTermination());
}
}
boolean shouldClose() {
/*
* return true if the connection is marked as "final stream" and there
* are no active streams on that connection and the connection isn't
* reserved for a new stream.
*/
final boolean shouldClose() {
stateLock.lock();
try {
return finalStream() && streams.isEmpty();
return finalStream() && streams.isEmpty() && numReservedClientStreams == 0;
} finally {
stateLock.unlock();
}
@ -840,22 +826,8 @@ class Http2Connection {
return clientSettings.getParameter(MAX_CONCURRENT_STREAMS);
}
void close() {
if (markHalfClosedLocal()) {
// we send a GOAWAY frame only if the remote side hasn't already indicated
// the intention to close the connection by previously sending a GOAWAY of its own
if (connection.channel().isOpen() && !isMarked(closedState, HALF_CLOSED_REMOTE)) {
Log.logTrace("Closing HTTP/2 connection: to {0}", connection.address());
GoAwayFrame f = new GoAwayFrame(0,
ErrorFrame.NO_ERROR,
"Requested by user".getBytes(UTF_8));
// TODO: set last stream. For now zero ok.
sendFrame(f);
}
}
}
long count;
final void asyncReceive(ByteBuffer buffer) {
// We don't need to read anything and
// we don't want to send anything back to the server
@ -904,44 +876,26 @@ class Http2Connection {
} catch (Throwable e) {
String msg = Utils.stackTrace(e);
Log.logTrace(msg);
shutdown(e);
close(Http2TerminationCause.forException(e));
}
}
Throwable getRecordedCause() {
return cause.get();
/**
* Closes the connection normally (with a NO_ERROR termination cause), if not already closed.
*/
@Override
public final void close() {
close(Http2TerminationCause.noErrorTermination());
}
void shutdown(Throwable t) {
int state = closedState;
if (debug.on()) debug.log(() -> "Shutting down h2c (state="+describeClosedState(state)+"): " + t);
stateLock.lock();
try {
if (!markShutdownRequested()) return;
cause.compareAndSet(null, t);
} finally {
stateLock.unlock();
}
if (Log.errors()) {
if (t!= null && (!(t instanceof EOFException) || isActive())) {
Log.logError(t);
} else if (t != null) {
Log.logError("Shutting down connection: {0}", t.getMessage());
} else {
Log.logError("Shutting down connection");
}
}
client2.removeFromPool(this);
subscriber.stop(cause.get());
for (Stream<?> s : streams.values()) {
try {
s.connectionClosing(t);
} catch (Throwable e) {
Log.logError("Failed to close stream {0}: {1}", s.streamid, e);
}
}
connection.close(cause.get());
/**
* Closes the connection with the given termination cause, if not already closed.
*
* @param tc the termination cause. cannot be null.
*/
final void close(final Http2TerminationCause tc) {
Objects.requireNonNull(tc, "termination cause cannot be null");
this.connTerminator.terminate(tc);
}
/**
@ -981,15 +935,14 @@ class Http2Connection {
} else {
if (frame instanceof SettingsFrame) {
// The stream identifier for a SETTINGS frame MUST be zero
framesDecoder.close(
protocolError(ErrorFrame.PROTOCOL_ERROR,
"The stream identifier for a SETTINGS frame MUST be zero");
protocolError(GoAwayFrame.PROTOCOL_ERROR);
return;
}
if (frame instanceof PushPromiseFrame && !serverPushEnabled()) {
String protocolError = "received a PUSH_PROMISE when SETTINGS_ENABLE_PUSH is 0";
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
return;
}
@ -1144,7 +1097,9 @@ class Http2Connection {
// Otherwise, if the frame is dropped after having been added to the
// inputQ, releaseUnconsumed above should be called.
final void dropDataFrame(DataFrame df) {
if (isMarked(closedState, SHUTDOWN_REQUESTED)) return;
if (!isOpen()) {
return;
}
if (debug.on()) {
debug.log("Dropping data frame for stream %d (%d payload bytes)",
df.streamid(), df.payloadLength());
@ -1154,7 +1109,9 @@ class Http2Connection {
final void ensureWindowUpdated(DataFrame df) {
try {
if (isMarked(closedState, SHUTDOWN_REQUESTED)) return;
if (!isOpen()) {
return;
}
int length = df.payloadLength();
if (length > 0) {
windowUpdater.update(length);
@ -1251,12 +1208,16 @@ class Http2Connection {
case AltSvcFrame.TYPE -> processAltSvcFrame(0, (AltSvcFrame) frame,
connection, connection.client());
default -> protocolError(ErrorFrame.PROTOCOL_ERROR);
default -> protocolError(ErrorFrame.PROTOCOL_ERROR, "unknown frame: " + frame);
}
}
boolean isOpen() {
return !isMarkedForShutdown() && connection.channel().isOpen();
/**
* Returns true if this connection hasn't been terminated and the underlying
* {@linkplain NetworkChannel#isOpen() channel is open}. false otherwise.
*/
final boolean isOpen() {
return this.connTerminator.terminationCause.get() == null && connection.channel().isOpen();
}
void resetStream(int streamid, int code) {
@ -1295,6 +1256,7 @@ class Http2Connection {
}
}
private void decrementStreamsCount0(int streamid) {
Stream<?> s = streams.get(streamid);
if (s == null || !s.deRegister())
@ -1346,8 +1308,7 @@ class Http2Connection {
// corresponding entry in the window controller.
windowController.removeStream(streamid);
}
if (finalStream() && streams.isEmpty()) {
// should be only 1 stream, but there might be more if server push
if (shouldClose()) {
close();
} else {
// Start timer if property present and not already created
@ -1372,9 +1333,7 @@ class Http2Connection {
/**
* Increments this connection's send Window by the amount in the given frame.
*/
private void handleWindowUpdate(WindowUpdateFrame f)
throws IOException
{
private void handleWindowUpdate(WindowUpdateFrame f) {
int amount = f.getUpdate();
if (amount <= 0) {
// ## temporarily disable to workaround a bug in Jetty where it
@ -1383,37 +1342,19 @@ class Http2Connection {
} else {
boolean success = windowController.increaseConnectionWindow(amount);
if (!success) {
protocolError(ErrorFrame.FLOW_CONTROL_ERROR); // overflow
protocolError(ErrorFrame.FLOW_CONTROL_ERROR, null); // overflow
}
}
}
private void protocolError(int errorCode)
throws IOException
{
protocolError(errorCode, null);
private void protocolError(final int errorCode, final String msg) {
final Http2TerminationCause terminationCause =
Http2TerminationCause.forH2Error(errorCode, msg);
framesDecoder.close(terminationCause.getLogMsg());
close(terminationCause);
}
private void protocolError(int errorCode, String msg)
throws IOException
{
String protocolError = "protocol error" + (msg == null?"":(": " + msg));
ProtocolException protocolException =
new ProtocolException(protocolError);
this.cause.compareAndSet(null, protocolException);
if (markHalfClosedLocal()) {
framesDecoder.close(protocolError);
subscriber.stop(protocolException);
if (debug.on()) debug.log("Sending GOAWAY due to " + protocolException);
GoAwayFrame frame = new GoAwayFrame(0, errorCode);
sendFrame(frame);
}
shutdown(protocolException);
}
private void handleSettings(SettingsFrame frame)
throws IOException
{
private void handleSettings(SettingsFrame frame) {
assert frame.streamid() == 0;
if (!frame.getFlag(SettingsFrame.ACK)) {
int newWindowSize = frame.getParameter(INITIAL_WINDOW_SIZE);
@ -1430,9 +1371,7 @@ class Http2Connection {
}
}
private void handlePing(PingFrame frame)
throws IOException
{
private void handlePing(PingFrame frame) {
frame.setFlag(PingFrame.ACK);
sendUnorderedFrame(frame);
}
@ -1442,7 +1381,7 @@ class Http2Connection {
assert lastProcessedStream >= 0 : "unexpected last stream id: "
+ lastProcessedStream + " in GOAWAY frame";
markHalfClosedRemote();
goAwayRecvd.set(true);
setFinalStream(); // don't allow any new streams on this connection
if (debug.on()) {
debug.log("processing incoming GOAWAY with last processed stream id:%s in frame %s",
@ -1599,20 +1538,20 @@ class Http2Connection {
// must be done with "stateLock" held to co-ordinate idle connection management
stateLock.lock();
try {
cancelIdleShutdownEvent();
// consider the reservation successful only if the connection's state hasn't moved
// to "being closed"
return isOpen();
cancelIdleCloseEvent();
// consider the reservation successful only if the connection is open and
// hasn't been chosen for idle termination
return !this.connTerminator.isMarkedForIdleTermination() && isOpen();
} finally {
stateLock.unlock();
}
}
/**
* Cancels any event that might have been scheduled to shutdown this connection. Must be called
* Cancels any event that might have been scheduled to close this connection. Must be called
* with the stateLock held.
*/
private void cancelIdleShutdownEvent() {
private void cancelIdleCloseEvent() {
assert stateLock.isHeldByCurrentThread() : "Current thread doesn't hold " + stateLock;
if (idleConnectionTimeoutEvent == null) {
return;
@ -1627,20 +1566,23 @@ class Http2Connection {
// the stream is closed.
stateLock.lock();
try {
if (!isMarkedForShutdown()) {
if (isOpen() && !this.connTerminator.isMarkedForIdleTermination()) {
if (debug.on()) {
debug.log("Opened stream %d", streamid);
}
client().streamReference();
streams.put(streamid, stream);
cancelIdleShutdownEvent();
// don't consider the connection idle anymore
cancelIdleCloseEvent();
return;
}
} finally {
stateLock.unlock();
}
if (debug.on()) debug.log("connection closed: closing stream %d", stream);
stream.cancel(new IOException("Stream " + streamid + " cancelled", cause.get()));
final Http2TerminationCause terminationCause = getTerminationCause();
assert terminationCause != null : "termination cause is null";
stream.cancel(new IOException("Stream " + streamid + " cancelled", terminationCause.getCloseCause()));
}
/**
@ -1743,11 +1685,10 @@ class Http2Connection {
int streamid = nextstreamid;
Throwable cause = null;
synchronized (this) {
if (isMarked(closedState, SHUTDOWN_REQUESTED)) {
cause = this.cause.get();
if (cause == null) {
cause = new IOException("Connection closed");
}
if (!isOpen()) {
final Http2TerminationCause terminationCause = getTerminationCause();
assert terminationCause != null : "termination cause is null";
cause = terminationCause.getCloseCause();
}
}
if (cause != null) {
@ -1762,7 +1703,7 @@ class Http2Connection {
return stream;
} else {
stream.cancelImpl(new IOException("Request cancelled"));
if (finalStream() && streams.isEmpty()) {
if (shouldClose()) {
close();
}
return null;
@ -1795,14 +1736,12 @@ class Http2Connection {
}
publisher.signalEnqueued();
} catch (IOException e) {
if (!isMarked(closedState, SHUTDOWN_REQUESTED)) {
if (!client2.stopping()) {
Log.logError(e);
shutdown(e);
} else if (debug.on()) {
debug.log("Failed to send %s while stopping: %s", frame, e);
}
if (!client2.stopping()) {
Log.logError(e);
} else if (debug.on()) {
debug.log("Failed to send %s while stopping: %s", frame, e);
}
close(Http2TerminationCause.forException(e));
}
}
@ -1817,14 +1756,12 @@ class Http2Connection {
publisher.enqueue(encodeFrame(frame));
publisher.signalEnqueued();
} catch (IOException e) {
if (!isMarked(closedState, SHUTDOWN_REQUESTED)) {
if (!client2.stopping()) {
Log.logError(e);
shutdown(e);
} else if (debug.on()) {
debug.log("Failed to send %s while stopping: %s", frame, e);
}
if (!client2.stopping()) {
Log.logError(e);
} else if (debug.on()) {
debug.log("Failed to send %s while stopping: %s", frame, e);
}
close(Http2TerminationCause.forException(e));
}
}
@ -1839,10 +1776,12 @@ class Http2Connection {
publisher.enqueueUnordered(encodeFrame(frame));
publisher.signalEnqueued();
} catch (IOException e) {
if (!isMarked(closedState, SHUTDOWN_REQUESTED)) {
if (!client2.stopping()) {
Log.logError(e);
shutdown(e);
} else if (debug.on()) {
debug.log("Failed to send %s while stopping: %s", frame, e);
}
close(Http2TerminationCause.forException(e));
}
}
@ -1868,6 +1807,7 @@ class Http2Connection {
try {
while (!queue.isEmpty() && !scheduler.isStopped()) {
ByteBuffer buffer = queue.poll();
assert buffer != null : "null buffer obtained from non-empty queue";
if (debug.on())
debug.log("sending %d to Http2Connection.asyncReceive",
buffer.remaining());
@ -1877,7 +1817,12 @@ class Http2Connection {
errorRef.compareAndSet(null, t);
} finally {
Throwable x = errorRef.get();
if (x != null) {
// if there was any error or if the TubeSubscriber completed normally,
// then close the connection
if (x != null || completed) {
// although the connection terminator stops the scheduler too,
// we don't want to wait that "long" and instead we should immediately
// stop the scheduler so that we don't enter "processQueue" anymore.
scheduler.stop();
if (client2.stopping()) {
if (debug.on()) {
@ -1888,7 +1833,11 @@ class Http2Connection {
debug.log("Stopping scheduler", x);
}
}
Http2Connection.this.shutdown(x);
// terminate the connection
final Http2TerminationCause tc = (x != null)
? Http2TerminationCause.forException(x)
: Http2TerminationCause.noErrorTermination();
Http2Connection.this.close(tc);
}
}
}
@ -1938,11 +1887,17 @@ class Http2Connection {
@Override
public void onComplete() {
if (completed) return;
String msg = isActive()
? "EOF reached while reading"
: "Idle connection closed by HTTP/2 peer";
if (debug.on()) debug.log(msg);
errorRef.compareAndSet(null, new EOFException(msg));
if (isActive()) {
final String msg = "EOF reached while reading";
errorRef.compareAndSet(null, new EOFException(msg));
if (debug.on()) {
debug.log(msg);
}
} else {
if (debug.on()) {
debug.log("HTTP/2 connection (with no active streams) closed by peer");
}
}
completed = true;
runOrSchedule();
}
@ -1955,16 +1910,13 @@ class Http2Connection {
dropped = true;
}
void stop(Throwable error) {
if (errorRef.compareAndSet(null, error)) {
completed = true;
scheduler.stop();
queue.clear();
if (subscription != null) {
subscription.cancel();
}
queue.clear();
private void close() {
scheduler.stop();
queue.clear();
if (subscription != null) {
subscription.cancel();
}
queue.clear();
}
}
@ -1990,6 +1942,7 @@ class Http2Connection {
static final class ConnectionWindowUpdateSender extends WindowUpdateSender {
final int initialWindowSize;
public ConnectionWindowUpdateSender(Http2Connection connection,
int initialWindowSize) {
super(connection, initialWindowSize);
@ -2004,13 +1957,9 @@ class Http2Connection {
@Override
protected boolean windowSizeExceeded(long received) {
if (connection.isOpen()) {
try {
connection.protocolError(ErrorFrame.FLOW_CONTROL_ERROR,
"connection window exceeded (%s > %s)"
.formatted(received, windowSize));
} catch (IOException io) {
connection.shutdown(io);
}
connection.protocolError(ErrorFrame.FLOW_CONTROL_ERROR,
"connection window exceeded (%s > %s)"
.formatted(received, windowSize));
}
return true;
}
@ -2033,72 +1982,144 @@ class Http2Connection {
}
}
private boolean isMarked(int state, int mask) {
return (state & mask) == mask;
}
private boolean isMarkedForShutdown() {
final int closedSt = closedState;
return isMarked(closedSt, IDLE_SHUTDOWN_INITIATED)
|| isMarked(closedSt, SHUTDOWN_REQUESTED);
}
private boolean markShutdownRequested() {
return markClosedState(SHUTDOWN_REQUESTED);
}
private boolean markHalfClosedLocal() {
return markClosedState(HALF_CLOSED_LOCAL);
}
private boolean markHalfClosedRemote() {
return markClosedState(HALF_CLOSED_REMOTE);
}
private boolean markIdleShutdownInitiated() {
return markClosedState(IDLE_SHUTDOWN_INITIATED);
}
private boolean markClosedState(int flag) {
int state, desired;
do {
state = desired = closedState;
if ((state & flag) == flag) return false;
desired = state | flag;
} while (!CLOSED_STATE.compareAndSet(this, state, desired));
return true;
}
String describeClosedState(int state) {
if (state == 0) return "active";
String desc = null;
if (isMarked(state, IDLE_SHUTDOWN_INITIATED)) {
desc = "idle-shutdown-initiated";
private void sendGoAway(final GoAwayFrame goAway) {
// currently we send a GOAWAY just once irrespective of what value the
// last stream id was in the GOAWAY frame
if (!goAwaySent.compareAndSet(false, true)) {
// already sent
return;
}
if (isMarked(state, SHUTDOWN_REQUESTED)) {
desc = desc == null ? "shutdown" : desc + "+shutdown";
if (Log.trace()) {
Log.logTrace("{0} sending GOAWAY {1}", connection, goAway);
} else if (debug.on()) {
debug.log("sending GOAWAY " + goAway);
}
if (isMarked(state, HALF_CLOSED_LOCAL | HALF_CLOSED_REMOTE)) {
if (desc == null) return "closed";
else return desc + "+closed";
}
if (isMarked(state, HALF_CLOSED_LOCAL)) {
if (desc == null) return "half-closed-local";
else return desc + "+half-closed-local";
}
if (isMarked(state, HALF_CLOSED_REMOTE)) {
if (desc == null) return "half-closed-remote";
else return desc + "+half-closed-remote";
}
return "0x" + Integer.toString(state, 16);
// this merely enqueues the frame
sendFrame(goAway);
}
private static final VarHandle CLOSED_STATE;
static {
try {
CLOSED_STATE = MethodHandles.lookup().findVarHandle(Http2Connection.class, "closedState", int.class);
} catch (Exception x) {
throw new ExceptionInInitializerError(x);
/**
* Returns the termination cause if the connection is closed, else returns null.
*/
final Http2TerminationCause getTerminationCause() {
return this.connTerminator.determineTerminationCause();
}
// Responsible for doing all the necessary work for closing a Http2Connection
private final class Terminator {
// the cause for closing the connection. Must only be set in the
// Terminator.terminate(Http2TerminationCause) method.
private final AtomicReference<Http2TerminationCause> terminationCause = new AtomicReference<>();
// true if it has been decided to terminate the connection due to being idle,
// false otherwise. should be accessed only when holding the stateLock
private boolean chosenForIdleTermination;
private void terminate(final Http2TerminationCause terminationCause) {
Objects.requireNonNull(terminationCause, "termination cause cannot be null");
// allow to be terminated only once
stateLock.lock();
try {
final boolean success = this.terminationCause.compareAndSet(null, terminationCause);
if (!success) {
// already terminated or is being terminated by some other thread
return;
}
// disable the idle timeout event, since we are now going to terminate the
// connection
Http2Connection.this.cancelIdleCloseEvent();
} finally {
stateLock.unlock();
}
// do the actual termination
doTerminate();
}
private void doTerminate() {
final Http2TerminationCause tc = terminationCause.get();
assert tc != null : "missing termination cause";
// we send a GOAWAY frame only if the remote side hasn't already indicated
// the intention to close the connection by previously sending a GOAWAY of its own
if (!Http2Connection.this.goAwayRecvd.get()) {
final int lastStream = 0; // TODO: set last stream. For now zero is ok.
final String peerVisibleReason = tc.getPeerVisibleReason();
final GoAwayFrame goAway;
if (peerVisibleReason == null) {
goAway = new GoAwayFrame(lastStream, tc.getCloseCode());
} else {
goAway = new GoAwayFrame(lastStream, tc.getCloseCode(),
peerVisibleReason.getBytes(UTF_8));
}
sendGoAway(goAway);
}
// now close the connection
if (Log.errors() || debug.on()) {
final String stateStr = "Abnormal close=" + tc.isAbnormalClose() +
", has active streams=" + isActive() +
", GOAWAY received=" + goAwayRecvd.get() +
", GOAWAY sent=" + goAwaySent.get();
if (Log.errors()) {
Log.logError("Closing connection {0} ({1}) due to: {2}",
connection, stateStr, tc);
} else {
debug.log("Closing connection (" + stateStr + ") due to: " + tc);
}
}
// close the TubeSubscriber
subscriber.close();
client2.removeFromPool(Http2Connection.this);
// notify the HTTP/2 streams of the connection closure
for (final Stream<?> s : streams.values()) {
try {
s.connectionClosing(tc.getCloseCause());
} catch (Throwable e) {
Log.logError("Failed to close stream {0}: {1}", s.streamid, e);
}
}
// close the underlying connection
connection.close(tc.getCloseCause());
}
private void markForIdleTermination() {
assert stateLock.isHeldByCurrentThread() : Thread.currentThread()
+ " not holding stateLock";
this.chosenForIdleTermination = true;
}
private boolean isMarkedForIdleTermination() {
assert stateLock.isHeldByCurrentThread() : Thread.currentThread()
+ " not holding stateLock";
return this.chosenForIdleTermination;
}
private void idleTimedOut() {
if (debug.on()) {
debug.log("closing connection due to being idle");
}
this.terminate(Http2TerminationCause.idleTimedOut());
}
/**
* Returns the termination cause for the connection. This method guarantees that if the
* {@linkplain Http2Connection#isOpen() connection is not open}, when this method is called,
* then it returns a non-null termination cause. Returns null if the connection is open.
*/
private Http2TerminationCause determineTerminationCause() {
final Http2TerminationCause tc = this.terminationCause.get();
if (tc != null) {
// already terminated, return the cause
return tc;
}
if (!connection.channel().isOpen()) {
// if the underlying SocketChannel isn't open, then terminate the connection.
// that way when Http2Connection.isOpen() returns false in that situation, then this
// getTerminationCause() will return a termination cause.
terminate(Http2TerminationCause.forException(new ClosedChannelException()));
final Http2TerminationCause terminated = this.terminationCause.get();
assert terminated != null : "missing termination cause";
return terminated;
}
return null; // connection still open
}
}
}

View File

@ -0,0 +1,281 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.net.http;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Objects;
import jdk.internal.net.http.common.Utils;
import jdk.internal.net.http.frame.ErrorFrame;
/**
* Termination cause for an {@linkplain Http2Connection HTTP/2 connection}
*/
public abstract sealed class Http2TerminationCause {
private String logMsg;
private String peerVisibleReason;
private final int closeCode;
private final Throwable originalCause;
private final IOException reportedCause;
private Http2TerminationCause(final int closeCode, final Throwable closeCause) {
this.closeCode = closeCode;
this.originalCause = closeCause;
if (closeCause != null) {
this.logMsg = closeCause.toString();
}
this.reportedCause = toReportedCause(this.originalCause, this.logMsg);
}
private Http2TerminationCause(final int closeCode, final String loggedAs) {
this.closeCode = closeCode;
this.originalCause = null;
this.logMsg = loggedAs;
this.reportedCause = toReportedCause(null, this.logMsg);
}
/**
* Returns the error code (specified for HTTP/2 ErrorFrame) that caused the
* connection termination.
*/
public final int getCloseCode() {
return this.closeCode;
}
/**
* Returns the {@link IOException} that is considered the cause of the connection termination.
* Even a {@linkplain #isAbnormalClose() normal} termination will have
* an {@code IOException} associated with it, so this method will always return a non-null instance.
*/
public final IOException getCloseCause() {
return this.reportedCause;
}
/**
* Returns {@code true} if the connection was terminated due to some exception. {@code false}
* otherwise.
* A normal connection termination (for example, the connection idle timing out locally)
* is not considered as an abnormal termination and this method returns {@code false} for
* such cases.
*/
public abstract boolean isAbnormalClose();
/**
* Returns the connection termination cause, represented as a string. Unlike the
* {@linkplain #getPeerVisibleReason() peer-visible reason}, this log message will not be
* sent across to the peer and it is thus allowed to include additional details that might
* help debugging a connection termination.
*/
public final String getLogMsg() {
return logMsg;
}
/**
* Returns the connection termination cause, represented as a string. This represents the
* "debugData" that is sent to the peer in a
* {@linkplain jdk.internal.net.http.frame.GoAwayFrame GOAWAY frame}.
*/
public final String getPeerVisibleReason() {
return this.peerVisibleReason;
}
/**
* Sets the connection termination cause, represented as a string, which will be sent
* to the peer in a {@linkplain jdk.internal.net.http.frame.GoAwayFrame GOAWAY frame}.
* Unlike the {@link #getLogMsg() log message},
* it is expected that this peer-visible reason will not contain anything that is not meant
* to be viewed by the peer.
*/
protected final void setPeerVisibleReason(final String reasonPhrase) {
this.peerVisibleReason = reasonPhrase;
}
/**
* Returns a connection termination cause that represents an
* {@linkplain #isAbnormalClose() abnormal} termination due to the given {@code cause}.
*
* @param cause the termination cause, cannot be null.
*/
public static Http2TerminationCause forException(final Throwable cause) {
Objects.requireNonNull(cause);
if (cause instanceof ProtocolException pe) {
return new ProtocolError(pe);
}
return new InternalError(cause);
}
/**
* Returns a connection termination cause that represents a
* {@linkplain #isAbnormalClose() normal} termination.
*/
public static Http2TerminationCause noErrorTermination() {
return NoError.INSTANCE;
}
/**
* Returns a connection termination cause that represents a
* {@linkplain #isAbnormalClose() normal} termination due to the connection
* being idle.
*/
public static Http2TerminationCause idleTimedOut() {
return NoError.IDLE_TIMED_OUT;
}
/**
* Returns a connection termination cause that represents an
* {@linkplain #isAbnormalClose() abnormal} termination due to the given {@code errorCode}.
* Although this method does no checks for the {@code errorCode}, it is expected to be one
* of the error codes specified by the HTTP/2 RFC for the ErrorFrame.
*
* @param errorCode the error code
* @param loggedAs optional log message to be associated with this termination cause
*/
public static Http2TerminationCause forH2Error(final int errorCode, final String loggedAs) {
if (errorCode == ErrorFrame.PROTOCOL_ERROR) {
return new ProtocolError(loggedAs);
} else if (errorCode == ErrorFrame.FLOW_CONTROL_ERROR) {
// we treat flow control error as a protocol error currently
return new ProtocolError(loggedAs, true);
}
return new H2StandardError(errorCode, loggedAs);
}
private static IOException toReportedCause(final Throwable original,
final String fallbackExceptionMsg) {
if (original == null) {
return fallbackExceptionMsg == null
? new IOException("connection terminated")
: new IOException(fallbackExceptionMsg);
} else if (original instanceof IOException ioe) {
return ioe;
} else {
return Utils.toIOException(original);
}
}
private static final class NoError extends Http2TerminationCause {
private static final IOException NO_ERROR_MARKER =
new IOException("HTTP/2 connection closed normally - no error");
private static final IOException NO_ERROR_IDLE_TIMED_OUT_MARKER =
new IOException("HTTP/2 connection idle timed out - no error");
static {
// remove the stacktrace from the marker exception instances
NO_ERROR_MARKER.setStackTrace(new StackTraceElement[0]);
NO_ERROR_IDLE_TIMED_OUT_MARKER.setStackTrace(new StackTraceElement[0]);
}
private static final NoError INSTANCE = new NoError(false);
private static final NoError IDLE_TIMED_OUT = new NoError(true);
private final boolean idleTimedOut;
private NoError(final boolean idleTimedOut) {
super(ErrorFrame.NO_ERROR,
idleTimedOut ? NO_ERROR_IDLE_TIMED_OUT_MARKER : NO_ERROR_MARKER);
this.idleTimedOut = idleTimedOut;
setPeerVisibleReason(idleTimedOut ? "idle timed out" : "no error");
}
@Override
public boolean isAbnormalClose() {
return false;
}
@Override
public String toString() {
return this.idleTimedOut
? "No error - idle timed out"
: "No error - normal termination";
}
}
private static sealed class H2StandardError extends Http2TerminationCause {
private H2StandardError(final int errCode, final String msg) {
super(errCode, msg);
setPeerVisibleReason(ErrorFrame.stringForCode(errCode));
}
private H2StandardError(final int errCode, final Throwable cause) {
super(errCode, cause);
setPeerVisibleReason(ErrorFrame.stringForCode(errCode));
}
@Override
public boolean isAbnormalClose() {
return getCloseCode() != ErrorFrame.NO_ERROR;
}
@Override
public String toString() {
return ErrorFrame.stringForCode(this.getCloseCode());
}
}
private static final class ProtocolError extends H2StandardError {
private ProtocolError(final String msg) {
this(msg, false);
}
private ProtocolError(final String msg, final boolean flowControlError) {
super(flowControlError
? ErrorFrame.FLOW_CONTROL_ERROR
: ErrorFrame.PROTOCOL_ERROR,
new ProtocolException(msg));
}
private ProtocolError(final ProtocolException pe) {
super(ErrorFrame.PROTOCOL_ERROR, pe);
}
@Override
public boolean isAbnormalClose() {
return true;
}
@Override
public String toString() {
return "Protocol error - " + this.getLogMsg();
}
}
private static final class InternalError extends Http2TerminationCause {
private InternalError(final Throwable cause) {
super(ErrorFrame.INTERNAL_ERROR, cause);
}
@Override
public boolean isAbnormalClose() {
return true;
}
@Override
public String toString() {
return "Internal error - " + this.getLogMsg();
}
}
}

View File

@ -540,9 +540,7 @@ abstract class HttpConnection implements Closeable {
* Closes this connection due to the given cause.
* @param cause the cause for which the connection is closed, may be null
*/
void close(Throwable cause) {
close();
}
abstract void close(Throwable cause);
/**
* {@return the underlying connection flow, if applicable}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.net.http;
import java.lang.reflect.Field;
import java.net.http.HttpClient;
import java.util.Objects;
import java.util.Set;
public final class HttpClientImplAccess {
private static final Field openedConnections; // Set<> jdk.internal.net.http.HttpClientImpl#openedConnections
static {
try {
openedConnections = Class.forName("jdk.internal.net.http.HttpClientImpl")
.getDeclaredField("openedConnections");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private HttpClientImplAccess() {
throw new AssertionError();
}
private static HttpClientImpl impl(final HttpClient client) {
if (client instanceof HttpClientImpl impl) return impl;
if (client instanceof HttpClientFacade facade) return facade.impl;
return null;
}
/**
* Returns the {@code jdk.internal.net.http.HttpClientImpl#openedConnections Set}.
* Returns null if the underlying client isn't of type jdk.internal.net.http.HttpClientImpl.
*/
public static Set<?> getOpenedConnections(final HttpClient client)
throws IllegalAccessException {
Objects.requireNonNull(client, "client");
final HttpClientImpl clientImpl = impl(client);
if (clientImpl == null) {
return null;
}
openedConnections.setAccessible(true);
return (Set<?>) openedConnections.get(clientImpl);
}
}

View File

@ -0,0 +1,208 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.net.ssl.SSLSession;
import jdk.httpclient.test.lib.http2.BodyOutputStream;
import jdk.httpclient.test.lib.http2.Http2Handler;
import jdk.httpclient.test.lib.http2.Http2TestExchange;
import jdk.httpclient.test.lib.http2.Http2TestExchangeSupplier;
import jdk.httpclient.test.lib.http2.Http2TestServer;
import jdk.httpclient.test.lib.http2.Http2TestServerConnection;
import jdk.internal.net.http.HttpClientImplAccess;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.test.lib.net.URIBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.Version.HTTP_2;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*
* @test
* @bug 8326498 8361091
* @summary verify that the HttpClient does not leak connections when dealing with
* sudden rush of HTTP/2 requests
* @library /test/lib /test/jdk/java/net/httpclient/lib ../access
* @build jdk.test.lib.net.SimpleSSLContext
* jdk.httpclient.test.lib.http2.Http2TestServer
* jdk.httpclient.test.lib.http2.Http2Handler
* jdk.httpclient.test.lib.http2.Http2TestExchange
* jdk.httpclient.test.lib.http2.Http2TestExchangeSupplier
* java.net.http/jdk.internal.net.http.HttpClientImplAccess
* @run junit ${test.main.class}
*/
class BurstyRequestsTest {
private static final String HANDLER_PATH = "/8326498/";
// we use a h2c server but it doesn't matter if it is h2c or h2
private static Http2TestServer http2Server;
@BeforeAll
static void beforeAll() throws Exception {
http2Server = new Http2TestServer(false, 0);
http2Server.setExchangeSupplier(new ExchangeSupplier());
http2Server.addHandler(new Handler(), HANDLER_PATH);
http2Server.start();
System.err.println("started HTTP/2 server " + http2Server.getAddress());
}
@AfterAll
static void afterAll() {
if (http2Server != null) {
System.err.println("stopping server " + http2Server.getAddress());
http2Server.stop();
}
}
/*
* Issues a burst of HTTP/2 requests to the same server (host/port) and expects all of
* them to complete normally.
* Once these requests have completed, the test then peeks into an internal field of the
* HttpClientImpl to verify that the client is holding on to at most 1 connection.
*/
@Test
void testOpenConnections() throws Exception {
final URI reqURI = URIBuilder.newBuilder()
.scheme("http")
.host(http2Server.getAddress().getAddress())
.port(http2Server.getAddress().getPort())
.path(HANDLER_PATH)
.build();
final HttpRequest req = HttpRequest.newBuilder().uri(reqURI).build();
final int numRequests = 20;
// latch for the tasks to wait on, before issuing the requests
final CountDownLatch startLatch = new CountDownLatch(numRequests);
final List<Future<Void>> futures = new ArrayList<>();
try (final ExecutorService executor = Executors.newCachedThreadPool();
final HttpClient client = HttpClient.newBuilder()
.proxy(NO_PROXY)
.version(HTTP_2)
.build()) {
// our test needs to peek into the internal field of jdk.internal.net.http.HttpClientImpl
final Set<?> openedConnections = HttpClientImplAccess.getOpenedConnections(client);
assertNotNull(openedConnections, "HttpClientImpl#openedConnections field is null or not available");
for (int i = 0; i < numRequests; i++) {
final Future<Void> f = executor.submit(new RequestIssuer(startLatch, client, req));
futures.add(f);
}
// wait for the requests to complete
for (final Future<Void> f : futures) {
f.get();
}
System.err.println("all " + numRequests + " requests completed successfully");
// the request completion happens asynchronously to the closing of the HTTP/2 Stream
// as well as the HTTP/2 connection. we wait for at most 1 connection to be retained
// by HttpClientImpl.
System.err.println("waiting for at least " + (numRequests - 1) + " connections to be closed");
// now verify that the current open TCP connections is not more than 1.
// we let the test timeout if we never reach that count.
int size = openedConnections.size();
System.err.println("currently " + size + " open connections: " + openedConnections);
while (size > 1) {
// wait
Thread.sleep(100);
final int prev = size;
size = openedConnections.size();
if (prev != size) {
System.err.println("currently " + size + " open connections: " + openedConnections);
}
}
// we expect at most 1 connection will stay open
assertTrue((size == 0 || size == 1),
"unexpected number of current open connections: " + size);
}
}
private static final class RequestIssuer implements Callable<Void> {
private final CountDownLatch startLatch;
private final HttpClient client;
private final HttpRequest request;
private RequestIssuer(final CountDownLatch startLatch, final HttpClient client,
final HttpRequest request) {
this.startLatch = startLatch;
this.client = client;
this.request = request;
}
@Override
public Void call() throws Exception {
this.startLatch.countDown(); // announce our arrival
this.startLatch.await(); // wait for other threads to arrive
// issue the request
final HttpResponse<Void> resp = this.client.send(request, BodyHandlers.discarding());
if (resp.statusCode() != 200) {
throw new AssertionError("unexpected response status code: " + resp.statusCode());
}
return null;
}
}
private static final class Handler implements Http2Handler {
private static final int NO_RESP_BODY = -1;
@Override
public void handle(final Http2TestExchange exchange) throws IOException {
System.err.println("handling request " + exchange.getRequestURI());
exchange.sendResponseHeaders(200, NO_RESP_BODY);
}
}
private static final class ExchangeSupplier implements Http2TestExchangeSupplier {
@Override
public Http2TestExchange get(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
// don't close the connection when/if the client sends a GOAWAY
conn.closeConnOnIncomingGoAway = false;
return Http2TestExchangeSupplier.ofDefault().get(streamid, method, reqheaders,
rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -250,11 +250,15 @@ public class H2GoAwayTest {
} catch (ExecutionException ee) {
final Throwable cause = ee.getCause();
if (!(cause instanceof IOException ioe)) {
System.err.println("unexpected exception: " + cause
+ ", for request " + REQ_URI_BASE + reqQueryPart);
throw cause;
}
// verify it failed for the right reason
if (ioe.getMessage() == null
|| !ioe.getMessage().contains("request not processed by peer")) {
System.err.println("unexpected exception message: " + ioe.getMessage()
+ ", for request " + REQ_URI_BASE + reqQueryPart);
// propagate the original failure
throw ioe;
}

View File

@ -117,6 +117,7 @@ public class Http2TestServerConnection {
final Properties properties;
volatile boolean stopping;
volatile int nextPushStreamId = 2;
public volatile boolean closeConnOnIncomingGoAway = true;
ConcurrentLinkedQueue<PingRequest> pings = new ConcurrentLinkedQueue<>();
// the max stream id of a processed H2 request. -1 implies none were processed.
private final AtomicInteger maxProcessedRequestStreamId = new AtomicInteger(-1);
@ -537,8 +538,12 @@ public class Http2TestServerConnection {
outputQ.put(frame);
return;
} else if (f instanceof GoAwayFrame) {
System.err.println(server.name + ": Closing connection: "+ f.toString());
close(ErrorFrame.NO_ERROR);
if (closeConnOnIncomingGoAway) {
System.err.println(server.name + ": Closing connection: "+ f.toString());
close(ErrorFrame.NO_ERROR);
} else {
System.err.println(server.name + ": Will not close connection for incoming GOAWAY: " + f);
}
} else if (f instanceof PingFrame) {
handlePing((PingFrame)f);
} else

View File

@ -501,9 +501,15 @@ public class ConnectionPoolTest {
@Override SocketChannel channel() {return channel;}
@Override
public void close() {
closed=finished=true;
System.out.println("closed: " + this);
this.close(null);
}
@Override
void close(final Throwable cause) {
closed=finished=true;
System.out.println("closed: " + this + " cause: " + cause);
}
@Override
public String toString() {
return "HttpConnectionStub: " + address + " proxy: " + proxy;