/
githubmirror
/
panama-vector
Обзор
Документация
Войти
/
githubmirror
/
panama-vector
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/jdk/java/net/httpclient/http2/H2GoAwayPromptConnectionClose.java
516 строк
23 KB
Jaikiran Pai
8389498: Enable HttpClient debug logging in java/net/httpclient/http2/H2GoAwayPromptConnectionClose.java test
31 июл 2026, 14:10
31 июл 2026, 14:10
17ae726
Код
Авторство
О чём код?
/* * Copyright (c) 2026, 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.io.OutputStream; import java.net.Inet4Address; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.ByteBuffer; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocket; import jdk.internal.net.http.frame.HeadersFrame; import jdk.internal.net.http.frame.Http2Frame; import jdk.internal.net.http.frame.SettingsFrame; import jdk.internal.net.http.hpack.Decoder; import jdk.internal.net.http.hpack.Encoder; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; 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.assertEquals; /* * @test * @bug 8385131 * @summary Verifies that if the HttpClient receives a GOAWAY frame on a HTTP/2 connection, * then the HttpClient terminates the connection as promptly as appropriate instead * of relying on the idle connection management to do so. * @library /test/lib * @build jdk.test.lib.net.SimpleSSLContext jdk.test.lib.RandomFactory jdk.test.lib.net.URIBuilder * @comment An arbitrary high value for idle connection timeout to prevent idle * connection management from closing the HTTP/2 connection * @run junit/othervm -Djdk.httpclient.keepalive.timeout.h2=36000 * -Djdk.internal.httpclient.debug=true * ${test.main.class} */ class H2GoAwayPromptConnectionClose { private static final int MAX_HEADER_TABLE_CAPACITY = 4096; private static final String REQUEST_PATH = "/" + H2GoAwayPromptConnectionClose.class.getSimpleName(); private static final SSLContext sslCtx = SimpleSSLContext.findSSLContext(); private static final ExecutorService executor = Executors.newCachedThreadPool(); private static HttpClient client; private static MinimalH2OnlyServer server; // a latch that is counted down when the server notices a EOF on the connection's input stream // due to the client closing the connection. private static final CountDownLatch connTerminationLatch = new CountDownLatch(1); @BeforeAll static void beforeAll() throws Exception { client = HttpClient.newBuilder().proxy(NO_PROXY).sslContext(sslCtx).build(); server = new MinimalH2OnlyServer(); System.err.println("server listening at " + server.getServerAddress()); executor.submit(server); } @AfterAll static void afterAll() throws Exception { System.err.println("stopping server " + server.getServerAddress()); closeQuietly(server); closeQuietly(client); executor.shutdownNow(); } private static void closeQuietly(final AutoCloseable closeable) { if (closeable == null) { return; } try { closeable.close(); } catch (Throwable t) { System.err.println("ignoring exception: " + t + " that occurred during closing " + closeable); } } /* * Issues a "https" HTTP/2 request and expects the request to complete normally. * On the server side, in addition to sending the HTTP/2 response, the server also sends * a GOAWAY frame to the client. * After the response is received, the test waits to be notified about the termination * of the underlying connection. The test completes successfully only if it receives the * connection termination notification. * If it isn't notified of the connection termination, then that's a sign (of a bug) that * the HttpClient did not promptly close the connection even after receiving a GOAWAY frame * with no additional requests in progress. */ @Test void testConnectionTermination() throws Exception { final InetSocketAddress serverAddr = server.getServerAddress(); final URI reqURI = URIBuilder.newBuilder() .scheme("https") .host(serverAddr.getAddress()) .port(serverAddr.getPort()) .path(REQUEST_PATH) .build(); final HttpRequest req = HttpRequest.newBuilder().version(HTTP_2).uri(reqURI).build(); System.err.println("issuing request: " + req); final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString()); assertEquals(200, resp.statusCode(), "unexpected status code"); assertEquals(HTTP_2, resp.version(), "unexpected HTTP version in response"); System.err.println("awaiting connection termination"); connTerminationLatch.await(); } /* * The test requires fine grained control over when the server sends the GOAWAY frame * over the HTTP/2 connection and also to detect that the client has closed the underlying * connection. So we read/write HTTP/2 protocol messages directly over a * ServerSocket/Socket pair, instead of using the HTTP servers in the test library. * This server only support "https" and negotiates "h2" protocol during the TLS handshake. */ private static final class MinimalH2OnlyServer implements Runnable, AutoCloseable { private final ServerSocket serverSocket; private volatile boolean stop; private MinimalH2OnlyServer() throws IOException { // create the SSLServerSocket final SSLServerSocket ss = (SSLServerSocket) sslCtx.getServerSocketFactory() .createServerSocket(0, 0, Inet4Address.getLoopbackAddress()); // configure "h2" ALPN on the server socket final SSLParameters sslParams = new SSLParameters(); sslParams.setApplicationProtocols(new String[]{"h2"}); ss.setSSLParameters(sslParams); this.serverSocket = ss; } private InetSocketAddress getServerAddress() { return (InetSocketAddress) this.serverSocket.getLocalSocketAddress(); } @Override public void run() { try { doRun(); } catch (Throwable t) { if (!stop) { System.err.println("Exception occurred in server " + t); t.printStackTrace(); } } } // accept socket connections and process them private void doRun() throws IOException { while (!stop) { final Socket socket = serverSocket.accept(); System.err.println("accepted connection from " + socket); // submit for processing the request executor.submit(new Handler(socket)); } } @Override public void close() throws IOException { this.stop = true; this.serverSocket.close(); } } // Processes the data on a single HTTP/2 socket connection private static final class Handler implements Runnable { private static final Random random = RandomFactory.getRandom(); // number of bytes that are taken by the (common) Length and Type fields of // a HTTP/2 frame private static final int SIZE_OF_LENGTH_AND_TYPE_FIELDS = 9; // empty SETTINGS frame private static final byte[] SERVER_SETTINGS_FRAME = new byte[]{0, 0, 0, 0x04, 0, 0, 0, 0, 0}; // SETTINGS frame ACKing the receipt of a SETTINGS frame from the client private static final byte[] ACK_CLIENT_SETTINGS_FRAME = new byte[]{0, 0, 0, 0x04, 0x01, 0, 0, 0, 0}; private final Socket socket; private final String logId; private record ParsedRequest(int streamId, Map<String, List<String>> headers) { /* * Returns the {@code :path} header value (if any) */ private String getRequestPath() { final List<String> vals = headers.get(":path"); if (vals == null || vals.isEmpty()) { return null; } return vals.getFirst(); } } private Handler(final Socket socket) { this.socket = socket; this.logId = "local=" + socket.getLocalSocketAddress() + ", remote=" + socket.getRemoteSocketAddress(); } private void log(final String msg) { System.err.println("[" + this.logId + "] " + msg); } @Override public void run() { try { handleSocketConnection(); } catch (Throwable t) { // either an unexpected connection or something went wrong with the // HTTP/2 request issued in this test. just log it; the test // doesn't rely on this exception to be propagated to notice any failures. log("ignoring exception " + t + " that occurred when handling" + " connection from: " + socket); t.printStackTrace(); } } /* * Parse and respond to the HTTP/2 data received over the socket. Also sends a GOAWAY frame * to the client. We don't close the socket connection ourselves and instead expect * the client to close it once it receives the GOAWAY frame and with no active streams * on the connection. */ private void handleSocketConnection() throws IOException { // read the HTTP/2 client preface readClientPreface(); log("received client preface"); // next read the client SETTINGS and send the corresponding SETTINGS ACK. // then send the server's SETTINGS and wait for a SETTINGS ACK from the client. final List<byte[]> allRecvdFrames = exchangeConnectionSettings(); log("completed exchanging SETTINGS frames between client and server"); // now read the HTTP/2 request's HEADERS frame final ParsedRequest req = waitForRequest(allRecvdFrames); final String reqPath = req.getRequestPath(); if (!REQUEST_PATH.equals(reqPath)) { throw new IOException("unexpected request path " + reqPath); } log("received HTTP/2 request " + req.headers + " on stream " + req.streamId); // send the HTTP/2 response for the stream as well as a GOAWAY frame on the connection. // we randomly decide which one gets sent first and it shouldn't matter // which order they are sent, the end result for the client should be the same // (i.e. the HTTP request should complete normally and the underlying connection // is closed) final boolean sendGoAwayFirst = random.nextBoolean(); if (sendGoAwayFirst) { sendGoAway(req.streamId); log("sent GOAWAY"); sendResponse(req.streamId); log("sent a HTTP/2 response"); } else { sendResponse(req.streamId); log("sent a HTTP/2 response"); sendGoAway(req.streamId); log("sent GOAWAY"); } // wait for the client to close the connection waitForEOF(); } private void readClientPreface() throws IOException { final byte[] preface = socket.getInputStream().readNBytes(24); if (preface.length != 24) { throw new IOException("Not a HTTP/2 client preface, received only " + preface.length + " bytes"); } } private List<byte[]> exchangeConnectionSettings() throws IOException { final List<byte[]> allRcvdFrames = new ArrayList<>(); // read the SETTINGS frame from the client final byte[] settingsFrame = expectFrame(socket.getInputStream(), SettingsFrame.TYPE); allRcvdFrames.add(settingsFrame); log("received SETTINGS frame"); final OutputStream out = socket.getOutputStream(); // send the SETTINGS ACK out.write(ACK_CLIENT_SETTINGS_FRAME); out.flush(); // respond with server SETTINGS out.write(SERVER_SETTINGS_FRAME); out.flush(); // expect the client to send the SETTINGS ACK frame final List<byte[]> moreFrames = waitForSettingsAckFrame(); allRcvdFrames.addAll(moreFrames); return allRcvdFrames; } private List<byte[]> waitForSettingsAckFrame() throws IOException { log("accumulating frames till a SETTINGS ACK frame is received"); final List<byte[]> recvdFrames = new ArrayList<>(); int recvdFrameType = -1; byte[] frame; do { frame = readHTTP2Frame(socket.getInputStream()); // keep track of received frames recvdFrames.add(frame); recvdFrameType = frameType(frame); log("received frame type " + recvdFrameType + " (" + Http2Frame.asString(recvdFrameType) + ")"); } while (recvdFrameType != SettingsFrame.TYPE); // received the SETTINGS frame, verify it has the ACK bit set final boolean ackBitSet = ((frame[4] & 0x01) == 1); if (!ackBitSet) { throw new IOException("ACK bit not set on SETTINGS frame from " + socket); } // return all the received frames return recvdFrames; } private ParsedRequest waitForRequest(final List<byte[]> alreadyRcvdFrames) throws IOException { byte[] headersFrame = findHeadersFrame(alreadyRcvdFrames); if (headersFrame == null) { // no HEADERS frame have been read so far, expect it to arrive headersFrame = expectFrame(socket.getInputStream(), HeadersFrame.TYPE); } // parse the HEADERS frame final int streamId = getStreamId(headersFrame); if (streamId <= 0) { throw new IOException("invalid stream id: " + streamId + " in HEADERS" + " frame on connection " + socket); } final Map<String, List<String>> reqHeaders = parseHeadersFrame(headersFrame); return new ParsedRequest(streamId, reqHeaders); } private static byte[] findHeadersFrame(final List<byte[]> frames) { for (final byte[] frame : frames) { final int frameType = frameType(frame); if (frameType == HeadersFrame.TYPE) { return frame; // found } } return null; // no HEADERS frame found } // parses the HTTP/2 HEADERS frame and returns back a Map of HTTP headers from the request private Map<String, List<String>> parseHeadersFrame(final byte[] frame) throws IOException { final ByteBuffer fieldBlock = ByteBuffer.wrap(frame, SIZE_OF_LENGTH_AND_TYPE_FIELDS, frame.length - SIZE_OF_LENGTH_AND_TYPE_FIELDS); // HPACK decoder final Decoder decoder = new Decoder(MAX_HEADER_TABLE_CAPACITY); final Map<String, List<String>> reqHeaders = new HashMap<>(); decoder.decode(fieldBlock, true, (name, value) -> { final String headerName = name.toString(); reqHeaders.putIfAbsent(headerName, new ArrayList<>()); reqHeaders.get(headerName).add(value.toString()); }); return reqHeaders; } private static byte[] expectFrame(final InputStream in, final int expectedFrameType) throws IOException { final byte[] frame = readHTTP2Frame(in); final int frameType = frameType(frame); if (frameType != expectedFrameType) { throw new IOException("unexpected frame, got type " + frameType + ", expected " + expectedFrameType); } return frame; } private static byte[] readHTTP2Frame(final InputStream in) throws IOException { final byte[] header = in.readNBytes(SIZE_OF_LENGTH_AND_TYPE_FIELDS); if (header.length != SIZE_OF_LENGTH_AND_TYPE_FIELDS) { throw new IOException("Incomplete HTTP/2 frame header, received only " + header.length + " bytes, expected " + SIZE_OF_LENGTH_AND_TYPE_FIELDS + " bytes"); } // 3 bytes of Length field that represent the payload length final int payloadLength = ((header[0] & 0xFF) << 16) | ((header[1] & 0xFF) << 8) | (header[2] & 0xFF); final byte[] payload = in.readNBytes(payloadLength); if (payload.length != payloadLength) { throw new IOException("Incomplete HTTP/2 frame payload, received only " + payload.length + " bytes, expected " + payloadLength + " bytes"); } final byte[] frame = new byte[header.length + payload.length]; System.arraycopy(header, 0, frame, 0, header.length); System.arraycopy(payload, 0, frame, header.length, payload.length); return frame; } private void sendGoAway(final int lastStreamId) throws IOException { // 4 bytes of lastStreamId + 4 bytes of error code + 0 bytes of debug data final int payloadLength = 8; byte[] frame = new byte[SIZE_OF_LENGTH_AND_TYPE_FIELDS + payloadLength]; frame[0] = (byte) ((payloadLength >> 16) & 0xFF); frame[1] = (byte) ((payloadLength >> 8) & 0xFF); frame[2] = (byte) (payloadLength & 0xFF); frame[3] = 0x07; // GOAWAY frame type frame[4] = 0; // no flags // stream id = 0, since GOAWAY belongs to the connection frame[5] = 0; frame[6] = 0; frame[7] = 0; frame[8] = 0; // last stream ID representing the last processed stream on the connection frame[9] = (byte) ((lastStreamId >> 24) & 0x7F); frame[10] = (byte) ((lastStreamId >> 16) & 0xFF); frame[11] = (byte) ((lastStreamId >> 8) & 0xFF); frame[12] = (byte) (lastStreamId & 0xFF); // Error code == NO_ERROR frame[13] = 0; frame[14] = 0; frame[15] = 0; frame[16] = 0; final OutputStream out = socket.getOutputStream(); out.write(frame); out.flush(); } private void sendResponse(final int streamId) throws IOException { // HPACK encoder final Encoder encoder = new Encoder(MAX_HEADER_TABLE_CAPACITY); encoder.header(":status", "200"); final ByteBuffer buf = ByteBuffer.allocate(1024); if (!encoder.encode(buf)) { throw new IOException("HPACK encoding didn't complete"); } buf.flip(); final byte[] fieldBlock = new byte[buf.remaining()]; buf.get(fieldBlock); final byte[] frame = new byte[SIZE_OF_LENGTH_AND_TYPE_FIELDS + fieldBlock.length]; frame[0] = (byte) ((fieldBlock.length >> 16) & 0xFF); frame[1] = (byte) ((fieldBlock.length >> 8) & 0xFF); frame[2] = (byte) (fieldBlock.length & 0xFF); frame[3] = 0x01; // HEADERS frame type frame[4] = 0x05; // flags = END_STREAM (0x01) | END_HEADERS (0x04) frame[5] = (byte) ((streamId >> 24) & 0x7F); frame[6] = (byte) ((streamId >> 16) & 0xFF); frame[7] = (byte) ((streamId >> 8) & 0xFF); frame[8] = (byte) (streamId & 0xFF); System.arraycopy(fieldBlock, 0, frame, SIZE_OF_LENGTH_AND_TYPE_FIELDS, fieldBlock.length); final OutputStream out = socket.getOutputStream(); out.write(frame); out.flush(); } private void waitForEOF() throws IOException { log("waiting to receive EOF"); final Instant start = Instant.now(); this.socket.getInputStream().readAllBytes(); // let the test know that the connection has been terminated connTerminationLatch.countDown(); final Instant end = Instant.now(); log("took " + Duration.between(start, end) + " to reach EOF"); } private static int frameType(final byte[] frame) { assert frame.length >= 4 : "expected at least 4 bytes, but found only " + frame.length; return frame[3]; } private static int getStreamId(final byte[] frame) { // Stream Identifier starts at the 6th byte in the frame and takes // up 4 bytes assert frame.length >= 9 : "expected at least 9 bytes, but found only " + frame.length; return ((frame[5] & 0x7F) << 24) | ((frame[6] & 0xFF) << 16) | ((frame[7] & 0xFF) << 8) | (frame[8] & 0xFF); } } }