Build1 distinct publisher2 min readUpdated
A Go notification service reset right after the 200 because of a copied Connection header, then died at 30 seconds on server defaults written for finite requests.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
The two failures produce the same error string, which is why the second one needed two rounds. The shared server package sets its WriteTimeout to 30 seconds, and the middleware that cancels the request context is also set to 30 seconds [9]. The effective deadline is 30 seconds whichever one you remove first, so the browser reports the same reconnect loop at the same second [1]. A partial fix is indistinguishable from no fix, which is an expensive property for a config bug to have.
The error text is no help in sorting the two apart. In the first incident the protocol really was at fault: the ingress re-emitted a hop-by-hop header that HTTP/2 forbids, and the stream was reset immediately after the 200 [6]. In the second, the identical browser error came out of a value in a Go struct [10]. Triage by symptom sends you to the ingress both times, and is correct once.
Worth noting what the offending header bought: nothing. HTTP/2 connections are multiplexed and persistent by design, and HTTP/1.1 already treats keep-alive as the default [7]. It survived in tutorials, got copied into a handler, and stayed there until a conformance check killed it [5].
The one default the author kept as protection is the idle timeout, and it holds only because of arithmetic. A heartbeat every 30 seconds against a 60-second IdleTimeout puts two writes inside every idle window [12], so one missed beat is survivable and two consecutive ones are not [2]. That is the real liveness budget of the endpoint, and it is nowhere in the config. ReadTimeout never fires at all, because an SSE client sends nothing after its request [12].
The cockpit incident is capacity rather than correctness. A browser allows roughly six connections per origin on HTTP/1.1; a permanent stream holds one, five remain for everything else, and a second tab exhausts the budget [14]. HTTP/2 collapses that into a single multiplexed tunnel that carries the stream alongside ordinary requests [15]. The author pulled SSE out of the cockpit [13], then reinstated it behind a check: the endpoint answers only when the request arrived through the HTTPS front, which sets a header the direct HTTP/1.1 origin does not [16]. Admission control on transport, enforced by the application.
Which is the same lesson as the August 2026 Go patch the author cites, where a timeout was not applied to HTTP/2 connections [17]. Configured is not the same as covering, and for anything that stays open, the reviewable question is which connections a deadline actually reaches [3].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
SSE is a one-way HTTP stream: the server opens a text/event-stream response, writes lines and flushes, and the browser receives messages as they arrive.
To the server an SSE stream is not a special case but a very slow request, and the guardrails in an HTTP server (write timeout, context timeout, idle timeout) exist to kill requests that drag on.
In the first incident the endpoint answered 200, then the browser showed net::ERR_HTTP2_PROTOCOL_ERROR and the client reconnected in a loop.
The cause was one line: the handler set a Connection: keep-alive header, copied from an old SSE tutorial. Connection is a hop-by-hop header, and HTTP/2 forbids hop-by-hop headers (RFC 9113 section 8.2.2).
The browser speaks HTTP/2 to the ingress; the ingress re-emits the response, and the illegal header resets the stream right after the 200.
The fix required both settings: the stream path added to BypassTimeoutPaths and a per-path WriteTimeoutOverride of 0. One alone was not enough, and it took the author two rounds to learn that.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
First-hand and mechanism-level, but single-source and unmeasured
The technical mechanisms are specific, internally consistent and partly checkable against a cited standard (RFC 9113 §8.2.2 on hop-by-hop headers), and the timeout arithmetic is shown with the actual default values and the code that overrides them. Against that, everything rests on one dev.to post: two self-reported incidents with no logs, error rates or reproduction outside the author's stack, and a relayed Go advisory with no vendor release note in the cluster. The fix API (BypassTimeoutPaths, WriteTimeoutOverride) belongs to an unnamed in-house package, so it is not independently verifiable.
Two self-reported deployments plus a broadly-scoped Go patch
Real deployment signal exists but is narrow: the author's own two production SSE endpoints, one of which was withdrawn and then reinstated behind an HTTP/2-only guard. The one broad-reach datum is the reported Go 1.25.13 / 1.26.6 security release, which touches every Go HTTP server on affected lines, but the article supplies no figures on who upgraded, and no third party is shown adopting the header or timeout practice the post prescribes.
Mildly overstated generalisation of one shop's defaults
The headline framing ('breaks twice before your handler runs', 'your Go server's default timeouts cut the stream at 30 seconds') generalises values that come from the author's in-house package, not from Go's own defaults, and the checklist item 'disable WriteTimeout on that path' is presented without the slow-client exposure it reintroduces. The linkage drawn between the two incidents and the reported CVE is thematic rather than causal. Otherwise the substance is deflationary and well-scoped - a copied header and a config mismatch - so the gap is small rather than promotional.
Practitioner write-up, no disclosed commercial stake
The only visible incentive is developer-platform reputation: a first-person postmortem on dev.to that markets the author's expertise and includes engagement devices (TL;DR, pre-production checklist). No vendor, product, sponsor, employer or paid offering is promoted; the tooling described is an unnamed internal package with nothing for a reader to buy, and the post's conclusions are self-critical (a copied header, two rounds to find the fix, a feature ripped out).
Moderate: coherent mechanisms, one unverified voice
Confidence is limited chiefly by cluster shape: one publisher, one author, no corroboration and no measurements. The core protocol claims are the kind that are stable and checkable against the cited standard, which lifts confidence above the floor; the bespoke configuration API, the generalisation of timeout defaults and the relayed security advisory each remain unconfirmed here.
build
Your meter now runs on someone else's machine: signed receipts, fsync, and failing open1 distinct publisher
build
An empty Detected fields panel is a Loki capacity incident that has not billed yet1 distinct publisher
build
Rate limit your MCP servers, because a retrying agent turns one error into a billing incident1 distinct publisher
build
The MCP transport your search results teach has been deprecated since March1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 24, 2026