diff --git a/packages/puppeteer-core/src/api/HTTPRequest.ts b/packages/puppeteer-core/src/api/HTTPRequest.ts new file mode 100644 index 00000000000..9e3c1b2454f --- /dev/null +++ b/packages/puppeteer-core/src/api/HTTPRequest.ts @@ -0,0 +1,567 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Protocol} from 'devtools-protocol'; + +import {CDPSession} from '../common/Connection.js'; +import {Frame} from '../common/Frame.js'; + +import {HTTPResponse} from './HTTPResponse.js'; + +/** + * @public + */ +export interface ContinueRequestOverrides { + /** + * If set, the request URL will change. This is not a redirect. + */ + url?: string; + method?: string; + postData?: string; + headers?: Record; +} + +/** + * @public + */ +export interface InterceptResolutionState { + action: InterceptResolutionAction; + priority?: number; +} + +/** + * Required response data to fulfill a request with. + * + * @public + */ +export interface ResponseForRequest { + status: number; + /** + * Optional response headers. All values are converted to strings. + */ + headers: Record; + contentType: string; + body: string | Buffer; +} + +/** + * Resource types for HTTPRequests as perceived by the rendering engine. + * + * @public + */ +export type ResourceType = Lowercase; + +/** + * The default cooperative request interception resolution priority + * + * @public + */ +export const DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0; + +/** + * Represents an HTTP request sent by a page. + * @remarks + * + * Whenever the page sends a request, such as for a network resource, the + * following events are emitted by Puppeteer's `page`: + * + * - `request`: emitted when the request is issued by the page. + * - `requestfinished` - emitted when the response body is downloaded and the + * request is complete. + * + * If request fails at some point, then instead of `requestfinished` event the + * `requestfailed` event is emitted. + * + * All of these events provide an instance of `HTTPRequest` representing the + * request that occurred: + * + * ``` + * page.on('request', request => ...) + * ``` + * + * NOTE: HTTP Error responses, such as 404 or 503, are still successful + * responses from HTTP standpoint, so request will complete with + * `requestfinished` event. + * + * If request gets a 'redirect' response, the request is successfully finished + * with the `requestfinished` event, and a new request is issued to a + * redirected url. + * + * @public + */ +export class HTTPRequest { + /** + * @internal + */ + _requestId = ''; + /** + * @internal + */ + _interceptionId: string | undefined; + /** + * @internal + */ + _failureText: string | null = null; + /** + * @internal + */ + _response: HTTPResponse | null = null; + /** + * @internal + */ + _fromMemoryCache = false; + /** + * @internal + */ + _redirectChain: HTTPRequest[] = []; + + /** + * Warning! Using this client can break Puppeteer. Use with caution. + * + * @experimental + */ + get client(): CDPSession { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + constructor() {} + + /** + * @returns the URL of the request + */ + url(): string { + throw new Error('Not implemented'); + } + + /** + * @returns the `ContinueRequestOverrides` that will be used + * if the interception is allowed to continue (ie, `abort()` and + * `respond()` aren't called). + */ + continueRequestOverrides(): ContinueRequestOverrides { + throw new Error('Not implemented'); + } + + /** + * @returns The `ResponseForRequest` that gets used if the + * interception is allowed to respond (ie, `abort()` is not called). + */ + responseForRequest(): Partial | null { + throw new Error('Not implemented'); + } + + /** + * @returns the most recent reason for aborting the request + */ + abortErrorReason(): Protocol.Network.ErrorReason | null { + throw new Error('Not implemented'); + } + + /** + * @returns An InterceptResolutionState object describing the current resolution + * action and priority. + * + * InterceptResolutionState contains: + * action: InterceptResolutionAction + * priority?: number + * + * InterceptResolutionAction is one of: `abort`, `respond`, `continue`, + * `disabled`, `none`, or `already-handled`. + */ + interceptResolutionState(): InterceptResolutionState { + throw new Error('Not implemented'); + } + + /** + * @returns `true` if the intercept resolution has already been handled, + * `false` otherwise. + */ + isInterceptResolutionHandled(): boolean { + throw new Error('Not implemented'); + } + + /** + * Adds an async request handler to the processing queue. + * Deferred handlers are not guaranteed to execute in any particular order, + * but they are guaranteed to resolve before the request interception + * is finalized. + */ + enqueueInterceptAction( + pendingHandler: () => void | PromiseLike + ): void; + enqueueInterceptAction(): void { + throw new Error('Not implemented'); + } + + /** + * Awaits pending interception handlers and then decides how to fulfill + * the request interception. + */ + async finalizeInterceptions(): Promise { + throw new Error('Not implemented'); + } + + /** + * Contains the request's resource type as it was perceived by the rendering + * engine. + */ + resourceType(): ResourceType { + throw new Error('Not implemented'); + } + + /** + * @returns the method used (`GET`, `POST`, etc.) + */ + method(): string { + throw new Error('Not implemented'); + } + + /** + * @returns the request's post body, if any. + */ + postData(): string | undefined { + throw new Error('Not implemented'); + } + + /** + * @returns an object with HTTP headers associated with the request. All + * header names are lower-case. + */ + headers(): Record { + throw new Error('Not implemented'); + } + + /** + * @returns A matching `HTTPResponse` object, or null if the response has not + * been received yet. + */ + response(): HTTPResponse | null { + throw new Error('Not implemented'); + } + + /** + * @returns the frame that initiated the request, or null if navigating to + * error pages. + */ + frame(): Frame | null { + throw new Error('Not implemented'); + } + + /** + * @returns true if the request is the driver of the current frame's navigation. + */ + isNavigationRequest(): boolean { + throw new Error('Not implemented'); + } + + /** + * @returns the initiator of the request. + */ + initiator(): Protocol.Network.Initiator { + throw new Error('Not implemented'); + } + + /** + * A `redirectChain` is a chain of requests initiated to fetch a resource. + * @remarks + * + * `redirectChain` is shared between all the requests of the same chain. + * + * For example, if the website `http://example.com` has a single redirect to + * `https://example.com`, then the chain will contain one request: + * + * ```ts + * const response = await page.goto('http://example.com'); + * const chain = response.request().redirectChain(); + * console.log(chain.length); // 1 + * console.log(chain[0].url()); // 'http://example.com' + * ``` + * + * If the website `https://google.com` has no redirects, then the chain will be empty: + * + * ```ts + * const response = await page.goto('https://google.com'); + * const chain = response.request().redirectChain(); + * console.log(chain.length); // 0 + * ``` + * + * @returns the chain of requests - if a server responds with at least a + * single redirect, this chain will contain all requests that were redirected. + */ + redirectChain(): HTTPRequest[] { + throw new Error('Not implemented'); + } + + /** + * Access information about the request's failure. + * + * @remarks + * + * @example + * + * Example of logging all failed requests: + * + * ```ts + * page.on('requestfailed', request => { + * console.log(request.url() + ' ' + request.failure().errorText); + * }); + * ``` + * + * @returns `null` unless the request failed. If the request fails this can + * return an object with `errorText` containing a human-readable error + * message, e.g. `net::ERR_FAILED`. It is not guaranteed that there will be + * failure text if the request fails. + */ + failure(): {errorText: string} | null { + throw new Error('Not implemented'); + } + + /** + * Continues request with optional request overrides. + * + * @remarks + * + * To use this, request + * interception should be enabled with {@link Page.setRequestInterception}. + * + * Exception is immediately thrown if the request interception is not enabled. + * + * @example + * + * ```ts + * await page.setRequestInterception(true); + * page.on('request', request => { + * // Override headers + * const headers = Object.assign({}, request.headers(), { + * foo: 'bar', // set "foo" header + * origin: undefined, // remove "origin" header + * }); + * request.continue({headers}); + * }); + * ``` + * + * @param overrides - optional overrides to apply to the request. + * @param priority - If provided, intercept is resolved using + * cooperative handling rules. Otherwise, intercept is resolved + * immediately. + */ + async continue( + overrides?: ContinueRequestOverrides, + priority?: number + ): Promise; + async continue(): Promise { + throw new Error('Not implemented'); + } + + /** + * Fulfills a request with the given response. + * + * @remarks + * + * To use this, request + * interception should be enabled with {@link Page.setRequestInterception}. + * + * Exception is immediately thrown if the request interception is not enabled. + * + * @example + * An example of fulfilling all requests with 404 responses: + * + * ```ts + * await page.setRequestInterception(true); + * page.on('request', request => { + * request.respond({ + * status: 404, + * contentType: 'text/plain', + * body: 'Not Found!', + * }); + * }); + * ``` + * + * NOTE: Mocking responses for dataURL requests is not supported. + * Calling `request.respond` for a dataURL request is a noop. + * + * @param response - the response to fulfill the request with. + * @param priority - If provided, intercept is resolved using + * cooperative handling rules. Otherwise, intercept is resolved + * immediately. + */ + async respond( + response: Partial, + priority?: number + ): Promise; + async respond(): Promise { + throw new Error('Not implemented'); + } + + /** + * Aborts a request. + * + * @remarks + * To use this, request interception should be enabled with + * {@link Page.setRequestInterception}. If it is not enabled, this method will + * throw an exception immediately. + * + * @param errorCode - optional error code to provide. + * @param priority - If provided, intercept is resolved using + * cooperative handling rules. Otherwise, intercept is resolved + * immediately. + */ + async abort(errorCode?: ErrorCode, priority?: number): Promise; + async abort(): Promise { + throw new Error('Not implemented'); + } +} + +/** + * @public + */ +export enum InterceptResolutionAction { + Abort = 'abort', + Respond = 'respond', + Continue = 'continue', + Disabled = 'disabled', + None = 'none', + AlreadyHandled = 'already-handled', +} + +/** + * @public + * + * @deprecated please use {@link InterceptResolutionAction} instead. + */ +export type InterceptResolutionStrategy = InterceptResolutionAction; + +/** + * @public + */ +export type ErrorCode = + | 'aborted' + | 'accessdenied' + | 'addressunreachable' + | 'blockedbyclient' + | 'blockedbyresponse' + | 'connectionaborted' + | 'connectionclosed' + | 'connectionfailed' + | 'connectionrefused' + | 'connectionreset' + | 'internetdisconnected' + | 'namenotresolved' + | 'timedout' + | 'failed'; + +/** + * @public + */ +export type ActionResult = 'continue' | 'abort' | 'respond'; + +/** + * @internal + */ +export function headersArray( + headers: Record +): Array<{name: string; value: string}> { + const result = []; + for (const name in headers) { + const value = headers[name]; + + if (!Object.is(value, undefined)) { + const values = Array.isArray(value) ? value : [value]; + + result.push( + ...values.map(value => { + return {name, value: value + ''}; + }) + ); + } + } + return result; +} + +/** + * @internal + * + * @remarks + * List taken from {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml} + * with extra 306 and 418 codes. + */ +export const STATUS_TEXTS: {[key: string]: string | undefined} = { + '100': 'Continue', + '101': 'Switching Protocols', + '102': 'Processing', + '103': 'Early Hints', + '200': 'OK', + '201': 'Created', + '202': 'Accepted', + '203': 'Non-Authoritative Information', + '204': 'No Content', + '205': 'Reset Content', + '206': 'Partial Content', + '207': 'Multi-Status', + '208': 'Already Reported', + '226': 'IM Used', + '300': 'Multiple Choices', + '301': 'Moved Permanently', + '302': 'Found', + '303': 'See Other', + '304': 'Not Modified', + '305': 'Use Proxy', + '306': 'Switch Proxy', + '307': 'Temporary Redirect', + '308': 'Permanent Redirect', + '400': 'Bad Request', + '401': 'Unauthorized', + '402': 'Payment Required', + '403': 'Forbidden', + '404': 'Not Found', + '405': 'Method Not Allowed', + '406': 'Not Acceptable', + '407': 'Proxy Authentication Required', + '408': 'Request Timeout', + '409': 'Conflict', + '410': 'Gone', + '411': 'Length Required', + '412': 'Precondition Failed', + '413': 'Payload Too Large', + '414': 'URI Too Long', + '415': 'Unsupported Media Type', + '416': 'Range Not Satisfiable', + '417': 'Expectation Failed', + '418': "I'm a teapot", + '421': 'Misdirected Request', + '422': 'Unprocessable Entity', + '423': 'Locked', + '424': 'Failed Dependency', + '425': 'Too Early', + '426': 'Upgrade Required', + '428': 'Precondition Required', + '429': 'Too Many Requests', + '431': 'Request Header Fields Too Large', + '451': 'Unavailable For Legal Reasons', + '500': 'Internal Server Error', + '501': 'Not Implemented', + '502': 'Bad Gateway', + '503': 'Service Unavailable', + '504': 'Gateway Timeout', + '505': 'HTTP Version Not Supported', + '506': 'Variant Also Negotiates', + '507': 'Insufficient Storage', + '508': 'Loop Detected', + '510': 'Not Extended', + '511': 'Network Authentication Required', +} as const; diff --git a/packages/puppeteer-core/src/api/HTTPResponse.ts b/packages/puppeteer-core/src/api/HTTPResponse.ts new file mode 100644 index 00000000000..00422e42826 --- /dev/null +++ b/packages/puppeteer-core/src/api/HTTPResponse.ts @@ -0,0 +1,169 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Protocol from 'devtools-protocol'; + +import {Frame} from '../common/Frame.js'; +import {SecurityDetails} from '../common/SecurityDetails.js'; + +import {HTTPRequest} from './HTTPRequest.js'; + +/** + * @public + */ +export interface RemoteAddress { + ip?: string; + port?: number; +} + +/** + * The HTTPResponse class represents responses which are received by the + * {@link Page} class. + * + * @public + */ +export class HTTPResponse { + /** + * @internal + */ + constructor() {} + + /** + * @internal + */ + _resolveBody(_err: Error | null): void { + throw new Error('Not implemented'); + } + + /** + * @returns The IP address and port number used to connect to the remote + * server. + */ + remoteAddress(): RemoteAddress { + throw new Error('Not implemented'); + } + + /** + * @returns The URL of the response. + */ + url(): string { + throw new Error('Not implemented'); + } + + /** + * @returns True if the response was successful (status in the range 200-299). + */ + ok(): boolean { + throw new Error('Not implemented'); + } + + /** + * @returns The status code of the response (e.g., 200 for a success). + */ + status(): number { + throw new Error('Not implemented'); + } + + /** + * @returns The status text of the response (e.g. usually an "OK" for a + * success). + */ + statusText(): string { + throw new Error('Not implemented'); + } + + /** + * @returns An object with HTTP headers associated with the response. All + * header names are lower-case. + */ + headers(): Record { + throw new Error('Not implemented'); + } + + /** + * @returns {@link SecurityDetails} if the response was received over the + * secure connection, or `null` otherwise. + */ + securityDetails(): SecurityDetails | null { + throw new Error('Not implemented'); + } + + /** + * @returns Timing information related to the response. + */ + timing(): Protocol.Network.ResourceTiming | null { + throw new Error('Not implemented'); + } + + /** + * @returns Promise which resolves to a buffer with response body. + */ + buffer(): Promise { + throw new Error('Not implemented'); + } + + /** + * @returns Promise which resolves to a text representation of response body. + */ + async text(): Promise { + const content = await this.buffer(); + return content.toString('utf8'); + } + + /** + * + * @returns Promise which resolves to a JSON representation of response body. + * + * @remarks + * + * This method will throw if the response body is not parsable via + * `JSON.parse`. + */ + async json(): Promise { + const content = await this.text(); + return JSON.parse(content); + } + + /** + * @returns A matching {@link HTTPRequest} object. + */ + request(): HTTPRequest { + throw new Error('Not implemented'); + } + + /** + * @returns True if the response was served from either the browser's disk + * cache or memory cache. + */ + fromCache(): boolean { + throw new Error('Not implemented'); + } + + /** + * @returns True if the response was served by a service worker. + */ + fromServiceWorker(): boolean { + throw new Error('Not implemented'); + } + + /** + * @returns A {@link Frame} that initiated this response, or `null` if + * navigating to error pages. + */ + frame(): Frame | null { + throw new Error('Not implemented'); + } +} diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts index 4e37bf1293d..e826c06629d 100644 --- a/packages/puppeteer-core/src/api/Page.ts +++ b/packages/puppeteer-core/src/api/Page.ts @@ -18,6 +18,8 @@ import type {Readable} from 'stream'; import {Protocol} from 'devtools-protocol'; +import type {HTTPRequest} from '../api/HTTPRequest.js'; +import type {HTTPResponse} from '../api/HTTPResponse.js'; import type {Accessibility} from '../common/Accessibility.js'; import type {ConsoleMessage} from '../common/ConsoleMessage.js'; import type {Coverage} from '../common/Coverage.js'; @@ -31,8 +33,6 @@ import type { FrameAddStyleTagOptions, FrameWaitForFunctionOptions, } from '../common/Frame.js'; -import type {HTTPRequest} from '../common/HTTPRequest.js'; -import type {HTTPResponse} from '../common/HTTPResponse.js'; import type { Keyboard, Mouse, diff --git a/packages/puppeteer-core/src/api/api.ts b/packages/puppeteer-core/src/api/api.ts index c77066cb272..704c8d127fe 100644 --- a/packages/puppeteer-core/src/api/api.ts +++ b/packages/puppeteer-core/src/api/api.ts @@ -19,3 +19,5 @@ export * from './BrowserContext.js'; export * from './Page.js'; export * from './JSHandle.js'; export * from './ElementHandle.js'; +export * from './HTTPResponse.js'; +export * from './HTTPRequest.js'; diff --git a/packages/puppeteer-core/src/common/Frame.ts b/packages/puppeteer-core/src/common/Frame.ts index c3152fbcc9a..517c70bca96 100644 --- a/packages/puppeteer-core/src/common/Frame.ts +++ b/packages/puppeteer-core/src/common/Frame.ts @@ -17,6 +17,7 @@ import {Protocol} from 'devtools-protocol'; import {ElementHandle} from '../api/ElementHandle.js'; +import {HTTPResponse} from '../api/HTTPResponse.js'; import {Page} from '../api/Page.js'; import {isErrorLike} from '../util/ErrorLike.js'; @@ -24,7 +25,6 @@ import {CDPSession} from './Connection.js'; import {ExecutionContext} from './ExecutionContext.js'; import {FrameManager} from './FrameManager.js'; import {getQueryHandlerAndSelector} from './GetQueryHandler.js'; -import {HTTPResponse} from './HTTPResponse.js'; import {MouseButton} from './Input.js'; import { IsolatedWorld, diff --git a/packages/puppeteer-core/src/common/HTTPRequest.ts b/packages/puppeteer-core/src/common/HTTPRequest.ts index de5eb4a2e9a..5150c7db3ea 100644 --- a/packages/puppeteer-core/src/common/HTTPRequest.ts +++ b/packages/puppeteer-core/src/common/HTTPRequest.ts @@ -15,120 +15,35 @@ */ import {Protocol} from 'devtools-protocol'; +import { + ContinueRequestOverrides, + ErrorCode, + headersArray, + HTTPRequest as BaseHTTPRequest, + InterceptResolutionAction, + InterceptResolutionState, + ResourceType, + ResponseForRequest, + STATUS_TEXTS, +} from '../api/HTTPRequest.js'; +import {HTTPResponse} from '../api/HTTPResponse.js'; import {assert} from '../util/assert.js'; import {CDPSession} from './Connection.js'; import {ProtocolError} from './Errors.js'; import {Frame} from './Frame.js'; -import {HTTPResponse} from './HTTPResponse.js'; import {debugError, isString} from './util.js'; /** - * @public + * @internal */ -export interface ContinueRequestOverrides { - /** - * If set, the request URL will change. This is not a redirect. - */ - url?: string; - method?: string; - postData?: string; - headers?: Record; -} - -/** - * @public - */ -export interface InterceptResolutionState { - action: InterceptResolutionAction; - priority?: number; -} - -/** - * Required response data to fulfill a request with. - * - * @public - */ -export interface ResponseForRequest { - status: number; - /** - * Optional response headers. All values are converted to strings. - */ - headers: Record; - contentType: string; - body: string | Buffer; -} - -/** - * Resource types for HTTPRequests as perceived by the rendering engine. - * - * @public - */ -export type ResourceType = Lowercase; - -/** - * The default cooperative request interception resolution priority - * - * @public - */ -export const DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0; - -/** - * Represents an HTTP request sent by a page. - * @remarks - * - * Whenever the page sends a request, such as for a network resource, the - * following events are emitted by Puppeteer's `page`: - * - * - `request`: emitted when the request is issued by the page. - * - `requestfinished` - emitted when the response body is downloaded and the - * request is complete. - * - * If request fails at some point, then instead of `requestfinished` event the - * `requestfailed` event is emitted. - * - * All of these events provide an instance of `HTTPRequest` representing the - * request that occurred: - * - * ``` - * page.on('request', request => ...) - * ``` - * - * NOTE: HTTP Error responses, such as 404 or 503, are still successful - * responses from HTTP standpoint, so request will complete with - * `requestfinished` event. - * - * If request gets a 'redirect' response, the request is successfully finished - * with the `requestfinished` event, and a new request is issued to a - * redirected url. - * - * @public - */ -export class HTTPRequest { - /** - * @internal - */ - _requestId: string; - /** - * @internal - */ - _interceptionId: string | undefined; - /** - * @internal - */ - _failureText: string | null = null; - /** - * @internal - */ - _response: HTTPResponse | null = null; - /** - * @internal - */ - _fromMemoryCache = false; - /** - * @internal - */ - _redirectChain: HTTPRequest[]; +export class HTTPRequest extends BaseHTTPRequest { + override _requestId: string; + override _interceptionId: string | undefined; + override _failureText: string | null = null; + override _response: HTTPResponse | null = null; + override _fromMemoryCache = false; + override _redirectChain: HTTPRequest[]; #client: CDPSession; #isNavigationRequest: boolean; @@ -150,18 +65,10 @@ export class HTTPRequest { #interceptHandlers: Array<() => void | PromiseLike>; #initiator: Protocol.Network.Initiator; - /** - * Warning! Using this client can break Puppeteer. Use with caution. - * - * @experimental - */ - get client(): CDPSession { + override get client(): CDPSession { return this.#client; } - /** - * @internal - */ constructor( client: CDPSession, frame: Frame | null, @@ -170,6 +77,7 @@ export class HTTPRequest { event: Protocol.Network.RequestWillBeSentEvent, redirectChain: HTTPRequest[] ) { + super(); this.#client = client; this._requestId = event.requestId; this.#isNavigationRequest = @@ -191,52 +99,26 @@ export class HTTPRequest { } } - /** - * @returns the URL of the request - */ - url(): string { + override url(): string { return this.#url; } - /** - * @returns the `ContinueRequestOverrides` that will be used - * if the interception is allowed to continue (ie, `abort()` and - * `respond()` aren't called). - */ - continueRequestOverrides(): ContinueRequestOverrides { + override continueRequestOverrides(): ContinueRequestOverrides { assert(this.#allowInterception, 'Request Interception is not enabled!'); return this.#continueRequestOverrides; } - /** - * @returns The `ResponseForRequest` that gets used if the - * interception is allowed to respond (ie, `abort()` is not called). - */ - responseForRequest(): Partial | null { + override responseForRequest(): Partial | null { assert(this.#allowInterception, 'Request Interception is not enabled!'); return this.#responseForRequest; } - /** - * @returns the most recent reason for aborting the request - */ - abortErrorReason(): Protocol.Network.ErrorReason | null { + override abortErrorReason(): Protocol.Network.ErrorReason | null { assert(this.#allowInterception, 'Request Interception is not enabled!'); return this.#abortErrorReason; } - /** - * @returns An InterceptResolutionState object describing the current resolution - * action and priority. - * - * InterceptResolutionState contains: - * action: InterceptResolutionAction - * priority?: number - * - * InterceptResolutionAction is one of: `abort`, `respond`, `continue`, - * `disabled`, `none`, or `already-handled`. - */ - interceptResolutionState(): InterceptResolutionState { + override interceptResolutionState(): InterceptResolutionState { if (!this.#allowInterception) { return {action: InterceptResolutionAction.Disabled}; } @@ -246,31 +128,17 @@ export class HTTPRequest { return {...this.#interceptResolutionState}; } - /** - * @returns `true` if the intercept resolution has already been handled, - * `false` otherwise. - */ - isInterceptResolutionHandled(): boolean { + override isInterceptResolutionHandled(): boolean { return this.#interceptionHandled; } - /** - * Adds an async request handler to the processing queue. - * Deferred handlers are not guaranteed to execute in any particular order, - * but they are guaranteed to resolve before the request interception - * is finalized. - */ - enqueueInterceptAction( + override enqueueInterceptAction( pendingHandler: () => void | PromiseLike ): void { this.#interceptHandlers.push(pendingHandler); } - /** - * Awaits pending interception handlers and then decides how to fulfill - * the request interception. - */ - async finalizeInterceptions(): Promise { + override async finalizeInterceptions(): Promise { await this.#interceptHandlers.reduce((promiseChain, interceptAction) => { return promiseChain.then(interceptAction); }, Promise.resolve()); @@ -288,94 +156,39 @@ export class HTTPRequest { } } - /** - * Contains the request's resource type as it was perceived by the rendering - * engine. - */ - resourceType(): ResourceType { + override resourceType(): ResourceType { return this.#resourceType; } - /** - * @returns the method used (`GET`, `POST`, etc.) - */ - method(): string { + override method(): string { return this.#method; } - /** - * @returns the request's post body, if any. - */ - postData(): string | undefined { + override postData(): string | undefined { return this.#postData; } - /** - * @returns an object with HTTP headers associated with the request. All - * header names are lower-case. - */ - headers(): Record { + override headers(): Record { return this.#headers; } - /** - * @returns A matching `HTTPResponse` object, or null if the response has not - * been received yet. - */ - response(): HTTPResponse | null { + override response(): HTTPResponse | null { return this._response; } - /** - * @returns the frame that initiated the request, or null if navigating to - * error pages. - */ - frame(): Frame | null { + override frame(): Frame | null { return this.#frame; } - /** - * @returns true if the request is the driver of the current frame's navigation. - */ - isNavigationRequest(): boolean { + override isNavigationRequest(): boolean { return this.#isNavigationRequest; } - /** - * @returns the initiator of the request. - */ - initiator(): Protocol.Network.Initiator { + override initiator(): Protocol.Network.Initiator { return this.#initiator; } - /** - * A `redirectChain` is a chain of requests initiated to fetch a resource. - * @remarks - * - * `redirectChain` is shared between all the requests of the same chain. - * - * For example, if the website `http://example.com` has a single redirect to - * `https://example.com`, then the chain will contain one request: - * - * ```ts - * const response = await page.goto('http://example.com'); - * const chain = response.request().redirectChain(); - * console.log(chain.length); // 1 - * console.log(chain[0].url()); // 'http://example.com' - * ``` - * - * If the website `https://google.com` has no redirects, then the chain will be empty: - * - * ```ts - * const response = await page.goto('https://google.com'); - * const chain = response.request().redirectChain(); - * console.log(chain.length); // 0 - * ``` - * - * @returns the chain of requests - if a server responds with at least a - * single redirect, this chain will contain all requests that were redirected. - */ - redirectChain(): HTTPRequest[] { + override redirectChain(): HTTPRequest[] { return this._redirectChain.slice(); } @@ -399,7 +212,7 @@ export class HTTPRequest { * message, e.g. `net::ERR_FAILED`. It is not guaranteed that there will be * failure text if the request fails. */ - failure(): {errorText: string} | null { + override failure(): {errorText: string} | null { if (!this._failureText) { return null; } @@ -437,7 +250,7 @@ export class HTTPRequest { * cooperative handling rules. Otherwise, intercept is resolved * immediately. */ - async continue( + override async continue( overrides: ContinueRequestOverrides = {}, priority?: number ): Promise { @@ -501,39 +314,7 @@ export class HTTPRequest { }); } - /** - * Fulfills a request with the given response. - * - * @remarks - * - * To use this, request - * interception should be enabled with {@link Page.setRequestInterception}. - * - * Exception is immediately thrown if the request interception is not enabled. - * - * @example - * An example of fulfilling all requests with 404 responses: - * - * ```ts - * await page.setRequestInterception(true); - * page.on('request', request => { - * request.respond({ - * status: 404, - * contentType: 'text/plain', - * body: 'Not Found!', - * }); - * }); - * ``` - * - * NOTE: Mocking responses for dataURL requests is not supported. - * Calling `request.respond` for a dataURL request is a noop. - * - * @param response - the response to fulfill the request with. - * @param priority - If provided, intercept is resolved using - * cooperative handling rules. Otherwise, intercept is resolved - * immediately. - */ - async respond( + override async respond( response: Partial, priority?: number ): Promise { @@ -614,20 +395,7 @@ export class HTTPRequest { }); } - /** - * Aborts a request. - * - * @remarks - * To use this, request interception should be enabled with - * {@link Page.setRequestInterception}. If it is not enabled, this method will - * throw an exception immediately. - * - * @param errorCode - optional error code to provide. - * @param priority - If provided, intercept is resolved using - * cooperative handling rules. Otherwise, intercept is resolved - * immediately. - */ - async abort( + override async abort( errorCode: ErrorCode = 'failed', priority?: number ): Promise { @@ -673,44 +441,6 @@ export class HTTPRequest { } } -/** - * @public - */ -export enum InterceptResolutionAction { - Abort = 'abort', - Respond = 'respond', - Continue = 'continue', - Disabled = 'disabled', - None = 'none', - AlreadyHandled = 'already-handled', -} - -/** - * @public - * - * @deprecated please use {@link InterceptResolutionAction} instead. - */ -export type InterceptResolutionStrategy = InterceptResolutionAction; - -/** - * @public - */ -export type ErrorCode = - | 'aborted' - | 'accessdenied' - | 'addressunreachable' - | 'blockedbyclient' - | 'blockedbyresponse' - | 'connectionaborted' - | 'connectionclosed' - | 'connectionfailed' - | 'connectionrefused' - | 'connectionreset' - | 'internetdisconnected' - | 'namenotresolved' - | 'timedout' - | 'failed'; - const errorReasons: Record = { aborted: 'Aborted', accessdenied: 'AccessDenied', @@ -728,31 +458,6 @@ const errorReasons: Record = { failed: 'Failed', } as const; -/** - * @public - */ -export type ActionResult = 'continue' | 'abort' | 'respond'; - -function headersArray( - headers: Record -): Array<{name: string; value: string}> { - const result = []; - for (const name in headers) { - const value = headers[name]; - - if (!Object.is(value, undefined)) { - const values = Array.isArray(value) ? value : [value]; - - result.push( - ...values.map(value => { - return {name, value: value + ''}; - }) - ); - } - } - return result; -} - async function handleError(error: ProtocolError) { if (['Invalid header'].includes(error.originalMessage)) { throw error; @@ -762,72 +467,3 @@ async function handleError(error: ProtocolError) { // errors. debugError(error); } - -// List taken from -// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml -// with extra 306 and 418 codes. -const STATUS_TEXTS: {[key: string]: string | undefined} = { - '100': 'Continue', - '101': 'Switching Protocols', - '102': 'Processing', - '103': 'Early Hints', - '200': 'OK', - '201': 'Created', - '202': 'Accepted', - '203': 'Non-Authoritative Information', - '204': 'No Content', - '205': 'Reset Content', - '206': 'Partial Content', - '207': 'Multi-Status', - '208': 'Already Reported', - '226': 'IM Used', - '300': 'Multiple Choices', - '301': 'Moved Permanently', - '302': 'Found', - '303': 'See Other', - '304': 'Not Modified', - '305': 'Use Proxy', - '306': 'Switch Proxy', - '307': 'Temporary Redirect', - '308': 'Permanent Redirect', - '400': 'Bad Request', - '401': 'Unauthorized', - '402': 'Payment Required', - '403': 'Forbidden', - '404': 'Not Found', - '405': 'Method Not Allowed', - '406': 'Not Acceptable', - '407': 'Proxy Authentication Required', - '408': 'Request Timeout', - '409': 'Conflict', - '410': 'Gone', - '411': 'Length Required', - '412': 'Precondition Failed', - '413': 'Payload Too Large', - '414': 'URI Too Long', - '415': 'Unsupported Media Type', - '416': 'Range Not Satisfiable', - '417': 'Expectation Failed', - '418': "I'm a teapot", - '421': 'Misdirected Request', - '422': 'Unprocessable Entity', - '423': 'Locked', - '424': 'Failed Dependency', - '425': 'Too Early', - '426': 'Upgrade Required', - '428': 'Precondition Required', - '429': 'Too Many Requests', - '431': 'Request Header Fields Too Large', - '451': 'Unavailable For Legal Reasons', - '500': 'Internal Server Error', - '501': 'Not Implemented', - '502': 'Bad Gateway', - '503': 'Service Unavailable', - '504': 'Gateway Timeout', - '505': 'HTTP Version Not Supported', - '506': 'Variant Also Negotiates', - '507': 'Insufficient Storage', - '508': 'Loop Detected', - '510': 'Not Extended', - '511': 'Network Authentication Required', -} as const; diff --git a/packages/puppeteer-core/src/common/HTTPResponse.ts b/packages/puppeteer-core/src/common/HTTPResponse.ts index 028abfc9890..c6a2d723767 100644 --- a/packages/puppeteer-core/src/common/HTTPResponse.ts +++ b/packages/puppeteer-core/src/common/HTTPResponse.ts @@ -15,6 +15,11 @@ */ import {Protocol} from 'devtools-protocol'; +import { + HTTPResponse as BaseHTTPResponse, + RemoteAddress, +} from '../api/HTTPResponse.js'; + import {CDPSession} from './Connection.js'; import {ProtocolError} from './Errors.js'; import {Frame} from './Frame.js'; @@ -22,20 +27,9 @@ import {HTTPRequest} from './HTTPRequest.js'; import {SecurityDetails} from './SecurityDetails.js'; /** - * @public + * @internal */ -export interface RemoteAddress { - ip?: string; - port?: number; -} - -/** - * The HTTPResponse class represents responses which are received by the - * {@link Page} class. - * - * @public - */ -export class HTTPResponse { +export class HTTPResponse extends BaseHTTPResponse { #client: CDPSession; #request: HTTPRequest; #contentPromise: Promise | null = null; @@ -51,15 +45,13 @@ export class HTTPResponse { #securityDetails: SecurityDetails | null; #timing: Protocol.Network.ResourceTiming | null; - /** - * @internal - */ constructor( client: CDPSession, request: HTTPRequest, responsePayload: Protocol.Network.Response, extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null ) { + super(); this.#client = client; this.#request = request; @@ -111,81 +103,47 @@ export class HTTPResponse { return statusText; } - /** - * @internal - */ - _resolveBody(err: Error | null): void { + override _resolveBody(err: Error | null): void { if (err) { return this.#bodyLoadedPromiseFulfill(err); } return this.#bodyLoadedPromiseFulfill(); } - /** - * @returns The IP address and port number used to connect to the remote - * server. - */ - remoteAddress(): RemoteAddress { + override remoteAddress(): RemoteAddress { return this.#remoteAddress; } - /** - * @returns The URL of the response. - */ - url(): string { + override url(): string { return this.#url; } - /** - * @returns True if the response was successful (status in the range 200-299). - */ - ok(): boolean { + override ok(): boolean { // TODO: document === 0 case? return this.#status === 0 || (this.#status >= 200 && this.#status <= 299); } - /** - * @returns The status code of the response (e.g., 200 for a success). - */ - status(): number { + override status(): number { return this.#status; } - /** - * @returns The status text of the response (e.g. usually an "OK" for a - * success). - */ - statusText(): string { + override statusText(): string { return this.#statusText; } - /** - * @returns An object with HTTP headers associated with the response. All - * header names are lower-case. - */ - headers(): Record { + override headers(): Record { return this.#headers; } - /** - * @returns {@link SecurityDetails} if the response was received over the - * secure connection, or `null` otherwise. - */ - securityDetails(): SecurityDetails | null { + override securityDetails(): SecurityDetails | null { return this.#securityDetails; } - /** - * @returns Timing information related to the response. - */ - timing(): Protocol.Network.ResourceTiming | null { + override timing(): Protocol.Network.ResourceTiming | null { return this.#timing; } - /** - * @returns Promise which resolves to a buffer with response body. - */ - buffer(): Promise { + override buffer(): Promise { if (!this.#contentPromise) { this.#contentPromise = this.#bodyLoadedPromise.then(async error => { if (error) { @@ -216,55 +174,19 @@ export class HTTPResponse { return this.#contentPromise; } - /** - * @returns Promise which resolves to a text representation of response body. - */ - async text(): Promise { - const content = await this.buffer(); - return content.toString('utf8'); - } - - /** - * - * @returns Promise which resolves to a JSON representation of response body. - * - * @remarks - * - * This method will throw if the response body is not parsable via - * `JSON.parse`. - */ - async json(): Promise { - const content = await this.text(); - return JSON.parse(content); - } - - /** - * @returns A matching {@link HTTPRequest} object. - */ - request(): HTTPRequest { + override request(): HTTPRequest { return this.#request; } - /** - * @returns True if the response was served from either the browser's disk - * cache or memory cache. - */ - fromCache(): boolean { + override fromCache(): boolean { return this.#fromDiskCache || this.#request._fromMemoryCache; } - /** - * @returns True if the response was served by a service worker. - */ - fromServiceWorker(): boolean { + override fromServiceWorker(): boolean { return this.#fromServiceWorker; } - /** - * @returns A {@link Frame} that initiated this response, or `null` if - * navigating to error pages. - */ - frame(): Frame | null { + override frame(): Frame | null { return this.#request.frame(); } } diff --git a/packages/puppeteer-core/src/common/LifecycleWatcher.ts b/packages/puppeteer-core/src/common/LifecycleWatcher.ts index 9dbae095a13..a63c4407fbf 100644 --- a/packages/puppeteer-core/src/common/LifecycleWatcher.ts +++ b/packages/puppeteer-core/src/common/LifecycleWatcher.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {HTTPResponse} from '../api/HTTPResponse.js'; import {assert} from '../util/assert.js'; import { DeferredPromise, @@ -25,7 +26,6 @@ import {TimeoutError} from './Errors.js'; import {Frame} from './Frame.js'; import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; import {HTTPRequest} from './HTTPRequest.js'; -import {HTTPResponse} from './HTTPResponse.js'; import {NetworkManagerEmittedEvents} from './NetworkManager.js'; import { addEventListener, diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts index f4ea6706621..60814d8b785 100644 --- a/packages/puppeteer-core/src/common/Page.ts +++ b/packages/puppeteer-core/src/common/Page.ts @@ -21,6 +21,7 @@ import {Protocol} from 'devtools-protocol'; import type {Browser} from '../api/Browser.js'; import type {BrowserContext} from '../api/BrowserContext.js'; import {ElementHandle} from '../api/ElementHandle.js'; +import {HTTPResponse} from '../api/HTTPResponse.js'; import {JSHandle} from '../api/JSHandle.js'; import { GeolocationOptions, @@ -60,7 +61,6 @@ import { } from './Frame.js'; import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; import {HTTPRequest} from './HTTPRequest.js'; -import {HTTPResponse} from './HTTPResponse.js'; import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js'; import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {MAIN_WORLD} from './IsolatedWorlds.js'; diff --git a/packages/puppeteer-core/src/common/bidi/Page.ts b/packages/puppeteer-core/src/common/bidi/Page.ts index f4015171e30..975b836b96e 100644 --- a/packages/puppeteer-core/src/common/bidi/Page.ts +++ b/packages/puppeteer-core/src/common/bidi/Page.ts @@ -16,13 +16,13 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; +import {HTTPResponse} from '../../api/HTTPResponse.js'; import { Page as PageBase, PageEmittedEvents, WaitForOptions, } from '../../api/Page.js'; import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js'; -import {HTTPResponse} from '../HTTPResponse.js'; import {EvaluateFunc, HandleFor} from '../types.js'; import {Context, getBidiHandle} from './Context.js'; diff --git a/packages/puppeteer-core/src/common/common.ts b/packages/puppeteer-core/src/common/common.ts index abcec3db30a..fe3570a9402 100644 --- a/packages/puppeteer-core/src/common/common.ts +++ b/packages/puppeteer-core/src/common/common.ts @@ -40,8 +40,6 @@ export * from './FirefoxTargetManager.js'; export * from './Frame.js'; export * from './FrameManager.js'; export * from './FrameTree.js'; -export * from './HTTPRequest.js'; -export * from './HTTPResponse.js'; export * from './Input.js'; export * from './IsolatedWorld.js'; export * from './IsolatedWorlds.js'; diff --git a/test/src/NetworkManager.spec.ts b/test/src/NetworkManager.spec.ts index aecb307e98c..ceedb8ae165 100644 --- a/test/src/NetworkManager.spec.ts +++ b/test/src/NetworkManager.spec.ts @@ -15,10 +15,10 @@ */ import expect from 'expect'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; import {Frame} from 'puppeteer-core/internal/common/Frame.js'; -import {HTTPRequest} from 'puppeteer-core/internal/common/HTTPRequest.js'; -import {HTTPResponse} from 'puppeteer-core/internal/common/HTTPResponse.js'; import { NetworkManager, NetworkManagerEmittedEvents, diff --git a/test/src/ignorehttpserrors.spec.ts b/test/src/ignorehttpserrors.spec.ts index e69478a676a..367a29e23b6 100644 --- a/test/src/ignorehttpserrors.spec.ts +++ b/test/src/ignorehttpserrors.spec.ts @@ -19,8 +19,8 @@ import {TLSSocket} from 'tls'; import expect from 'expect'; import {Browser} from 'puppeteer-core/internal/api/Browser.js'; import {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; import {Page} from 'puppeteer-core/internal/api/Page.js'; -import {HTTPResponse} from 'puppeteer-core/internal/common/HTTPResponse.js'; import {getTestState} from './mocha-utils.js'; diff --git a/test/src/navigation.spec.ts b/test/src/navigation.spec.ts index 6a79934d2b4..b93c89e034d 100644 --- a/test/src/navigation.spec.ts +++ b/test/src/navigation.spec.ts @@ -18,7 +18,7 @@ import {ServerResponse} from 'http'; import expect from 'expect'; import {TimeoutError} from 'puppeteer'; -import {HTTPRequest} from 'puppeteer-core/internal/common/HTTPRequest.js'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; import { getTestState, diff --git a/test/src/network.spec.ts b/test/src/network.spec.ts index 90a4395357a..a627db967fb 100644 --- a/test/src/network.spec.ts +++ b/test/src/network.spec.ts @@ -19,8 +19,8 @@ import {ServerResponse} from 'http'; import path from 'path'; import expect from 'expect'; -import {HTTPRequest} from 'puppeteer-core/internal/common/HTTPRequest.js'; -import {HTTPResponse} from 'puppeteer-core/internal/common/HTTPResponse.js'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; import { getTestState, diff --git a/test/src/requestinterception-experimental.spec.ts b/test/src/requestinterception-experimental.spec.ts index fcdf79a5d33..ba235d9ada2 100644 --- a/test/src/requestinterception-experimental.spec.ts +++ b/test/src/requestinterception-experimental.spec.ts @@ -18,12 +18,12 @@ import fs from 'fs'; import path from 'path'; import expect from 'expect'; -import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; import { ActionResult, HTTPRequest, InterceptResolutionAction, -} from 'puppeteer-core/internal/common/HTTPRequest.js'; +} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; import { getTestState, diff --git a/test/src/requestinterception.spec.ts b/test/src/requestinterception.spec.ts index 038aefc4d43..0a0dbf709b2 100644 --- a/test/src/requestinterception.spec.ts +++ b/test/src/requestinterception.spec.ts @@ -18,8 +18,8 @@ import fs from 'fs'; import path from 'path'; import expect from 'expect'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; -import {HTTPRequest} from 'puppeteer-core/internal/common/HTTPRequest.js'; import { getTestState,