Skip to content

Message middleware / interceptor API for vscode-jsonrpc #1757

@drewnoakes

Description

@drewnoakes

Feature Request: Message middleware / interceptor API for vscode-jsonrpc

Summary

Add a way to intercept or decorate outgoing and incoming JSON-RPC messages at the transport layer, so that callers can inject or extract arbitrary metadata (such as W3C Trace Context traceparent/tracestate headers) without forking the library.

Motivation

vscode-jsonrpc is used as the JS/TS transport in many language server and extension host scenarios. In our scenario, we are communicating between a VS Code extension and a .NET process. That process uses StreamJsonRpc, which offers an ActivityTracingStrategy that automatically injects W3C Trace Context into outgoing MessagePack envelopes and extracts it from incoming ones, enabling distributed tracing across the process boundary.

We would like the same on the JS side. Concretely: when a TS client sends a JSON-RPC request, we want to inject a traceparent field alongside jsonrpc, id, method, and params. StreamJsonRpc will then pick it up and parent the server-side span under the client's span, giving a cohesive cross-process trace in tools like Jaeger or the Aspire Dashboard.

Currently this is not possible because MessageWriter is constructed inside createMessageConnection() and is not accessible to callers after the fact. There is no hook to wrap or intercept messages before they are written to the stream.

Proposed API

A message middleware interface that can be passed to createMessageConnection():

export interface MessageMiddleware {
  /** Called before a message is written. Return the (optionally mutated) message, or a new one. */
  onSend?(message: Message): Message
  /** Called after a message is read, before it is dispatched. Return the (optionally mutated) message. */
  onReceive?(message: Message): Message
}

Usage:

import { createMessageConnection, MessageMiddleware } from 'vscode-jsonrpc'

const tracing: MessageMiddleware = {
  onSend(msg) {
    if (isRequestMessage(msg)) {
      // inject W3C traceparent alongside standard JSON-RPC fields
      ;(msg as any).traceparent = getCurrentTraceparent()
    }
    return msg
  },
  onReceive(msg) {
    if (isRequestMessage(msg)) {
      const tp = (msg as any).traceparent
      if (tp) setIncomingTraceparent(tp)
    }
    return msg
  },
}

const conn = createMessageConnection(reader, writer, logger, { middleware: tracing })

Alternatives considered

  • Subclassing MessageWriter — the writer instance is not exposed after createMessageConnection() returns, so this is not possible without forking.
  • Using a custom StreamMessageWriter wrapper — requires re-implementing the full framing protocol; fragile and error-prone.
  • Encoding metadata in method params — invasive; requires changes to every call site; breaks protocol contracts.

Notes

  • The traceparent field is a top-level JSON-RPC extension field (not inside params), which is valid per the JSON-RPC 2.0 spec (unknown fields are ignored). StreamJsonRpc already uses this convention.
  • This request is not limited to tracing — the same API would be useful for propagating auth tokens, request deadlines, or any other cross-cutting concern.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions