fix(relay): deliver an explicit empty body frame for zero-chunk body sources
A request body stream that yields no chunks still declares hasBody in the request head; send one empty HttpBody frame so the host can tell an intentionally empty body apart from body frames lost in transit.
This commit is contained in:
@@ -312,6 +312,27 @@ describe('createRelayTunnelClient', () => {
|
||||
expect(await c.text()).toBe('payload-xyz');
|
||||
});
|
||||
|
||||
test('a body source with zero chunks still delivers an explicit empty body frame', async () => {
|
||||
const frames: TunnelFrame[] = [];
|
||||
const { client } = await setupClient({ recordFrame: (frame) => frames.push(frame) });
|
||||
track(client);
|
||||
const emptyStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const response = await client.fetch('/echo-body', { method: 'POST', body: emptyStream });
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.text()).toBe('');
|
||||
// The head declares hasBody, so the host must see at least one HttpBody
|
||||
// frame — otherwise it treats the body as lost in transit and aborts.
|
||||
const request = frames.find((frame) => frame.frameType === TunnelFrameType.HttpRequest);
|
||||
expect(request).toBeDefined();
|
||||
const bodyFrames = frames.filter((frame) => frame.frameType === TunnelFrameType.HttpBody && frame.streamId === request!.streamId);
|
||||
expect(bodyFrames.length).toBe(1);
|
||||
expect(bodyFrames[0]!.payload.length).toBe(0);
|
||||
});
|
||||
|
||||
test('streams a response body incrementally', async () => {
|
||||
const { client } = await setupClient();
|
||||
track(client);
|
||||
|
||||
@@ -824,15 +824,24 @@ export const createRelayTunnelClient = (options: RelayTunnelClientOptions): Rela
|
||||
channel.send(encodeTunnelFrame(TunnelFrameType.HttpRequest, streamId, encodeJsonPayload(head)));
|
||||
void (async () => {
|
||||
try {
|
||||
let sentBodyFrame = false;
|
||||
if (request.body) {
|
||||
for await (const chunk of request.body) {
|
||||
if (finished || channel.dead) return;
|
||||
for (const piece of chunkPayload(chunk)) {
|
||||
channel.send(encodeTunnelFrame(TunnelFrameType.HttpBody, streamId, piece));
|
||||
sentBodyFrame = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!finished && !channel.dead) {
|
||||
// A body source that yielded no chunks (e.g. an empty stream) still
|
||||
// declared hasBody in the head. Emit one empty body frame so the
|
||||
// host can tell this apart from body frames lost in transit, which
|
||||
// it aborts as an ambiguous transport failure.
|
||||
if (request.body && !sentBodyFrame) {
|
||||
channel.send(encodeTunnelFrame(TunnelFrameType.HttpBody, streamId, EMPTY_PAYLOAD));
|
||||
}
|
||||
channel.send(encodeTunnelFrame(TunnelFrameType.StreamEnd, streamId, EMPTY_PAYLOAD));
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user