diff --git a/README.md b/README.md
index f9ab040..9a6f713 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ In summary, the `puppeteer-intercept-and-modify-requests` library offers a more
npm install puppeteer-intercept-and-modify-requests
```
-## Usage
+## Example Usage
To modify intercepted requests:
@@ -40,12 +40,18 @@ import { RequestInterceptionManager } from 'puppeteer-intercept-and-modify-reque
// assuming 'page' is your Puppeteer page object
const client = await page.target().createCDPSession()
+// note: if you want to intercept requests on ALL tabs, instead use:
+// const client = await browser.target().createCDPSession()
+
const interceptManager = new RequestInterceptionManager(client)
await interceptManager.intercept(
{
+ // specify the URL pattern to intercept:
urlPattern: `https://example.com/*`,
+ // optionally filter by resource type:
resourceType: 'Document',
+ // specify how you want to modify the response (may be async):
modifyResponse({ body }) {
return {
// replace break lines with horizontal lines:
@@ -55,6 +61,7 @@ await interceptManager.intercept(
},
{
urlPattern: '*/api/v4/user.json',
+ // specify how you want to modify the response (may be async):
modifyResponse({ body }) {
const parsed = JSON.parse(body)
// set role property to 'admin'
@@ -98,7 +105,7 @@ await interceptManager.intercept(
## Usage Examples
-Here's an example of how to use the `RequestInterceptionManager` to intercept and modify a request:
+Here's an example of how to use the `RequestInterceptionManager` to intercept and modify a request and a response:
```ts
import puppeteer from 'puppeteer'
@@ -141,9 +148,9 @@ This example modifies the request by adding a custom header and modifies the res
## Advanced Usage
-### Streaming and Modifying Response Chunks
+### Applying Delay
-You can also stream and modify response chunks using the `streamResponse` and `modifyResponseChunk` options. Here's an example of how to do this:
+You can apply a delay to a request or response using the `delay` property in the `modifyRequest` or `modifyResponse` functions. Here's an example of how to add a delay to a request:
```ts
import puppeteer from 'puppeteer'
@@ -161,11 +168,11 @@ async function main() {
const interceptionConfig: Interception = {
urlPattern: 'https://example.com/*',
- streamResponse: true,
- modifyResponseChunk: async ({ event, data }) => {
- // Modify response chunk
- const modifiedData = data.replace(/example/gi, 'intercepted')
- return { ...event, data: modifiedData }
+ modifyRequest: async ({ event }) => {
+ // Add a 500 ms delay to the request
+ return {
+ delay: 500,
+ }
},
}
@@ -177,11 +184,11 @@ async function main() {
main()
```
-In this example, the response is streamed and each response chunk has all occurrences of the word "example" replaced with "intercepted".
+In this example, a 500 ms delay is added to the request for the specified URL pattern.
-### Applying Delay
+### Handling Errors
-You can apply a delay to a request or response using the `delay` property in the `modifyRequest` or `modifyResponse` functions. Here's an example of how to add a delay to a request:
+You can handle errors using the `onError` option when creating a new `RequestInterceptionManager` instance. Here's an example of how to handle errors:
```ts
import puppeteer from 'puppeteer'
@@ -195,14 +202,18 @@ async function main() {
const page = await browser.newPage()
const client = await page.target().createCDPSession()
- const requestInterceptionManager = new RequestInterceptionManager(client)
+ const requestInterceptionManager = new RequestInterceptionManager(client, {
+ onError: (error) => {
+ console.error('Request interception error:', error)
+ },
+ })
const interceptionConfig: Interception = {
urlPattern: 'https://example.com/*',
modifyRequest: async ({ event }) => {
- // Add a 500 ms delay to the request
+ // Modify request headers
return {
- delay: 500,
+ headers: [{ name: 'X-Custom-Header', value: 'CustomValue' }],
}
},
}
@@ -215,11 +226,11 @@ async function main() {
main()
```
-In this example, a 500 ms delay is added to the request for the specified URL pattern.
+In this example, any errors that occur during request interception are logged to the console with the message "Request interception error:".
-### Handling Errors
+### Failing a Request
-You can handle errors using the `onError` option when creating a new `RequestInterceptionManager` instance. Here's an example of how to handle errors:
+To fail a request, return an object containing an `errorReason` property in the `modifyRequest` function. Here's an example of how to fail a request:
```ts
import puppeteer from 'puppeteer'
@@ -233,18 +244,14 @@ async function main() {
const page = await browser.newPage()
const client = await page.target().createCDPSession()
- const requestInterceptionManager = new RequestInterceptionManager(client, {
- onError: (error) => {
- console.error('Request interception error:', error)
- },
- })
+ const requestInterceptionManager = new RequestInterceptionManager(client)
const interceptionConfig: Interception = {
urlPattern: 'https://example.com/*',
modifyRequest: async ({ event }) => {
- // Modify request headers
+ // Fail the request with the error reason "BlockedByClient"
return {
- headers: [{ name: 'X-Custom-Header', value: 'CustomValue' }],
+ errorReason: 'BlockedByClient',
}
},
}
@@ -257,11 +264,23 @@ async function main() {
main()
```
-In this example, any errors that occur during request interception are logged to the console with the message "Request interception error:".
+In this example, the request for the specified URL pattern is blocked with the error reason "BlockedByClient".
-### Failing a Request
+### Intercepting network requests from all Pages (rather than just the one)
-To fail a request, return an object containing an `errorReason` property in the `modifyRequest` function. Here's an example of how to fail a request:
+When creating the `RequestInterceptionManager` instance, you can pass in the `client` object from the `CDPSession` of the `Browser` object. This will allow you to intercept requests from all the pages rather than just the one. Here's an example of how to do this:
+
+```ts
+// intercept requests on ALL tabs, instead use:
+const client = await browser.target().createCDPSession()
+const interceptManager = new RequestInterceptionManager(client)
+
+// ...
+```
+
+### Streaming and Modifying Response Chunks
+
+You can also stream and modify response chunks using the `streamResponse` and `modifyResponseChunk` options. Here's an example of how to do this:
```ts
import puppeteer from 'puppeteer'
@@ -279,11 +298,11 @@ async function main() {
const interceptionConfig: Interception = {
urlPattern: 'https://example.com/*',
- modifyRequest: async ({ event }) => {
- // Fail the request with the error reason "BlockedByClient"
- return {
- errorReason: 'BlockedByClient',
- }
+ streamResponse: true,
+ modifyResponseChunk: async ({ event, data }) => {
+ // Modify response chunk
+ const modifiedData = data.replace(/example/gi, 'intercepted')
+ return { ...event, data: modifiedData }
},
}
@@ -295,4 +314,4 @@ async function main() {
main()
```
-In this example, the request for the specified URL pattern is blocked with the error reason "BlockedByClient".
+In this example, the response is streamed and each response chunk has all occurrences of the word "example" replaced with "intercepted".
diff --git a/src/main.test.ts b/src/main.test.ts
index 879425c..b2696e5 100644
--- a/src/main.test.ts
+++ b/src/main.test.ts
@@ -12,6 +12,8 @@ let server: Server
let browser: puppeteerType.Browser
let page: puppeteerType.Page
let client: puppeteerType.CDPSession
+let browserClient: puppeteerType.CDPSession
+let manager: RequestInterceptionManager
const host = 'localhost'
let port = 3_000
@@ -37,6 +39,7 @@ describe('RequestInterceptionManager', () => {
beforeAll(async () => {
browser = await puppeteer.launch()
+ browserClient = await browser.target().createCDPSession()
})
afterAll(async () => {
@@ -51,6 +54,7 @@ describe('RequestInterceptionManager', () => {
afterEach(async () => {
await page.close()
await stopServer()
+ await manager.clear()
})
describe.each`
@@ -66,7 +70,7 @@ describe('RequestInterceptionManager', () => {
res.end('Hello, world!')
})
- const manager = new RequestInterceptionManager(client)
+ manager = new RequestInterceptionManager(client)
await manager.intercept({
urlPattern: '*',
modifyResponse: ({ body }) => ({
@@ -101,7 +105,7 @@ describe('RequestInterceptionManager', () => {
}
})
- const manager = new RequestInterceptionManager(client)
+ manager = new RequestInterceptionManager(client)
await manager.intercept({
urlPattern: '*/original',
modifyRequest: ({ event }) => ({
@@ -128,7 +132,7 @@ describe('RequestInterceptionManager', () => {
res.end('Hello, world!')
})
- const manager = new RequestInterceptionManager(client)
+ manager = new RequestInterceptionManager(client)
await manager.intercept({
urlPattern: 'non-existent-url/*',
modifyResponse: ({ body }) => ({
@@ -155,7 +159,7 @@ describe('RequestInterceptionManager', () => {
res.end()
})
- const manager = new RequestInterceptionManager(client)
+ manager = new RequestInterceptionManager(client)
await manager.intercept({
urlPattern: '*',
modifyResponse: () => ({
@@ -190,7 +194,7 @@ describe('RequestInterceptionManager', () => {
}
})
- const manager = new RequestInterceptionManager(client)
+ manager = new RequestInterceptionManager(client)
await manager.intercept({
urlPattern: '*/redirected',
modifyResponse: ({ body, event: { responseStatusCode } }) => ({
@@ -238,7 +242,7 @@ describe('RequestInterceptionManager', () => {
res.end('It is forbidden')
})
- const manager = new RequestInterceptionManager(client)
+ manager = new RequestInterceptionManager(client)
await manager.intercept({
urlPattern: '*',
modifyResponse: ({ body }) => ({
@@ -278,7 +282,7 @@ describe('RequestInterceptionManager', () => {
}
})
- const manager = new RequestInterceptionManager(client)
+ manager = new RequestInterceptionManager(client)
await manager.intercept(
{
urlPattern: '*/first',
@@ -313,6 +317,63 @@ describe('RequestInterceptionManager', () => {
expect(firstResponse).toBe('First intercepted')
expect(secondResponse).toBe('Second intercepted')
})
+
+ it('should intercept and modify requests on new tabs', async () => {
+ await startServer((req, res) => {
+ if (req.url === '/') {
+ res.writeHead(200, { 'Content-Type': 'text/html' })
+ res.end('Open new tab')
+ } else if (req.url === '/new-tab') {
+ res.writeHead(200, { 'Content-Type': 'text/plain' })
+ res.end('Hello, new tab!')
+ } else {
+ res.writeHead(404, { 'Content-Type': 'text/plain' })
+ res.end('Not found')
+ }
+ })
+
+ // Set up request interception for the initial page
+ manager = new RequestInterceptionManager(browserClient)
+ await manager.intercept({
+ urlPattern: '*',
+ modifyResponse: ({ body }) =>
+ body
+ ? {
+ body: body.replace('new tab', 'new tab intercepted'),
+ }
+ : undefined,
+ ...options,
+ })
+
+ await page.goto(`http://localhost:${port}`)
+
+ // Listen for a new page to be opened
+ const newPagePromise = browser
+ .waitForTarget(
+ (target) =>
+ target.type() === 'page' &&
+ target.url() === `http://localhost:${port}/new-tab`,
+ )
+ .then((target) => target.page())
+ .then((newPage) => newPage!)
+
+ // Click the link to open a new tab
+ await page.click('a')
+
+ // Wait for the new page to be opened
+ const newPage = await newPagePromise
+
+ // Check if the request on the new tab was intercepted and modified
+ const newText = await newPage.evaluate(() => document.body.textContent)
+ expect(newText).toBe('Hello, new tab intercepted!')
+
+ const newResponse = await newPage.evaluate(async (url) => {
+ const response = await fetch(url)
+ return response.text()
+ }, `http://localhost:${port}/new-tab`)
+
+ expect(newResponse).toBe('Hello, new tab intercepted!')
+ })
},
)
@@ -349,7 +410,7 @@ describe('RequestInterceptionManager', () => {
sendNextMessage()
})
- const manager = new RequestInterceptionManager(client)
+ manager = new RequestInterceptionManager(client)
await manager.intercept({
urlPattern: '*/stream',
// Replace "world" with "Jest" in the response chunk
diff --git a/src/main.ts b/src/main.ts
index bc0fdd9..2f9f932 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -62,14 +62,14 @@ const wait = promisify(setTimeout)
export class RequestInterceptionManager {
interceptions: Map = new Map()
#client: CDPSession
+ #requestPausedHandler: (event: Protocol.Fetch.RequestPausedEvent) => void
+ #isInstalled = false
+
// eslint-disable-next-line no-console
constructor(client: CDPSession, { onError = console.error } = {}) {
this.#client = client
- client.on(
- 'Fetch.requestPaused',
- (event: Protocol.Fetch.RequestPausedEvent) =>
- void this.onRequestPausedEvent(event).catch(onError),
- )
+ this.#requestPausedHandler = (event: Protocol.Fetch.RequestPausedEvent) =>
+ void this.onRequestPausedEvent(event).catch(onError)
}
async intercept(...interceptions: Interception[]) {
@@ -90,6 +90,7 @@ export class RequestInterceptionManager {
}
async enable(): Promise {
+ this.#install()
return this.#client.send('Fetch.enable', {
handleAuthRequests: false,
patterns: [...this.interceptions.values()].map(
@@ -103,7 +104,12 @@ export class RequestInterceptionManager {
}
async disable(): Promise {
- return this.#client.send('Fetch.disable')
+ this.#uninstall()
+ try {
+ await this.#client.send('Fetch.disable')
+ } catch {
+ // ignore (most likely session closed)
+ }
}
async clear() {
@@ -203,6 +209,20 @@ export class RequestInterceptionManager {
}
}
+ #install() {
+ if (this.#isInstalled) return
+
+ this.#client.on('Fetch.requestPaused', this.#requestPausedHandler)
+ this.#isInstalled = true
+ }
+
+ #uninstall() {
+ if (!this.#isInstalled) return
+
+ this.#client.off('Fetch.requestPaused', this.#requestPausedHandler)
+ this.#isInstalled = false
+ }
+
async #getResponseBody(
event: Protocol.Fetch.RequestPausedEvent,
): Promise<{ body: string | undefined; base64Encoded?: boolean }> {