-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
server sent events changed in bun v1.1.27 and 1.1.26 #13811
Comments
Having a similar problem. SSE response got disconnected after roughly 10 seconds after the connection has been established. In v1.1.26 it works without an issue, but after upgrading to v1.1.27 SSE connection (response type |
Try setting @cirospaciari maybe we should disable idle timeout when you start streaming |
Testing with the code: import EventEmitter, { once } from "node:events";
import net from "node:net";
const debug = console.debug;
const trace = console.trace;
const emitter = new EventEmitter();
emitter.setMaxListeners(0);
const subscribe = (req, channel, event, data) => {
return new Response(
new ReadableStream({
type: "direct",
pull(controller) {
let id = +(req.headers.get("last-event-id") ?? 0);
const handler = async (ev, dat) => {
await controller.write(`id:${id}\nevent:${ev}\ndata:${dat !== undefined ? JSON.stringify(dat) : ""}\n\n`);
await controller.flush();
id++;
};
debug(`subscribing to ${channel}`);
emitter.on(channel, handler);
req.signal.onabort = () => {
debug(`unsubscribing from ${channel}`);
emitter.off(channel, handler);
};
handler(event ?? "connect", data);
return new Promise(() => void 0);
},
}),
{
status: 200,
headers: { "content-type": "text/event-stream;charset=utf-8" },
},
);
};
const emit = (channel, event, data) => {
if (emitter.listeners(channel).length === 0) {
debug(`no listeners for ${channel}`);
return;
}
trace(`emitting to ${channel} (${emitter.listenerCount(channel)} listeners)`);
emitter.emit(channel, event, data);
};
const server = Bun.serve({
port: 0,
async fetch(req) {
return subscribe(req, "test", "connect", { hello: "world" });
},
});
console.log(`Server running on ${server.url}`);
setInterval(() => {
emit("test", "ping", { time: Date.now() });
}, 30000);
async function addClient() {
const socket = net.connect(server.port, "localhost");
await once(socket, "connect");
socket.write("GET / HTTP/1.1\r\n");
socket.write("Host: localhost\r\n");
socket.write("Connection: keep-alive\r\n");
socket.write("Accept: text/event-stream\r\n");
socket.write("\r\n");
}
for (let i = 0; i < 3000; i++) {
await addClient();
} In this case I leave the connection idle for 30s left timeouts and unsubscribes everyone, If I keep it active it will not timeout (1 event each second for example), if The warning: warn: Possible EventEmitter memory leak detected. 2049 metrics listeners added to [EventEmitter2]. Use emitter.setMaxListeners() to increase limit
at overflowWarning (node:events:32:25)
at addListener (node:events:268:57)
at pull (/Users/simylein/Private/simylein/lib/event/event.ts:20:13) Shows that you have too many active listeners in this case active connections sending events. Do you have any crash report or log that would show a error occurring causing the application to stop? Or the server is down due to too many connections? |
As far as I am able to understand the pull method inside the readable stream runs recursively in bun v1.1.27 and not in bun v1.1.26 with my setup. I will try to get a more minimal reproduction but I suspect it has to do with the non resolving promise. |
@simylein in the version You can also set a total request timeout and resolve the promise when you got aborted, never leaving non resolving promises: const subscribe = (req, channel, event, data) => {
return new Response(
new ReadableStream({
type: "direct",
pull(controller) {
let id = +(req.headers.get("last-event-id") ?? 0);
const handler = async (ev, dat) => {
await controller.write(`id:${id}\nevent:${ev}\ndata:${dat !== undefined ? JSON.stringify(dat) : ""}\n\n`);
await controller.flush();
id++;
};
debug(`subscribing to ${channel}`);
emitter.on(channel, handler);
const { promise, resolve } = Promise.withResolvers();
// total requestTimeout 5min
const timer = setTimeout(resolve, 5 * 60 * 1000);
req.signal.onabort = () => {
debug(`unsubscribing from ${channel}`);
emitter.off(channel, handler);
resolve(); // resolve the promise when the client disconnects
clearTimeout(timer); // clear the timeout when the client disconnects
};
handler(event ?? "connect", data);
return promise;
},
}),
{
status: 200,
headers: { "content-type": "text/event-stream;charset=utf-8" },
},
);
}; |
you are right, this minimal sample is working as intended. import { serve } from 'bun';
import EventEmitter from 'events';
const debug = (message: string): void => {
console.log(message);
};
const trace = (message: string): void => {
console.log(message);
};
export const emitter = new EventEmitter();
emitter.setMaxListeners(2048);
export const subscribe = (req: Request, channel: string, event?: string, data?: unknown): Response => {
return new Response(
new ReadableStream({
type: 'direct',
pull(controller: ReadableStreamDirectController) {
let id = +(req.headers.get('last-event-id') ?? 0);
const handler = async (ev: string, dat: unknown): Promise<void> => {
await controller.write(`id:${id}\nevent:${ev}\ndata:${dat !== undefined ? JSON.stringify(dat) : ''}\n\n`);
await controller.flush();
id++;
};
debug(`subscribing to ${channel}`);
emitter.on(channel, handler);
req.signal.onabort = () => {
debug(`unsubscribing from ${channel}`);
emitter.off(channel, handler);
};
void handler(event ?? 'connect', data);
return new Promise(() => void 0);
},
}),
{
status: 200,
headers: { 'content-type': 'text/event-stream;charset=utf-8' },
},
);
};
export const emit = (channel: string, event: string, data?: unknown): void => {
trace(`emitting to ${channel}`);
emitter.emit(channel, event, data);
};
serve({
port: 4000,
fetch(request: Request): Response {
return subscribe(request, 'metrics');
},
});
setInterval(() => emit('metrics', 'log'), 1000); There must be something else I did wrong with my setup. |
@simylein Will wait for a repro, @Jarred-Sumner are you aware of some stream change in |
increasing the |
import { serve, Serve, Server } from 'bun';
import EventEmitter from 'events';
const req = (path: string, ip: string): void => {
console.log(`req: ${path} from ${ip}`);
};
const res = (status: number, bytes: number): void => {
console.log(`res: ${status} ${bytes} bytes`);
};
const trace = (message: string): void => {
console.log(`trace: ${message}`);
};
const debug = (message: string): void => {
console.log(`debug: ${message}`);
};
const info = (message: string): void => {
console.log(`info: ${message}`);
};
const warn = (message: string): void => {
console.log(`warn: ${message}`);
};
type FluxifyServer = Server & {
routes: Routes;
};
type FluxifyRequest = Request & {
ip: string;
};
type Routes = Map<string, Routes | Route>;
type Route = {
method: string;
endpoint: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: ({ param, query, body, jwt, req }: any) => Response;
};
declare global {
// eslint-disable-next-line no-var
var server: FluxifyServer;
}
const traverse = (router: Routes, endpoint: string): Routes | null => {
if (endpoint.endsWith('/') && endpoint !== '/') {
return null;
}
const frags = endpoint.split('/').filter((frag) => !!frag);
const walk = (parent: Routes, ind: number): Routes | null => {
if (ind >= frags.length) {
if ([...parent.values()].every((value) => value instanceof Map)) {
return null;
}
return parent;
}
const child = parent.get(frags[ind]) as Routes | undefined;
if (!child) {
for (const [key, value] of parent.entries()) {
if (key.startsWith(':')) {
const result = walk(value as Routes, ind + 1);
if (result) {
return result;
}
}
}
return null;
}
return walk(child, ind + 1);
};
return walk(router, 0);
};
const pick = (matching: Routes | null, method: string): Route | undefined => {
const route = matching?.get(method === 'head' ? 'get' : method) as Route | undefined;
if (!route) {
return matching?.get('all') as Route | undefined;
}
return route;
};
const bootstrap = (): FluxifyServer => {
const options: Serve = {
port: 4000,
async fetch(request: FluxifyRequest, server: Server): Promise<Response> {
request.ip = server.requestIP(request)?.address ?? '';
const url = new URL(request.url);
req(url.pathname, request.ip);
const matchingRoutes = traverse(global.server.routes, url.pathname);
const targetRoute = pick(matchingRoutes, request.method.toLowerCase());
if (targetRoute) {
const data = await targetRoute.handler({ param: null, query: null, body: null, jwt: null, req: request });
if (data instanceof Response) {
res(data.status, (await data.clone().blob()).size);
return data;
}
}
return new Response(null, { status: 404 });
},
};
info(`starting http server...`);
if (!global.server) {
global.server = serve(options) as FluxifyServer;
} else {
global.server.reload(options);
}
global.server.routes = routes;
info(`listening on localhost:${server.port}`);
return global.server;
};
const routes: Routes = new Map();
const register = (route: Route): void => {
const frags = route.endpoint.split('/').filter((frag) => !!frag);
const walk = (parent: Routes, ind: number): void => {
if (ind >= frags.length) {
if (parent.has(route.method)) {
return warn(`ambiguous route ${route.method} ${route.endpoint}`);
}
parent.set(route.method, route);
return debug(`mapped route ${route.method} ${route.endpoint}`);
}
if (!parent.has(frags[ind])) {
parent.set(frags[ind], new Map());
}
const child = parent.get(frags[ind])! as Routes;
walk(child, ind + 1);
};
walk(routes, 0);
};
const emitter = new EventEmitter();
emitter.setMaxListeners(2048);
const subscribe = (request: Request, channel: string, event?: string, data?: unknown): Response => {
return new Response(
new ReadableStream({
type: 'direct',
pull(controller: ReadableStreamDirectController) {
let id = +(request.headers.get('last-event-id') ?? 0);
const handler = async (ev: string, dat: unknown): Promise<void> => {
await controller.write(`id:${id}\nevent:${ev}\ndata:${dat !== undefined ? JSON.stringify(dat) : ''}\n\n`);
await controller.flush();
id++;
};
debug(`subscribing to ${channel}`);
emitter.on(channel, handler);
request.signal.onabort = () => {
debug(`unsubscribing from ${channel}`);
emitter.off(channel, handler);
};
void handler(event ?? 'connect', data);
return new Promise(() => void 0);
},
}),
{
status: 200,
headers: { 'content-type': 'text/event-stream;charset=utf-8' },
},
);
};
const emit = (channel: string, event: string, data?: unknown): void => {
trace(`emitting to ${channel}`);
emitter.emit(channel, event, data);
};
register({
method: 'get',
endpoint: '/sse',
handler: ({ req: request }) => {
info('streaming metrics');
return subscribe(request, 'metrics');
},
});
bootstrap();
setInterval(() => emit('metrics', 'log'), 2000); bun v1.1.25
bun v1.1.27
|
@simylein looks like if you comment the line Is related to |
Pull will be called as many times it takes until |
Note: this is not correct - |
I can confirm that in |
What version of Bun is running?
1.1.27+267afa293
What platform is your computer?
Darwin 23.6.0 arm64 arm
What steps can reproduce the bug?
I am using
serve
to stream server sent events over http.In bun v1.1.25 (and before) everything works as expected.
Starting in 1.1.26 I get disconnected after 8 seconds.
However, in 1.1.27 it completely breaks and takes down my Linux server.
I am using the following code to pub sub a readable stream.
Have there been breaking changes or is this new behaviour intentional?
If so, can someone advise how server sent events are supposed to be used now?
What is the expected behavior?
bun v1.1.25 behaviour
What do you see instead?
bun v1.1.26 behaviour
bun v1.1.27 behaviour
Additional information
The code is a section of fluxify (https://github.com/simylein/fluxify) and it's server sent events handling.
The text was updated successfully, but these errors were encountered: