feat: introduce BrowserContext._fetch (#8349)

This commit is contained in:
Yury Semikhatsky 2021-08-24 14:29:04 -07:00 committed by GitHub
parent c9718359f1
commit c0010d16c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 609 additions and 9 deletions

144
package-lock.json generated
View File

@ -36,6 +36,7 @@
"mime": "^2.4.6", "mime": "^2.4.6",
"minimatch": "^3.0.3", "minimatch": "^3.0.3",
"ms": "^2.1.2", "ms": "^2.1.2",
"node-fetch": "^2.6.1",
"pirates": "^4.0.1", "pirates": "^4.0.1",
"pixelmatch": "^5.2.1", "pixelmatch": "^5.2.1",
"pngjs": "^5.0.0", "pngjs": "^5.0.0",
@ -62,6 +63,7 @@
"@types/mime": "^2.0.3", "@types/mime": "^2.0.3",
"@types/minimatch": "^3.0.3", "@types/minimatch": "^3.0.3",
"@types/node": "^10.17.28", "@types/node": "^10.17.28",
"@types/node-fetch": "^2.5.12",
"@types/pixelmatch": "^5.2.1", "@types/pixelmatch": "^5.2.1",
"@types/pngjs": "^3.4.2", "@types/pngjs": "^3.4.2",
"@types/progress": "^2.0.3", "@types/progress": "^2.0.3",
@ -1481,6 +1483,16 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
}, },
"node_modules/@types/node-fetch": {
"version": "2.5.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz",
"integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==",
"dev": true,
"dependencies": {
"@types/node": "*",
"form-data": "^3.0.0"
}
},
"node_modules/@types/pixelmatch": { "node_modules/@types/pixelmatch": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.3.tgz",
@ -2359,6 +2371,12 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"node_modules/atob": { "node_modules/atob": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@ -3249,6 +3267,18 @@
"node": ">=0.1.90" "node": ">=0.1.90"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
@ -3698,6 +3728,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/des.js": { "node_modules/des.js": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
@ -4973,6 +5012,20 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formidable": { "node_modules/formidable": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz",
@ -6674,6 +6727,27 @@
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/mime-db": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
"integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.32",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz",
"integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==",
"dev": true,
"dependencies": {
"mime-db": "1.49.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-response": { "node_modules/mimic-response": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
@ -6893,6 +6967,14 @@
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true "dev": true
}, },
"node_modules/node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/node-libs-browser": { "node_modules/node-libs-browser": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@ -11337,6 +11419,16 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
}, },
"@types/node-fetch": {
"version": "2.5.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz",
"integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==",
"dev": true,
"requires": {
"@types/node": "*",
"form-data": "^3.0.0"
}
},
"@types/pixelmatch": { "@types/pixelmatch": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.3.tgz",
@ -12083,6 +12175,12 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"atob": { "atob": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@ -12829,6 +12927,15 @@
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
}, },
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
},
"commander": { "commander": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
@ -13197,6 +13304,12 @@
} }
} }
}, },
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"des.js": { "des.js": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
@ -14265,6 +14378,17 @@
"integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
"dev": true "dev": true
}, },
"form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"formidable": { "formidable": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz",
@ -15631,6 +15755,21 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz",
"integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg=="
}, },
"mime-db": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
"integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==",
"dev": true
},
"mime-types": {
"version": "2.1.32",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz",
"integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==",
"dev": true,
"requires": {
"mime-db": "1.49.0"
}
},
"mimic-response": { "mimic-response": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
@ -15820,6 +15959,11 @@
} }
} }
}, },
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-libs-browser": { "node-libs-browser": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",

View File

@ -67,6 +67,7 @@
"mime": "^2.4.6", "mime": "^2.4.6",
"minimatch": "^3.0.3", "minimatch": "^3.0.3",
"ms": "^2.1.2", "ms": "^2.1.2",
"node-fetch": "^2.6.1",
"pirates": "^4.0.1", "pirates": "^4.0.1",
"pixelmatch": "^5.2.1", "pixelmatch": "^5.2.1",
"pngjs": "^5.0.0", "pngjs": "^5.0.0",
@ -90,6 +91,7 @@
"@types/mime": "^2.0.3", "@types/mime": "^2.0.3",
"@types/minimatch": "^3.0.3", "@types/minimatch": "^3.0.3",
"@types/node": "^10.17.28", "@types/node": "^10.17.28",
"@types/node-fetch": "^2.5.12",
"@types/pixelmatch": "^5.2.1", "@types/pixelmatch": "^5.2.1",
"@types/pngjs": "^3.4.2", "@types/pngjs": "^3.4.2",
"@types/progress": "^2.0.3", "@types/progress": "^2.0.3",

View File

@ -28,7 +28,7 @@ import { Events } from './events';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import { Waiter } from './waiter'; import { Waiter } from './waiter';
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types'; import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
import { isUnderTest, headersObjectToArray, mkdirIfNeeded } from '../utils/utils'; import { isUnderTest, headersObjectToArray, mkdirIfNeeded, isString } from '../utils/utils';
import { isSafeCloseError } from '../utils/errors'; import { isSafeCloseError } from '../utils/errors';
import * as api from '../../types/types'; import * as api from '../../types/types';
import * as structs from '../../types/structs'; import * as structs from '../../types/structs';
@ -209,6 +209,21 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
}); });
} }
async _fetch(url: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}): Promise<network.FetchResponse> {
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
const result = await channel.fetch({
url,
method: options.method,
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
});
if (result.error)
throw new Error(`Request failed: ${result.error}`);
return new network.FetchResponse(result.response!);
});
}
async setGeolocation(geolocation: { longitude: number, latitude: number, accuracy?: number } | null): Promise<void> { async setGeolocation(geolocation: { longitude: number, latitude: number, accuracy?: number } | null): Promise<void> {
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await channel.setGeolocation({ geolocation: geolocation || undefined }); await channel.setGeolocation({ geolocation: geolocation || undefined });

View File

@ -454,6 +454,52 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
} }
} }
export class FetchResponse {
private readonly _initializer: channels.FetchResponse;
private readonly _headers: Headers;
private readonly _body: Buffer;
constructor(initializer: channels.FetchResponse) {
this._initializer = initializer;
this._headers = headersArrayToObject(this._initializer.headers, true /* lowerCase */);
this._body = Buffer.from(initializer.body, 'base64');
}
ok(): boolean {
return this._initializer.status === 0 || (this._initializer.status >= 200 && this._initializer.status <= 299);
}
url(): string {
return this._initializer.url;
}
status(): number {
return this._initializer.status;
}
statusText(): string {
return this._initializer.statusText;
}
headers(): Headers {
return { ...this._headers };
}
async body(): Promise<Buffer> {
return this._body;
}
async text(): Promise<string> {
const content = await this.body();
return content.toString('utf8');
}
async json(): Promise<object> {
const content = await this.text();
return JSON.parse(content);
}
}
export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> implements api.WebSocket { export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> implements api.WebSocket {
private _page: Page; private _page: Page;
private _isClosed: boolean; private _isClosed: boolean;

View File

@ -17,6 +17,7 @@
import { BrowserContext } from '../server/browserContext'; import { BrowserContext } from '../server/browserContext';
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher'; import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
import { playwrightFetch } from '../server/fetch';
import { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { RouteDispatcher, RequestDispatcher, ResponseDispatcher } from './networkDispatchers'; import { RouteDispatcher, RequestDispatcher, ResponseDispatcher } from './networkDispatchers';
@ -27,6 +28,7 @@ import { CallMetadata } from '../server/instrumentation';
import { ArtifactDispatcher } from './artifactDispatcher'; import { ArtifactDispatcher } from './artifactDispatcher';
import { Artifact } from '../server/artifact'; import { Artifact } from '../server/artifact';
import { Request, Response } from '../server/network'; import { Request, Response } from '../server/network';
import { headersArrayToObject } from '../utils/utils';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel { export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
private _context: BrowserContext; private _context: BrowserContext;
@ -104,6 +106,26 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}, 'main'); }, 'main');
} }
async fetch(params: channels.BrowserContextFetchParams): Promise<channels.BrowserContextFetchResult> {
const { fetchResponse, error } = await playwrightFetch(this._context, {
url: params.url,
method: params.method,
headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
});
let response;
if (fetchResponse) {
response = {
url: fetchResponse.url,
status: fetchResponse.status,
statusText: fetchResponse.statusText,
headers: fetchResponse.headers,
body: fetchResponse.body.toString('base64')
};
}
return { response, error };
}
async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> { async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> {
return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage(metadata)) }; return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage(metadata)) };
} }

View File

@ -152,6 +152,14 @@ export type InterceptedResponse = {
}[], }[],
}; };
export type FetchResponse = {
url: string,
status: number,
statusText: string,
headers: NameValue[],
body: Binary,
};
// ----------- Root ----------- // ----------- Root -----------
export type RootInitializer = {}; export type RootInitializer = {};
export interface RootChannel extends Channel { export interface RootChannel extends Channel {
@ -706,6 +714,7 @@ export interface BrowserContextChannel extends EventTargetChannel {
close(params?: BrowserContextCloseParams, metadata?: Metadata): Promise<BrowserContextCloseResult>; close(params?: BrowserContextCloseParams, metadata?: Metadata): Promise<BrowserContextCloseResult>;
cookies(params: BrowserContextCookiesParams, metadata?: Metadata): Promise<BrowserContextCookiesResult>; cookies(params: BrowserContextCookiesParams, metadata?: Metadata): Promise<BrowserContextCookiesResult>;
exposeBinding(params: BrowserContextExposeBindingParams, metadata?: Metadata): Promise<BrowserContextExposeBindingResult>; exposeBinding(params: BrowserContextExposeBindingParams, metadata?: Metadata): Promise<BrowserContextExposeBindingResult>;
fetch(params: BrowserContextFetchParams, metadata?: Metadata): Promise<BrowserContextFetchResult>;
grantPermissions(params: BrowserContextGrantPermissionsParams, metadata?: Metadata): Promise<BrowserContextGrantPermissionsResult>; grantPermissions(params: BrowserContextGrantPermissionsParams, metadata?: Metadata): Promise<BrowserContextGrantPermissionsResult>;
newPage(params?: BrowserContextNewPageParams, metadata?: Metadata): Promise<BrowserContextNewPageResult>; newPage(params?: BrowserContextNewPageParams, metadata?: Metadata): Promise<BrowserContextNewPageResult>;
setDefaultNavigationTimeoutNoReply(params: BrowserContextSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<BrowserContextSetDefaultNavigationTimeoutNoReplyResult>; setDefaultNavigationTimeoutNoReply(params: BrowserContextSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<BrowserContextSetDefaultNavigationTimeoutNoReplyResult>;
@ -802,6 +811,21 @@ export type BrowserContextExposeBindingOptions = {
needsHandle?: boolean, needsHandle?: boolean,
}; };
export type BrowserContextExposeBindingResult = void; export type BrowserContextExposeBindingResult = void;
export type BrowserContextFetchParams = {
url: string,
method?: string,
headers?: NameValue[],
postData?: Binary,
};
export type BrowserContextFetchOptions = {
method?: string,
headers?: NameValue[],
postData?: Binary,
};
export type BrowserContextFetchResult = {
response?: FetchResponse,
error?: string,
};
export type BrowserContextGrantPermissionsParams = { export type BrowserContextGrantPermissionsParams = {
permissions: string[], permissions: string[],
origin?: string, origin?: string,

View File

@ -220,6 +220,17 @@ InterceptedResponse:
value: string value: string
FetchResponse:
type: object
properties:
url: string
status: number
statusText: string
headers:
type: array
items: NameValue
body: binary
LaunchOptions: LaunchOptions:
type: mixin type: mixin
properties: properties:
@ -591,6 +602,18 @@ BrowserContext:
name: string name: string
needsHandle: boolean? needsHandle: boolean?
fetch:
parameters:
url: string
method: string?
headers:
type: array?
items: NameValue
postData: binary?
returns:
response: FetchResponse?
error: string?
grantPermissions: grantPermissions:
parameters: parameters:
permissions: permissions:

View File

@ -149,6 +149,13 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
value: tString, value: tString,
})), })),
}); });
scheme.FetchResponse = tObject({
url: tString,
status: tNumber,
statusText: tString,
headers: tArray(tType('NameValue')),
body: tBinary,
});
scheme.RootInitializeParams = tObject({ scheme.RootInitializeParams = tObject({
sdkLanguage: tString, sdkLanguage: tString,
}); });
@ -379,6 +386,12 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
name: tString, name: tString,
needsHandle: tOptional(tBoolean), needsHandle: tOptional(tBoolean),
}); });
scheme.BrowserContextFetchParams = tObject({
url: tString,
method: tOptional(tString),
headers: tOptional(tArray(tType('NameValue'))),
postData: tOptional(tBinary),
});
scheme.BrowserContextGrantPermissionsParams = tObject({ scheme.BrowserContextGrantPermissionsParams = tObject({
permissions: tArray(tString), permissions: tArray(tString),
origin: tOptional(tString), origin: tOptional(tString),

148
src/server/fetch.ts Normal file
View File

@ -0,0 +1,148 @@
/**
* Copyright (c) Microsoft Corporation.
*
* 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 { HttpsProxyAgent } from 'https-proxy-agent';
import nodeFetch from 'node-fetch';
import * as url from 'url';
import { BrowserContext } from './browserContext';
import * as types from './types';
export async function playwrightFetch(context: BrowserContext, params: types.FetchOptions): Promise<{fetchResponse?: types.FetchResponse, error?: string}> {
try {
const cookies = await context.cookies(params.url);
const valueArray = cookies.map(c => `${c.name}=${c.value}`);
const clientCookie = params.headers?.['cookie'];
if (clientCookie)
valueArray.unshift(clientCookie);
const cookieHeader = valueArray.join('; ');
if (cookieHeader) {
if (!params.headers)
params.headers = {};
params.headers['cookie'] = cookieHeader;
}
if (!params.method)
params.method = 'GET';
let agent;
if (context._options.proxy) {
// TODO: support bypass proxy
const proxyOpts = url.parse(context._options.proxy.server);
if (context._options.proxy.username)
proxyOpts.auth = `${context._options.proxy.username}:${context._options.proxy.password || ''}`;
agent = new HttpsProxyAgent(proxyOpts);
}
// TODO(https://github.com/microsoft/playwright/issues/8381): set user agent
const response = await nodeFetch(params.url, {
method: params.method,
headers: params.headers,
body: params.postData,
agent
});
const body = await response.buffer();
const setCookies = response.headers.raw()['set-cookie'];
if (setCookies) {
const url = new URL(response.url);
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
const defaultPath = '/' + url.pathname.split('/').slice(0, -1).join('/');
const cookies: types.SetNetworkCookieParam[] = [];
for (const header of setCookies) {
// Decode cookie value?
const cookie: types.SetNetworkCookieParam | null = parseCookie(header);
if (!cookie)
continue;
if (!cookie.domain)
cookie.domain = url.hostname;
if (!canSetCookie(cookie.domain!, url.hostname))
continue;
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4
if (!cookie.path || !cookie.path.startsWith('/'))
cookie.path = defaultPath;
cookies.push(cookie);
}
if (cookies.length)
await context.addCookies(cookies);
}
const headers: types.HeadersArray = [];
for (const [name, value] of response.headers.entries())
headers.push({ name, value });
return {
fetchResponse: {
url: response.url,
status: response.status,
statusText: response.statusText,
headers,
body
}
};
} catch (e) {
return { error: String(e) };
}
}
function canSetCookie(cookieDomain: string, hostname: string) {
// TODO: check public suffix list?
hostname = '.' + hostname;
if (!cookieDomain.startsWith('.'))
cookieDomain = '.' + cookieDomain;
return hostname.endsWith(cookieDomain);
}
function parseCookie(header: string) {
const pairs = header.split(';').filter(s => s.trim().length > 0).map(p => p.split('=').map(s => s.trim()));
if (!pairs.length)
return null;
const [name, value] = pairs[0];
const cookie: types.NetworkCookie = {
name,
value,
domain: '',
path: '',
expires: -1,
httpOnly: false,
secure: false,
sameSite: 'Lax' // None for non-chromium
};
for (let i = 1; i < pairs.length; i++) {
const [name, value] = pairs[i];
switch (name.toLowerCase()) {
case 'expires':
const expiresMs = (+new Date(value));
if (isFinite(expiresMs))
cookie.expires = expiresMs / 1000;
break;
case 'max-age':
const maxAgeSec = parseInt(value, 10);
if (isFinite(maxAgeSec))
cookie.expires = Date.now() / 1000 + maxAgeSec;
break;
case 'domain':
cookie.domain = value || '';
break;
case 'path':
cookie.path = value || '';
break;
case 'secure':
cookie.secure = true;
break;
case 'httponly':
cookie.httpOnly = true;
break;
}
}
return cookie;
}

View File

@ -211,14 +211,6 @@ export type NormalizedContinueOverrides = {
interceptResponse?: boolean, interceptResponse?: boolean,
}; };
export type NormalizedResponseContinueOverrides = {
status?: number,
statusText?: string,
headers?: HeadersArray,
body?: string,
isBase64?: boolean,
};
export type NetworkCookie = { export type NetworkCookie = {
name: string, name: string,
value: string, value: string,
@ -375,3 +367,21 @@ export type SetStorageState = {
cookies?: SetNetworkCookieParam[], cookies?: SetNetworkCookieParam[],
origins?: OriginStorage[] origins?: OriginStorage[]
}; };
export type FetchOptions = {
url: string,
method?: string,
headers?: { [name: string]: string },
postData?: Buffer,
};
export type FetchResponse = {
url: string,
status: number,
statusText: string,
headers: {
name: string,
value: string,
}[],
body: Buffer,
};

View File

@ -0,0 +1,153 @@
/**
* Copyright (c) Microsoft Corporation. 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 { contextTest as it, expect } from './config/browserTest';
it('should work', async ({context, server}) => {
// @ts-expect-error
const response = await context._fetch(server.PREFIX + '/simple.json');
expect(response.url()).toBe(server.PREFIX + '/simple.json');
expect(response.status()).toBe(200);
expect(response.statusText()).toBe('OK');
expect(response.ok()).toBeTruthy();
expect(response.url()).toBe(server.PREFIX + '/simple.json');
expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');
expect(await response.text()).toBe('{"foo": "bar"}\n');
});
it('should add session cookies to request', async ({context, server, isLinux}) => {
await context.addCookies([{
name: 'username',
value: 'John Doe',
domain: isLinux ? '.my.localhost' : 'localhost',
path: '/',
expires: -1,
httpOnly: false,
secure: false,
sameSite: 'Lax',
}]);
const [req] = await Promise.all([
server.waitForRequest('/simple.json'),
// @ts-expect-error
context._fetch(`http://${isLinux ? 'www.my.localhost' : 'localhost'}:${server.PORT}/simple.json`),
]);
expect(req.headers.cookie).toEqual('username=John Doe');
});
it('should follow redirects', async ({context, server, isLinux}) => {
server.setRedirect('/redirect1', '/redirect2');
server.setRedirect('/redirect2', '/simple.json');
await context.addCookies([{
name: 'username',
value: 'John Doe',
domain: isLinux ? '.my.localhost' : 'localhost',
path: '/',
expires: -1,
httpOnly: false,
secure: false,
sameSite: 'Lax',
}]);
const [req, response] = await Promise.all([
server.waitForRequest('/simple.json'),
// @ts-expect-error
context._fetch(`http://${isLinux ? 'www.my.localhost' : 'localhost'}:${server.PORT}/redirect1`),
]);
expect(req.headers.cookie).toEqual('username=John Doe');
expect(response.url()).toBe(`http://${isLinux ? 'www.my.localhost' : 'localhost'}:${server.PORT}/simple.json`);
expect(await response.json()).toEqual({foo: 'bar'});
});
it('should add cookies from Set-Cookie header', async ({context, page, server}) => {
server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', ['session=value', 'foo=bar; max-age=3600']);
res.end();
});
// @ts-expect-error
await context._fetch(server.PREFIX + '/setcookie.html');
const cookies = await context.cookies();
expect(new Set(cookies.map(c => ({ name: c.name, value: c.value })))).toEqual(new Set([
{
name: 'session',
value: 'value'
},
{
name: 'foo',
value: 'bar'
},
]));
await page.goto(server.EMPTY_PAGE);
expect((await page.evaluate(() => document.cookie)).split(';').map(s => s.trim()).sort()).toEqual(['foo=bar', 'session=value']);
});
it('should work with context level proxy', async ({browserOptions, browserType, contextOptions, server, proxyServer}) => {
server.setRoute('/target.html', async (req, res) => {
res.end('<title>Served by the proxy</title>');
});
const browser = await browserType.launch({
...browserOptions,
proxy: { server: 'http://per-context' }
});
try {
proxyServer.forwardTo(server.PORT);
const context = await browser.newContext({
...contextOptions,
proxy: { server: `localhost:${proxyServer.PORT}` }
});
const [request, response] = await Promise.all([
server.waitForRequest('/target.html'),
// @ts-expect-error
context._fetch(`http://non-existent.com/target.html`)
]);
expect(response.status()).toBe(200);
expect(request.url).toBe('/target.html');
} finally {
await browser.close();
}
});
it('should work with http credentials', async ({context, server}) => {
server.setAuth('/empty.html', 'user', 'pass');
const [request, response] = await Promise.all([
server.waitForRequest('/empty.html'),
// @ts-expect-error
context._fetch(server.EMPTY_PAGE, {
headers: {
'authorization': 'Basic ' + Buffer.from('user:pass').toString('base64')
}
})
]);
expect(response.status()).toBe(200);
expect(request.url).toBe('/empty.html');
});
it('should support post data', async ({context, server}) => {
const [request, response] = await Promise.all([
server.waitForRequest('/simple.json'),
// @ts-expect-error
context._fetch(`${server.PREFIX}/simple.json`, {
method: 'POST',
postData: 'My request'
})
]);
expect(request.method).toBe('POST');
expect((await request.postBody).toString()).toBe('My request');
expect(response.status()).toBe(200);
expect(request.url).toBe('/simple.json');
});