Skip to content

Commit c2f7aa6

Browse files
cliffhallclaude
andcommitted
fix(network): always render Response Body section, note streaming
Streaming responses (text/event-stream, application/x-ndjson, POST to /mcp) are intentionally not body-captured by the fetch tracker, so the section was silently omitted. Now whenever a response was received we render the section, with either the body, a "(empty)" note, or a "Streaming response — body not captured" note keyed off content-type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f837838 commit c2f7aa6

3 files changed

Lines changed: 87 additions & 2 deletions

File tree

clients/web/src/components/groups/NetworkEntry/NetworkEntry.stories.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ const errorEntry: FetchRequestEntry = {
5757
category: "transport",
5858
};
5959

60+
const streamingEntry: FetchRequestEntry = {
61+
id: "n-stream",
62+
timestamp: new Date("2026-03-17T10:30:12Z"),
63+
method: "POST",
64+
url: "http://localhost:3000/mcp",
65+
requestHeaders: {
66+
accept: "application/json, text/event-stream",
67+
"content-type": "application/json",
68+
"mcp-session-id": "0a0b0a5-fd27-4c95-a805-c0fba67e00fb",
69+
},
70+
requestBody: '{"method":"resources/templates/list","jsonrpc":"2.0","id":4}',
71+
responseStatus: 200,
72+
responseStatusText: "OK",
73+
responseHeaders: {
74+
"cache-control": "no-cache",
75+
"content-type": "text/event-stream",
76+
"mcp-session-id": "0a0b0a5-fd27-4c95-a805-c0fba67e00fb",
77+
},
78+
duration: 26,
79+
category: "transport",
80+
};
81+
6082
const transportError: FetchRequestEntry = {
6183
id: "n-4",
6284
timestamp: new Date("2026-03-17T10:30:15Z"),
@@ -83,6 +105,10 @@ export const HttpError: Story = {
83105
args: { entry: errorEntry, isListExpanded: true },
84106
};
85107

108+
export const StreamingResponse: Story = {
109+
args: { entry: streamingEntry, isListExpanded: true },
110+
};
111+
86112
export const FetchError: Story = {
87113
args: { entry: transportError, isListExpanded: true },
88114
};

clients/web/src/components/groups/NetworkEntry/NetworkEntry.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,49 @@ describe("NetworkEntry", () => {
110110
expect(screen.getAllByText("(none)").length).toBe(2);
111111
});
112112

113+
it("shows a 'streaming' placeholder when responseBody is missing but content-type is SSE", async () => {
114+
const user = userEvent.setup();
115+
const sse: FetchRequestEntry = {
116+
...baseEntry,
117+
responseHeaders: { "content-type": "text/event-stream" },
118+
responseBody: undefined,
119+
};
120+
renderWithMantine(<NetworkEntry entry={sse} isListExpanded={false} />);
121+
await user.click(screen.getByRole("button", { name: "Expand" }));
122+
expect(screen.getByText("Response Body")).toBeInTheDocument();
123+
expect(
124+
screen.getByText(/Streaming response body not captured/),
125+
).toBeInTheDocument();
126+
});
127+
128+
it("shows '(empty)' for a non-streaming response with no body", async () => {
129+
const user = userEvent.setup();
130+
const empty: FetchRequestEntry = {
131+
...baseEntry,
132+
responseHeaders: { "content-type": "application/json" },
133+
responseBody: undefined,
134+
};
135+
renderWithMantine(<NetworkEntry entry={empty} isListExpanded={false} />);
136+
await user.click(screen.getByRole("button", { name: "Expand" }));
137+
expect(screen.getByText("Response Body")).toBeInTheDocument();
138+
expect(screen.getByText("(empty)")).toBeInTheDocument();
139+
});
140+
141+
it("omits the Response Body section entirely when no response was received", async () => {
142+
const user = userEvent.setup();
143+
const pending: FetchRequestEntry = {
144+
...baseEntry,
145+
responseStatus: undefined,
146+
responseStatusText: undefined,
147+
responseHeaders: undefined,
148+
responseBody: undefined,
149+
duration: undefined,
150+
};
151+
renderWithMantine(<NetworkEntry entry={pending} isListExpanded={false} />);
152+
await user.click(screen.getByRole("button", { name: "Expand" }));
153+
expect(screen.queryByText("Response Body")).not.toBeInTheDocument();
154+
});
155+
113156
it("shows a 'too large' notice when a body exceeds the inline preview limit", async () => {
114157
const user = userEvent.setup();
115158
const huge = "x".repeat(5000);

clients/web/src/components/groups/NetworkEntry/NetworkEntry.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ function categoryColor(category: FetchRequestEntry["category"]): string {
8383
return category === "auth" ? "violet" : "blue";
8484
}
8585

86+
function isStreamingResponse(entry: FetchRequestEntry): boolean {
87+
const contentType = entry.responseHeaders?.["content-type"] ?? "";
88+
return (
89+
contentType.includes("text/event-stream") ||
90+
contentType.includes("application/x-ndjson")
91+
);
92+
}
93+
8694
function HeadersTable({ headers }: { headers: Record<string, string> }) {
8795
const rows = Object.entries(headers);
8896
if (rows.length === 0) {
@@ -184,12 +192,20 @@ export function NetworkEntry({ entry, isListExpanded }: NetworkEntryProps) {
184192
<HeadersTable headers={entry.responseHeaders} />
185193
</Stack>
186194
)}
187-
{entry.responseBody && (
195+
{entry.responseStatus !== undefined && (
188196
<Stack gap="xs">
189197
<Text size="sm" fw={500}>
190198
Response Body
191199
</Text>
192-
<BodyPreview body={entry.responseBody} />
200+
{entry.responseBody ? (
201+
<BodyPreview body={entry.responseBody} />
202+
) : (
203+
<Text size="xs" c="dimmed">
204+
{isStreamingResponse(entry)
205+
? "Streaming response — body not captured"
206+
: "(empty)"}
207+
</Text>
208+
)}
193209
</Stack>
194210
)}
195211
{entry.error && (

0 commit comments

Comments
 (0)