skip to content

A fault report spans three lines in a Server-Sent Events stream - how does the client rebuild that one payload?

level: middleimportance: must knowfreq 60%

answer

  1. one payload, several lines on the wire
  2. the colon splits name from value
  3. exactly one space after the colon goes
  4. each data line adds a line feed
  5. final line feed removed at dispatch

basics

~20 s

Each data line contributes its value. The client joins them with a line feed and removes the final one when the block dispatches, so three data lines become one three-line payload. A single space after the colon is stripped.

solid answer

~40 s

A payload is built incrementally. For every `data` line in the block, the client takes the value - everything after the first colon, minus one leading space if there is one - appends it to the payload buffer, then appends a line feed. When the blank line arrives, the client removes the **single trailing line feed** it just added and dispatches what is left as one event. So three `data` lines produce one payload of three lines with no trailing newline, not three events and not one run-together string. Only the first space after the colon is removed, so `data: Door open` carries two leading spaces into the payload, and `data:` with nothing after it contributes an empty line.

code

http · 4 lines
http
event: fault
data: Car A halted between floors 9 and 10
data:
data:   Door sensor still reports open

go deeper

for a junior

Know that repeated data lines build one payload rather than several events, and that the blank line is what ends the block. The joining rule is the part interviewers ask for by name.

for a middle

Explain the assembly precisely: append value, append a line feed, then drop the final line feed at dispatch. Add the one-leading-space rule and the first-colon split, since both bite in practice.

for a senior

Be ready for the edge cases that reach production: a bare data line producing an empty payload line, a trailing space surviving into the payload, and a block with no data field dispatching nothing.

for a principal

Judge the design: structure carried in line prefixes rather than an escaping scheme keeps the stream human-readable and hand-debuggable, at the price of a payload that can never contain a blank line.

## One field at a time The parser works line by line and holds no lookahead. For each non-blank, non-comment line it splits at the **first** `U+003A COLON (:)`: the name is what came before, the value is what came after. Then one rule about whitespace, and one only: > If the value begins with a single `U+0020 SPACE`, remove that space. Do nothing else to the value. That is why `data: hello` and `data:hello` carry the identical payload, while `data: hello` carries one leading space and `data: hello` carries two. Nothing is trimmed from the end of a value, so a trailing space survives into the payload - a real source of mismatched string comparisons on the receiving side. ## How data lines join When the field name is `data`, the client does two things in order: 1. Append the value to the payload buffer. 2. Append a `U+000A LINE FEED (LF)` to the payload buffer. Step 2 runs after **every** `data` line, including the last one. That would leave a stray newline on every payload, so the dispatch step corrects it: when the blank line arrives and the payload buffer is non-empty, the client removes the last line feed and delivers the rest. Walk the three-line fault block from the code example through those rules: | line on the wire | value after the space rule | payload buffer afterwards | |---|---|---| | `data: Car A halted between floors 9 and 10` | `Car A halted between floors 9 and 10` | that text, then LF | | `data:` | empty string | previous, then LF (an empty line) | | `data: Door sensor still reports open` | ` Door sensor still reports open` | previous, that text, then LF | | (blank line) | - | final LF removed, event dispatched | The application receives one string of three lines, the middle one empty and the third indented by two spaces - never three separate events. ## Line terminators A line ends with any of three things: a lone CR, a lone LF, or CR followed immediately by LF, which counts as **one** terminator rather than two. That last rule matters, because if CRLF counted as two line endings then every CRLF-terminated emitter would dispatch after every single line. An emitter is therefore free to use whichever convention its output layer produces; a mixed stream still parses. ## Why the payload is assembled, not escaped The alternative design - one line per event, with newlines escaped inside it - would have forced every payload through an escaping scheme, and the grammar deliberately does not do that. Instead the structure lives in the prefixes: - a newline in the payload becomes a new `data` line; - an empty line in the payload becomes a bare `data:`; - a blank line, with no prefix at all, is reserved for dispatch and can therefore never appear inside a payload; - a colon in the *value* is untouched, because only the **first** colon on the line splits name from value, which is what lets a payload carry a timestamp or a JSON object without any quoting. That last point is the one candidates miss: `data: {"at":"09:41:02"}` parses perfectly, because splitting stops at the first colon. ## Consequences worth naming - **Multi-line payloads cost nothing.** A stack trace, a formatted fault report or a block of generated text goes out as one event, one `data` line per line of text. - **The trailing newline is not yours.** If you need one in the payload, write an extra bare `data:` line; the one the parser appends is always removed. - **A block with no `data` field at all dispatches nothing.** A block carrying only a type name is not an empty event - it is no event, and the buffers are cleared when the blank line arrives. - **Byte counting is not framing here.** The grammar is defined over lines and characters; nothing in the block declares a length, and nothing needs to. The whole assembly is about ten lines of code, which is the point: a client can be written by hand, and an emitter can be debugged by reading the response body with your eyes.

  • What does a block carrying an event name but no data line dispatch?
    Nothing. When the blank line arrives the client checks the payload buffer; it is empty, so no event is dispatched and the buffered type name is discarded along with it. An emitter that wants an empty event must write a bare `data:` line to put something in the buffer.
  • Which line terminators does the grammar accept?
    A lone carriage return, a lone line feed, or a carriage return followed immediately by a line feed - and that pair counts as one terminator, not two. A stream written with CRLF therefore does not produce a phantom blank line after every field, and mixed terminators still parse.
  • A payload contains a colon, such as a timestamp. Does it need escaping?
    No. Only the first colon on the line separates the field name from the value, so every later colon is ordinary payload text. That is why a JSON object or a clock time can be written straight into a `data` line with no quoting scheme at all.

saying these in an interview costs you the question

  • Thinks a multi-line payload must be escaped into one data line
  • Believes every space after the colon is stripped from the value
  • Expects three data lines to arrive as three separate events
  • Assumes the delivered payload keeps a trailing line feed
  • Thinks a colon inside the payload has to be escaped