dotnet / dotnet/dev-proxy

MockStdioResponsePlugin: @stdin.body.id placeholder fails to resolve when messages arrive back-to-back after an id-less message

Open
#1,757 0 comments 1 reaction 2 assignees Claimed by @garrytrinder View on GitHub
Dominant language
C#
Stars
832
Forks
89
Avg merge
12h 1m
Merged PRs (30d)
23

Description

## Description

When using `MockStdioResponsePlugin` with `blockUnmockedRequests: true`, the `@stdin.body.id` placeholder stops resolving correctly for messages that immediately follow an id-less JSON-RPC notification (e.g. `notifications/initialized`). The placeholder is output as the literal text `@stdin.body.id` instead of the numeric id from the current message.

## Steps to reproduce

**Dev Proxy version:** 3.1.0
**OS:** macOS (Apple Silicon)

### Config (`.devproxy/devproxyrc.json`)

```json
{
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.1.0/rc.schema.json",
"plugins": [
{
"name": "MockStdioResponsePlugin",
"enabled": true,
"pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
"configSection": "mockStdioResponsePlugin"
}
],
"mockStdioResponsePlugin": {
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.1.0/mockstdioresponseplugin.schema.json",
"mocksFile": "stdio-mocks.json",
"blockUnmockedRequests": true
}
}
```

### Mocks (`.devproxy/stdio-mocks.json`)

```json
{
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.1.0/mockstdioresponseplugin.mocksfile.schema.json",
"mocks": [
{
"request": { "bodyFragment": "protocolVersion" },
"response": {
"stdout": "{\"jsonrpc\":\"2.0\",\"id\":@stdin.body.id,\"result\":{\"protocolVersion\":\"2024-11-05\"}}\n"
}
},
{
"request": { "bodyFragment": "accept_eula" },
"response": {
"stdout": "{\"jsonrpc\":\"2.0\",\"id\":@stdin.body.id,\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"ok\"}]}}\n"
}
}
]
}
```

### Repro script (`repro.mjs`)

```javascript
import { spawn } from "node:child_process";

const proc = spawn("devproxy", ["stdio", "--config-file", ".devproxy/devproxyrc.json", "--", "cat"], {
stdio: ["pipe", "pipe", "pipe"],
});

let buf = "";
proc.stdout.on("data", (c) => {
buf += c;
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i).trim();
buf = buf.slice(i + 1);
if (!line) continue;
console.log("← stdout:", line);
}
});

// 1. Send initialize (has id) — works fine
proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05" } }) + "\n");

// 2. Wait for response, then send notification + accept_eula back-to-back
setTimeout(() => {
// Fire-and-forget notification (no id field)
proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
// Immediately followed by a request with id
proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "accept_eula", arguments: {} } }) + "\n");
}, 500);

setTimeout(() => {
proc.kill();
process.exit(0);
}, 3000);
```

### Run

```bash
node repro.mjs
```

## Expected behavior

```
← stdout: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05"}}
← stdout: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"ok"}]}}
```

## Actual behavior

```
← stdout: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05"}}
← stdout: {"jsonrpc":"2.0","id":@stdin.body.id,"result":{"content":[{"type":"text","text":"ok"}]}}
```

The first message resolves `@stdin.body.id` to `1` correctly. The second message (after the id-less notification) outputs the literal placeholder text.

## Workaround

Adding a delay (e.g. 100ms) between the notification and the subsequent request makes it work — suggesting the issue is related to how messages are buffered/parsed when they arrive in the same I/O read.

## Context

This blocks using `MockStdioResponsePlugin` to mock MCP servers that follow the standard JSON-RPC handshake (initialize → notifications/initialized → tools/call), where the notification and first tool call are sent synchronously in the same callback.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.