diff --git a/MIGRATING.md b/MIGRATING.md index 5af1b007f..d95dced21 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -489,10 +489,8 @@ import { http, HttpResponse, bypass } from 'msw' export const handlers = [ http.get('/resource', async ({ request }) => { - const fetchArgs = await bypass(request) - // Use the regular "fetch" from your environment. - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch(bypass(request)) const json = await originalResponse.json() // ...handle the original response, maybe return a mocked one. diff --git a/src/core/bypass.test.ts b/src/core/bypass.test.ts index a4de4d595..aca432070 100644 --- a/src/core/bypass.test.ts +++ b/src/core/bypass.test.ts @@ -1,24 +1,26 @@ /** * @jest-environment jsdom */ -import { Headers } from 'headers-polyfill' import { bypass } from './bypass' it('returns bypassed request given a request url string', async () => { - const [url, init] = await bypass('/user') - const headers = new Headers(init.headers) + const request = bypass('https://api.example.com/resource') // Relative URLs are rebased against the current location. - expect(url).toBe('/user') - expect(headers.get('x-msw-intention')).toBe('bypass') + expect(request.method).toBe('GET') + expect(request.url).toBe('https://api.example.com/resource') + expect(Object.fromEntries(request.headers.entries())).toEqual({ + 'x-msw-intention': 'bypass', + }) }) it('returns bypassed request given a request url', async () => { - const [url, init] = await bypass(new URL('/user', 'https://api.github.com')) - const headers = new Headers(init.headers) + const request = bypass(new URL('/resource', 'https://api.example.com')) - expect(url).toBe('https://api.github.com/user') - expect(headers.get('x-msw-intention')).toBe('bypass') + expect(request.url).toBe('https://api.example.com/resource') + expect(Object.fromEntries(request.headers)).toEqual({ + 'x-msw-intention': 'bypass', + }) }) it('returns bypassed request given request instance', async () => { @@ -29,12 +31,17 @@ it('returns bypassed request given request instance', async () => { }, body: 'hello world', }) - const [url, init] = await bypass(original) - const headers = new Headers(init.headers) - - expect(url).toBe('http://localhost/resource') - expect(init.method).toBe('POST') - expect(init.body).toEqual(await original.arrayBuffer()) - expect(headers.get('x-msw-intention')).toBe('bypass') - expect(headers.get('x-my-header')).toBe('value') + const request = bypass(original) + + expect(request.method).toBe('POST') + expect(request.url).toBe('http://localhost/resource') + + const bypassedRequestBody = await request.text() + expect(original.bodyUsed).toBe(false) + + expect(bypassedRequestBody).toEqual(await original.text()) + expect(Object.fromEntries(request.headers.entries())).toEqual({ + ...Object.fromEntries(original.headers.entries()), + 'x-msw-intention': 'bypass', + }) }) diff --git a/src/core/bypass.ts b/src/core/bypass.ts index 3e37866d3..848d79d45 100644 --- a/src/core/bypass.ts +++ b/src/core/bypass.ts @@ -1,92 +1,34 @@ import { invariant } from 'outvariant' -import { Headers } from 'headers-polyfill' export type BypassRequestInput = string | URL | Request /** - * Derives request input and init from the given Request info - * to define a request that will always be ignored by MSW. + * Creates a `Request` instance that will always be bypassed by MSW. * * @example - * import fetch, { Request } from 'node-fetch' * import { bypass } from 'msw' * - * fetch(...bypass('/resource')) - * fetch(...bypass(new URL('/resource', 'https://example.com))) - * fetch(...bypass(new Request('https://example.com/resource'))) + * fetch(bypass('/resource')) + * fetch(bypass(new URL('/resource', 'https://example.com))) + * fetch(bypass(new Request('https://example.com/resource'))) */ -export async function bypass( - input: BypassRequestInput, - init?: RequestInit, -): Promise<[string, RequestInit]> { - if (isRequest(input)) { - invariant( - !input.bodyUsed, - 'Failed to create a bypassed request to "%s %s": given request instance already has its body read. Make sure to clone the intercepted request if you wish to read its body before bypassing it.', - input.method, - input.url, - ) - } +export function bypass(input: BypassRequestInput, init?: RequestInit): Request { + const request = input instanceof Request ? input : new Request(input, init) + + invariant( + !request.bodyUsed, + 'Failed to create a bypassed request to "%s %s": given request instance already has its body read. Make sure to clone the intercepted request if you wish to read its body before bypassing it.', + request.method, + request.url, + ) - const url = isRequest(input) ? input.url : input.toString() - const resolvedInit: RequestInit = - typeof init !== 'undefined' ? init : await getRequestInit(input) + const requestClone = request.clone() // Set the internal header that would instruct MSW // to bypass this request from any further request matching. // Unlike "passthrough()", bypass is meant for performing // additional requests within pending request resolution. - const headers = new Headers(resolvedInit.headers) - headers.set('x-msw-intention', 'bypass') - resolvedInit.headers = headers - - return [url, resolvedInit] -} - -function isRequest(input: BypassRequestInput): input is Request { - return ( - typeof input === 'object' && - input.constructor.name === 'Request' && - 'clone' in input && - typeof input.clone === 'function' - ) -} - -async function getRequestInit(input: BypassRequestInput): Promise { - if (!isRequest(input)) { - return {} - } - - const init: RequestInit = { - // Set each request init property explicitly - // to prevent leaking internal properties of whichever - // Request polyfill provided as the input. - mode: input.mode, - method: input.method, - cache: input.cache, - headers: input.headers, - credentials: input.credentials, - signal: input.signal, - referrerPolicy: input.referrerPolicy, - referrer: input.referrer, - redirect: input.redirect, - integrity: input.integrity, - keepalive: input.keepalive, - } - - // Include "RequestInit.body" only for appropriate requests. - if (init.method !== 'HEAD' && input.method !== 'GET') { - init.body = await input.clone().arrayBuffer() - - /** - * `RequestInit.duplex` is not present in TypeScript but is - * required if you wish to send `ReadableStream` as a request body. - * @see https://developer.chrome.com/articles/fetch-streaming-requests - * @see https://github.com/whatwg/fetch/pull/1457 - */ - // @ts-ignore - init.duplex = input.duplex - } + requestClone.headers.set('x-msw-intention', 'bypass') - return init + return requestClone } diff --git a/test/browser/graphql-api/response-patching.mocks.ts b/test/browser/graphql-api/response-patching.mocks.ts index 03473e939..5db10c3bc 100644 --- a/test/browser/graphql-api/response-patching.mocks.ts +++ b/test/browser/graphql-api/response-patching.mocks.ts @@ -11,8 +11,7 @@ interface GetUserQuery { const worker = setupWorker( graphql.query('GetUser', async ({ request }) => { - const fetchArgs = await bypass(request) - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch(bypass(request)) const originalJson = await originalResponse.json() return HttpResponse.json({ diff --git a/test/browser/rest-api/response-patching.mocks.ts b/test/browser/rest-api/response-patching.mocks.ts index 05643d431..1ca6bccfb 100644 --- a/test/browser/rest-api/response-patching.mocks.ts +++ b/test/browser/rest-api/response-patching.mocks.ts @@ -3,8 +3,7 @@ import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*/user', async ({ request }) => { - const fetchArgs = await bypass(request.url) - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch(bypass(request.url)) const body = await originalResponse.json() return HttpResponse.json( @@ -22,8 +21,7 @@ const worker = setupWorker( }), http.get('*/repos/:owner/:repoName', async ({ request }) => { - const fetchArgs = await bypass(request) - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() return HttpResponse.json( @@ -41,11 +39,12 @@ const worker = setupWorker( http.get('*/headers', async ({ request }) => { const proxyUrl = new URL('/headers-proxy', request.url) - const fetchArgs = await bypass(proxyUrl, { - method: 'POST', - headers: request.headers, - }) - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch( + bypass(proxyUrl, { + method: 'POST', + headers: request.headers, + }), + ) const body = await originalResponse.json() return HttpResponse.json(body, { @@ -56,8 +55,7 @@ const worker = setupWorker( }), http.post('*/posts', async ({ request }) => { - const fetchArgs = await bypass(request) - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() return HttpResponse.json( @@ -75,8 +73,7 @@ const worker = setupWorker( }), http.get('*/posts', async ({ request }) => { - const fetchArgs = await bypass(request) - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() return HttpResponse.json( @@ -93,8 +90,7 @@ const worker = setupWorker( }), http.head('*/posts', async ({ request }) => { - const fetchArgs = await bypass(request) - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch(bypass(request)) return HttpResponse.json( { diff --git a/test/node/graphql-api/response-patching.node.test.ts b/test/node/graphql-api/response-patching.node.test.ts index c2d836466..abf349fa7 100644 --- a/test/node/graphql-api/response-patching.node.test.ts +++ b/test/node/graphql-api/response-patching.node.test.ts @@ -9,8 +9,7 @@ import { createGraphQLClient, gql } from '../../support/graphql' const server = setupServer( graphql.query('GetUser', async ({ request }) => { - const requestInfo = await bypass(request) - const originalResponse = await fetch(...requestInfo) + const originalResponse = await fetch(bypass(request)) const { requestHeaders, queryResult } = await originalResponse.json() return HttpResponse.json({ diff --git a/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts b/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts index a2814b4ac..0c9891509 100644 --- a/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts @@ -21,8 +21,7 @@ interface ResponseBody { const server = setupServer( http.get('https://test.mswjs.io/user', async () => { - const fetchArgs = await bypass(httpServer.http.url('/user')) - const originalResponse = await fetch(...fetchArgs) + const originalResponse = await fetch(bypass(httpServer.http.url('/user'))) const body = await originalResponse.json() return HttpResponse.json({ @@ -34,13 +33,15 @@ const server = setupServer( const url = new URL(request.url) const shouldBypass = url.searchParams.get('bypass') === 'true' - const fetchArgs = await bypass( - new Request(httpServer.http.url('/user'), { - method: 'POST', - }), - ) const performRequest = shouldBypass - ? () => fetch(...fetchArgs).then((res) => res.json()) + ? () => + fetch( + bypass( + new Request(httpServer.http.url('/user'), { + method: 'POST', + }), + ), + ).then((res) => res.json()) : () => fetch('https://httpbin.org/post', { method: 'POST' }).then((res) => res.json(),