| 252 | } |
| 253 | |
| 254 | func (r *recordingBody) Read(p []byte) (int, error) { |
| 255 | n, err := r.inner.Read(p) |
| 256 | |
| 257 | r.mu.Lock() |
| 258 | r.accumulateReadLocked(p, n, err) |
| 259 | r.mu.Unlock() |
| 260 | |
| 261 | // Record non-EOF errors immediately. EOF is handled |
| 262 | // below for SSE or deferred to Close() for validation. |
| 263 | if err != nil && !errors.Is(err, io.EOF) { |
| 264 | r.record(err) |
| 265 | return n, err |
| 266 | } |
| 267 | |
| 268 | // For server-sent-events bodies, record eagerly on EOF. Streaming |
| 269 | // consumers like fantasy's Anthropic SSE adapter iterate the |
| 270 | // response to EOF and abandon it without calling Close(), so the |
| 271 | // Close-only recording path would never fire and the attempt would |
| 272 | // be lost. The recording is provisional so Close() can still |
| 273 | // upgrade it to failed if inner.Close() surfaces a transport error. |
| 274 | // Non-SSE bodies stay on the Close-only path so that JSON |
| 275 | // integrity, content-length validation, and inner-Close errors |
| 276 | // keep their existing semantics. |
| 277 | if errors.Is(err, io.EOF) && isSSEContentType(r.contentType) { |
| 278 | r.recordProvisional(io.EOF) |
| 279 | } |
| 280 | return n, err |
| 281 | } |
| 282 | |
| 283 | func (r *recordingBody) Close() error { |
| 284 | r.mu.Lock() |