mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(fetch): timeout option and default timeout (#8762)
This commit is contained in:
parent
4e8d26c622
commit
77b3b0965a
@ -216,7 +216,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _fetch(url: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}): Promise<network.FetchResponse> {
|
async _fetch(url: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer, timeout?: number } = {}): Promise<network.FetchResponse> {
|
||||||
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
||||||
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
||||||
const result = await channel.fetch({
|
const result = await channel.fetch({
|
||||||
@ -224,6 +224,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
|||||||
method: options.method,
|
method: options.method,
|
||||||
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
||||||
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
|
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
|
||||||
|
timeout: options.timeout,
|
||||||
});
|
});
|
||||||
if (result.error)
|
if (result.error)
|
||||||
throw new Error(`Request failed: ${result.error}`);
|
throw new Error(`Request failed: ${result.error}`);
|
||||||
|
|||||||
@ -113,6 +113,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||||||
method: params.method,
|
method: params.method,
|
||||||
headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
|
headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
|
||||||
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
|
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
|
||||||
|
timeout: params.timeout,
|
||||||
});
|
});
|
||||||
let response;
|
let response;
|
||||||
if (fetchResponse) {
|
if (fetchResponse) {
|
||||||
|
|||||||
@ -858,11 +858,13 @@ export type BrowserContextFetchParams = {
|
|||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
|
timeout?: number,
|
||||||
};
|
};
|
||||||
export type BrowserContextFetchOptions = {
|
export type BrowserContextFetchOptions = {
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
|
timeout?: number,
|
||||||
};
|
};
|
||||||
export type BrowserContextFetchResult = {
|
export type BrowserContextFetchResult = {
|
||||||
response?: FetchResponse,
|
response?: FetchResponse,
|
||||||
|
|||||||
@ -621,6 +621,7 @@ BrowserContext:
|
|||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
items: NameValue
|
||||||
postData: binary?
|
postData: binary?
|
||||||
|
timeout: number?
|
||||||
returns:
|
returns:
|
||||||
response: FetchResponse?
|
response: FetchResponse?
|
||||||
error: string?
|
error: string?
|
||||||
|
|||||||
@ -397,6 +397,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||||||
method: tOptional(tString),
|
method: tOptional(tString),
|
||||||
headers: tOptional(tArray(tType('NameValue'))),
|
headers: tOptional(tArray(tType('NameValue'))),
|
||||||
postData: tOptional(tBinary),
|
postData: tOptional(tBinary),
|
||||||
|
timeout: tOptional(tNumber),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextGrantPermissionsParams = tObject({
|
scheme.BrowserContextGrantPermissionsParams = tObject({
|
||||||
permissions: tArray(tString),
|
permissions: tArray(tString),
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import * as https from 'https';
|
|||||||
import { BrowserContext } from './browserContext';
|
import { BrowserContext } from './browserContext';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { pipeline, Readable, Transform } from 'stream';
|
import { pipeline, Readable, Transform } from 'stream';
|
||||||
|
import { monotonicTime } from '../utils/utils';
|
||||||
|
|
||||||
export async function playwrightFetch(context: BrowserContext, params: types.FetchOptions): Promise<{fetchResponse?: types.FetchResponse, error?: string}> {
|
export async function playwrightFetch(context: BrowserContext, params: types.FetchOptions): Promise<{fetchResponse?: types.FetchResponse, error?: string}> {
|
||||||
try {
|
try {
|
||||||
@ -50,11 +51,16 @@ export async function playwrightFetch(context: BrowserContext, params: types.Fet
|
|||||||
agent = new HttpsProxyAgent(proxyOpts);
|
agent = new HttpsProxyAgent(proxyOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeout = context._timeoutSettings.timeout(params);
|
||||||
|
const deadline = monotonicTime() + timeout;
|
||||||
|
|
||||||
const fetchResponse = await sendRequest(context, new URL(params.url, context._options.baseURL), {
|
const fetchResponse = await sendRequest(context, new URL(params.url, context._options.baseURL), {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
agent,
|
agent,
|
||||||
maxRedirects: 20
|
maxRedirects: 20,
|
||||||
|
timeout,
|
||||||
|
deadline
|
||||||
}, params.postData);
|
}, params.postData);
|
||||||
return { fetchResponse };
|
return { fetchResponse };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -95,7 +101,7 @@ async function updateRequestCookieHeader(context: BrowserContext, url: URL, opti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendRequest(context: BrowserContext, url: URL, options: http.RequestOptions & { maxRedirects: number }, postData?: Buffer): Promise<types.FetchResponse>{
|
async function sendRequest(context: BrowserContext, url: URL, options: http.RequestOptions & { maxRedirects: number, deadline: number }, postData?: Buffer): Promise<types.FetchResponse>{
|
||||||
await updateRequestCookieHeader(context, url, options);
|
await updateRequestCookieHeader(context, url, options);
|
||||||
return new Promise<types.FetchResponse>((fulfill, reject) => {
|
return new Promise<types.FetchResponse>((fulfill, reject) => {
|
||||||
const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest)
|
const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest)
|
||||||
@ -125,11 +131,13 @@ async function sendRequest(context: BrowserContext, url: URL, options: http.Requ
|
|||||||
delete headers[`content-type`];
|
delete headers[`content-type`];
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectOptions: http.RequestOptions & { maxRedirects: number } = {
|
const redirectOptions: http.RequestOptions & { maxRedirects: number, deadline: number } = {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
agent: options.agent,
|
agent: options.agent,
|
||||||
maxRedirects: options.maxRedirects - 1,
|
maxRedirects: options.maxRedirects - 1,
|
||||||
|
timeout: options.timeout,
|
||||||
|
deadline: options.deadline
|
||||||
};
|
};
|
||||||
|
|
||||||
// HTTP-redirect fetch step 4: If locationURL is null, then return response.
|
// HTTP-redirect fetch step 4: If locationURL is null, then return response.
|
||||||
@ -189,6 +197,16 @@ async function sendRequest(context: BrowserContext, url: URL, options: http.Requ
|
|||||||
body.on('error',reject);
|
body.on('error',reject);
|
||||||
});
|
});
|
||||||
request.on('error', reject);
|
request.on('error', reject);
|
||||||
|
const rejectOnTimeout = () => {
|
||||||
|
reject(new Error(`Request timed out after ${options.timeout}ms`));
|
||||||
|
request.abort();
|
||||||
|
};
|
||||||
|
const remaining = options.deadline - monotonicTime();
|
||||||
|
if (remaining <= 0) {
|
||||||
|
rejectOnTimeout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request.setTimeout(remaining, rejectOnTimeout);
|
||||||
if (postData)
|
if (postData)
|
||||||
request.write(postData);
|
request.write(postData);
|
||||||
request.end();
|
request.end();
|
||||||
|
|||||||
@ -377,6 +377,7 @@ export type FetchOptions = {
|
|||||||
method?: string,
|
method?: string,
|
||||||
headers?: { [name: string]: string },
|
headers?: { [name: string]: string },
|
||||||
postData?: Buffer,
|
postData?: Buffer,
|
||||||
|
timeout?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FetchResponse = {
|
export type FetchResponse = {
|
||||||
|
|||||||
@ -193,6 +193,16 @@ it('should add cookies from Set-Cookie header', async ({context, page, server})
|
|||||||
expect((await page.evaluate(() => document.cookie)).split(';').map(s => s.trim()).sort()).toEqual(['foo=bar', 'session=value']);
|
expect((await page.evaluate(() => document.cookie)).split(';').map(s => s.trim()).sort()).toEqual(['foo=bar', 'session=value']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not lose body while handling 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('text content');
|
||||||
|
});
|
||||||
|
// @ts-expect-error
|
||||||
|
const response = await context._fetch(server.PREFIX + '/setcookie.html');
|
||||||
|
expect(await response.text()).toBe('text content');
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle cookies on redirects', async ({context, server, browserName, isWindows}) => {
|
it('should handle cookies on redirects', async ({context, server, browserName, isWindows}) => {
|
||||||
server.setRoute('/redirect1', (req, res) => {
|
server.setRoute('/redirect1', (req, res) => {
|
||||||
res.setHeader('Set-Cookie', 'r1=v1;SameSite=Lax');
|
res.setHeader('Set-Cookie', 'r1=v1;SameSite=Lax');
|
||||||
@ -576,3 +586,30 @@ it('should throw informatibe error on corrupted deflate body', async function({c
|
|||||||
expect(error.message).toContain(`failed to decompress 'deflate' encoding`);
|
expect(error.message).toContain(`failed to decompress 'deflate' encoding`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support timeout option', async function({context, server}) {
|
||||||
|
server.setRoute('/slow', (req, res) => {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'content-length': 4096,
|
||||||
|
'content-type': 'text/html',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
const error = await context._fetch(server.PREFIX + '/slow', { timeout: 10 }).catch(e => e);
|
||||||
|
expect(error.message).toContain(`Request timed out after 10ms`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect timeout after redirects', async function({context, server}) {
|
||||||
|
server.setRedirect('/redirect', '/slow');
|
||||||
|
server.setRoute('/slow', (req, res) => {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'content-length': 4096,
|
||||||
|
'content-type': 'text/html',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context.setDefaultTimeout(100);
|
||||||
|
// @ts-expect-error
|
||||||
|
const error = await context._fetch(server.PREFIX + '/redirect').catch(e => e);
|
||||||
|
expect(error.message).toContain(`Request timed out after 100ms`);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user