本文へスキップ
バージョン: 22.5.0

リクエストインターセプト

リクエストインターセプトが有効になると、続行、応答、または中止されない限り、すべてのリクエストが停止します。

すべて画像リクエストを中止する単純なリクエストインターセプタの例

import puppeteer from 'puppeteer';

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort();
else interceptedRequest.continue();
});
await page.goto('https://example.com');
await browser.close();
})();

複数のインターセプトハンドラと非同期解決

デフォルトでは、Puppeteer は、request.abortrequest.continue、またはrequest.respondのいずれかが既に呼び出された後にそれらのいずれかが呼び出されると、Request is already handled!例外を発生させます。

不明なハンドラが既にabort/continue/respondを呼び出している可能性があると常に想定してください。ハンドラが登録した唯一のものであっても、サードパーティのパッケージが独自のハンドラを登録する場合があります。したがって、abort/continue/respondを呼び出す前に、request.isInterceptResolutionHandledを使用して解決状態を常に確認することが重要です。

重要な点として、インターセプトの解決は、ハンドラが非同期操作を待機している間に別のリスナーによって処理される可能性があります。したがって、request.isInterceptResolutionHandledの戻り値は、同期コードブロック内でのみ安全です。常にrequest.isInterceptResolutionHandledabort/continue/respond同期的に一緒に実行してください。

この例は、2つの同期ハンドラが連携して動作することを示しています。

/*
This first handler will succeed in calling request.continue because the request interception has never been resolved.
*/
page.on('request', interceptedRequest => {
if (interceptedRequest.isInterceptResolutionHandled()) return;
interceptedRequest.continue();
});

/*
This second handler will return before calling request.abort because request.continue was already
called by the first handler.
*/
page.on('request', interceptedRequest => {
if (interceptedRequest.isInterceptResolutionHandled()) return;
interceptedRequest.abort();
});

この例は、非同期ハンドラが連携して動作することを示しています。

/*
This first handler will succeed in calling request.continue because the request interception has never been resolved.
*/
page.on('request', interceptedRequest => {
// The interception has not been handled yet. Control will pass through this guard.
if (interceptedRequest.isInterceptResolutionHandled()) return;

// It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler.
return new Promise(resolve => {
// Continue after 500ms
setTimeout(() => {
// Inside, check synchronously to verify that the intercept wasn't handled already.
// It might have been handled during the 500ms while the other handler awaited an async op of its own.
if (interceptedRequest.isInterceptResolutionHandled()) {
resolve();
return;
}
interceptedRequest.continue();
resolve();
}, 500);
});
});
page.on('request', async interceptedRequest => {
// The interception has not been handled yet. Control will pass through this guard.
if (interceptedRequest.isInterceptResolutionHandled()) return;

await someLongAsyncOperation();
// The interception *MIGHT* have been handled by the first handler, we can't be sure.
// Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception.
if (interceptedRequest.isInterceptResolutionHandled()) return;
interceptedRequest.continue();
});

より詳細な調査のために(後述の協調的インターセプトモードを参照)、abort/continue/respondを使用する前に、request.interceptResolutionStateを同期的に呼び出すこともできます。

これは、request.interceptResolutionStateを使用した上記の例を書き直したものです。

/*
This first handler will succeed in calling request.continue because the request interception has never been resolved.
*/
page.on('request', interceptedRequest => {
// The interception has not been handled yet. Control will pass through this guard.
const {action} = interceptedRequest.interceptResolutionState();
if (action === InterceptResolutionAction.AlreadyHandled) return;

// It is not strictly necessary to return a promise, but doing so will allow Puppeteer to await this handler.
return new Promise(resolve => {
// Continue after 500ms
setTimeout(() => {
// Inside, check synchronously to verify that the intercept wasn't handled already.
// It might have been handled during the 500ms while the other handler awaited an async op of its own.
const {action} = interceptedRequest.interceptResolutionState();
if (action === InterceptResolutionAction.AlreadyHandled) {
resolve();
return;
}
interceptedRequest.continue();
resolve();
}, 500);
});
});
page.on('request', async interceptedRequest => {
// The interception has not been handled yet. Control will pass through this guard.
if (
interceptedRequest.interceptResolutionState().action ===
InterceptResolutionAction.AlreadyHandled
)
return;

await someLongAsyncOperation();
// The interception *MIGHT* have been handled by the first handler, we can't be sure.
// Therefore, we must check again before calling continue() or we risk Puppeteer raising an exception.
if (
interceptedRequest.interceptResolutionState().action ===
InterceptResolutionAction.AlreadyHandled
)
return;
interceptedRequest.continue();
});

協調的インターセプトモード

request.abortrequest.continue、およびrequest.respondは、協調的インターセプトモードで動作するためのオプションのpriorityを受け入れることができます。すべてのハンドラが協調的インターセプトモードを使用している場合、Puppeteer は、すべてのインターセプトハンドラが登録順に実行され、待機されることを保証します。インターセプトは、最も高い優先順位の解決に解決されます。協調的インターセプトモードのルールを以下に示します。

  • すべての解決は、abort/continue/respondに数値のpriority引数を指定する必要があります。
  • いずれかの解決が数値のpriorityを指定しない場合、レガシーモードがアクティブになり、協調的インターセプトモードは非アクティブになります。
  • 非同期ハンドラは、インターセプトの解決が最終決定される前に終了します。
  • 最も高い優先順位のインターセプト解決が「勝ち」、つまり、インターセプトは最終的に、どの解決に最も高い優先順位が与えられたかによって中止/応答/継続されます。
  • 同点の場合は、abort > respond > continueとなります。

標準化のため、協調的インターセプトモードの優先順位を指定する場合は、明確な理由がない限り、0またはDEFAULT_INTERCEPT_RESOLUTION_PRIORITYHTTPRequestからエクスポート)を使用してください。これにより、respondcontinueより、abortrespondより優先的に処理し、他のハンドラが協調的に動作することを可能にします。意図的に異なる優先順位を使用する場合は、優先順位が高い方が優先順位が低い方よりも優先されます。負の優先順位も許容されます。たとえば、continue({}, 4)continue({}, -2)よりも優先されます。

下位互換性を維持するために、priorityを指定せずにインターセプトを解決するハンドラ(レガシーモード)は、すぐに解決されます。協調的インターセプトモードを機能させるには、すべての解決でpriorityを使用する必要があります。実際には、これは、制御できないハンドラがpriorityなしで(レガシーモードで)abort/continue/respondを呼び出している可能性があるため、request.isInterceptResolutionHandledをテストする必要があることを意味します。

この例では、レガシーモードが優先され、少なくとも1つのハンドラがインターセプトを解決する際にpriorityを省略するため、リクエストはすぐに中止されます。

// Final outcome: immediate abort()
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Legacy Mode: interception is aborted immediately.
request.abort('failed');
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Control will never reach this point because the request was already aborted in Legacy Mode

// Cooperative Intercept Mode: votes for continue at priority 0.
request.continue({}, 0);
});

この例では、レガシーモードが優先され、少なくとも1つのハンドラがpriorityを指定していないため、リクエストは継続されます。

// Final outcome: immediate continue()
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to abort at priority 0.
request.abort('failed', 0);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Control reaches this point because the request was cooperatively aborted which postpones resolution.

// { action: InterceptResolutionAction.Abort, priority: 0 }, because abort @ 0 is the current winning resolution
console.log(request.interceptResolutionState());

// Legacy Mode: intercept continues immediately.
request.continue({});
});
page.on('request', request => {
// { action: InterceptResolutionAction.AlreadyHandled }, because continue in Legacy Mode was called
console.log(request.interceptResolutionState());
});

この例では、すべてのハンドラがpriorityを指定しているため、協調的インターセプトモードがアクティブです。abort()よりも優先順位が高いため、continue()が優先されます。

// Final outcome: cooperative continue() @ 5
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to abort at priority 10
request.abort('failed', 0);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to continue at priority 5
request.continue(request.continueRequestOverrides(), 5);
});
page.on('request', request => {
// { action: InterceptResolutionAction.Continue, priority: 5 }, because continue @ 5 > abort @ 0
console.log(request.interceptResolutionState());
});

この例では、すべてのハンドラがpriorityを指定しているため、協調的インターセプトモードがアクティブです。continue()と優先順位が同点ですが、respond()continue()よりも優先されるため、respond()が優先されます。

// Final outcome: cooperative respond() @ 15
page.setRequestInterception(true);
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to abort at priority 10
request.abort('failed', 10);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to continue at priority 15
request.continue(request.continueRequestOverrides(), 15);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to respond at priority 15
request.respond(request.responseForRequest(), 15);
});
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;

// Cooperative Intercept Mode: votes to respond at priority 12
request.respond(request.responseForRequest(), 12);
});
page.on('request', request => {
// { action: InterceptResolutionAction.Respond, priority: 15 }, because respond @ 15 > continue @ 15 > respond @ 12 > abort @ 10
console.log(request.interceptResolutionState());
});

協調的リクエスト継続

Puppeteer は、request.continue()を明示的に呼び出す必要があります。そうでないと、リクエストはハングします。ハンドラが特別なアクションを実行しない、または「オプトアウト」することを意図している場合でも、request.continue()を呼び出す必要があります。

協調的インターセプトモードの導入により、協調的リクエスト継続には、無意見と意見のあるという2つのユースケースが発生します。

最初のケース(一般的)は、ハンドラがリクエストに対して特別なことを何も行わないことを意図していることです。他のアクションについては意見がなく、デフォルトで継続する、または意見を持つ可能性のある他のハンドラに委任することを意図しています。しかし、他のハンドラがない場合は、リクエストがハングしないようにrequest.continue()を呼び出す必要があります。

これは、誰もがより良いアイデアを持っていない場合にリクエストを継続することを意図しているため、無意見の継続と呼ばれます。このタイプの継続には、request.continue({...}, DEFAULT_INTERCEPT_RESOLUTION_PRIORITY)(または0)を使用します。

2番目のケース(一般的ではない)は、ハンドラが実際に意見を持っており、他の場所で発行された優先順位の低いabort()またはrespond()をオーバーライドすることによって継続を強制することを意図していることです。これは意見のある継続と呼ばれます。オーバーライドする継続優先順位を指定する必要があるまれなケースでは、カスタム優先順位を使用します。

要約すると、request.continueの使用がデフォルト/バイパス動作を意味するのか、ハンドラの意図されたユースケースに該当するのかを検討してください。スコープ内のユースケースにはカスタム優先順位を使用し、それ以外の場合はデフォルトの優先順位を使用することを検討してください。ハンドラには、意見のあるケースと無意見のケースの両方がある可能性があることに注意してください。

パッケージメンテナ向けの協調的インターセプトモードへのアップグレード

パッケージメンテナであり、パッケージがインターセプトハンドラを使用している場合、インターセプトハンドラを更新して協調的インターセプトモードを使用できます。次の既存のハンドラがあるとします。

page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort();
else interceptedRequest.continue();
});

協調的インターセプトモードを使用するには、continue()abort()をアップグレードします。

page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort('failed', 0);
else
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
0
);
});

これらの簡単なアップグレードにより、ハンドラは協調的インターセプトモードを使用するようになります。

ただし、上記はいくつかの微妙な問題を引き起こすため、少し堅牢なソリューションをお勧めします。

  1. 下位互換性。いずれかのハンドラがまだレガシーモードの解決を使用している場合(つまり、優先順位を指定していない場合)、そのハンドラは、ハンドラが最初に実行された場合でも、インターセプトをすぐに解決します。これは、ユーザーが行ったことがパッケージのアップグレードだけなのに、突然ハンドラがインターセプトを解決しておらず、別のハンドラが優先順位を取っているため、ユーザーにとって不安定な動作を引き起こす可能性があります。
  2. ハードコードされた優先順位。パッケージユーザーは、ハンドラのデフォルトの解決優先順位を指定できません。これは、ユーザーがユースケースに基づいて優先順位を操作する場合に重要になる可能性があります。たとえば、あるユーザーはパッケージに高い優先順位を付けたい一方で、別のユーザーは低い優先順位を付けたい場合があります。

これらの両方の問題を解決するために、推奨されるアプローチは、パッケージからsetInterceptResolutionConfig()をエクスポートすることです。ユーザーは、setInterceptResolutionConfig()を呼び出してパッケージで協調的インターセプトモードを明示的にアクティブ化できるため、インターセプトの解決方法の変更に驚かされることはありません。また、必要に応じて、ユースケースに適したカスタム優先順位をsetInterceptResolutionConfig(priority)を使用して指定することもできます。

// Defaults to undefined which preserves Legacy Mode behavior
let _priority = undefined;

// Export a module configuration function
export const setInterceptResolutionConfig = (priority = 0) =>
(_priority = priority);

/**
* Note that this handler uses `DEFAULT_INTERCEPT_RESOLUTION_PRIORITY` to "pass" on this request. It is important to use
* the default priority when your handler has no opinion on the request and the intent is to continue() by default.
*/
page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
)
interceptedRequest.abort('failed', _priority);
else
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
DEFAULT_INTERCEPT_RESOLUTION_PRIORITY // Unopinionated continuation
);
});

パッケージで解決優先順位をより詳細に制御する必要がある場合は、このような構成パターンを使用します。

interface InterceptResolutionConfig {
abortPriority?: number;
continuePriority?: number;
}

// This approach supports multiple priorities based on situational
// differences. You could, for example, create a config that
// allowed separate priorities for PNG vs JPG.
const DEFAULT_CONFIG: InterceptResolutionConfig = {
abortPriority: undefined, // Default to Legacy Mode
continuePriority: undefined, // Default to Legacy Mode
};

// Defaults to undefined which preserves Legacy Mode behavior
let _config: Partial<InterceptResolutionConfig> = {};

export const setInterceptResolutionConfig = (
config: InterceptResolutionConfig
) => (_config = {...DEFAULT_CONFIG, ...config});

page.on('request', interceptedRequest => {
if (request.isInterceptResolutionHandled()) return;
if (
interceptedRequest.url().endsWith('.png') ||
interceptedRequest.url().endsWith('.jpg')
) {
interceptedRequest.abort('failed', _config.abortPriority);
} else {
// Here we use a custom-configured priority to allow for Opinionated
// continuation.
// We would only want to allow this if we had a very clear reason why
// some use cases required Opinionated continuation.
interceptedRequest.continue(
interceptedRequest.continueRequestOverrides(),
_config.continuePriority // Why would we ever want priority!==0 here?
);
}
});

上記のソリューションにより、ユーザーが協調的インターセプトモードを使用しているときに解決チェーンでのパッケージの重要度を調整することを可能にしながら、下位互換性が確保されます。ユーザーがコードとサードパーティのパッケージのすべてを協調的インターセプトモードを使用するように完全にアップグレードするまで、パッケージは期待どおりに動作し続けます。いずれかのハンドラまたはパッケージがまだレガシーモードを使用している場合、パッケージもレガシーモードで動作できます。