Skip to content

Instantly share code, notes, and snippets.

@janhoy
Created July 4, 2026 01:42
Show Gist options
  • Select an option

  • Save janhoy/fdc807bd2436f0d1aa94de6a3fab80b9 to your computer and use it in GitHub Desktop.

Select an option

Save janhoy/fdc807bd2436f0d1aa94de6a3fab80b9 to your computer and use it in GitHub Desktop.
Repro: Jetty 12.1.10 HTTP/2 client loses in-flight response during server graceful shutdown (regression from jetty/jetty.project#15134)
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.InputStreamResponseListener;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.GracefulHandler;
import org.eclipse.jetty.util.Callback;
/**
* Standalone repro: in-flight h2c response truncated when Server.stop() runs with
* GracefulHandler + stopTimeout (mimics Solr's graceful.mod / jetty-graceful.xml).
*
* Run with -Dhttp1=true to use an HTTP/1.1 client instead of h2c.
*/
public class GracefulRepro {
static final byte[] PAYLOAD = new byte[256 * 1024];
public static void main(String[] args) throws Exception {
Arrays.fill(PAYLOAD, (byte) 'x');
int iters = Integer.getInteger("iters", 30);
boolean http1 = Boolean.getBoolean("http1");
int failures = 0;
for (int i = 0; i < iters; i++) {
if (!runOnce(i, http1)) failures++;
}
System.out.println(
"RESULT: "
+ failures
+ "/"
+ iters
+ " truncated, protocol="
+ (http1 ? "HTTP/1.1" : "h2c")
+ ", jetty="
+ org.eclipse.jetty.util.Jetty.VERSION);
}
static boolean runOnce(int iter, boolean http1) throws Exception {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch gate = new CountDownLatch(1);
Server server = new Server();
HttpConfiguration config = new HttpConfiguration();
ServerConnector connector =
new ServerConnector(
server, new HttpConnectionFactory(config), new HTTP2CServerConnectionFactory(config));
connector.setHost("127.0.0.1");
connector.setPort(0);
long sit = Long.getLong("shutdownIdleTimeout", Long.MIN_VALUE);
if (sit != Long.MIN_VALUE) connector.setShutdownIdleTimeout(sit);
server.addConnector(connector);
server.setHandler(
new Handler.Abstract() {
@Override
public boolean handle(Request rq, Response rs, Callback cb) throws Exception {
entered.countDown();
gate.await(30, TimeUnit.SECONDS);
rs.setStatus(200);
rs.write(
true,
ByteBuffer.wrap(PAYLOAD),
Callback.from(
() -> {
System.out.println("server: response write SUCCEEDED");
cb.succeeded();
},
t -> {
System.out.println("server: response write FAILED: " + t);
cb.failed(t);
}));
return true;
}
});
// Same wiring as Solr's graceful.mod / JettySolrRunner enableGracefulShutdown
server.insertHandler(new GracefulHandler());
server.setStopTimeout(15_000);
server.start();
int port = connector.getLocalPort();
HttpClient client =
http1 ? new HttpClient() : new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client()));
client.start();
boolean ok = false;
Thread stopper = null;
try {
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("http://127.0.0.1:" + port + "/test").send(listener);
if (!entered.await(10, TimeUnit.SECONDS)) throw new IllegalStateException("no request");
stopper =
new Thread(
() -> {
try {
server.stop();
} catch (Exception e) {
System.out.println("iter " + iter + ": server.stop() threw " + e);
}
});
stopper.start();
gate.countDown();
org.eclipse.jetty.client.Response rsp = listener.get(30, TimeUnit.SECONDS);
byte[] body = listener.getInputStream().readAllBytes();
ok = rsp.getStatus() == 200 && body.length == PAYLOAD.length;
if (!ok)
System.out.println(
"iter " + iter + ": status=" + rsp.getStatus() + " bytes=" + body.length + "/" + PAYLOAD.length);
} catch (Exception e) {
System.out.println("iter " + iter + ": CLIENT EXCEPTION " + e);
} finally {
if (stopper != null) stopper.join(20_000);
try {
client.stop();
} catch (Exception ignore) {
}
try {
server.stop();
} catch (Exception ignore) {
}
}
return ok;
}
}
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.GracefulHandler;
import org.eclipse.jetty.util.Callback;
/**
* One-shot server for cross-client testing. On first request: schedules server.stop() after
* 200ms, handler sleeps 500ms (so stop is in progress), then writes 256KB. Prints PORT=n on
* stdout. Exits when stopped.
*/
public class GracefulReproServer {
public static void main(String[] args) throws Exception {
byte[] payload = new byte[256 * 1024];
Arrays.fill(payload, (byte) 'x');
Server server = new Server();
HttpConfiguration config = new HttpConfiguration();
ServerConnector connector =
new ServerConnector(
server, new HttpConnectionFactory(config), new HTTP2CServerConnectionFactory(config));
connector.setHost("127.0.0.1");
connector.setPort(0);
server.addConnector(connector);
server.setHandler(
new Handler.Abstract() {
@Override
public boolean handle(Request rq, Response rs, Callback cb) throws Exception {
new Thread(
() -> {
try {
Thread.sleep(200);
server.stop();
} catch (Exception e) {
System.out.println("stop threw: " + e);
}
})
.start();
Thread.sleep(500); // stop() is now waiting gracefully for us
rs.setStatus(200);
rs.write(
true,
ByteBuffer.wrap(payload),
Callback.from(
() -> {
System.out.println("WRITE_OK");
cb.succeeded();
},
t -> {
System.out.println("WRITE_FAILED " + t);
cb.failed(t);
}));
return true;
}
});
server.insertHandler(new GracefulHandler());
server.setStopTimeout(15_000);
server.start();
System.out.println("PORT=" + connector.getLocalPort());
System.out.flush();
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60);
while (!server.isStopped() && System.nanoTime() < deadline) Thread.sleep(100);
System.out.println("STOPPED");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment