fix(network): keep redirect chain order consistent between text and JSON (#2221)
## Problem `get_network_request` returns, from the **same call**, both a human-readable text block (`toStringDetailed()`) and a `structuredContent.networkRequest` object (`toJSONDetailed()`) — emitted together in `McpResponse.ts`. For a request that went through HTTP redirects, the redirect chain comes out in **opposite orders** in the two representations: - `toJSONDetailed()` (`NetworkFormatter.ts`) reverses `redirectChain()` once → newest→oldest in the JSON. - the text formatter then reverses it **a second time** → oldest→newest in the text. Because each path calls `redirectChain()` separately and Puppeteer returns a fresh copy on every call (`HTTPRequest#redirectChain()` does `this._redirectChain.slice()`), the two reverses operate on different arrays and don't cancel. So a consumer reading the text and a consumer parsing the structured JSON from the same response see contradictory redirect orders. ## Solution Drop the redundant `.reverse()` in the text formatter so the rendered text uses the order already produced by `toJSONDetailed()`. Both representations are now consistent (newest→oldest), and `structuredContent` is unchanged. ## Why the existing tests didn't catch it - The existing "handles redirect chain" test uses a **single-element** chain, where reversing is a no-op. - `getMockRequest().redirectChain()` returned the **same array reference** on every call, unlike real Puppeteer — so the two reverses accidentally agreed in tests. This PR makes the mock return a fresh copy per call (matching Puppeteer) and adds a regression test with a multi-element chain that asserts the text and JSON orders match. ## Testing - `npm test` for the formatter suite passes. New test `renders the redirect chain in the same order in text and JSON` is **red** before the fix (text `[first, second]` vs JSON `[second, first]`) and **green** after, with no change to existing snapshots. - `npm run typecheck` and Prettier/ESLint are clean. No existing issue tracked this; found via code inspection and confirmed empirically.
This commit is contained in:
@@ -314,7 +314,9 @@ function converNetworkRequestDetailedToStringDetailed(
|
||||
if (redirectChain?.length) {
|
||||
response.push(`### Redirect chain`);
|
||||
let indent = 0;
|
||||
for (const request of redirectChain.reverse()) {
|
||||
// `redirectChain` is already ordered by toJSONDetailed(); don't reverse it
|
||||
// again here or the text output contradicts structuredContent (the JSON).
|
||||
for (const request of redirectChain) {
|
||||
response.push(
|
||||
`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)}`,
|
||||
);
|
||||
|
||||
@@ -307,6 +307,38 @@ describe('NetworkFormatter', () => {
|
||||
const result = formatter.toStringDetailed();
|
||||
t.assert.snapshot(result);
|
||||
});
|
||||
it('renders the redirect chain in the same order in text and JSON', async () => {
|
||||
// A >=2 element chain makes ordering observable (a single redirect hides
|
||||
// the bug). toStringDetailed() and toJSONDetailed() are emitted from the
|
||||
// same get_network_request call, so they must agree on the order.
|
||||
const first = getMockRequest({url: 'http://example.com/first'});
|
||||
const second = getMockRequest({url: 'http://example.com/second'});
|
||||
const request = getMockRequest({
|
||||
url: 'http://example.com/final',
|
||||
redirectChain: [first, second],
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
requestIdResolver: () => 2,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
const text = formatter.toStringDetailed();
|
||||
const json = formatter.toJSONDetailed();
|
||||
|
||||
const textOrder = [
|
||||
...text.matchAll(/http:\/\/example\.com\/(first|second)/g),
|
||||
].map(m => m[0]);
|
||||
const jsonOrder = (json.redirectChain ?? []).map(entry => entry.url);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
textOrder,
|
||||
jsonOrder,
|
||||
`redirect chain order differs between text (${JSON.stringify(textOrder)}) ` +
|
||||
`and JSON (${JSON.stringify(jsonOrder)})`,
|
||||
);
|
||||
});
|
||||
it('shows saved to file message in toStringDetailed', async () => {
|
||||
const request = {
|
||||
method: () => 'POST',
|
||||
|
||||
+4
-1
@@ -200,7 +200,10 @@ export function getMockRequest(
|
||||
);
|
||||
},
|
||||
redirectChain(): HTTPRequest[] {
|
||||
return options.redirectChain ?? [];
|
||||
// Puppeteer returns a fresh copy on every call (HTTPRequest returns
|
||||
// `this._redirectChain.slice()`); mirror that so formatters can't share
|
||||
// and accidentally mutate the same array across calls.
|
||||
return [...(options.redirectChain ?? [])];
|
||||
},
|
||||
isNavigationRequest() {
|
||||
return options.navigationRequest ?? false;
|
||||
|
||||
Reference in New Issue
Block a user