Skip to content

Commit b8c0ef3

Browse files
thymikeeclaude
andcommitted
fix(proxy): abort a request whose client left before the adapter loaded
Loading node-http.ts on the first request lets the response close before the disconnect listeners attach, so the signal also checks res.closed. Adapter tests assert the response ends again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 5ed888a commit b8c0ef3

2 files changed

Lines changed: 20 additions & 5 deletions

File tree

‎packages/proxy/src/node-http.test.ts‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ import { serveProxyRequest } from './node-http.ts';
1111
async function serveThroughListener(
1212
socket: Socket,
1313
host: string,
14-
): Promise<{ seenUrl: string | null; status: number }> {
14+
{ responseClosed = false } = {},
15+
): Promise<{ seenUrl: string | null; clientGone: boolean; status: number; ended: boolean }> {
1516
let seenUrl: string | null = null;
17+
let clientGone = false;
18+
let ended = false;
1619
const proxy: DaemonProxy = {
1720
instanceId: 'test',
1821
handle: async (request) => {
1922
seenUrl = request.url;
23+
clientGone = request.signal.aborted;
2024
return new Response(null, { status: 204 });
2125
},
2226
};
@@ -30,11 +34,14 @@ async function serveThroughListener(
3034
statusCode: 200,
3135
writableFinished: false,
3236
destroyed: false,
37+
closed: responseClosed,
3338
setHeader: () => {},
34-
end: () => {},
39+
end: () => {
40+
ended = true;
41+
},
3542
});
3643
await serveProxyRequest(proxy, req, res as unknown as ServerResponse);
37-
return { seenUrl, status: res.statusCode };
44+
return { seenUrl, clientGone, status: res.statusCode, ended };
3845
}
3946

4047
test('requests arriving over TLS reach the proxy with an https URL', async () => {
@@ -49,12 +56,19 @@ test('plain requests reach the proxy with an http URL', async () => {
4956

5057
test('a Host header that cannot form a URL is answered with 400 instead of a dropped socket', async () => {
5158
const served = await serveThroughListener(new Socket(), 'gateway example');
52-
expect(served).toEqual({ seenUrl: null, status: 400 });
59+
expect(served).toMatchObject({ seenUrl: null, status: 400, ended: true });
5360
});
5461

5562
test('a URL carrying credentials is answered with 400 instead of a dropped socket', async () => {
5663
const served = await serveThroughListener(new Socket(), 'user:secret@gateway.example.test');
57-
expect(served).toEqual({ seenUrl: null, status: 400 });
64+
expect(served).toMatchObject({ seenUrl: null, status: 400, ended: true });
65+
});
66+
67+
test('a client gone before the adapter loads reaches the proxy as an aborted request', async () => {
68+
const served = await serveThroughListener(new Socket(), 'gateway.example.test', {
69+
responseClosed: true,
70+
});
71+
expect(served.clientGone).toBe(true);
5872
});
5973

6074
test('a method Fetch refuses is answered with 404 instead of a dropped socket', async () => {

‎packages/proxy/src/node-http.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,6 @@ function clientGoneSignal(req: IncomingMessage, res: ServerResponse): AbortSigna
7272
};
7373
req.on('aborted', abortIfResponseIncomplete);
7474
res.on('close', abortIfResponseIncomplete);
75+
if (res.closed) abortIfResponseIncomplete();
7576
return clientGone.signal;
7677
}

0 commit comments

Comments
 (0)