feat(route): add cors header in route.fulfill (#12943)
This commit is contained in:
parent
4ab4c0bda1
commit
5734c18ef8
|
|
@ -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.
|
[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
|
## method: Route.request
|
||||||
- returns: <[Request]>
|
- returns: <[Request]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
await this._raceWithPageClose(this._channel.abort({ errorCode }));
|
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 fetchResponseUid;
|
||||||
let { status: statusOption, headers: headersOption, body } = options;
|
let { status: statusOption, headers: headersOption, body } = options;
|
||||||
if (options.response) {
|
if (options.response) {
|
||||||
|
|
@ -282,6 +282,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
await this._raceWithPageClose(this._channel.fulfill({
|
await this._raceWithPageClose(this._channel.fulfill({
|
||||||
status: statusOption || 200,
|
status: statusOption || 200,
|
||||||
headers: headersObjectToArray(headers),
|
headers: headersObjectToArray(headers),
|
||||||
|
cors: options.cors,
|
||||||
body,
|
body,
|
||||||
isBase64,
|
isBase64,
|
||||||
fetchResponseUid
|
fetchResponseUid
|
||||||
|
|
|
||||||
|
|
@ -3133,6 +3133,7 @@ export type RouteContinueResult = void;
|
||||||
export type RouteFulfillParams = {
|
export type RouteFulfillParams = {
|
||||||
status?: number,
|
status?: number,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
|
cors?: 'allow' | 'none',
|
||||||
body?: string,
|
body?: string,
|
||||||
isBase64?: boolean,
|
isBase64?: boolean,
|
||||||
fetchResponseUid?: string,
|
fetchResponseUid?: string,
|
||||||
|
|
@ -3140,6 +3141,7 @@ export type RouteFulfillParams = {
|
||||||
export type RouteFulfillOptions = {
|
export type RouteFulfillOptions = {
|
||||||
status?: number,
|
status?: number,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
|
cors?: 'allow' | 'none',
|
||||||
body?: string,
|
body?: string,
|
||||||
isBase64?: boolean,
|
isBase64?: boolean,
|
||||||
fetchResponseUid?: string,
|
fetchResponseUid?: string,
|
||||||
|
|
|
||||||
|
|
@ -2449,6 +2449,11 @@ Route:
|
||||||
headers:
|
headers:
|
||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
items: NameValue
|
||||||
|
cors:
|
||||||
|
type: enum?
|
||||||
|
literals:
|
||||||
|
- allow
|
||||||
|
- none
|
||||||
body: string?
|
body: string?
|
||||||
isBase64: boolean?
|
isBase64: boolean?
|
||||||
fetchResponseUid: string?
|
fetchResponseUid: string?
|
||||||
|
|
|
||||||
|
|
@ -1167,6 +1167,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
scheme.RouteFulfillParams = tObject({
|
scheme.RouteFulfillParams = tObject({
|
||||||
status: tOptional(tNumber),
|
status: tOptional(tNumber),
|
||||||
headers: tOptional(tArray(tType('NameValue'))),
|
headers: tOptional(tArray(tType('NameValue'))),
|
||||||
|
cors: tOptional(tEnum(['allow', 'none'])),
|
||||||
body: tOptional(tString),
|
body: tOptional(tString),
|
||||||
isBase64: tOptional(tBoolean),
|
isBase64: tOptional(tBoolean),
|
||||||
fetchResponseUid: tOptional(tString),
|
fetchResponseUid: tOptional(tString),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import * as frames from './frames';
|
import * as frames from './frames';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
|
import * as channels from '../protocol/channels';
|
||||||
import { assert } from '../utils/utils';
|
import { assert } from '../utils/utils';
|
||||||
import { ManualPromise } from '../utils/async';
|
import { ManualPromise } from '../utils/async';
|
||||||
import { SdkObject } from './instrumentation';
|
import { SdkObject } from './instrumentation';
|
||||||
|
|
@ -248,7 +249,7 @@ export class Route extends SdkObject {
|
||||||
await this._delegate.abort(errorCode);
|
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();
|
this._startHandling();
|
||||||
let body = overrides.body;
|
let body = overrides.body;
|
||||||
let isBase64 = overrides.isBase64 || false;
|
let isBase64 = overrides.isBase64 || false;
|
||||||
|
|
@ -264,9 +265,22 @@ export class Route extends SdkObject {
|
||||||
isBase64 = false;
|
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({
|
await this._delegate.fulfill({
|
||||||
status: overrides.status || 200,
|
status: overrides.status || 200,
|
||||||
headers: overrides.headers || [],
|
headers,
|
||||||
body,
|
body,
|
||||||
isBase64,
|
isBase64,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
9
packages/playwright-core/types/types.d.ts
vendored
9
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -14722,6 +14722,15 @@ export interface Route {
|
||||||
*/
|
*/
|
||||||
contentType?: string;
|
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.
|
* Response headers. Header values will be converted to a string.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,64 @@ it('should get the same headers as the server CORS', async ({ page, server, brow
|
||||||
expect(headers).toEqual(serverRequest.headers);
|
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('should return postData', async ({ page, server, isAndroid }) => {
|
||||||
it.fixme(isAndroid, 'Post data does not work');
|
it.fixme(isAndroid, 'Post data does not work');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,7 @@ it('should support cors with GET', async ({ page, server, browserName }) => {
|
||||||
const headers = request.url().endsWith('allow') ? { 'access-control-allow-origin': '*' } : {};
|
const headers = request.url().endsWith('allow') ? { 'access-control-allow-origin': '*' } : {};
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
|
cors: 'none',
|
||||||
headers,
|
headers,
|
||||||
status: 200,
|
status: 200,
|
||||||
body: JSON.stringify(['electric', 'gas']),
|
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 }) => {
|
it('should support cors with POST', async ({ page, server }) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
await page.route('**/cars', async route => {
|
await page.route('**/cars', async route => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue