Skip to content

Commit 609e812

Browse files
authored
Merge pull request #71 from gedaiu/continuation-frames
Add HTTP/2 CONTINUATION frame support
2 parents fb3bde8 + 5859d06 commit 609e812

File tree

9 files changed

+683
-109
lines changed

9 files changed

+683
-109
lines changed

source/vibe/http/internal/http2/exchange.d

Lines changed: 161 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,44 @@ ubyte[] buildHeaderFrame(alias type)(string statusLine, InetHeaderMap headers,
9191
return buildHeaderFrame!type(statusLine, headers, context, context.table, alloc);
9292
}
9393

94+
/// Splits an HPACK-encoded header block into a HEADERS frame followed by
95+
/// zero or more CONTINUATION frames, respecting maxFrameSize.
96+
/// extraFlags are ORed into the HEADERS frame flags (e.g., END_STREAM).
97+
/// END_HEADERS is automatically set on the last frame.
98+
/// Returns the complete serialized frame sequence.
99+
package ubyte[] buildSplitHeaderFrames(ubyte[] hpackPayload, uint maxFrameSize, uint streamId,
100+
ubyte extraFlags, scope IAllocator alloc) @safe
101+
{
102+
auto result = AllocAppender!(ubyte[])(alloc);
103+
104+
if (hpackPayload.length <= maxFrameSize) {
105+
result.createHTTP2FrameHeader(cast(uint) hpackPayload.length, HTTP2FrameType.HEADERS,
106+
cast(ubyte)(HTTP2FrameFlag.END_HEADERS | extraFlags), streamId);
107+
result.put(hpackPayload);
108+
} else {
109+
// First HEADERS frame without END_HEADERS
110+
result.createHTTP2FrameHeader(cast(uint) maxFrameSize, HTTP2FrameType.HEADERS,
111+
extraFlags, streamId);
112+
result.put(hpackPayload[0 .. maxFrameSize]);
113+
114+
auto remaining = hpackPayload[maxFrameSize .. $];
115+
116+
while (remaining.length > 0) {
117+
auto len = remaining.length > maxFrameSize ? maxFrameSize : remaining.length;
118+
bool isLast = (len == remaining.length);
119+
ubyte flags = isLast ? HTTP2FrameFlag.END_HEADERS : 0x0;
120+
121+
result.createHTTP2FrameHeader(cast(uint) len, HTTP2FrameType.CONTINUATION,
122+
flags, streamId);
123+
result.put(remaining[0 .. len]);
124+
125+
remaining = remaining[len .. $];
126+
}
127+
}
128+
129+
return result.data;
130+
}
131+
94132
/// generates an HTTP/2 pseudo-header representation to encode a HTTP/1.1 start message line
95133
private void convertStartMessage(T)(string src, ref T dst, ref IndexingTable table, StartLine type, bool isTLS = true) @safe
96134
{
@@ -155,6 +193,122 @@ unittest {
155193
assert(res == expected);
156194
}
157195

196+
// Tests for buildSplitHeaderFrames
197+
unittest {
198+
import std.experimental.allocator.mallocator;
199+
200+
scope alloc = new RegionListAllocator!(shared(Mallocator), false)(1024, Mallocator.instance);
201+
202+
// helper: parse a frame header from raw bytes
203+
HTTP2FrameHeader parseHeader(ubyte[] data) {
204+
return unpackHTTP2FrameHeader(data);
205+
}
206+
207+
// Small payload fits in a single HEADERS frame
208+
{
209+
ubyte[] payload = [0x88, 0x86, 0x84]; // 3 bytes
210+
auto result = buildSplitHeaderFrames(payload, 16384, 1, 0x0, alloc);
211+
212+
assert(result.length == HTTP2HeaderLength + 3);
213+
auto hdr = parseHeader(result);
214+
assert(hdr.type == HTTP2FrameType.HEADERS);
215+
assert(hdr.payloadLength == 3);
216+
assert(hdr.flags == HTTP2FrameFlag.END_HEADERS);
217+
assert(hdr.streamId == 1);
218+
assert(result[HTTP2HeaderLength .. $] == payload);
219+
}
220+
221+
// Single frame with END_STREAM flag
222+
{
223+
ubyte[] payload = [0x88];
224+
auto result = buildSplitHeaderFrames(payload, 16384, 5, HTTP2FrameFlag.END_STREAM, alloc);
225+
226+
auto hdr = parseHeader(result);
227+
assert(hdr.type == HTTP2FrameType.HEADERS);
228+
assert(hdr.flags == (HTTP2FrameFlag.END_HEADERS | HTTP2FrameFlag.END_STREAM));
229+
assert(hdr.streamId == 5);
230+
}
231+
232+
// Payload exactly at maxFrameSize boundary: single frame
233+
{
234+
auto payload = new ubyte[](10);
235+
payload[] = 0xAB;
236+
auto result = buildSplitHeaderFrames(payload, 10, 3, 0x0, alloc);
237+
238+
assert(result.length == HTTP2HeaderLength + 10);
239+
auto hdr = parseHeader(result);
240+
assert(hdr.type == HTTP2FrameType.HEADERS);
241+
assert(hdr.flags == HTTP2FrameFlag.END_HEADERS);
242+
assert(hdr.payloadLength == 10);
243+
}
244+
245+
// Payload exceeds maxFrameSize: HEADERS + 1 CONTINUATION
246+
{
247+
auto payload = new ubyte[](15);
248+
foreach (i, ref b; payload) b = cast(ubyte)(i & 0xFF);
249+
auto result = buildSplitHeaderFrames(payload, 10, 7, 0x0, alloc);
250+
251+
// First frame: HEADERS, 10 bytes, no END_HEADERS
252+
assert(result.length == 2 * HTTP2HeaderLength + 15);
253+
auto hdr1 = parseHeader(result);
254+
assert(hdr1.type == HTTP2FrameType.HEADERS);
255+
assert(hdr1.payloadLength == 10);
256+
assert(hdr1.flags == 0x0);
257+
assert(hdr1.streamId == 7);
258+
assert(result[HTTP2HeaderLength .. HTTP2HeaderLength + 10] == payload[0 .. 10]);
259+
260+
// Second frame: CONTINUATION, 5 bytes, END_HEADERS
261+
auto frame2 = result[HTTP2HeaderLength + 10 .. $];
262+
auto hdr2 = parseHeader(frame2);
263+
assert(hdr2.type == HTTP2FrameType.CONTINUATION);
264+
assert(hdr2.payloadLength == 5);
265+
assert(hdr2.flags == HTTP2FrameFlag.END_HEADERS);
266+
assert(hdr2.streamId == 7);
267+
assert(frame2[HTTP2HeaderLength .. $] == payload[10 .. 15]);
268+
}
269+
270+
// Payload needs 3 frames: HEADERS + 2 CONTINUATION
271+
{
272+
auto payload = new ubyte[](25);
273+
payload[] = 0xCC;
274+
auto result = buildSplitHeaderFrames(payload, 10, 1, HTTP2FrameFlag.END_STREAM, alloc);
275+
276+
assert(result.length == 3 * HTTP2HeaderLength + 25);
277+
278+
// Frame 1: HEADERS, 10 bytes, END_STREAM only (no END_HEADERS)
279+
auto hdr1 = parseHeader(result);
280+
assert(hdr1.type == HTTP2FrameType.HEADERS);
281+
assert(hdr1.payloadLength == 10);
282+
assert(hdr1.flags == HTTP2FrameFlag.END_STREAM);
283+
284+
// Frame 2: CONTINUATION, 10 bytes, no flags
285+
auto f2 = result[HTTP2HeaderLength + 10 .. $];
286+
auto hdr2 = parseHeader(f2);
287+
assert(hdr2.type == HTTP2FrameType.CONTINUATION);
288+
assert(hdr2.payloadLength == 10);
289+
assert(hdr2.flags == 0x0);
290+
291+
// Frame 3: CONTINUATION, 5 bytes, END_HEADERS
292+
auto f3 = f2[HTTP2HeaderLength + 10 .. $];
293+
auto hdr3 = parseHeader(f3);
294+
assert(hdr3.type == HTTP2FrameType.CONTINUATION);
295+
assert(hdr3.payloadLength == 5);
296+
assert(hdr3.flags == HTTP2FrameFlag.END_HEADERS);
297+
}
298+
299+
// Empty payload: single HEADERS frame with 0 length
300+
{
301+
ubyte[] payload = [];
302+
auto result = buildSplitHeaderFrames(payload, 16384, 1, 0x0, alloc);
303+
304+
assert(result.length == HTTP2HeaderLength);
305+
auto hdr = parseHeader(result);
306+
assert(hdr.type == HTTP2FrameType.HEADERS);
307+
assert(hdr.payloadLength == 0);
308+
assert(hdr.flags == HTTP2FrameFlag.END_HEADERS);
309+
}
310+
}
311+
158312
/* ======================================================= */
159313
/* HTTP/2 REQUEST HANDLING */
160314
/* ======================================================= */
@@ -373,15 +527,9 @@ bool handleHTTP2Request(UStream)(ref HTTP2ConnectionStream!UStream stream,
373527
h2context, table, alloc, istls);
374528
}();
375529

376-
// send HEADERS frame
377-
if (headerFrame.length < h2context.settings.maxFrameSize) {
378-
headerFrame[4] += 0x4; // set END_HEADERS flag (sending complete header)
379-
cstream.write(headerFrame);
380-
381-
} else {
382-
// TODO CONTINUATION frames
383-
assert(false);
384-
}
530+
// send HEADERS frame (+ CONTINUATION if needed)
531+
cstream.write(buildSplitHeaderFrames(headerFrame[HTTP2HeaderLength .. $],
532+
h2context.settings.maxFrameSize, stream.streamId, 0x0, alloc));
385533

386534
logDebug("Sent HEADERS frame on streamID " ~ stream.streamId.to!string);
387535

@@ -485,7 +633,7 @@ bool handleHTTP2Request(UStream)(ref HTTP2ConnectionStream!UStream stream,
485633
// spawn the asynchronous data sender
486634
sendDataTask();
487635

488-
} else if (dataWriter.data.length > 0) { // HEAD response, HEADERS frame, no DATA
636+
} else { // HEAD response or no body (e.g. 204): HEADERS frame only, no DATA
489637

490638
// write the status line
491639
writeLine("%s %d %s",
@@ -499,46 +647,19 @@ bool handleHTTP2Request(UStream)(ref HTTP2ConnectionStream!UStream stream,
499647
h2context, table, alloc, istls);
500648
}();
501649

502-
// send HEADERS frame
503-
if (headerFrame.length < h2context.settings.maxFrameSize) {
504-
headerFrame[4] += 0x5; // set END_HEADERS, END_STREAM flag
505-
cstream.write(headerFrame);
506-
} else {
507-
// TODO CONTINUATION frames
508-
assert(false);
509-
}
650+
// send HEADERS frame with END_STREAM (+ CONTINUATION if needed)
651+
cstream.write(buildSplitHeaderFrames(headerFrame[HTTP2HeaderLength .. $],
652+
h2context.settings.maxFrameSize, stream.streamId, HTTP2FrameFlag.END_STREAM, alloc));
510653

511654
logDebug("Sent HEADERS frame on streamID " ~ stream.streamId.to!string);
512655

513-
logDebug("[Data] No DATA frame to send");
514-
515656
if (stream.state == HTTP2StreamState.HALF_CLOSED_REMOTE) {
516657
stream.state = HTTP2StreamState.CLOSED;
517658
} else {
518659
stream.state = HTTP2StreamState.HALF_CLOSED_LOCAL;
519660
}
520661
closeStream(h2context.multiplexer, stream.streamId);
521662

522-
} else { // 404: no DATA for the given path
523-
524-
writeLine("%s %d %s",
525-
"HTTP/2",
526-
404,
527-
"Not Found");
528-
529-
// build the HEADERS frame
530-
() @trusted {
531-
headerFrame = buildHeaderFrame!(StartLine.RESPONSE)(statusLine.data, res.headers,
532-
h2context, table, alloc, istls);
533-
}();
534-
535-
if (headerFrame.length < h2context.settings.maxFrameSize) {
536-
headerFrame[4] += 0x5; // set END_HEADERS, END_STREAM flag
537-
cstream.write(headerFrame);
538-
}
539-
540-
logDebug("No response: sent 404 HEADERS frame");
541-
542663
}
543664

544665
return true;

source/vibe/http/internal/http2/frame.d

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ import std.algorithm.mutation;
2323

2424
enum uint HTTP2HeaderLength = 9;
2525

26+
enum HTTP2FrameFlag : ubyte {
27+
END_STREAM = 0x1,
28+
END_HEADERS = 0x4,
29+
PADDED = 0x8,
30+
PRIORITY = 0x20,
31+
}
32+
// ACK is the same bit as END_STREAM, used on SETTINGS/PING frames (RFC 7540 §6.2, §6.7)
33+
alias HTTP2FrameFlagACK = HTTP2FrameFlag.END_STREAM;
34+
2635
enum HTTP2FrameType {
2736
DATA = 0x0,
2837
HEADERS = 0x1,

0 commit comments

Comments
 (0)