Playwright lets you capture, monitor, and modify network requests during browser automation. This capability transforms how you test APIs, debug applications, and scrape dynamic websites.
In this guide, you'll learn how to intercept network traffic in Playwright, including practical examples for capturing API responses, modifying requests, and handling real-world scenarios.
What is Network Interception in Playwright?
Network interception in Playwright allows you to capture HTTP requests and responses before they reach the server or browser.
Using page.route(), you can intercept API calls, modify request headers, mock responses, or block resources entirely.
This approach gives you complete control over network traffic without modifying application code or configuring external proxies.
Why Intercept Network Requests?
Network interception solves several testing and automation challenges.
You can mock API responses to test error scenarios without breaking production data. Extract JSON from background requests instead of parsing HTML.
Block images and stylesheets to speed up scraping by 70%. Validate that your UI sends correct API payloads.
Capture authentication tokens from login flows for later use.
Basic Network Interception with page.route()
The page.route() method intercepts requests matching a URL pattern.
Here's the simplest example:
import { test } from '@playwright/test';
test('intercept API request', async ({ page }) => {
await page.route('**/api/users', route => {
console.log('Request URL:', route.request().url());
route.continue();
});
await page.goto('https://example.com');
});
This code intercepts all requests to /api/users endpoints.
The route.continue() call lets the request proceed normally. Without it, the request hangs indefinitely.
The ** wildcard matches any subdomain or path prefix.
Intercepting Specific HTTP Methods
You can filter by HTTP method to intercept only POST or PUT requests:
await page.route('**/api/users', async route => {
const request = route.request();
if (request.method() === 'POST') {
console.log('POST request intercepted');
console.log('Request body:', request.postDataJSON());
}
await route.continue();
});
This approach helps when the same endpoint handles multiple operations.
GET requests pass through while POST requests get logged.
Listening to Network Events
Playwright fires events for every request and response.
Use page.on() to listen without blocking requests:
page.on('request', request => {
console.log('>>', request.method(), request.url());
});
page.on('response', response => {
console.log('<<', response.status(), response.url());
});
await page.goto('https://example.com');
These events capture all network activity.
You see every asset load, API call, and redirect. This helps debug unexpected network behavior.
Unlike page.route(), these listeners don't modify requests.
Filtering Responses by Content Type
You can filter responses to capture only JSON APIs:
page.on('response', async response => {
const contentType = response.headers()['content-type'];
if (contentType && contentType.includes('application/json')) {
const url = response.url();
const json = await response.json();
console.log('API Response:', url, json);
}
});
This extracts JSON data from all API calls automatically.
Perfect for discovering hidden endpoints in web scraping.
Using waitForResponse for Specific API Calls
When you need to capture a specific API response, use waitForResponse().
This method waits for a matching response before continuing:
test('capture login API response', async ({ page }) => {
const responsePromise = page.waitForResponse('**/api/login');
await page.getByRole('button', { name: 'Login' }).click();
const response = await responsePromise;
const data = await response.json();
console.log('Auth token:', data.token);
});
Notice the promise is created before the click action.
Playwright waits for the response after the button triggers the request. This prevents race conditions.
You can also use predicates for complex matching:
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/') && response.status() === 200
);
Capturing Multiple Simultaneous Requests
Some actions trigger multiple API calls:
test('capture multiple API calls', async ({ page }) => {
const response1Promise = page.waitForResponse('**/api/user');
const response2Promise = page.waitForResponse('**/api/posts');
await page.getByText('Load Dashboard').click();
const [userRes, postsRes] = await Promise.all([
response1Promise,
response2Promise
]);
console.log('User:', await userRes.json());
console.log('Posts:', await postsRes.json());
});
Promise.all() waits for both responses to complete.
This ensures you capture all data before assertions.
Mocking API Responses
Mock responses replace real API calls with predefined data.
Use route.fulfill() to return custom JSON:
await page.route('**/api/products', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Test Product', price: 29.99 }
])
});
});
await page.goto('https://shop.example.com');
The page displays your mocked product data.
The actual API never receives the request.
This approach tests how your UI handles different data shapes. You can mock empty arrays, error responses, or edge cases.
Mocking Error Responses
Test error handling by returning failed responses:
await page.route('**/api/checkout', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Payment processor unavailable' })
});
});
Your UI should display an appropriate error message.
This validates error handling without breaking production.
Modifying Requests Before They're Sent
You can change request headers, body, or method.
Here's how to add custom headers:
await page.route('**/api/**', async route => {
const headers = route.request().headers();
await route.continue({
headers: {
...headers,
'Authorization': 'Bearer test-token-123',
'X-Custom-Header': 'custom-value'
}
});
});
This injects authentication into every API request.
Useful when testing with different user permissions.
Modifying POST Request Bodies
You can also change the request payload:
await page.route('**/api/users', async route => {
if (route.request().method() === 'POST') {
const original = route.request().postDataJSON();
await route.continue({
postData: JSON.stringify({
...original,
testMode: true
})
});
} else {
await route.continue();
}
});
This adds a testMode flag to every user creation.
The server processes modified data without UI changes.
Modifying Responses After They Arrive
Sometimes you need the real API response but want to tweak it.
Use route.fetch() to get the original response:
await page.route('**/api/prices', async route => {
const response = await route.fetch();
const body = await response.json();
// Add 10% discount to all prices
const modified = body.map(item => ({
...item,
price: item.price * 0.9
}));
await route.fulfill({
response,
body: JSON.stringify(modified)
});
});
This approach keeps the original status code and headers.
Only the response body changes.
Perfect for testing discount logic or promotional scenarios without backend changes.
Blocking Requests to Speed Up Tests
Block images, fonts, and stylesheets to accelerate page loads:
await page.route('**/*', route => {
const resourceType = route.request().resourceType();
if (['image', 'stylesheet', 'font'].includes(resourceType)) {
route.abort();
} else {
route.continue();
}
});
This prevents resource downloads entirely.
Your page loads 3-5x faster without visual assets.
Essential for web scraping at scale.
Pattern Matching: Glob vs Regex vs Predicates
Playwright supports three matching strategies.
Glob patterns use wildcards for simple matching:
**/api/usersmatches any path ending in/api/users**/*.{png,jpg}matches all images**/api/**matches all API endpoints
Regular expressions offer more precision:
await page.route(/\/api\/users\/\d+/, route => {
// Matches /api/users/123 but not /api/users/abc
route.continue();
});
Predicate functions provide maximum flexibility:
await page.route(route => {
const url = route.request().url();
const hasToken = url.includes('token=');
return url.includes('/api/') && !hasToken;
}, route => {
route.continue();
});
Choose predicates when glob patterns become too complex.
Performance Optimization with CDP Sessions
Standard page.route() disables browser caching.
Every resource downloads fresh, even when cached. This slows repeated requests to the same domain.
For better performance, use Chrome DevTools Protocol (CDP):
const client = await page.context().newCDPSession(page);
await client.send('Network.setBlockedURLs', {
urls: ['*.png', '*.jpg', '*.css']
});
await client.send('Network.enable');
CDP sessions maintain cache while blocking resources.
This reduces bandwidth and speeds up scraping significantly.
Only works with Chromium browsers, not Firefox or WebKit.
Extracting Authentication Tokens
Capture tokens from login responses for later use:
let authToken = null;
page.on('response', async response => {
if (response.url().includes('/api/login') && response.status() === 200) {
const data = await response.json();
authToken = data.accessToken;
console.log('Captured token:', authToken);
}
});
await page.goto('https://example.com/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password123');
await page.click('#login-button');
// Wait for token capture
await page.waitForTimeout(1000);
// Use token in subsequent requests
console.log('Using token:', authToken);
Store the token in a variable accessible across tests.
This eliminates repeated login flows.
Common Pitfalls and How to Avoid Them
Forgetting route.continue() or route.fulfill()
If you intercept a request but don't call continue() or fulfill(), the request hangs forever. Always handle intercepted routes.
Race conditions with waitForResponse
Create the promise before triggering the action:
// Correct
const responsePromise = page.waitForResponse('**/api/data');
await page.click('#load-data');
const response = await responsePromise;
// Wrong - may miss the response
await page.click('#load-data');
const response = await page.waitForResponse('**/api/data');
Service Workers blocking interception
Service Workers intercept requests before Playwright sees them.
Disable them in context configuration:
const context = await browser.newContext({
serviceWorkers: 'block'
});
Overly broad patterns blocking critical requests
The pattern **/* matches everything, including navigation.
Be specific or add exclusions:
await page.route('**/*', route => {
const url = route.request().url();
if (url.includes('/api/')) {
route.abort();
} else {
route.continue();
}
});
Real-World Use Cases
API contract testing
Verify your frontend sends correct request payloads:
test('user creation sends valid data', async ({ page }) => {
let capturedRequest = null;
await page.route('**/api/users', route => {
capturedRequest = route.request().postDataJSON();
route.continue();
});
await page.fill('#name', 'John Doe');
await page.fill('#email', 'john@example.com');
await page.click('#submit');
expect(capturedRequest).toMatchObject({
name: 'John Doe',
email: 'john@example.com'
});
});
Web scraping with JSON extraction
Skip HTML parsing by capturing API responses:
const products = [];
page.on('response', async response => {
if (response.url().includes('/api/products')) {
const data = await response.json();
products.push(...data.items);
}
});
await page.goto('https://shop.example.com');
console.log('Extracted products:', products);
Testing with offline scenarios
Simulate network failures:
await page.route('**/api/**', route => route.abort('failed'));
await page.goto('https://example.com');
// Verify UI shows "Connection lost" message
Debugging Network Issues
Enable request/response logging for troubleshooting:
page.on('request', request => {
console.log(`→ ${request.method()} ${request.url()}`);
});
page.on('response', response => {
console.log(`← ${response.status()} ${response.url()}`);
});
page.on('requestfailed', request => {
console.log(`✗ FAILED: ${request.url()} - ${request.failure().errorText}`);
});
Failed requests indicate blocked resources or network errors.
Check the error text to diagnose issues.
Conclusion
Playwright's network interception gives you complete control over HTTP traffic. You can mock APIs for testing, extract JSON for scraping, or block resources for performance.
Start with page.route() for request interception. Use page.on() for passive monitoring. Choose waitForResponse() when capturing specific API calls.
For production scraping, combine CDP sessions with selective blocking to maintain cache and optimize speed.
The key is matching your pattern strategy to the task. Use glob patterns for simplicity, regex for precision, and predicates for complex logic.