fix admin SSE handshake through reverse proxies (#3598)

This commit is contained in:
Mikhail Merkulov
2026-07-29 16:07:59 -07:00
committed by GitHub
parent 2118f02fc7
commit a175dd0047
2 changed files with 56 additions and 4 deletions
+19 -4
View File
@@ -200,6 +200,24 @@ export type ProbeHealthResult =
| { ok: true; status: 200; body: { status: 'ok'; version: string; engine: string; [k: string]: unknown } }
| { ok: false; status: 503; body: { error: 'service_unavailable'; error_description: string } };
type AdminSseResponse = Pick<Response, 'setHeader' | 'flushHeaders' | 'write'>;
/**
* Complete the admin EventSource handshake immediately.
*
* `flushHeaders()` alone can leave reverse proxies and browsers waiting for
* the first response body bytes. An SSE comment is protocol-valid, ignored by
* EventSource consumers, and makes the stream observable end-to-end without
* fabricating an application event.
*/
export function openAdminSseStream(res: AdminSseResponse): void {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
res.write(': connected\n\n');
}
/**
* Pure async health probe. Races `engine.getStats()` against a timeout,
* returns a tagged result. No Express coupling — easy to unit-test with a
@@ -1697,10 +1715,7 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
// SSE live activity feed
// ---------------------------------------------------------------------------
app.get('/admin/events', requireAdmin, (req: Request, res: Response) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
openAdminSseStream(res);
sseClients.add(res);
req.on('close', () => sseClients.delete(res));
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, test } from 'bun:test';
import { openAdminSseStream } from '../src/commands/serve-http.ts';
describe('admin SSE handshake', () => {
test('flushes a protocol-valid comment immediately after the headers', () => {
const calls: string[] = [];
const headers = new Map<string, string>();
openAdminSseStream({
setHeader(name: string, value: string | number | readonly string[]) {
headers.set(name, String(value));
calls.push(`header:${name}`);
return this;
},
flushHeaders() {
calls.push('flush');
},
write(chunk: unknown) {
calls.push(`write:${String(chunk)}`);
return true;
},
});
expect(headers).toEqual(new Map([
['Content-Type', 'text/event-stream'],
['Cache-Control', 'no-cache'],
['Connection', 'keep-alive'],
]));
expect(calls).toEqual([
'header:Content-Type',
'header:Cache-Control',
'header:Connection',
'flush',
'write:: connected\n\n',
]);
});
});