Server-Sent Events

A text/event-stream channel over an ordinary response, with the wire format's line structure enforced on every field.

SSE is line-oriented: each field is name: value on one line, and a blank line ends an event. A value carrying LF, CR or NUL therefore closes its own field and opens whatever follows, so a value taken from user input can forge later events, forge the event id the client echoes back as Last-Event-ID, or forge the data a handler reads. CVE-2026-33128 (h3), CVE-2026-29085 (Hono) and CVE-2026-44217 (sse-channel) are that one class.

event and id refuse all three characters. data may carry LF, which the framework writes as the several data: lines the spec defines for it, and refuses CR and NUL. An inbound Last-Event-ID carrying any of them reads as null rather than reaching a handler.

b.sse.serializeEvent(opts, errorClass?) #

stable0.8.15soc2
{
  event: string,   // event name; LF / CR / NUL refused
  id:    string,   // event id the client echoes as Last-Event-ID
  data:  string,   // payload; LF splits it into several data: lines
  retry: number,   // reconnection advice in ms, a non-negative integer
}

Encode one event as the string the wire format calls for, without writing it anywhere. A caller that buffers events through its own queue encodes here and writes later.

The validation is the one a channel applies: event and id refuse LF, CR and NUL; data may carry LF and becomes several data: lines; retry is a non-negative whole number of milliseconds. Bad input throws, because a refused event is a programming mistake and dropping it would hide the injection the refusal exists to report.

var b = require("@blamejs/core");
b.sse.serializeEvent({ event: "tick", id: "7", data: "{\"n\":1}" });
// → "id: 7\nevent: tick\ndata: {\"n\":1}\n\n"

b.sse.create(req, res, opts?) #

stable0.8.15soc2
{
  heartbeatMs: number,          // `:keepalive` interval in ms; default 15000, 0 disables
  retryMs:     number,          // reconnection advice sent once on open
  errorClass:  FrameworkError,  // class thrown on bad input; default SseError
  audit:       boolean,         // default true — emit the stream lifecycle
  proxyBuffer: boolean,         // default true — send X-Accel-Buffering: no for nginx
}

Wire a response as an event stream and answer the channel to write on. The SSE headers go out immediately, and the connection stays open until the caller closes it or the client goes away.

The channel carries send({ event, id, data, retry }), which writes one event and throws on a field the wire format cannot hold; comment(text), which writes a : line; close(), which ends the response and stops the heartbeat and may be called more than once; and lastEventId, the value the request's Last-Event-ID header carried, or null when it carried none or carried LF, CR or NUL.

Nothing here is router-specific: it takes the raw request and response, and reads the same over HTTP/1.1 chunked transfer as over an HTTP/2 stream.

var b = require("@blamejs/core");
var app = b.router.create();
app.get("/events", function (req, res) {
  var channel = b.sse.create(req, res, { heartbeatMs: 15000 });
  channel.send({ event: "hello", id: "1", data: "ready" });
  req.on("close", function () { channel.close(); });
});

Last updated 2026-10-12T14:21:56.879Z by seeder.