skip to content

In a Server-Sent Events stream a server sends retry: 5s and an id value containing U+0000 NULL - what happens to each?

level: middleimportance: nice to knowfreq 28%

answer

  1. parsed values, not just copied
  2. all ASCII digits or ignored entirely
  3. no salvaging of leading digits
  4. NULL anywhere in id, field dropped
  5. ignored means previous value stands

basics

~20 s

Both fields are ignored. A retry value is only accepted when it is entirely ASCII digits, and an id value containing U+0000 NULL is discarded, so each buffer silently keeps the value it already held.

solid answer

~50 s

A Server-Sent Events client validates these two values before it stores them, and rejects them silently rather than repairing them. The `retry` field is accepted only when its value consists entirely of ASCII digits, read as a base-ten count of milliseconds; `5s`, `5000ms` and `5_000` are all ignored, and the leading digits are *not* salvaged. The `id` field is accepted only when its value contains no `U+0000 NULL`; if it does, the field is ignored. In both cases "ignored" means the corresponding buffer keeps its previous value - the client does not reset it, and nothing is reported to the receiving application. So a feed emitting `retry: 5s` reconnects on whatever interval it was already using, and an event whose `id` was rejected leaves the earlier id in place to be sent back as `Last-Event-ID` on the next reconnect.

code

http · 9 lines
http
HTTP/1.1 200 OK
Content-Type: text/event-stream

retry: 5s
id: 4712
data: {"platform":"6","status":"on time"}

retry: 5000
data: {"platform":"6","status":"delayed 6 min"}

go deeper

for a junior

Remember that a retry value must be digits only and is counted in milliseconds, and that a value the client rejects simply has no effect at all.

for a middle

Explain that ignoring a field leaves the previous buffer value in place, so a rejected retry keeps the old interval and a rejected id is re-sent on the next reconnect.

for a senior

Show how you would detect it: reconnect intervals in access logs against the value you emitted, and the Last-Event-ID a reconnect actually carries against the ids you sent.

for a principal

Recognise the pattern across this protocol - the client protects its own uptime and discards what it cannot use - and decide where your systems verify a feed instead of trusting it.

Two field values in an event stream are parsed rather than merely copied, and both have a rule about what to do when the parse does not succeed. The rule is the same in each case and it is the quiet one: ignore the field. Nothing is repaired, nothing is reset, nothing is reported. ## The two parse rules - **`retry`** - the value is accepted only if it consists of nothing but ASCII digits, and is then read as a base-ten integer number of milliseconds. Any other value - a unit suffix, a decimal point, a sign, a separator, surrounding whitespace beyond the single optional `U+0020 SPACE` after the colon - causes the field to be ignored. - **`id`** - the value is accepted unless it contains `U+0000 NULL`, in which case the field is ignored. There is no length limit and no character set restriction beyond that one prohibited character. | the server sends | what the client stores | |---|---| | `retry: 5000` | reconnection time becomes 5000 milliseconds | | `retry: 5s` | nothing changes; the previous reconnection time stands | | `retry: 5000ms` | nothing changes; the suffix is not stripped | | `id: 4712` | the last event ID buffer becomes that value | | an `id` value containing `U+0000 NULL` | nothing changes; the earlier id stands | ## What "ignored" actually means This is where the rule is most often got wrong. Ignoring a field is not the same as clearing it. The client holds a reconnection time and a last event ID; a rejected field leaves both exactly as they were. The consequences differ for the two fields: - A rejected `retry` means the client keeps the interval it already had. If the server has never successfully sent one, that is the client's own default, which the specification does not fix to any particular number. A team that writes `retry: 5s`, watches reconnects happen, and concludes the field worked has simply seen the default in action. - A rejected `id` means the last event ID buffer still holds the id of some earlier event. If the connection then drops, the client sends that older id back as `Last-Event-ID`, so the server resumes from further back than the client actually reached. The visible symptom on the receiving side is duplicate events after a reconnect, not missing ones - which is the friendlier of the two failures, but it is still not what anyone intended. ## Why silent rejection rather than an error The same reasoning runs through all of this client's tolerance rules: the receiving side reconnects on its own and frequently runs unattended, so a strict parser would turn a formatting slip in one field into an outage. Rejecting the field and continuing keeps the stream alive at the cost of a value that never took effect. It also keeps the parse trivially cheap and unambiguous. There is no locale, no unit table, no partial-parse behaviour to disagree about between implementations - a value is either all digits or it is not, which means two clients cannot reconnect on different intervals because they disagreed about `5s`. ## Catching it Since no error is raised anywhere, the check belongs on the emitting side: 1. Emit `retry` from an integer count of milliseconds, never from a formatted duration. A duration formatter that happens to write `5s` is the usual source of this bug. 2. Send `retry` rarely - typically once when the stream opens - and assert on its exact bytes in a test that reads the raw response body, not the parsed events. 3. Derive `id` values from something that cannot contain `U+0000 NULL`. The risk appears when an id is built from binary data, from a serialised structure, or from a field copied out of an upstream system. 4. When you are testing resumption, check the `Last-Event-ID` value the client actually sends on the reconnect request. That single value tells you whether every `id` you emitted was accepted; if it lags behind the events you sent, some of them were rejected. The pattern is the one running through every client tolerance rule: the client's job is to stay up, and it will discard your value rather than complain about it. Anything you want to be sure took effect, you verify from the emitting side or from the wire.

  • How would you notice in production that your retry values were being ignored?
    Compare the interval between a client's reconnect requests in the access log against the value you believe you sent. If they disagree, the field never took effect and the client is using the interval it already held. Nothing else surfaces it, because a rejected field is not reported to the receiving application or logged by the client.
  • A rejected id leaves an older value in the buffer. What does that cause on the next reconnect?
    The client sends the older value as `Last-Event-ID`, so the server is asked to resume from a point earlier than the client actually reached. The receiving side then sees events it has already processed. Duplicates after a reconnect, rather than gaps, are the signature of ids that were silently rejected.

saying these in an interview costs you the question

  • Thinks the client parses the leading digits of a retry value
  • Believes an invalid value resets the buffer to a default
  • Assumes a rejected field surfaces an error somewhere
  • Expects a NULL to be stripped out of an id value
  • Treats retry as seconds rather than milliseconds