diff --git a/http/http2/src/main/java/io/helidon/http/http2/Http2Stream.java b/http/http2/src/main/java/io/helidon/http/http2/Http2Stream.java index f4aeac21d24..d02a5a04a85 100644 --- a/http/http2/src/main/java/io/helidon/http/http2/Http2Stream.java +++ b/http/http2/src/main/java/io/helidon/http/http2/Http2Stream.java @@ -58,8 +58,9 @@ public interface Http2Stream { * * @param header frame header * @param data frame data + * @param endOfStream whether this is the last data that would be received */ - void data(Http2FrameHeader header, BufferData data); + void data(Http2FrameHeader header, BufferData data, boolean endOfStream); /** * Priority. diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2CallEntityChain.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2CallEntityChain.java index d866dc0faa2..f93ca4ef7f7 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2CallEntityChain.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2CallEntityChain.java @@ -66,6 +66,8 @@ protected WebClientServiceResponse doProceed(WebClientServiceRequest serviceRequ stream.flowControl().inbound().incrementWindowSize(clientRequest().requestPrefetch()); whenSent.complete(serviceRequest); + stream.waitFor100Continue(); + if (entityBytes.length != 0) { stream.writeData(BufferData.create(entityBytes), true); } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java index 7a75c19a80d..4f39e9f0932 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnection.java @@ -79,6 +79,7 @@ class Http2ClientConnection { private final DataReader reader; private final DataWriter dataWriter; private final Semaphore pingPongSemaphore = new Semaphore(0); + private final Http2ClientConfig clientConfig; private volatile int lastStreamId; private Http2Settings serverSettings = Http2Settings.builder() @@ -87,13 +88,14 @@ class Http2ClientConnection { private volatile boolean closed = false; - Http2ClientConnection(Http2ClientProtocolConfig protocolConfig, ClientConnection connection) { + Http2ClientConnection(Http2ClientImpl http2Client, ClientConnection connection) { + this.protocolConfig = http2Client.protocolConfig(); + this.clientConfig = http2Client.clientConfig(); this.connectionFlowControl = ConnectionFlowControl.clientBuilder(this::writeWindowsUpdate) .maxFrameSize(protocolConfig.maxFrameSize()) .initialWindowSize(protocolConfig.initialWindowSize()) .blockTimeout(protocolConfig.flowControlBlockTimeout()) .build(); - this.protocolConfig = protocolConfig; this.connection = connection; this.ctx = connection.helidonSocket(); this.dataWriter = connection.writer(); @@ -105,7 +107,7 @@ static Http2ClientConnection create(Http2ClientImpl http2Client, ClientConnection connection, boolean sendSettings) { - Http2ClientConnection h2conn = new Http2ClientConnection(http2Client.protocolConfig(), connection); + Http2ClientConnection h2conn = new Http2ClientConnection(http2Client, connection); h2conn.start(http2Client.protocolConfig(), http2Client.webClient().executor(), sendSettings); return h2conn; @@ -139,7 +141,8 @@ Http2ClientStream createStream(Http2StreamConfig config) { Http2ClientStream stream = new Http2ClientStream(this, serverSettings, ctx, - config.timeout(), + config, + clientConfig, streamIdSeq); return stream; } diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java index 3ef661911e3..ac652db1368 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientStream.java @@ -27,7 +27,9 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.socket.SocketContext; +import io.helidon.http.HeaderValues; import io.helidon.http.Headers; +import io.helidon.http.WritableHeaders; import io.helidon.http.http2.Http2ErrorCode; import io.helidon.http.http2.Http2Exception; import io.helidon.http.http2.Http2Flag; @@ -60,6 +62,7 @@ class Http2ClientStream implements Http2Stream, ReleasableResource { private final Http2Settings serverSettings; private final SocketContext ctx; private final Duration timeout; + private final Http2ClientConfig http2ClientConfig; private final LockingStreamIdSequence streamIdSeq; private final Http2FrameListener sendListener = new Http2LoggingFrameListener("cl-send"); private final Http2FrameListener recvListener = new Http2LoggingFrameListener("cl-recv"); @@ -67,7 +70,7 @@ class Http2ClientStream implements Http2Stream, ReleasableResource { private final List continuationData = new ArrayList<>(); private Http2StreamState state = Http2StreamState.IDLE; - private ReadState readState = ReadState.HEADERS; + private ReadState readState = ReadState.INIT; private Http2Headers currentHeaders; // accessed from stream thread an connection thread private volatile StreamFlowControl flowControl; @@ -81,12 +84,14 @@ class Http2ClientStream implements Http2Stream, ReleasableResource { Http2ClientStream(Http2ClientConnection connection, Http2Settings serverSettings, SocketContext ctx, - Duration timeout, + Http2StreamConfig http2StreamConfig, + Http2ClientConfig http2ClientConfig, LockingStreamIdSequence streamIdSeq) { this.connection = connection; this.serverSettings = serverSettings; this.ctx = ctx; - this.timeout = timeout; + this.timeout = http2StreamConfig.timeout(); + this.http2ClientConfig = http2ClientConfig; this.streamIdSeq = streamIdSeq; } @@ -102,6 +107,7 @@ public Http2StreamState streamState() { @Override public void headers(Http2Headers headers, boolean endOfStream) { + this.state = Http2StreamState.checkAndGetState(this.state, Http2FrameType.HEADERS, false, endOfStream, true); readState = readState.check(endOfStream ? ReadState.END : ReadState.DATA); this.currentHeaders = headers; this.hasEntity = !endOfStream; @@ -109,8 +115,7 @@ public void headers(Http2Headers headers, boolean endOfStream) { @Override public void trailers(Http2Headers headers, boolean endOfStream) { - // Doesn't really matter of we received endOfStream - // end of the trailers is end of the exchange + this.state = Http2StreamState.checkAndGetState(this.state, Http2FrameType.HEADERS, false, endOfStream, true); readState = readState.check(ReadState.END); this.trailers.complete(headers.httpHeaders()); } @@ -158,8 +163,9 @@ public void windowUpdate(Http2WindowUpdate windowUpdate) { } @Override - public void data(Http2FrameHeader header, BufferData data) { - readState = readState.check(ReadState.DATA); + public void data(Http2FrameHeader header, BufferData data, boolean endOfStream) { + this.state = Http2StreamState.checkAndGetState(this.state, header.type(), false, endOfStream, false); + readState = readState.check(endOfStream ? ReadState.END : ReadState.DATA); flowControl.inbound().incrementWindowSize(header.length()); } @@ -225,7 +231,7 @@ BufferData read(int i) { BufferData read() { while (state == Http2StreamState.HALF_CLOSED_LOCAL && readState != ReadState.END && hasEntity) { - Http2FrameData frameData = readOne(); + Http2FrameData frameData = readOne(timeout); if (frameData != null) { return frameData.data(); } @@ -233,8 +239,26 @@ BufferData read() { return BufferData.empty(); } + void waitFor100Continue() { + Duration readContinueTimeout = http2ClientConfig.readContinueTimeout(); + try { + while (readState == ReadState.CONTINUE_100_HEADERS) { + readOne(readContinueTimeout); + } + } catch (StreamTimeoutException ignored) { + // Timeout, continue as if it was received + readState = readState.check(ReadState.HEADERS); + LOGGER.log(DEBUG, "Server didn't respond within 100 Continue timeout in " + + readContinueTimeout + + ", sending data."); + } + } + void write(Http2Headers http2Headers, boolean endOfStream) { this.state = Http2StreamState.checkAndGetState(this.state, Http2FrameType.HEADERS, true, endOfStream, true); + this.readState = readState.check(http2Headers.httpHeaders().contains(HeaderValues.EXPECT_100) + ? ReadState.CONTINUE_100_HEADERS + : ReadState.HEADERS); Http2Flag.HeaderFlags flags; if (endOfStream) { flags = Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM); @@ -248,7 +272,7 @@ void write(Http2Headers http2Headers, boolean endOfStream) { // greater than all streams that the initiating endpoint has opened or reserved. this.streamId = streamIdSeq.lockAndNext(); this.connection.updateLastStreamId(streamId); - this.buffer = new StreamBuffer(streamId, timeout); + this.buffer = new StreamBuffer(streamId); // fixme Configurable initial win size, max frame size this.flowControl = connection.flowControl().createStreamFlowControl( @@ -278,8 +302,8 @@ void writeData(BufferData entityBytes, boolean endOfStream) { } Http2Headers readHeaders() { - while (currentHeaders == null) { - Http2FrameData frameData = readOne(); + while (readState == ReadState.HEADERS) { + Http2FrameData frameData = readOne(timeout); if (frameData != null) { throw new IllegalStateException("Unexpected frame type " + frameData.header() + ", HEADERS are expected."); } @@ -291,8 +315,8 @@ ClientOutputStream outputStream() { return new ClientOutputStream(); } - private Http2FrameData readOne() { - Http2FrameData frameData = buffer.poll(); + private Http2FrameData readOne(Duration pollTimeout) { + Http2FrameData frameData = buffer.poll(pollTimeout); if (frameData != null) { @@ -303,35 +327,68 @@ private Http2FrameData readOne() { boolean endOfStream = (flags & Http2Flag.END_OF_STREAM) == Http2Flag.END_OF_STREAM; boolean endOfHeaders = (flags & Http2Flag.END_OF_HEADERS) == Http2Flag.END_OF_HEADERS; - this.state = Http2StreamState.checkAndGetState(this.state, - frameData.header().type(), - false, - endOfStream, - endOfHeaders); - switch (frameData.header().type()) { case DATA: - data(frameData.header(), frameData.data()); + data(frameData.header(), frameData.data(), endOfStream); return frameData; + case HEADERS, CONTINUATION: continuationData.add(frameData); + + // (HEADERS[100-continue] CONTINUATION*)* + // ^------- endOfHeaders + // HEADERS+ + // CONTINUATION* + // ^------- endOfHeaders + // DATA* + // (HEADERS[trailers] CONTINUATION*)+ + // ^------- endOfHeaders if (endOfHeaders) { var requestHuffman = Http2HuffmanDecoder.create(); - Http2Headers http2Headers = Http2Headers.create(this, - connection.getInboundDynamicTable(), - requestHuffman, - continuationData.toArray(new Http2FrameData[0])); - - recvListener.headers(ctx, streamId, http2Headers); - - if (readState == ReadState.HEADERS) { + // HTTP/1.1 100 Continue HEADERS + // Extension-Field: bar ==> - END_STREAM + // + END_HEADERS + // :status = 100 + // extension-field = bar + // + // HTTP/1.1 200 OK HEADERS + // Content-Type: image/jpeg ==> - END_STREAM + // Transfer-Encoding: chunked + END_HEADERS + // Trailer: Foo :status = 200 + // content-type = image/jpeg + // 123 trailer = Foo + // {binary data} + // 0 DATA + // Foo: bar - END_STREAM + // {binary data} + // + // HEADERS + // + END_STREAM + // + END_HEADERS + // foo = bar + switch (readState) { + case CONTINUE_100_HEADERS -> { + Http2Headers http2Headers = readHeaders(requestHuffman, false); + // Clear out for headers + continuationData.clear(); + this.continue100(http2Headers, endOfStream); + } + case HEADERS -> { + // Add extension headers from 100 Continue + Http2Headers http2Headers = readHeaders(requestHuffman, true); // Clear out for trailers continuationData.clear(); this.headers(http2Headers, endOfStream); - } else { + } + case DATA, TRAILERS -> { + Http2Headers http2Headers = readHeaders(requestHuffman, false); this.trailers(http2Headers, endOfStream); } + default -> { + throw new IllegalStateException("Client is in wrong read state " + readState.name()); + } + } } break; default: @@ -341,6 +398,25 @@ private Http2FrameData readOne() { return null; } + private void continue100(Http2Headers headers, boolean endOfStream) { + // no stream state check as 100 continues are an exception + this.currentHeaders = headers; + readState = readState.check(ReadState.HEADERS); + this.hasEntity = !endOfStream; + } + + private Http2Headers readHeaders(Http2HuffmanDecoder decoder, boolean mergeWithPrevious) { + Http2Headers http2Headers = Http2Headers.create(this, + connection.getInboundDynamicTable(), + decoder, + mergeWithPrevious && currentHeaders != null + ? currentHeaders + : Http2Headers.create(WritableHeaders.create()), + continuationData.toArray(new Http2FrameData[0])); + recvListener.headers(ctx, streamId, http2Headers); + return http2Headers; + } + private void splitAndWrite(Http2FrameData frameData) { int maxFrameSize = this.serverSettings.value(Http2Setting.MAX_FRAME_SIZE).intValue(); @@ -395,7 +471,9 @@ private enum ReadState { END, TRAILERS(END), DATA(TRAILERS, END), - HEADERS(DATA, TRAILERS); + HEADERS(DATA, TRAILERS), + CONTINUE_100_HEADERS(HEADERS), + INIT(CONTINUE_100_HEADERS, HEADERS); private final Set allowedTransitions; diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/StreamBuffer.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/StreamBuffer.java index 1644845de73..78c98316417 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/StreamBuffer.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/StreamBuffer.java @@ -32,14 +32,12 @@ class StreamBuffer { private final Semaphore dequeSemaphore = new Semaphore(1); private final Queue buffer = new ArrayDeque<>(); private final int streamId; - private final Duration timeout; - StreamBuffer(int streamId, Duration timeout) { + StreamBuffer(int streamId) { this.streamId = streamId; - this.timeout = timeout; } - Http2FrameData poll() { + Http2FrameData poll(Duration timeout) { try { // Block deque thread when queue is empty // avoid CPU burning diff --git a/webclient/tests/http1/src/test/java/io/helidon/webclient/tests/HeadersTest.java b/webclient/tests/http1/src/test/java/io/helidon/webclient/tests/HeadersTest.java index bda80902c43..6f9e8d2ba96 100644 --- a/webclient/tests/http1/src/test/java/io/helidon/webclient/tests/HeadersTest.java +++ b/webclient/tests/http1/src/test/java/io/helidon/webclient/tests/HeadersTest.java @@ -156,6 +156,8 @@ public void testInvalidTextContentTypeStrict() { Headers h = res.headers(); // Raw protocol data value assertThat(res.headers(), hasHeader(INVALID_CONTENT_TYPE_TEXT)); + Header rawContentType = h.get(HeaderNames.CONTENT_TYPE); + assertThat(rawContentType.get(), is(INVALID_CONTENT_TYPE_TEXT.get())); // Media type parsed value is invalid, IllegalArgumentException shall be thrown var ex = assertThrows(IllegalArgumentException.class, h::contentType); assertThat(ex.getMessage(), is("Cannot parse media type: text")); diff --git a/webclient/tests/http2/src/test/java/io/helidon/webclient/tests/http2/HeadersTest.java b/webclient/tests/http2/src/test/java/io/helidon/webclient/tests/http2/HeadersTest.java index 8733d59e7bc..fb4defc5641 100644 --- a/webclient/tests/http2/src/test/java/io/helidon/webclient/tests/http2/HeadersTest.java +++ b/webclient/tests/http2/src/test/java/io/helidon/webclient/tests/http2/HeadersTest.java @@ -22,10 +22,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import io.helidon.http.Header; import io.helidon.http.HeaderNames; @@ -37,10 +39,16 @@ import io.vertx.core.MultiMap; import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; import io.vertx.core.http.Http2Settings; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.http.HttpVersion; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -53,14 +61,17 @@ import static org.junit.jupiter.api.Assertions.assertThrows; class HeadersTest { + + private static final System.Logger LOGGER = System.getLogger(HeadersTest.class.getName()); private static final Header BEFORE_HEADER = HeaderValues.create("test", "before"); private static final Header TRAILER_HEADER = HeaderValues.create("Trailer-header", "trailer-test"); private static final Duration TIMEOUT = Duration.ofSeconds(10); private static final String DATA = "Helidon!!!".repeat(10); - private static final Vertx VERTX = Vertx.vertx(); + private static final Vertx VERTX = Vertx.vertx(new VertxOptions().setBlockedThreadCheckInterval(1000*60*60)); private static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); private static HttpServer SERVER; private static Http2Client CLIENT; + private static HttpClient VERTX_CLIENT; @BeforeAll static void beforeAll() throws ExecutionException, InterruptedException, TimeoutException { @@ -73,6 +84,7 @@ static void beforeAll() throws ExecutionException, InterruptedException, Timeout .requestHandler(req -> { HttpServerResponse res = req.response(); switch (req.path()) { + case "/upgrade" -> res.end(); case "/trailer" -> { res.putHeader(BEFORE_HEADER.name(), BEFORE_HEADER.get()); res.putHeader(HeaderNames.TRAILER.defaultCase(), "Trailer-header"); @@ -116,6 +128,35 @@ static void beforeAll() throws ExecutionException, InterruptedException, Timeout res.trailers().addAll(trailers); res.end(); } + case "/100-continue" -> { + AtomicBoolean continueSent = new AtomicBoolean(false); + req.body(event -> { + if(continueSent.get()) { + res.putHeader(HeaderNames.CONTENT_LENGTH.defaultCase(), + String.valueOf(event.result().length())); + res.write(event.result().toString()); + } else { + res.setStatusCode(500); + res.write("Got data before 100 continue!"); + } + res.end(); + }); + res.putHeader("before_100", "test"); + req.end(); + VERTX.executeBlocking(future -> { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + LOGGER.log(System.Logger.Level.INFO, "100 test interrupted", e); + } + continueSent.set(true); + if (!"true".equals(req.getHeader("no-continue"))) { + res.writeContinue(); + } + res.putHeader("after_100", "test"); + future.complete(); + }); + } default -> res.setStatusCode(404).end(); } }) @@ -127,7 +168,12 @@ static void beforeAll() throws ExecutionException, InterruptedException, Timeout int port = SERVER.actualPort(); CLIENT = Http2Client.builder() .baseUri("http://localhost:" + port + "/") + .readContinueTimeout(Duration.ofSeconds(2)) .build(); + + HttpClientOptions clientOptions = new HttpClientOptions() + .setProtocolVersion(HttpVersion.HTTP_2); + VERTX_CLIENT = VERTX.createHttpClient(clientOptions); } @AfterAll @@ -158,6 +204,68 @@ void trailerHeader() { } } + @Test + void continue100Vertx() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture finished = new CompletableFuture<>(); + + VERTX_CLIENT.request(HttpMethod.GET, SERVER.actualPort(), "localhost", "/upgrade") + .onSuccess(HttpClientRequest::end) + .toCompletionStage().toCompletableFuture().join(); + VERTX_CLIENT.request(HttpMethod.PUT, SERVER.actualPort(), "localhost", "/100-continue") + .onSuccess(request -> { + request.response().onSuccess(response -> { + response.end(); + response.body().onSuccess(event -> finished.complete(event.toString())); + }).onFailure(finished::completeExceptionally); + + request.putHeader("Expect", "100-Continue"); + + request.continueHandler(v -> { + // OK to send rest of body + request.putHeader(HeaderNames.CONTENT_LENGTH.defaultCase(), String.valueOf(DATA.length())); + request.write(DATA); + request.end(); + }); + + request.sendHead(); + }); + + assertThat(finished.get(TIMEOUT.toMillis(), MILLISECONDS), is(DATA)); + } + + @Test + void continue100() { + try (Http2ClientResponse res = CLIENT + .method(Method.PUT) + .path("/100-continue") + .priorKnowledge(true) + .header(HeaderValues.EXPECT_100) + .submit(DATA)) { + assertThat(res.as(String.class), is(DATA)); + Header before100Header = HeaderValues.create("before_100", "test"); + Header after100Header = HeaderValues.create("after_100", "test"); + assertThat(res.headers(), hasHeader(before100Header)); + assertThat(res.headers(), hasHeader(after100Header)); + } + } + + @Test + void continue100Timeout() { + try (Http2ClientResponse res = CLIENT + .method(Method.PUT) + .path("/100-continue") + .priorKnowledge(true) + .header(HeaderValues.EXPECT_100) + .header(HeaderValues.create("no-continue", "true")) + .submit(DATA)) { + assertThat(res.as(String.class), is(DATA)); + Header before100Header = HeaderValues.create("before_100", "test"); + Header after100Header = HeaderValues.create("after_100", "test"); + assertThat(res.headers(), hasHeader(before100Header)); + assertThat(res.headers(), hasHeader(after100Header)); + } + } + @Test void noTrailerHeader() { diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java index 3e77b2bec29..17ca91d6d7d 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java @@ -574,13 +574,14 @@ private void dataFrame() { buffer = inProgressFrame(); } + boolean endOfStream = frameHeader.flags(Http2FrameTypes.DATA).endOfStream(); + // TODO buffer now contains the actual data bytes - stream.stream().data(frameHeader, buffer); + stream.stream().data(frameHeader, buffer, endOfStream); // 5.1 - In HALF-CLOSED state we need to wait for either RST-STREAM or DATA with endStream flag // even when handler has already finished - if ((REMOVABLE_STREAMS.contains(stream.stream.streamState())) - && frameHeader.flags(Http2FrameTypes.DATA).endOfStream()) { + if ((REMOVABLE_STREAMS.contains(stream.stream.streamState())) && endOfStream) { streams.remove(streamId); } diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java index 8d30478a9a7..36409763bcf 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java @@ -237,7 +237,7 @@ public void trailers(Http2Headers headers, boolean endOfStream) { } @Override - public void data(Http2FrameHeader header, BufferData data) { + public void data(Http2FrameHeader header, BufferData data, boolean endOfStream) { // todo check number of queued items and modify flow control if we seem to be collecting messages if (expectedLength != -1 && expectedLength < header.length()) { state = Http2StreamState.CLOSED;