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.
|
||||
|
||||
### 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]>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2449,6 +2449,11 @@ Route:
|
|||
headers:
|
||||
type: array?
|
||||
items: NameValue
|
||||
cors:
|
||||
type: enum?
|
||||
literals:
|
||||
- allow
|
||||
- none
|
||||
body: string?
|
||||
isBase64: boolean?
|
||||
fetchResponseUid: string?
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue