Created
July 4, 2026 01:42
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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