micro/go-micro · warning
x402 response already written
Error message
x402 response already written
What it means
errResponseWritten is a sentinel error in gateway/mcp/mcp.go marking that the x402 payment gate already wrote an HTTP response (the 402 payment-required challenge). Internal callers (handleCallTool, invokeTool, mcpToolsCall) return it so outer layers know to stop and not write a second response to the same HTTP connection.
Source
Thrown at gateway/mcp/mcp.go:733
if raw {
// Framework tools respond directly with their result.
w.Header().Set("Content-Type", "application/json")
w.Write(payload)
return
}
// Return response with trace ID
w.Header().Set("Content-Type", "application/json")
w.Header().Set(TraceIDKey, traceID)
json.NewEncoder(w).Encode(map[string]interface{}{
"result": payload,
"trace_id": traceID,
})
}
// errResponseWritten marks that the x402 payment gate already wrote an HTTP
// response (the 402 challenge); the caller must not write anything further.
var errResponseWritten = errors.New("x402 response already written")
// toolError is a tool-call failure carrying the HTTP status the legacy REST
// transport returns. The streamable MCP transport maps it to a JSON-RPC error
// (or an isError result for execution failures).
type toolError struct {
status int
message string
}
func (e *toolError) Error() string { return e.message }
// invokeTool runs the shared tool-call pipeline (lookup, x402 payment gate,
// auth/scope inspection, rate limiting, circuit breaker, tracing, audit, and
// the RPC or framework-handler dispatch) used by both the legacy REST
// /mcp/call endpoint and the streamable-HTTP MCP transport. On success it
// returns the tool's JSON payload and trace id; raw is true for framework
// tools whose payload is the response itself. On failure it returns a
// *toolError, or errResponseWritten if the x402 gate already wrote the 402View on GitHub (pinned to 24529f1404)
Solutions
- Client-side: supply a valid x402 payment (payment header/proof) for the requested tool so the gate does not emit a 402.
- Server-side: when handling errResponseWritten (use errors.Is), return without writing another HTTP response or JSON-RPC result.
- If tests trip this, mock or bypass the x402 gate for the tool being tested.
Example fix
// before
if err := invokeTool(ctx, req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // double write!
}
// after
if err := invokeTool(ctx, req); err != nil {
if errors.Is(err, errResponseWritten) {
return // x402 gate already wrote the 402 challenge
}
http.Error(w, err.Error(), http.StatusInternalServerError)
} Defensive patterns
Strategy: type-guard
Validate before calling
paymentHeader := req.Header.Get("X-Payment")
if paymentHeader == "" {
// expect a 402 challenge; don't treat the gate response as a tool error
} Type guard
func isResponseWritten(err error) bool {
return err != nil && strings.Contains(err.Error(), "x402 response already written")
} Try / catch
if err := handler(w, r); err != nil {
if errors.Is(err, errResponseWritten) {
return // 402 challenge already sent; do not write again
}
writeJSONRPCError(w, err)
} Prevention
- Always compare with errors.Is against the sentinel rather than string matching where exported.
- Ensure HTTP handler wrappers short-circuit after this sentinel to avoid 'superfluous WriteHeader' logs.
- Supply valid x402 payment proofs in clients/integration tests hitting paid tools.
When it happens
Trigger: A MCP tool call hits an endpoint behind the x402 payment gate without a valid payment; the gate writes the 402 challenge response and returns errResponseWritten up the call stack to suppress further writes.
Common situations: Clients calling paid MCP tools without x402 payment headers; expired or invalid payment proofs; developers seeing this sentinel when debugging gate code and mistaking it for a tool execution error.
Related errors
AI-assisted analysis of micro/go-micro@24529f1404 (2026-09-01).
Data as JSON: /api/errors/1d4fdc108975541f.
Report an issue: GitHub.