Skip to content

Commit

Permalink
fix(bypass): make bypass synchronous (#1745)
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Sep 20, 2023
1 parent 8f2b94d commit cbb25fb
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 119 deletions.
4 changes: 1 addition & 3 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
40 changes: 24 additions & 16 deletions src/core/bypass.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
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 () => {
Expand All @@ -28,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',
})
})
89 changes: 16 additions & 73 deletions src/core/bypass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,32 @@ import { invariant } from 'outvariant'
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<RequestInit> {
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
}
3 changes: 1 addition & 2 deletions test/browser/graphql-api/response-patching.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ interface GetUserQuery {

const worker = setupWorker(
graphql.query<GetUserQuery>('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({
Expand Down
26 changes: 11 additions & 15 deletions test/browser/rest-api/response-patching.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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, {
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
{
Expand Down
3 changes: 1 addition & 2 deletions test/node/graphql-api/response-patching.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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(),
Expand Down

0 comments on commit cbb25fb

Please sign in to comment.