Your Server-Sent Events emitter writes a final fault block and closes the response, but clients never see that event - why?
answer
- bytes on the wire are not an event
- the parser is still holding the block
- nothing dispatches without the blank line
- a block with no data field dispatches nothing
- colon-first lines fire nothing at all
basics
~20 sThe block was never terminated by a blank line, so the client is still accumulating it. An incomplete block sitting in the parser's buffers when the body ends is discarded, not delivered - the end of the response dispatches nothing.
solid answer
~40 sNothing was lost in transit: the bytes arrived and the client parsed them into its buffers, but **only a blank line dispatches**, and the emitter never wrote one after the last block. When the body ends, the client does not flush what it is holding - the partial block is simply discarded. The fix is grammatical rather than operational: the last byte an emitter writes for an event is the terminating blank line, not the last `data` line. The same symptom - bytes on the wire, no event delivered - has two other grammar-level causes worth checking: a block carrying no `data` field at all dispatches nothing when its blank line arrives, and a line beginning with a colon is a comment that fires nothing.
code
http · 7 lines: stream opened
event: fault
data: {"car":"C","code":"OVERSPEED"}
event: fault
data: {"car":"C","code":"DOORS_LOCKED"}go deeper
Remember that writing the field lines is not publishing an event. The blank line after them is what delivers it, and leaving it out means the event is never seen.
Explain the buffering: field lines accumulate, a blank line dispatches, and nothing else does. Say what happens to an incomplete block when the body ends - it is discarded, not flushed.
Diagnose from the raw body rather than the application. Distinguish an unterminated block, a block with no data field and a comment line, since all three show bytes on the wire and no delivered event.
Make the failure structurally impossible: events are emitted through one operation that always writes its terminator, so no code path can publish half a block and no reviewer has to notice.
## The bytes arrived; the event did not This is the most common Server-Sent Events bug that survives a code review, because every layer looks healthy. Capture the response body and the fault block is visibly there, spelled correctly, with the right field names. The receiving application simply never sees it. Nothing timed out, nothing was dropped, and no error is reported anywhere - because as far as the grammar is concerned nothing went wrong. The client is still waiting for the block to end. The parser is a small state machine over buffers. `data` lines append to a payload buffer, an `event` line sets a type buffer, and **a blank line is the only transition that dispatches**. There is no timer, no size threshold and no end-of-body rule that flushes a partial block. When the response body ends with a block still accumulating, the client discards those buffers. ## Three grammar-level reasons nothing was dispatched | what the emitter wrote | what the client did | delivered? | |---|---|---| | field lines, then the body ended | kept the buffers, then discarded them | no | | `event: fault` and a blank line, no `data` line | found the payload buffer empty, cleared both buffers | no | | `: fault pending` (colon first) | ignored the line entirely | no | | field lines, then a blank line | assembled and dispatched one event | yes | The second row is worth sitting with. A block that names a type but carries no `data` field is not an empty event - it is no event. If you genuinely want to dispatch something with an empty payload, write a bare `data:` line: the field is then present, its value is the empty string, and the payload buffer holds one line feed, which the dispatch step removes to give an empty-string payload. Presence of the field, not content, is what decides. The third row is the comment rule, and it is the reason a debugging line written as `: about to send fault` produces exactly nothing, which is what it is for. ## Why the end of the body is not a dispatch It would be easy to assume that closing the response means "and dispatch whatever you have". The grammar deliberately does not say that, and the reason is that the client cannot tell a deliberate ending from a truncation. A response can end because the emitter finished, because an intermediary cut it, or because the network failed mid-write - and in the last two cases the accumulated bytes are half an event. Delivering them would hand the application a fault report missing its second half, indistinguishable from a real one. Discarding an unterminated block is the safe reading of an ambiguous ending: an event is delivered only when the emitter has explicitly said it is complete. ## Diagnosing it Work in this order, because each step is cheaper than the next: 1. **Read the raw body, not the application's view.** Every event must be followed by an empty line. If the last block is not, you have the answer already. 2. **Count the blank lines against the events you expect.** N events means N blank lines in the body, one per block. 3. **Check each block for at least one `data` line.** A type-only block explains a missing event with no missing bytes. 4. **Check the field spellings literally.** A capitalised or misspelt name is ignored in silence, and a block whose only `data` line was misspelt has an empty payload buffer - which lands you back on row two of the table. 5. **Only then look outside the grammar.** If every block is terminated, well formed and carries data, the cause is no longer the wire format. ## What to write instead The rule for an emitter is one sentence: **the last thing written for any event is its blank line.** Practically that means: - treat "write an event" as one operation that ends with the terminator, never as "write the fields" plus a separate step someone can forget; - when ending a stream deliberately, terminate the final block before ending the body - anything written after that terminator and before the end is discarded; - never rely on the next event's field lines to "push" the previous one out, because they do not: the previous block stays open until a blank line arrives, and the new lines simply join it, producing one corrupted event instead of two good ones. That last failure is the nastier sibling of this bug. A missing terminator in the middle of a stream does not lose one event - it merges two, so the application receives a payload with another event's data appended to it, and often parses it as a single malformed record long after the emitter has moved on.
- What does a block containing `data:` with an empty value dispatch?An event whose payload is the empty string. The field is present, so the payload buffer receives an empty value plus a line feed, and that line feed is removed at dispatch. That is different from a block with no `data` field at all, which dispatches nothing.
- Does a comment line affect the block currently being accumulated?No. A line whose first character is a colon is ignored completely: it contributes no field, it does not dispatch, and it leaves the payload and type buffers exactly as they were. A partially built block survives any number of comment lines in the middle of it.
- Where does the blank line between the response headers and the body fit into this?It does not. That separator belongs to HTTP message framing and the event-stream parser never sees it; parsing starts at the first byte of the body. It is not a dispatch, and it does not terminate anything in the grammar.
- What happens if an emitter forgets the terminator in the middle of a stream rather than at the end?Two events merge into one. The next block's field lines join the block still open, so its `data` lines append to the same payload buffer and its `event` line overwrites the type. The application receives one event carrying both payloads, usually as an unparsable record.
saying these in an interview costs you the question
- Blames the network for an event that was never terminated
- Thinks closing the response flushes the block still being accumulated
- Assumes a comment line dispatches an empty event
- Believes a block with only an event name still dispatches
- Adds a flush per line instead of writing the terminating blank line