Turns out that json.Encoder always appends a newline but (as GP said) json.Decoder will not attempt to read it if the JSON is complete at the power-of-two byte boundary (IIRC the buffer doubles for the first few iterations). The extra byte in the pipe when closed then causes reads and writes to go boom. We switched to using json.Marshal because it doesn't output a newline (sadly the encoded form isn't streamed as a result).
I still get twitchy around encoding/json as a result of that experience.
[1]: https://github.com/docker/docker/issues/14203#issuecomment-1...