network-request-inspector
Intercept all HTTP requests/responses with full headers, timing breakdown (DNS, connect, TLS, TTFB, download), status codes, payload sizes, CORS validation, and redirect chain analysis via CDP Network domain.
SKILL.md
| Name | network-request-inspector |
| Description | Intercept all HTTP requests/responses with full headers, timing breakdown (DNS, connect, TLS, TTFB, download), status codes, payload sizes, CORS validation, and redirect chain analysis via CDP Network domain. |
name: network-request-inspector description: > Intercept all HTTP requests/responses with full headers, timing breakdown (DNS, connect, TLS, TTFB, download), status codes, payload sizes, CORS validation, and redirect chain analysis via CDP Network domain.
Network Request Inspector
Capture the complete HTTP request/response lifecycle for every resource loaded by a page. Uses CDP Network domain events to record headers, timing breakdowns, redirect chains, and payload sizes. Cross-references with the browser Performance API for server timing data, and independently verifies CORS and security headers via curl.
When to Use
- Debugging slow page loads by identifying which requests have high TTFB or DNS latency.
- Investigating failed requests (4xx/5xx) and their response bodies.
- Validating CORS headers are correctly configured for cross-origin fetches.
- Tracing redirect chains to detect unnecessary hops or open redirects.
- Auditing per-domain connection overhead (DNS, TLS handshake) to justify preconnect hints.
- Measuring total transfer size and identifying oversized payloads.
Prerequisites
- Playwright MCP server connected and responding (all
mcp__playwright__browser_*tools available). - Chromium-based browser required for CDP Network domain timing data.
- Target page must be reachable from the browser instance.
Workflow
Phase 1: Install CDP Network Interceptor
Set up CDP session with listeners for the full request lifecycle. This must be done before navigating to the target page so that all requests are captured.
browser_run_code({
code: `async (page) => {
const client = await page.context().newCDPSession(page);
await client.send('Network.enable', {
maxTotalBufferSize: 10000000,
maxResourceBufferSize: 5000000
});
const requests = new Map();
const redirectChains = new Map();
client.on('Network.requestWillBeSent', (params) => {
const entry = {
url: params.request.url,
method: params.request.method,
requestHeaders: params.request.headers,
timestamp: params.timestamp,
wallTime: params.wallTime,
initiator: {
type: params.initiator.type,
url: params.initiator.url || null,
lineNumber: params.initiator.lineNumber || null
},
resourceType: params.type,
redirectChain: []
};
// Track redirect chains
if (params.redirectResponse) {
const prev = requests.get(params.requestId);
if (prev) {
const chainId = redirectChains.get(params.requestId) || params.requestId;
redirectChains.set(params.requestId, chainId);
entry.redirectChain = [
...(prev.redirectChain || []),
{
url: prev.url,
status: params.redirectResponse.status,
headers: params.redirectResponse.headers
}
];
}
}
requests.set(params.requestId, entry);
});
client.on('Network.responseReceived', (params) => {
const req = requests.get(params.requestId);
if (req) {
req.status = params.response.status;
req.statusText = params.response.statusText;
req.responseHeaders = params.response.headers;
req.mimeType = params.response.mimeType;
req.protocol = params.response.protocol;
req.remoteAddress = params.response.remoteIPAddress
? params.response.remoteIPAddress + ':' + params.response.remotePort
: null;
req.securityState = params.response.securityState;
// Detailed timing breakdown (milliseconds relative to request start)
const t = params.response.timing;
if (t) {
req.timing = {
dnsStart: t.dnsStart,
dnsEnd: t.dnsEnd,
dnsMs: t.dnsEnd > 0 ? Math.round(t.dnsEnd - t.dnsStart) : 0,
connectStart: t.connectStart,
connectEnd: t.connectEnd,
connectMs: t.connectEnd > 0 ? Math.round(t.connectEnd - t.connectStart) : 0,
sslStart: t.sslStart,
sslEnd: t.sslEnd,
sslMs: t.sslEnd > 0 ? Math.round(t.sslEnd - t.sslStart) : 0,
sendStart: t.sendStart,
sendEnd: t.sendEnd,
sendMs: Math.round(t.sendEnd - t.sendStart),
receiveHeadersEnd: t.receiveHeadersEnd,
ttfbMs: Math.round(t.receiveHeadersEnd - t.sendEnd),
workerStart: t.workerStart,
workerReady: t.workerReady
};
}
// CORS analysis for cross-origin requests
const origin = new URL(params.response.url).origin;
const pageOrigin = req._pageOrigin;
if (pageOrigin && origin !== pageOrigin) {
const h = params.response.headers;
req.cors = {
crossOrigin: true,
allowOrigin: h['access-control-allow-origin'] || h['Access-Control-Allow-Origin'] || null,
allowMethods: h['access-control-allow-methods'] || h['Access-Control-Allow-Methods'] || null,
allowHeaders: h['access-control-allow-headers'] || h['Access-Control-Allow-Headers'] || null,
allowCredentials: h['access-control-allow-credentials'] || h['Access-Control-Allow-Credentials'] || null,
exposeHeaders: h['access-control-expose-headers'] || h['Access-Control-Expose-Headers'] || null
};
}
}
});
client.on('Network.loadingFinished', (params) => {
const req = requests.get(params.requestId);
if (req) {
req.encodedDataLength = params.encodedDataLength;
req.finished = true;
req.endTimestamp = params.timestamp;
if (req.timestamp) {
req.totalTimeMs = Math.round((params.timestamp - req.timestamp) * 1000);
}
}
});
client.on('Network.loadingFailed', (params) => {
const req = requests.get(params.requestId);
if (req) {
req.failed = true;
req.errorText = params.errorText;
req.canceled = params.canceled || false;
req.blockedReason = params.blockedReason || null;
req.corsErrorStatus = params.corsErrorStatus || null;
}
});
globalThis.__networkInspector = { client, requests };
return 'Network inspector installed — navigate to target page now';
}`
})
Phase 2: Navigate and Capture
Navigate to the target page and wait for the network to settle.
browser_navigate({ url: "<target_url>" })
browser_evaluate({
function: `() => {
// Store the page origin for CORS analysis
if (globalThis.__networkInspector) {
for (const [, req] of globalThis.__networkInspector.requests) {
req._pageOrigin = window.location.origin;
}
}
return window.location.origin;
}`
})
Wait for late-loading resources (analytics, lazy images, deferred scripts):
browser_wait_for({ time: 5 })
Phase 3: Collect Performance API Resource Entries
Supplement CDP data with the browser Performance API for server timing and decoded body sizes.
browser_evaluate({
function: `() => {
const entries = performance.getEntriesByType('resource').map(e => ({
name: e.name,
initiatorType: e.initiatorType,
transferSize: e.transferSize,
decodedBodySize: e.decodedBodySize,
encodedBodySize: e.encodedBodySize,
duration: Math.round(e.duration),
serverTiming: e.serverTiming ? e.serverTiming.map(st => ({
name: st.name,
duration: st.duration,
description: st.description
})) : []
}));
return { count: entries.length, entries };
}`
})
Phase 4: Harvest CDP Data
Extract the collected request map for analysis.
browser_run_code({
code: `async (page) => {
const inspector = globalThis.__networkInspector;
if (!inspector) return { error: 'Inspector not installed' };
const results = [];
for (const [id, req] of inspector.requests) {
// Strip internal fields
const { _pageOrigin, ...clean } = req;
results.push({ requestId: id, ...clean });
}
// Sort by start timestamp
results.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
// Summary statistics
const finished = results.filter(r => r.finished);
const failed = results.filter(r => r.failed);
const byDomain = {};
for (const r of results) {
try {
const domain = new URL(r.url).hostname;
if (!byDomain[domain]) byDomain[domain] = { count: 0, totalBytes: 0, totalTimeMs: 0 };
byDomain[domain].count++;
byDomain[domain].totalBytes += r.encodedDataLength || 0;
byDomain[domain].totalTimeMs += r.totalTimeMs || 0;
} catch {}
}
return {
total: results.length,
finished: finished.length,
failed: failed.length,
totalTransferBytes: finished.reduce((s, r) => s + (r.encodedDataLength || 0), 0),
byDomain,
requests: results
};
}`
})
Phase 5: Cross-reference with Built-in Network Log
Use the built-in tool as a baseline sanity check.
browser_network_requests({ includeStatic: true })
Phase 6: Independent CORS and Security Header Verification
For each unique cross-origin domain found in Phase 4, verify headers
independently with curl. Replace <url> with the actual resource URL and
<origin> with the page origin.
curl -sI -H "Origin: <origin>" "<url>" | grep -iE "^(access-control|x-frame|x-content|strict-transport|content-security|referrer-policy|permissions-policy)"
Phase 7: Cleanup
Detach the CDP session.
browser_run_code({
code: `async (page) => {
if (globalThis.__networkInspector) {
await globalThis.__networkInspector.client.detach();
delete globalThis.__networkInspector;
}
return 'CDP session detached';
}`
})
Report Template
## Network Request Inspector Report -- <URL>
**Date:** <timestamp>
**Total Requests:** <N> | **Finished:** <N> | **Failed:** <N>
**Total Transfer Size:** <N> KB
### Request Summary (sorted by total time, top 20 slowest)
| # | URL (truncated) | Method | Status | Type | Size (KB) | DNS | Connect | TLS | TTFB | Total | Protocol |
|---|-----------------|--------|--------|------|-----------|-----|---------|-----|------|-------|----------|
| 1 | /api/data | GET | 200 | XHR | 45.2 | 0 | 0 | 0 | 320 | 385 | h2 |
| 2 | /images/hero.jpg| GET | 200 | Img | 280.5 | 23 | 45 | 32 | 180 | 350 | h2 |
| ... |
### Failed Requests
| URL | Method | Error | Blocked Reason | CORS Error |
|-----|--------|-------|----------------|------------|
| /api/metrics | POST | net::ERR_CONNECTION_REFUSED | — | — |
### Redirect Chains
| Final URL | Hops | Chain |
|-----------|------|-------|
| /dashboard | 2 | /login (302) -> /auth/sso (302) -> /dashboard (200) |
### CORS Analysis
| Resource Domain | Access-Control-Allow-Origin | Allow-Credentials | Issues |
|----------------|-----------------------------|-------------------|--------|
| cdn.example.com | * | — | None |
| api.third-party.com | — | — | MISSING CORS headers |
### Per-Domain Breakdown
| Domain | Requests | Total Size (KB) | Avg Time (ms) | Connection Reuse |
|--------|----------|-----------------|----------------|------------------|
| cdn.example.com | 15 | 450.3 | 85 | Yes (h2) |
| api.example.com | 8 | 23.1 | 210 | Yes (h2) |
### Recommendations
- **Slow TTFB on /api/data (320ms):** Server processing time is high. Consider caching or query optimization.
- **Missing CORS headers on api.third-party.com:** Cross-origin requests will fail. Contact the API provider or proxy through your own domain.
- **Unnecessary redirect chain on /login (2 hops):** Consolidate to a single redirect to reduce latency.
- **Large image /images/hero.jpg (280 KB):** Consider WebP/AVIF format and responsive srcset.
Limitations
- CDP timing data is relative to the request start, not wall clock time. Requests reusing a warm connection will show 0 for DNS/connect/TLS.
- Request bodies are not captured by default to avoid memory pressure. Use
Network.getRequestPostDatafor individual requests if needed. - Response bodies require explicit
Network.getResponseBodycalls per request. This skill focuses on metadata and timing. - Service Worker requests may appear as separate entries with
workerStarttiming data. - HTTP/3 (QUIC) timing may differ from traditional TCP-based breakdowns.