Skip to content

fix(interceptors): avoid marshalling non-serializable request internals#360

Open
Pnkcaht wants to merge 2 commits intodocker:mainfrom
Pnkcaht:fix/before-interceptor-marshal
Open

fix(interceptors): avoid marshalling non-serializable request internals#360
Pnkcaht wants to merge 2 commits intodocker:mainfrom
Pnkcaht:fix/before-interceptor-marshal

Conversation

@Pnkcaht
Copy link
Contributor

@Pnkcaht Pnkcaht commented Jan 23, 2026

What I did

Hardened the interceptor execution flow to prevent before-interceptors from attempting to marshal non-serializable request internals on streaming and SSE transports.

This change limits the payload sent to before interceptors to a minimal, explicitly serializable tool-call shape, instead of the full mcp.Request object. By doing so, it avoids leaking internal callback functions into JSON marshaling and restores correct interceptor behavior across streaming transports.

The implementation preserves existing interceptor semantics while ensuring compatibility with documented interceptor usage patterns (for example, jq -r ".params.arguments").

Related issue

Fixed #358

What was the problem ?

When running docker mcp gateway run with a before interceptor on streaming or SSE transports, the gateway could fail with:
json: unsupported type: func(mcp.CloseSSEStreamArgs)

In these cases:

  • The gateway attempted to marshal the full mcp.Request
  • Streaming/SSE requests contained internal callback functions
  • JSON marshaling failed immediately
  • Before interceptors became unusable on streaming transports

This resulted in interceptors appearing broken, even though the failure was caused by internal, non-JSON-safe fields unrelated to interceptor intent.

Diagram

Before Interceptor Execution Flow (Streaming / SSE)

flowchart TD
    A[Tool call received] --> B[Extract serializable tool-call payload]
    B --> C[Marshal minimal JSON payload]
    C --> D[Run before interceptor]
    D --> E{Interceptor returns output?}
    E -->|Yes| F[Use interceptor result]
    E -->|No| G[Continue normal execution]
Loading

Explanation

The diagram illustrates the updated execution flow for before interceptors:

Payload Extraction

Instead of serializing the full mcp.Request, the middleware extracts only the tool-call payload (method and params) that is safe and relevant for interceptors.

Safe Marshaling

The extracted payload contains no internal callbacks or transport-specific fields, ensuring JSON marshaling succeeds on all transports, including streaming and SSE.

Interceptor Compatibility

This preserves compatibility with existing interceptors that expect fields like .params.arguments.

Normal Flow Preservation

If the interceptor returns no output, the request proceeds unchanged through the normal execution path.

Before

  • Full mcp.Request was marshaled for before interceptors
  • Streaming/SSE requests contained non-serializable callback functions
  • JSON marshaling failed early
  • Before interceptors were unusable on streaming transports

After

  • Only a minimal, serializable tool-call payload is marshaled
  • Internal request callbacks are excluded
  • Before interceptors work reliably on streaming and SSE
  • Existing interceptor contracts remain unchanged

Testing

This change was validated through local builds and manual testing using streaming transports with before interceptors enabled.

No new automated tests were added as part of this PR, as the issue is transport-specific and tied to streaming/SSE runtime behavior. Follow-up work may introduce targeted integration tests once streaming interceptor coverage is expanded in CI.

Signed-off-by: pnkcaht <samzoovsk19@gmail.com>
@Pnkcaht Pnkcaht requested a review from a team as a code owner January 23, 2026 18:30
Signed-off-by: pnkcaht <samzoovsk19@gmail.com>
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.

before interceptors fail to marshal request: json: unsupported type func(mcp.CloseSSEStreamArgs)

2 participants