Skip to content

Commit 449aca9

Browse files
droid-ashclaude
andcommitted
fix: remove request/response body capture to avoid binary encoding issues
Binary response bodies (compressed, protobuf, images) were being stored as UTF-8 text in the HAR, producing garbled output. Rather than adding binary detection heuristics, remove body capture entirely for now. Changes: - NetworkCaptureManager: stop reading req.body/response.body - capture.ts (standalone): same - reportWriter: remove body text redaction (header redaction stays) - reportServer: remove body fields from view model - reportTemplate: remove Request Body / Response Body tabs from detail panel, keep Headers tab only (General + Response Headers + Request Headers) - docs/network-logging.md: mark request/response body as "Not yet" What stays: method, URL, status, all headers, timing, size, query params, TLS failure labeling, video sync, search, status filters. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ccfbdae commit 449aca9

6 files changed

Lines changed: 16 additions & 125 deletions

File tree

docs/network-logging.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ A **Network** tab appears alongside the existing Recording, Device Logs, and Act
9191
- **Request table** — method, URL path, status code, duration, response size
9292
- **Status filter chips** — All / 2xx / 3xx / 4xx / 5xx
9393
- **Search** — filter by URL
94-
- **Click a row** — detail panel slides in with three tabs:
95-
- **Headers** — all request and response headers
96-
- **Request Body** — formatted JSON or raw text (for POST/PUT requests)
97-
- **Response Body** — formatted JSON or raw text
94+
- **Click a row** — detail panel slides in showing:
95+
- General info (full URL, status, duration, size)
96+
- All response headers
97+
- All request headers
9898
- **Video sync** — click a request row and the recording seeks to that moment
9999
- **Download** — link to the full `.har` file
100100

@@ -136,8 +136,8 @@ Press Ctrl+C to stop — writes a `.har` file to the current directory. Useful f
136136
| Full URL (scheme, host, path, query) | Yes |
137137
| All request headers | Yes |
138138
| All response headers | Yes |
139-
| Request body (text, up to 512 KB) | Yes |
140-
| Response body (text, up to 512 KB) | Yes |
139+
| Request body | Not yet |
140+
| Response body | Not yet |
141141
| Response status code and text | Yes |
142142
| Response size | Yes |
143143
| Request timing (start, duration) | Yes |

packages/cli/src/commands/logNetwork/capture.ts

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ export interface CapturedEntry {
1515
statusMessage: string;
1616
requestHeaders: Record<string, string>;
1717
responseHeaders: Record<string, string>;
18-
requestBodyText: string | undefined;
1918
requestBodySize: number;
20-
responseBodyText: string | undefined;
2119
responseBodySize: number;
2220
durationMs: number;
2321
responseSize: number;
@@ -42,7 +40,7 @@ export class NetworkCapture {
4240
private _entries: CapturedEntry[] = [];
4341
private _tlsErrors: TlsError[] = [];
4442
private _port = 0;
45-
private _pendingRequests = new Map<string, { startedAt: Date; method: string; url: string; headers: Record<string, string>; bodyText: string | undefined; bodySize: number }>();
43+
private _pendingRequests = new Map<string, { startedAt: Date; method: string; url: string; headers: Record<string, string>; bodySize: number }>();
4644

4745
get port(): number { return this._port; }
4846
get entries(): readonly CapturedEntry[] { return this._entries; }
@@ -55,16 +53,12 @@ export class NetworkCapture {
5553
this._port = this._server.port;
5654

5755
await this._server.on('request', (req) => {
58-
const bodyBuffer = req.body?.buffer;
59-
const contentType = flattenHeaderValue(req.headers['content-type']);
60-
const isText = isTextMime(contentType);
6156
this._pendingRequests.set(req.id, {
6257
startedAt: new Date(),
6358
method: req.method,
6459
url: req.url,
6560
headers: flattenHeaders(req.headers),
66-
bodyText: isText && bodyBuffer ? Buffer.from(bodyBuffer).toString('utf8').slice(0, 512 * 1024) : undefined,
67-
bodySize: bodyBuffer?.byteLength ?? 0,
61+
bodySize: req.body?.buffer?.byteLength ?? 0,
6862
});
6963
});
7064

@@ -73,9 +67,7 @@ export class NetworkCapture {
7367
const pending = this._pendingRequests.get(response.id);
7468
this._pendingRequests.delete(response.id);
7569

76-
const respBuffer = response.body?.buffer;
77-
const respContentType = flattenHeaderValue(response.headers['content-type']);
78-
const isText = isTextMime(respContentType);
70+
const respSize = response.body?.buffer?.byteLength ?? 0;
7971

8072
const entry: CapturedEntry = {
8173
startedAt: pending?.startedAt ?? completedAt,
@@ -86,12 +78,10 @@ export class NetworkCapture {
8678
statusMessage: response.statusMessage ?? '',
8779
requestHeaders: pending?.headers ?? {},
8880
responseHeaders: flattenHeaders(response.headers),
89-
requestBodyText: pending?.bodyText,
9081
requestBodySize: pending?.bodySize ?? 0,
91-
responseBodyText: isText && respBuffer ? Buffer.from(respBuffer).toString('utf8').slice(0, 512 * 1024) : undefined,
92-
responseBodySize: respBuffer?.byteLength ?? 0,
82+
responseBodySize: respSize,
9383
durationMs: completedAt.getTime() - (pending?.startedAt ?? completedAt).getTime(),
94-
responseSize: respBuffer?.byteLength ?? 0,
84+
responseSize: respSize,
9585
};
9686
this._entries.push(entry);
9787
callbacks.onEntry(entry);
@@ -136,9 +126,6 @@ export class NetworkCapture {
136126
httpVersion: 'HTTP/1.1',
137127
headers: headersToHar(e.requestHeaders),
138128
queryString: parseQueryString(e.url),
139-
...(e.requestBodyText !== undefined
140-
? { postData: { mimeType: e.requestHeaders['content-type'] ?? 'application/octet-stream', text: e.requestBodyText } }
141-
: {}),
142129
bodySize: e.requestBodySize,
143130
headersSize: -1,
144131
},
@@ -150,7 +137,6 @@ export class NetworkCapture {
150137
content: {
151138
size: e.responseSize,
152139
mimeType: e.responseHeaders['content-type'] ?? 'application/octet-stream',
153-
...(e.responseBodyText !== undefined ? { text: e.responseBodyText } : {}),
154140
},
155141
bodySize: e.responseSize,
156142
headersSize: -1,
@@ -173,17 +159,6 @@ function flattenHeaders(headers: Record<string, string | string[] | undefined>):
173159
return flat;
174160
}
175161

176-
function flattenHeaderValue(v: string | string[] | undefined): string {
177-
if (!v) return '';
178-
return Array.isArray(v) ? v[0] ?? '' : v;
179-
}
180-
181-
function isTextMime(ct: string): boolean {
182-
if (!ct) return false;
183-
const l = ct.toLowerCase();
184-
return l.includes('json') || l.includes('text') || l.includes('xml') || l.includes('html') || l.includes('javascript') || l.includes('css') || l.includes('form-urlencoded');
185-
}
186-
187162
function headersToHar(headers: Record<string, string>): Array<{ name: string; value: string }> {
188163
return Object.entries(headers).map(([name, value]) => ({ name, value }));
189164
}

packages/cli/src/reportServer.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,10 +318,6 @@ export async function buildReportRunManifestViewModel(
318318
responseSize: ((e['response'] as Record<string, unknown>)?.['content'] as Record<string, unknown>)?.['size'] ?? (e['response'] as Record<string, unknown>)?.['bodySize'] ?? 0,
319319
requestHeaders: (e['request'] as Record<string, unknown>)?.['headers'],
320320
responseHeaders: (e['response'] as Record<string, unknown>)?.['headers'],
321-
requestBodyText: ((e['request'] as Record<string, unknown>)?.['postData'] as Record<string, unknown>)?.['text'],
322-
requestBodyMimeType: ((e['request'] as Record<string, unknown>)?.['postData'] as Record<string, unknown>)?.['mimeType'],
323-
responseBodyText: ((e['response'] as Record<string, unknown>)?.['content'] as Record<string, unknown>)?.['text'],
324-
responseBodyMimeType: ((e['response'] as Record<string, unknown>)?.['content'] as Record<string, unknown>)?.['mimeType'],
325321
}));
326322
const tlsErrors = har?.log?._tlsErrors ?? [];
327323
return { entries, tlsErrors };

packages/cli/src/reportTemplate.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ export interface NetworkLogEntry {
3131
responseSize: number;
3232
requestHeaders?: Array<{ name: string; value: string }>;
3333
responseHeaders?: Array<{ name: string; value: string }>;
34-
requestBodyText?: string;
35-
requestBodyMimeType?: string;
36-
responseBodyText?: string;
37-
responseBodyMimeType?: string;
3834
}
3935

4036
export interface NetworkTlsErrorEntry {
@@ -1577,13 +1573,7 @@ export function renderHtmlReport(manifest: ReportRunManifest): string {
15771573
function renderNetDetailContent(detail, entry, tab) {
15781574
var body = detail.querySelector('.network-log-detail-body');
15791575
if (!body) return;
1580-
if (tab === 'headers') {
1581-
body.innerHTML = renderHeaderSection('Response Headers', entry.responseHeaders) + renderHeaderSection('Request Headers', entry.requestHeaders) + renderHeaderSection('General', [{ name: 'URL', value: entry.url || '' }, { name: 'Status', value: (entry.status || '') + ' ' + (entry.statusText || '') }, { name: 'Duration', value: (entry.time || 0) + 'ms' }, { name: 'Size', value: formatNetBytes(entry.responseSize || 0) }]);
1582-
} else if (tab === 'request') {
1583-
body.innerHTML = entry.requestBodyText ? '<pre>' + escapeHtmlJS(formatBodyText(entry.requestBodyText, entry.requestBodyMimeType)) + '</pre>' : '<div style="color:var(--text-muted);padding:20px">No request body</div>';
1584-
} else if (tab === 'response') {
1585-
body.innerHTML = entry.responseBodyText ? '<pre>' + escapeHtmlJS(formatBodyText(entry.responseBodyText, entry.responseBodyMimeType)) + '</pre>' : '<div style="color:var(--text-muted);padding:20px">No response body' + (entry.responseSize > 0 ? ' (binary, ' + formatNetBytes(entry.responseSize) + ')' : '') + '</div>';
1586-
}
1576+
body.innerHTML = renderHeaderSection('General', [{ name: 'URL', value: entry.url || '' }, { name: 'Status', value: (entry.status || '') + ' ' + (entry.statusText || '') }, { name: 'Duration', value: (entry.time || 0) + 'ms' }, { name: 'Size', value: formatNetBytes(entry.responseSize || 0) }]) + renderHeaderSection('Response Headers', entry.responseHeaders) + renderHeaderSection('Request Headers', entry.requestHeaders);
15871577
}
15881578
15891579
function renderHeaderSection(title, headers) {
@@ -1595,14 +1585,6 @@ export function renderHtmlReport(manifest: ReportRunManifest): string {
15951585
return html + '</div>';
15961586
}
15971587
1598-
function formatBodyText(text, mimeType) {
1599-
if (!text) return '';
1600-
if (mimeType && mimeType.indexOf('json') !== -1) {
1601-
try { return JSON.stringify(JSON.parse(text), null, 2); } catch(e) { return text; }
1602-
}
1603-
return text;
1604-
}
1605-
16061588
function formatNetBytes(b) {
16071589
if (!b || b <= 0) return '0 B';
16081590
if (b < 1024) return b + ' B';
@@ -2208,8 +2190,6 @@ function renderSpecDetailSection(
22082190
<div class="network-log-detail-header"></div>
22092191
<div class="network-log-detail-tabs">
22102192
<button class="net-detail-tab is-active" data-detail-tab="headers" onclick="switchNetDetailTab(this)" type="button">Headers</button>
2211-
<button class="net-detail-tab" data-detail-tab="request" onclick="switchNetDetailTab(this)" type="button">Request</button>
2212-
<button class="net-detail-tab" data-detail-tab="response" onclick="switchNetDetailTab(this)" type="button">Response</button>
22132193
</div>
22142194
<div class="network-log-detail-body"></div>
22152195
</div>
@@ -2240,10 +2220,6 @@ function renderNetworkLogRows(entries: NonNullable<ReportManifestTestRecord['net
22402220
responseSize: entry.responseSize,
22412221
requestHeaders: entry.requestHeaders,
22422222
responseHeaders: entry.responseHeaders,
2243-
requestBodyText: entry.requestBodyText,
2244-
requestBodyMimeType: entry.requestBodyMimeType,
2245-
responseBodyText: entry.responseBodyText,
2246-
responseBodyMimeType: entry.responseBodyMimeType,
22472223
}));
22482224
html += `<div class="network-log-row ${statusClass}" data-network-ts="${escapeHtml(entry.startedDateTime)}" data-net-status-code="${entry.status}" onclick="handleNetRowClick(this)" data-entry='${entryJson}'>
22492225
<span class="net-method">${escapeHtml(entry.method)}</span>

packages/cli/src/reportWriter.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -782,14 +782,6 @@ export class ReportWriter {
782782
redactHarHeaders(entry.request?.headers);
783783
redactHarHeaders(entry.response?.headers);
784784
redactHarQueryParams(entry.request?.queryString);
785-
if (entry.request?.postData?.text) {
786-
const redacted = redactResolvedValue(entry.request.postData.text, bindings);
787-
if (redacted !== undefined) entry.request.postData.text = redacted;
788-
}
789-
if (entry.response?.content?.text) {
790-
const redacted = redactResolvedValue(entry.response.content.text, bindings);
791-
if (redacted !== undefined) entry.response.content.text = redacted;
792-
}
793785
}
794786
}
795787
await fsp.writeFile(targetPath, JSON.stringify(har, null, 2), 'utf-8');

packages/device-node/src/device/NetworkCaptureManager.ts

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ export interface NetworkCapturedEntry {
3939
statusMessage: string;
4040
requestHeaders: Record<string, string>;
4141
responseHeaders: Record<string, string>;
42-
requestBodyText: string | undefined;
4342
requestBodySize: number;
44-
responseBodyText: string | undefined;
4543
responseBodySize: number;
4644
durationMs: number;
4745
}
@@ -65,8 +63,6 @@ export interface DeviceNetworkCaptureController {
6563

6664
const MAP_KEY_DELIMITER = '###';
6765
const TEMP_DIR = path.join(os.tmpdir(), 'finalrun-network');
68-
const MAX_TEXT_BODY_BYTES = 512 * 1024; // 512 KB
69-
7066
export class NetworkCaptureManager implements DeviceNetworkCaptureController {
7167
private _server: Mockttp | null = null;
7268
private _entries: NetworkCapturedEntry[] = [];
@@ -81,7 +77,7 @@ export class NetworkCaptureManager implements DeviceNetworkCaptureController {
8177
// Pending requests (waiting for response).
8278
private readonly _pendingRequests = new Map<
8379
string,
84-
{ startedAt: Date; method: string; url: string; headers: Record<string, string>; bodyText: string | undefined; bodySize: number }
80+
{ startedAt: Date; method: string; url: string; headers: Record<string, string>; bodySize: number }
8581
>();
8682

8783
get proxyPort(): number {
@@ -114,19 +110,14 @@ export class NetworkCaptureManager implements DeviceNetworkCaptureController {
114110
await this._server.start();
115111
this._proxyPort = this._server.port;
116112

117-
// Subscribe to request events — store full request data.
113+
// Subscribe to request events — store request metadata (no body).
118114
await this._server.on('request', (req) => {
119-
const bodyBuffer = req.body?.buffer;
120-
const contentType = flattenHeaderValue(req.headers['content-type']);
121-
const isText = isTextContentType(contentType);
122-
123115
this._pendingRequests.set(req.id, {
124116
startedAt: new Date(),
125117
method: req.method,
126118
url: req.url,
127119
headers: flattenHeaders(req.headers),
128-
bodyText: isText && bodyBuffer ? truncateBody(Buffer.from(bodyBuffer).toString('utf8')) : undefined,
129-
bodySize: bodyBuffer?.byteLength ?? 0,
120+
bodySize: req.body?.buffer?.byteLength ?? 0,
130121
});
131122
});
132123

@@ -136,10 +127,6 @@ export class NetworkCaptureManager implements DeviceNetworkCaptureController {
136127
const pending = this._pendingRequests.get(response.id);
137128
this._pendingRequests.delete(response.id);
138129

139-
const responseBuffer = response.body?.buffer;
140-
const responseContentType = flattenHeaderValue(response.headers['content-type']);
141-
const isText = isTextContentType(responseContentType);
142-
143130
const entry: NetworkCapturedEntry = {
144131
startedAt: pending?.startedAt ?? completedAt,
145132
completedAt,
@@ -149,10 +136,8 @@ export class NetworkCaptureManager implements DeviceNetworkCaptureController {
149136
statusMessage: response.statusMessage ?? '',
150137
requestHeaders: pending?.headers ?? {},
151138
responseHeaders: flattenHeaders(response.headers),
152-
requestBodyText: pending?.bodyText,
153139
requestBodySize: pending?.bodySize ?? 0,
154-
responseBodyText: isText && responseBuffer ? truncateBody(Buffer.from(responseBuffer).toString('utf8')) : undefined,
155-
responseBodySize: responseBuffer?.byteLength ?? 0,
140+
responseBodySize: response.body?.buffer?.byteLength ?? 0,
156141
durationMs: completedAt.getTime() - (pending?.startedAt ?? completedAt).getTime(),
157142
};
158143
this._entries.push(entry);
@@ -320,14 +305,6 @@ function buildHar(entries: NetworkCapturedEntry[], tlsErrors: NetworkTlsError[])
320305
httpVersion: 'HTTP/1.1',
321306
headers: headersToHar(e.requestHeaders),
322307
queryString: parseQueryString(e.url),
323-
...(e.requestBodyText !== undefined
324-
? {
325-
postData: {
326-
mimeType: e.requestHeaders['content-type'] ?? 'application/octet-stream',
327-
text: e.requestBodyText,
328-
},
329-
}
330-
: {}),
331308
bodySize: e.requestBodySize,
332309
headersSize: -1,
333310
},
@@ -339,7 +316,6 @@ function buildHar(entries: NetworkCapturedEntry[], tlsErrors: NetworkTlsError[])
339316
content: {
340317
size: e.responseBodySize,
341318
mimeType: e.responseHeaders['content-type'] ?? 'application/octet-stream',
342-
...(e.responseBodyText !== undefined ? { text: e.responseBodyText } : {}),
343319
},
344320
bodySize: e.responseBodySize,
345321
headersSize: -1,
@@ -376,11 +352,6 @@ function flattenHeaders(headers: Record<string, string | string[] | undefined>):
376352
return flat;
377353
}
378354

379-
function flattenHeaderValue(value: string | string[] | undefined): string {
380-
if (value === undefined) return '';
381-
return Array.isArray(value) ? value[0] ?? '' : value;
382-
}
383-
384355
function headersToHar(headers: Record<string, string>): Array<{ name: string; value: string }> {
385356
return Object.entries(headers).map(([name, value]) => ({ name, value }));
386357
}
@@ -394,25 +365,6 @@ function parseQueryString(url: string): Array<{ name: string; value: string }> {
394365
}
395366
}
396367

397-
function isTextContentType(contentType: string): boolean {
398-
if (!contentType) return false;
399-
const ct = contentType.toLowerCase();
400-
return (
401-
ct.includes('json') ||
402-
ct.includes('text') ||
403-
ct.includes('xml') ||
404-
ct.includes('html') ||
405-
ct.includes('javascript') ||
406-
ct.includes('css') ||
407-
ct.includes('form-urlencoded')
408-
);
409-
}
410-
411-
function truncateBody(text: string): string {
412-
if (text.length <= MAX_TEXT_BODY_BYTES) return text;
413-
return text.slice(0, MAX_TEXT_BODY_BYTES);
414-
}
415-
416368
function sanitizeForFilename(value: string): string {
417369
return value
418370
.replaceAll(/[/\\:*?"<>|]/g, '_')

0 commit comments

Comments
 (0)