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) => {
 | 
			
		||||
      const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
 | 
			
		||||
      const result = await channel.fetch({
 | 
			
		||||
@ -224,6 +224,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
 | 
			
		||||
        method: options.method,
 | 
			
		||||
        headers: options.headers ? headersObjectToArray(options.headers) : undefined,
 | 
			
		||||
        postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
 | 
			
		||||
        timeout: options.timeout,
 | 
			
		||||
      });
 | 
			
		||||
      if (result.error)
 | 
			
		||||
        throw new Error(`Request failed: ${result.error}`);
 | 
			
		||||
 | 
			
		||||
@ -113,6 +113,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
 | 
			
		||||
      method: params.method,
 | 
			
		||||
      headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
 | 
			
		||||
      postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
 | 
			
		||||
      timeout: params.timeout,
 | 
			
		||||
    });
 | 
			
		||||
    let response;
 | 
			
		||||
    if (fetchResponse) {
 | 
			
		||||
 | 
			
		||||
@ -858,11 +858,13 @@ export type BrowserContextFetchParams = {
 | 
			
		||||
  method?: string,
 | 
			
		||||
  headers?: NameValue[],
 | 
			
		||||
  postData?: Binary,
 | 
			
		||||
  timeout?: number,
 | 
			
		||||
};
 | 
			
		||||
export type BrowserContextFetchOptions = {
 | 
			
		||||
  method?: string,
 | 
			
		||||
  headers?: NameValue[],
 | 
			
		||||
  postData?: Binary,
 | 
			
		||||
  timeout?: number,
 | 
			
		||||
};
 | 
			
		||||
export type BrowserContextFetchResult = {
 | 
			
		||||
  response?: FetchResponse,
 | 
			
		||||
 | 
			
		||||
@ -621,6 +621,7 @@ BrowserContext:
 | 
			
		||||
          type: array?
 | 
			
		||||
          items: NameValue
 | 
			
		||||
        postData: binary?
 | 
			
		||||
        timeout: number?
 | 
			
		||||
      returns:
 | 
			
		||||
        response: FetchResponse?
 | 
			
		||||
        error: string?
 | 
			
		||||
 | 
			
		||||
@ -397,6 +397,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
 | 
			
		||||
    method: tOptional(tString),
 | 
			
		||||
    headers: tOptional(tArray(tType('NameValue'))),
 | 
			
		||||
    postData: tOptional(tBinary),
 | 
			
		||||
    timeout: tOptional(tNumber),
 | 
			
		||||
  });
 | 
			
		||||
  scheme.BrowserContextGrantPermissionsParams = tObject({
 | 
			
		||||
    permissions: tArray(tString),
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ import * as https from 'https';
 | 
			
		||||
import { BrowserContext } from './browserContext';
 | 
			
		||||
import * as types from './types';
 | 
			
		||||
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}> {
 | 
			
		||||
  try {
 | 
			
		||||
@ -50,11 +51,16 @@ export async function playwrightFetch(context: BrowserContext, params: types.Fet
 | 
			
		||||
      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), {
 | 
			
		||||
      method,
 | 
			
		||||
      headers,
 | 
			
		||||
      agent,
 | 
			
		||||
      maxRedirects: 20
 | 
			
		||||
      maxRedirects: 20,
 | 
			
		||||
      timeout,
 | 
			
		||||
      deadline
 | 
			
		||||
    }, params.postData);
 | 
			
		||||
    return { fetchResponse };
 | 
			
		||||
  } 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);
 | 
			
		||||
  return new Promise<types.FetchResponse>((fulfill, reject) => {
 | 
			
		||||
    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`];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const redirectOptions: http.RequestOptions & { maxRedirects: number } = {
 | 
			
		||||
        const redirectOptions: http.RequestOptions & { maxRedirects: number, deadline: number } = {
 | 
			
		||||
          method,
 | 
			
		||||
          headers,
 | 
			
		||||
          agent: options.agent,
 | 
			
		||||
          maxRedirects: options.maxRedirects - 1,
 | 
			
		||||
          timeout: options.timeout,
 | 
			
		||||
          deadline: options.deadline
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // 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);
 | 
			
		||||
    });
 | 
			
		||||
    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)
 | 
			
		||||
      request.write(postData);
 | 
			
		||||
    request.end();
 | 
			
		||||
 | 
			
		||||
@ -377,6 +377,7 @@ export type FetchOptions = {
 | 
			
		||||
  method?: string,
 | 
			
		||||
  headers?: { [name: string]: string },
 | 
			
		||||
  postData?: Buffer,
 | 
			
		||||
  timeout?: number,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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']);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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}) => {
 | 
			
		||||
  server.setRoute('/redirect1', (req, res) => {
 | 
			
		||||
    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`);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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