Skip to content

Commit

Permalink
6543 HTTP/2 header continuation (helidon-io#6907)
Browse files Browse the repository at this point in the history
* 6543 HTTP/2 Client outbound header continuation
  • Loading branch information
danielkec committed Jun 8, 2023
1 parent bc225b5 commit 6da19c2
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,62 @@ public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlag
// we must enforce parallelism of exactly 1, to make sure the dynamic table is updated
// and then immediately written

int maxFrameSize = flowControl.maxFrameSize();

return withStreamLock(() -> {
int written = 0;
headerBuffer.clear();
headers.write(outboundDynamicTable, responseHuffman, headerBuffer);
Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(),
Http2FrameTypes.HEADERS,
flags,
streamId);

// Fast path when headers fits within the SETTINGS_MAX_FRAME_SIZE
if (headerBuffer.available() <= maxFrameSize) {
Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(),
Http2FrameTypes.HEADERS,
flags,
streamId);
written += frameHeader.length();
written += Http2FrameHeader.LENGTH;

noLockWrite(new Http2FrameData(frameHeader, headerBuffer));
return written;
}

// Split header frame to smaller continuation frames RFC 9113 §6.10
BufferData[] fragments = Http2Headers.split(headerBuffer, maxFrameSize);

// First header fragment
BufferData fragment = fragments[0];
Http2FrameHeader frameHeader;
frameHeader = Http2FrameHeader.create(fragment.available(),
Http2FrameTypes.HEADERS,
Http2Flag.HeaderFlags.create(0),
streamId);
written += frameHeader.length();
written += Http2FrameHeader.LENGTH;
noLockWrite(new Http2FrameData(frameHeader, fragment));

// Header continuation fragments in the middle
for (int i = 1; i < fragments.length; i++) {
fragment = fragments[i];
frameHeader = Http2FrameHeader.create(fragment.available(),
Http2FrameTypes.CONTINUATION,
Http2Flag.ContinuationFlags.create(0),
streamId);
written += frameHeader.length();
written += Http2FrameHeader.LENGTH;
noLockWrite(new Http2FrameData(frameHeader, fragment));
}

noLockWrite(new Http2FrameData(frameHeader, headerBuffer));

// Last header continuation fragment
fragment = fragments[fragments.length - 1];
frameHeader = Http2FrameHeader.create(fragment.available(),
Http2FrameTypes.CONTINUATION,
// Last fragment needs to indicate the end of headers
Http2Flag.ContinuationFlags.create(flags.value() | Http2Flag.END_OF_HEADERS),
streamId);
written += frameHeader.length();
written += Http2FrameHeader.LENGTH;
noLockWrite(new Http2FrameData(frameHeader, fragment));
return written;
});
}
Expand All @@ -104,17 +147,8 @@ public int writeHeaders(Http2Headers headers,
return withStreamLock(() -> {
int bytesWritten = 0;

headerBuffer.clear();
headers.write(outboundDynamicTable, responseHuffman, headerBuffer);
bytesWritten += headerBuffer.available();

Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(),
Http2FrameTypes.HEADERS,
flags,
streamId);
bytesWritten += Http2FrameHeader.LENGTH;
bytesWritten += writeHeaders(headers, streamId, flags, flowControl);

noLockWrite(new Http2FrameData(frameHeader, headerBuffer));
writeData(dataFrame, flowControl);
bytesWritten += Http2FrameHeader.LENGTH;
bytesWritten += dataFrame.header().length();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -96,7 +96,12 @@ public enum Http2ErrorCode {
* The endpoint requires that HTTP/1.1 be used
* instead of HTTP/2.
*/
HTTP_1_1_REQUIRED(0xd);
HTTP_1_1_REQUIRED(0xd),
/**
* Request header fields are too large.
* RFC6585
*/
REQUEST_HEADER_FIELDS_TOO_LARGE(431);

private static final Map<Integer, Http2ErrorCode> BY_CODE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,27 @@ public void write(DynamicTable table, Http2HuffmanEncoder huffman, BufferData gr
}
}

static BufferData[] split(BufferData bufferData, int size) {
int length = bufferData.available();
if (length <= size) {
return new BufferData[]{bufferData};
}

int lastFragmentSize = length % size;
// Avoid creating 0 length last fragment
int allFrames = (length / size) + (lastFragmentSize != 0 ? 1 : 0);
BufferData[] result = new BufferData[allFrames];

for (int i = 0; i < allFrames; i++) {
boolean lastFrame = (allFrames == i + 1);
byte[] frag = new byte[lastFrame ? (lastFragmentSize != 0 ? lastFragmentSize : size) : size];
bufferData.read(frag);
result[i] = BufferData.create(frag);
}

return result;
}

private static Http2Headers create(ServerRequestHeaders httpHeaders, PseudoHeaders pseudoHeaders) {
return new Http2Headers(httpHeaders, pseudoHeaders);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.helidon.logging.common.LogConfig;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

Expand Down Expand Up @@ -65,6 +66,15 @@ private static Stream<SplitTest> splitMultiple() {
);
}

@Test
void splitHeaders() {
BufferData bf = BufferData.create("This is so long text!");
BufferData[] split = Http2Headers.split(bf, 12);
assertThat(split.length, is(2));
assertThat(split[0].available(), is(12));
assertThat(split[1].available(), is(9));
}

@ParameterizedTest
@MethodSource
void splitMultiple(SplitTest args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ public interface Http2Config {
/**
* The maximum field section size that the sender is prepared to accept in bytes.
* See RFC 9113 section 6.5.2 for details.
* Default is maximal unsigned int.
* Default is 8192.
*
* @return maximal header list size in bytes
*/
@ConfiguredOption("0xFFFFFFFFL")
@ConfiguredOption("8192")
long maxHeaderListSize();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,11 +404,10 @@ private void readFrame() {

private void doContinuation() {
Http2Flag.ContinuationFlags flags = frameHeader.flags(Http2FrameTypes.CONTINUATION);
List<Http2FrameData> continuationData = stream(frameHeader.streamId()).contData();
if (continuationData.isEmpty()) {
throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received continuation without headers.");
}
continuationData.add(new Http2FrameData(frameHeader, inProgressFrame()));

stream(frameHeader.streamId())
.addContinuation(new Http2FrameData(frameHeader, inProgressFrame()));

if (flags.endOfHeaders()) {
state = State.HEADERS;
} else {
Expand Down Expand Up @@ -548,9 +547,7 @@ private void doHeaders() {
// first frame, expecting continuation
if (frameHeader.type() == Http2FrameType.HEADERS && !frameHeader.flags(Http2FrameTypes.HEADERS).endOfHeaders()) {
// this needs to retain the data until we receive last continuation, cannot use the same data
streamContext.contData().clear();
streamContext.contData().add(new Http2FrameData(frameHeader, inProgressFrame().copy()));
streamContext.continuationHeader = frameHeader;
streamContext.addHeadersToBeContinued(frameHeader, inProgressFrame().copy());
this.continuationExpectedStreamId = streamId;
this.state = State.READ_FRAME;
return;
Expand All @@ -563,14 +560,12 @@ private void doHeaders() {

if (frameHeader.type() == Http2FrameType.CONTINUATION) {
// end of continuations with header frames
List<Http2FrameData> frames = streamContext.contData();
headers = Http2Headers.create(stream,
requestDynamicTable,
requestHuffman,
frames.toArray(new Http2FrameData[0]));
endOfStream = streamContext.continuationHeader.flags(Http2FrameTypes.HEADERS).endOfStream();
frames.clear();
streamContext.continuationHeader = null;
streamContext.contData());
endOfStream = streamContext.contHeader().flags(Http2FrameTypes.HEADERS).endOfStream();
streamContext.clearContinuations();
continuationExpectedStreamId = 0;
} else {
endOfStream = frameHeader.flags(Http2FrameTypes.HEADERS).endOfStream();
Expand Down Expand Up @@ -720,6 +715,7 @@ private StreamContext stream(int streamId) {
}

streamContext = new StreamContext(streamId,
http2Config.maxHeaderListSize(),
new Http2Stream(ctx,
routing,
http2Config,
Expand Down Expand Up @@ -797,26 +793,58 @@ public void run() {

private static class StreamContext {
private final List<Http2FrameData> continuationData = new ArrayList<>();
private final long maxHeaderListSize;
private final int streamId;
private final Http2Stream stream;
private long headerListSize = 0;

private Http2FrameHeader continuationHeader;

StreamContext(int streamId, Http2Stream stream) {
StreamContext(int streamId, long maxHeaderListSize, Http2Stream stream) {
this.streamId = streamId;
this.maxHeaderListSize = maxHeaderListSize;
this.stream = stream;
}

public Http2Stream stream() {
return stream;
}

public Http2FrameHeader contHeader() {
Http2FrameData[] contData() {
return continuationData.toArray(new Http2FrameData[0]);
}

Http2FrameHeader contHeader() {
return continuationHeader;
}

public List<Http2FrameData> contData() {
return continuationData;
void addContinuation(Http2FrameData frameData) {
if (continuationData.isEmpty()) {
throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received continuation without headers.");
}
this.continuationData.add(frameData);
addAndValidateHeaderListSize(frameData.header().length());
}

void addHeadersToBeContinued(Http2FrameHeader frameHeader, BufferData bufferData) {
clearContinuations();
continuationHeader = frameHeader;
this.continuationData.add(new Http2FrameData(frameHeader, bufferData));
addAndValidateHeaderListSize(frameHeader.length());
}

private void addAndValidateHeaderListSize(int headerSizeIncrement){
// Check MAX_HEADER_LIST_SIZE
headerListSize += headerSizeIncrement;
if (headerListSize > maxHeaderListSize){
throw new Http2Exception(Http2ErrorCode.REQUEST_HEADER_FIELDS_TOO_LARGE,
"Request Header Fields Too Large");
}
}

private void clearContinuations() {
continuationData.clear();
headerListSize = 0;
}
}
}
Loading

0 comments on commit 6da19c2

Please sign in to comment.