Test Server-Sent Events (SSE) flows, one-way server-to-client push only (not bidirectional, use websocket-tests for client-to-server messaging): `EventSource` API on the browser side (`onmessage`, `onerror`, `readyState` 0/1/2), event stream format (`data:`, `event:`, `id:`, `retry:`), `Last-Event-ID` reconnect-with-replay header, content-type `text/event-stream`, and HTTP/1.1 connection-pool limits. Use Playwright for browser-side, raw HTTP client for server-side stream tests. Use when a feature pushes updates over `text/event-stream` and the reconnect interval, `Last-Event-ID` replay, or per-origin connection ceiling has no coverage.
70
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Tests the SSE surfaces per the WHATWG SSE spec: stream format,
readyState lifecycle, Last-Event-ID reconnect-with-replay, and
the HTTP/1.1 connection-pool ceiling.
Last-Event-ID,
and HTTP/1.1 connection limits all behave as designed.Per the WHATWG SSE spec, response must use
Content-Type: text/event-stream (UTF-8) and stream lines:
| Field | Meaning |
|---|---|
data: | Appends to message payload (multiple data: lines join with newlines) |
event: | Custom event type (default = message) |
id: | Sets last event ID for reconnect replay |
retry: | Reconnect interval (ms) |
: | Comment line (kept-alive heartbeat) |
Empty line ends a message. Example:
event: order_update
id: 142
data: {"orderId":"o123","status":"shipped"}
event: order_update
id: 143
data: {"orderId":"o124","status":"shipped"}import { test, expect } from '@playwright/test';
test('client receives server-pushed events', async ({ page }) => {
await page.goto('https://localhost:3000/dashboard');
const events = await page.evaluate(() => {
return new Promise<any[]>((resolve) => {
const collected: any[] = [];
const es = new EventSource('/api/orders/stream');
es.addEventListener('order_update', (e: any) => {
collected.push(JSON.parse(e.data));
if (collected.length === 2) {
es.close();
resolve(collected);
}
});
});
});
expect(events).toHaveLength(2);
expect(events[0].orderId).toBe('o123');
});readyState lifecyclePer the WHATWG SSE spec, readyState values:
| Value | State |
|---|---|
| 0 | CONNECTING |
| 1 | OPEN |
| 2 | CLOSED |
test('readyState transitions through CONNECTING → OPEN', async ({ page }) => {
await page.goto('https://localhost:3000/dashboard');
const transitions = await page.evaluate(() => {
return new Promise<number[]>((resolve) => {
const states: number[] = [];
const es = new EventSource('/api/stream');
states.push(es.readyState); // 0
es.onopen = () => {
states.push(es.readyState); // 1
es.close();
states.push(es.readyState); // 2
resolve(states);
};
});
});
expect(transitions).toEqual([0, 1, 2]);
});Reconnect-with-replay via Last-Event-ID, the retry: interval,
204 No Content disable, and the HTTP/1.1 connection-pool ceiling
are in references/sse-test-recipes.md.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Wrong content-type | Browser doesn't recognize as SSE | Content-Type: text/event-stream (Step 1) |
| Skip newline-newline message terminator | Browser buffers indefinitely | Always end messages with \n\n (Step 1) |
No id: on events | Last-Event-ID replay impossible | Always emit id: (see references) |
Multiple EventSource per page on HTTP/1.1 | Connection pool starvation | One stream + multiplex via event: (see references) |
| Use SSE for two-way comms | One-way only; need WebSocket for client→server | Use websocket-tests skill instead |
Last-Event-ID only - server must persist event
IDs (or generate from timestamp) for replay to work.websocket-tests - bidirectional
alternativegrpc-streaming-tests - typed
RPC streaming alternative