mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
fix admin SSE handshake through reverse proxies (#3598)
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user