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');
|
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 () => {
|
test('streams a response body incrementally', async () => {
|
||||||
const { client } = await setupClient();
|
const { client } = await setupClient();
|
||||||
track(client);
|
track(client);
|
||||||
|
|||||||
@@ -824,15 +824,24 @@ export const createRelayTunnelClient = (options: RelayTunnelClientOptions): Rela
|
|||||||
channel.send(encodeTunnelFrame(TunnelFrameType.HttpRequest, streamId, encodeJsonPayload(head)));
|
channel.send(encodeTunnelFrame(TunnelFrameType.HttpRequest, streamId, encodeJsonPayload(head)));
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
|
let sentBodyFrame = false;
|
||||||
if (request.body) {
|
if (request.body) {
|
||||||
for await (const chunk of request.body) {
|
for await (const chunk of request.body) {
|
||||||
if (finished || channel.dead) return;
|
if (finished || channel.dead) return;
|
||||||
for (const piece of chunkPayload(chunk)) {
|
for (const piece of chunkPayload(chunk)) {
|
||||||
channel.send(encodeTunnelFrame(TunnelFrameType.HttpBody, streamId, piece));
|
channel.send(encodeTunnelFrame(TunnelFrameType.HttpBody, streamId, piece));
|
||||||
|
sentBodyFrame = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!finished && !channel.dead) {
|
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));
|
channel.send(encodeTunnelFrame(TunnelFrameType.StreamEnd, streamId, EMPTY_PAYLOAD));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user