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 402

View on GitHub (pinned to 24529f1404)

Solutions

  1. Client-side: supply a valid x402 payment (payment header/proof) for the requested tool so the gate does not emit a 402.
  2. Server-side: when handling errResponseWritten (use errors.Is), return without writing another HTTP response or JSON-RPC result.
  3. 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

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.