Skip to content

fix: defer writeHead to prevent Content-Length truncation in payment rewriter#161

Merged
badjer merged 2 commits intomainfrom
fix/content-length-rewrite-truncation
Apr 14, 2026
Merged

fix: defer writeHead to prevent Content-Length truncation in payment rewriter#161
badjer merged 2 commits intomainfrom
fix/content-length-rewrite-truncation

Conversation

@badjer
Copy link
Copy Markdown
Contributor

@badjer badjer commented Apr 14, 2026

Summary

  • Bug: @hono/node-server's responseViaCache sets Content-Length from the original MCP response body, then calls writeHead before res.end. The payment response rewriter hooks res.end and swaps in a larger body (with full x402/mpp challenge data), but Content-Length is already on the wire with the original smaller size. The client only reads that many bytes → truncated JSON → SyntaxError: Unterminated string in JSON at position 257.
  • Fix: Defer writeHead until res.end fires, then update Content-Length to match the rewritten body before flushing both. For SSE transports (res.write before res.end), the deferred writeHead flushes at the first write call.

Test plan

  • Existing responseRewriter.test.ts tests pass (13/13)
  • Manual: run dev:resource + dev:cli against local auth/accounts — payment challenge JSON should no longer be truncated
  • Manual: test against SSE-based MCP server (e.g. ../search with file: deps) to verify SSE streaming still works

🤖 Generated with Claude Code

badjer and others added 2 commits April 14, 2026 11:02
…Length truncation

@hono/node-server's responseViaCache sets Content-Length from the
original body size and calls writeHead before res.end. When the payment
response rewriter intercepts res.end and swaps in a larger rewritten
body (with full x402/mpp challenge data), the Content-Length already on
the wire reflects the original smaller body. The client reads only that
many bytes, truncating the JSON and causing a parse error.

Fix: defer writeHead until res.end fires, then update Content-Length to
match the (potentially rewritten) body before flushing both. For SSE
(res.write before res.end), the deferred writeHead flushes at the first
write call — no Content-Length conflict since SSE doesn't use the
responseViaCache path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests installPaymentResponseRewriter by simulating @hono/node-server's
exact call patterns:
- JSON path: writeHead(status, {Content-Length}) then end(body)
- SSE path: writeHead(status, headers) then write(chunk)... then end()

Coverage:
- Content-Length updated to match rewritten body size
- Content-Length preserved when no rewrite occurs (no challenge, non-matching body)
- writeHead(status, statusMessage, headers) three-arg form
- end() without writeHead (implicit headers)
- Buffer body handling
- Hook restoration after end()
- SSE: writeHead flushed on first write, not duplicated
- SSE: payment error chunks rewritten, normal chunks unchanged
- SSE: only error chunk rewritten in multi-chunk stream
- Content-Length === actual body byte length invariant

Also exports installPaymentResponseRewriter for direct unit testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@badjer badjer merged commit c55311b into main Apr 14, 2026
1 check passed
@badjer badjer deleted the fix/content-length-rewrite-truncation branch April 14, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant