feat(route): add cors header in route.fulfill (#12943)

This commit is contained in:
Yury Semikhatsky 2022-03-25 14:56:57 -07:00 committed by GitHub
parent 4ab4c0bda1
commit 5734c18ef8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 195 additions and 3 deletions

View file

@ -226,6 +226,15 @@ is resolved relative to the current working directory.
[APIResponse] to fulfill route's request with. Individual fields of the response (such as headers) can be overridden using fulfill options.
### option: Route.fulfill.cors
- `cors` <[CorsMode]<"allow"|"none">>
Wheb set to "allow" or omitted, the fulfilled response will have
["Access-Control-Allow-Origin"](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin)
header set to request's origin. If the option is set to "none" then
[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) headers won't be added to the response.
Note that all CORS headers configured via `headers` option will take precedence.
## method: Route.request
- returns: <[Request]>

View file

@ -239,7 +239,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
await this._raceWithPageClose(this._channel.abort({ errorCode }));
}
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string } = {}) {
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, cors?: 'allow' | 'none', body?: string | Buffer, path?: string } = {}) {
let fetchResponseUid;
let { status: statusOption, headers: headersOption, body } = options;
if (options.response) {
@ -282,6 +282,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
await this._raceWithPageClose(this._channel.fulfill({
status: statusOption || 200,
headers: headersObjectToArray(headers),
cors: options.cors,
body,
isBase64,
fetchResponseUid

View file

@ -3133,6 +3133,7 @@ export type RouteContinueResult = void;
export type RouteFulfillParams = {
status?: number,
headers?: NameValue[],
cors?: 'allow' | 'none',
body?: string,
isBase64?: boolean,
fetchResponseUid?: string,
@ -3140,6 +3141,7 @@ export type RouteFulfillParams = {
export type RouteFulfillOptions = {
status?: number,
headers?: NameValue[],
cors?: 'allow' | 'none',
body?: string,
isBase64?: boolean,
fetchResponseUid?: string,

View file

@ -2449,6 +2449,11 @@ Route:
headers:
type: array?
items: NameValue
cors:
type: enum?
literals:
- allow
- none
body: string?
isBase64: boolean?
fetchResponseUid: string?

View file

@ -1167,6 +1167,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.RouteFulfillParams = tObject({
status: tOptional(tNumber),
headers: tOptional(tArray(tType('NameValue'))),
cors: tOptional(tEnum(['allow', 'none'])),
body: tOptional(tString),
isBase64: tOptional(tBoolean),
fetchResponseUid: tOptional(tString),

View file

@ -16,6 +16,7 @@
import * as frames from './frames';
import * as types from './types';
import * as channels from '../protocol/channels';
import { assert } from '../utils/utils';
import { ManualPromise } from '../utils/async';
import { SdkObject } from './instrumentation';
@ -248,7 +249,7 @@ export class Route extends SdkObject {
await this._delegate.abort(errorCode);
}
async fulfill(overrides: { status?: number, headers?: types.HeadersArray, body?: string, isBase64?: boolean, useInterceptedResponseBody?: boolean, fetchResponseUid?: string }) {
async fulfill(overrides: channels.RouteFulfillParams) {
this._startHandling();
let body = overrides.body;
let isBase64 = overrides.isBase64 || false;
@ -264,9 +265,22 @@ export class Route extends SdkObject {
isBase64 = false;
}
}
const headers = [...(overrides.headers || [])];
if (overrides.cors !== 'none') {
const corsHeader = headers.find(({ name }) => name === 'access-control-allow-origin');
// See https://github.com/microsoft/playwright/issues/12929
if (!corsHeader) {
const origin = this._request.headerValue('origin');
if (origin) {
headers.push({ name: 'access-control-allow-origin', value: origin });
headers.push({ name: 'access-control-allow-credentials', value: 'true' });
headers.push({ name: 'vary', value: 'Origin' });
}
}
}
await this._delegate.fulfill({
status: overrides.status || 200,
headers: overrides.headers || [],
headers,
body,
isBase64,
});

View file

@ -14722,6 +14722,15 @@ export interface Route {
*/
contentType?: string;
/**
* Wheb set to "allow" or omitted, the fulfilled response will have
* ["Access-Control-Allow-Origin"](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin)
* header set to request's origin. If the option is set to "none" then
* [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) headers won't be added to the response. Note that all
* CORS headers configured via `headers` option will take precedence.
*/
cors?: "allow"|"none";
/**
* Response headers. Header values will be converted to a string.
*/

View file

@ -114,6 +114,64 @@ it('should get the same headers as the server CORS', async ({ page, server, brow
expect(headers).toEqual(serverRequest.headers);
});
it('should not get preflight CORS requests when intercepting', async ({ page, server, browserName }) => {
await page.goto(server.PREFIX + '/empty.html');
const requests = [];
server.setRoute('/something', (request, response) => {
requests.push(request.method);
if (request.method === 'OPTIONS') {
response.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE',
'Access-Control-Allow-Headers': '*',
'Cache-Control': 'no-cache'
});
response.end();
return;
}
response.writeHead(200, { 'Access-Control-Allow-Origin': '*' });
response.end('done');
});
// First check the browser will send preflight request when interception is OFF.
{
const text = await page.evaluate(async url => {
const data = await fetch(url, {
method: 'DELETE',
headers: { 'X-PINGOTHER': 'pingpong' }
});
return data.text();
}, server.CROSS_PROCESS_PREFIX + '/something');
expect(text).toBe('done');
expect(requests).toEqual(['OPTIONS', 'DELETE']);
}
// Now check the browser will NOT send preflight request when interception is ON.
{
requests.length = 0;
const routed = [];
await page.route('**/something', route => {
routed.push(route.request().method());
route.continue();
});
const text = await page.evaluate(async url => {
const data = await fetch(url, {
method: 'DELETE',
headers: { 'X-PINGOTHER': 'pingpong' }
});
return data.text();
}, server.CROSS_PROCESS_PREFIX + '/something');
expect(text).toBe('done');
// Check that there was no preflight (OPTIONS) request.
expect(routed).toEqual(['DELETE']);
if (browserName === 'firefox')
expect(requests).toEqual(['OPTIONS', 'DELETE']);
else
expect(requests).toEqual(['DELETE']);
}
});
it('should return postData', async ({ page, server, isAndroid }) => {
it.fixme(isAndroid, 'Post data does not work');

View file

@ -519,6 +519,7 @@ it('should support cors with GET', async ({ page, server, browserName }) => {
const headers = request.url().endsWith('allow') ? { 'access-control-allow-origin': '*' } : {};
await route.fulfill({
contentType: 'application/json',
cors: 'none',
headers,
status: 200,
body: JSON.stringify(['electric', 'gas']),
@ -547,6 +548,98 @@ it('should support cors with GET', async ({ page, server, browserName }) => {
}
});
it('should add Access-Control-Allow-Origin by default when fulfill', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await page.route('**/cars', async route => {
await route.fulfill({
contentType: 'application/json',
status: 200,
body: JSON.stringify(['electric', 'gas']),
});
});
const [result, response] = await Promise.all([
page.evaluate(async () => {
const response = await fetch('https://example.com/cars', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
mode: 'cors',
body: JSON.stringify({ 'number': 1 })
});
return response.json();
}),
page.waitForResponse('https://example.com/cars')
]);
expect(result).toEqual(['electric', 'gas']);
expect(await response.headerValue('Access-Control-Allow-Origin')).toBe(server.PREFIX);
});
it('should respect cors false', async ({ page, server, browserName }) => {
server.setRoute('/something', (request, response) => {
if (request.method === 'OPTIONS') {
response.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE',
'Access-Control-Allow-Headers': '*',
'Cache-Control': 'no-cache'
});
response.end();
return;
}
response.writeHead(404, { 'Access-Control-Allow-Origin': '*' });
response.end('NOT FOUND');
});
// First check the browser will send preflight request when interception is OFF.
{
await page.route('**/something', async route => {
await route.fulfill({
contentType: 'text/plain',
status: 200,
body: 'done',
});
});
const [response, text] = await Promise.all([
page.waitForResponse(server.CROSS_PROCESS_PREFIX + '/something'),
page.evaluate(async url => {
const data = await fetch(url, {
method: 'GET',
headers: { 'X-PINGOTHER': 'pingpong' }
});
return data.text();
}, server.CROSS_PROCESS_PREFIX + '/something')
]);
expect(text).toBe('done');
expect(await response.headerValue('Access-Control-Allow-Origin')).toBe('null');
}
// Fetch request should when CORS headers are missing on the response.
{
await page.route('**/something', async route => {
await route.fulfill({
contentType: 'text/plain',
status: 200,
cors: 'none',
body: 'done',
});
});
const error = await page.evaluate(async url => {
const data = await fetch(url, {
method: 'GET',
headers: { 'X-PINGOTHER': 'pingpong' }
});
return data.text();
}, server.CROSS_PROCESS_PREFIX + '/something').catch(e => e);
if (browserName === 'chromium')
expect(error.message).toContain('Failed to fetch');
else if (browserName === 'webkit')
expect(error.message).toContain('Load failed');
else if (browserName === 'firefox')
expect(error.message).toContain('NetworkError when attempting to fetch resource.');
}
});
it('should support cors with POST', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await page.route('**/cars', async route => {