Skip to content

Commit

Permalink
fix: null-body responses enforced in createResponseListener instead o… (
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcosta7 authored Dec 1, 2023
1 parent b440f12 commit ed09722
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 35 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
"@bundled-es-modules/js-levenshtein": "^2.0.1",
"@bundled-es-modules/statuses": "^1.0.1",
"@mswjs/cookies": "^1.1.0",
"@mswjs/interceptors": "^0.25.11",
"@mswjs/interceptors": "^0.25.13",
"@open-draft/until": "^2.1.0",
"@types/cookie": "^0.4.1",
"@types/js-levenshtein": "^1.1.1",
Expand Down
20 changes: 15 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion src/browser/setupWorker/start/createResponseListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
SetupWorkerInternalContext,
} from '../glossary'
import { ServiceWorkerMessage } from './utils/createMessageChannel'
import { isResponseWithoutBody } from '@mswjs/interceptors'

export function createResponseListener(context: SetupWorkerInternalContext) {
return (
Expand All @@ -28,7 +29,18 @@ export function createResponseListener(context: SetupWorkerInternalContext) {
const response =
responseJson.status === 0
? Response.error()
: new Response(responseJson.body, responseJson)
: new Response(
/**
* Responses may be streams here, but when we create a response object
* with null-body status codes, like 204, 205, 304 Response will
* throw when passed a non-null body, so ensure it's null here
* for those codes
*/
isResponseWithoutBody(responseJson.status)
? null
: responseJson.body,
responseJson,
)

context.emitter.emit(
responseJson.isMockedResponse ? 'response:mocked' : 'response:bypass',
Expand Down
9 changes: 2 additions & 7 deletions src/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,6 @@ async function handleRequest(event, requestId) {
if (client && activeClientIds.has(client.id)) {
;(async function () {
const responseClone = response.clone()
// When performing original requests, response body will
// always be a ReadableStream, even for 204 responses.
// But when creating a new Response instance on the client,
// the body for a 204 response must be null.
const responseBody = response.status === 204 ? null : responseClone.body

sendToClient(
client,
Expand All @@ -137,11 +132,11 @@ async function handleRequest(event, requestId) {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseBody,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
},
[responseBody],
[responseClone.body],
)
})()
}
Expand Down
4 changes: 2 additions & 2 deletions test/browser/msw-api/regression/null-body.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { http, HttpResponse } from 'msw'
import { setupWorker } from 'msw/browser'

const worker = setupWorker(
http.get('/api/books', () => {
return new HttpResponse(null, { status: 204 })
http.get<{ code: string }>('/api/:code', ({ params }) => {
return new HttpResponse(null, { status: parseInt(params.code) })
}),
)

Expand Down
33 changes: 16 additions & 17 deletions test/browser/msw-api/regression/null-body.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { sleep } from '../../../support/utils'
import { test, expect } from '../../playwright.extend'
import { expect, test } from '../../playwright.extend'

test('gracefully handles a 204 response null body during life-cycle events', async ({
loadExample,
fetch,
page,
}) => {
await loadExample(require.resolve('./null-body.mocks.ts'))
for (const code of [204, 205, 304]) {
test(`gracefully handles a ${code} response null body during life-cycle events`, async ({
loadExample,
fetch,
page,
}) => {
await loadExample(require.resolve('./null-body.mocks.ts'))

const errors: Array<Error> = []
page.on('pageerror', (pageError) => {
errors.push(pageError)
})

await fetch('/api/books')
await sleep(500)
const errors: Array<Error> = []
page.on('pageerror', (pageError) => {
errors.push(pageError)
})

expect(errors).toEqual([])
})
await fetch(`/api/${code}`)
expect(errors).toEqual([])
})
}
74 changes: 72 additions & 2 deletions test/browser/msw-api/req/passthrough.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpResponse, passthrough, http } from 'msw'
import { HttpResponse, http, passthrough } from 'msw'
import { SetupWorkerApi } from 'msw/browser'
import { test, expect } from '../../playwright.extend'
import { expect, test } from '../../playwright.extend'

const PASSTHROUGH_EXAMPLE = require.resolve('./passthrough.mocks.ts')

Expand Down Expand Up @@ -128,3 +128,73 @@ test('performs a request as-is if nothing was returned from the resolver', async
name: 'John',
})
})

for (const code of [204, 205, 304]) {
test(`performs a ${code} request as-is if passthrough was returned from the resolver`, async ({
createServer,
loadExample,
fetch,
page,
}) => {
const server = await createServer((app) => {
app.post('/user', (_, res) => {
res.status(code).send()
})
})

await loadExample(PASSTHROUGH_EXAMPLE)
const endpointUrl = server.http.url('/user')

const errors: Array<Error> = []
page.on('pageerror', (pageError) => {
errors.push(pageError)
})

await page.evaluate((endpointUrl) => {
const { worker, http, passthrough } = window.msw
worker.use(
http.post<never, ResponseBody>(endpointUrl, () => {
return passthrough()
}),
)
}, endpointUrl)

const res = await fetch(endpointUrl, { method: 'POST' })
expect(res.status()).toBe(code)
expect(errors).toEqual([])
})

test(`performs a ${code} request as-is if nothing was returned from the resolver`, async ({
createServer,
loadExample,
fetch,
page,
}) => {
const server = await createServer((app) => {
app.post('/user', (_, res) => {
res.status(code).send()
})
})

await loadExample(PASSTHROUGH_EXAMPLE)
const endpointUrl = server.http.url('/user')

const errors: Array<Error> = []
page.on('pageerror', (pageError) => {
errors.push(pageError)
})

await page.evaluate((endpointUrl) => {
const { worker, http } = window.msw
worker.use(
http.post<never, ResponseBody>(endpointUrl, () => {
return
}),
)
}, endpointUrl)

const res = await fetch(endpointUrl, { method: 'POST' })
expect(res.status()).toBe(code)
expect(errors).toEqual([])
})
}
31 changes: 31 additions & 0 deletions test/node/msw-api/req/passthrough.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const httpServer = new HttpServer((app) => {
app.post<never, ResponseBody>('/user', (req, res) => {
res.json({ name: 'John' })
})
app.post('/code/:code', (req, res) => {
res.status(parseInt(req.params.code)).send()
})
})

const server = setupServer()
Expand Down Expand Up @@ -88,3 +91,31 @@ it('performs a request as-is if nothing was returned from the resolver', async (
name: 'John',
})
})

for (const code of [204, 205, 304]) {
it(`performs a ${code} request as-is if nothing was returned from the resolver`, async () => {
const endpointUrl = httpServer.http.url(`/code/${code}`)
server.use(
http.post<ResponseBody>(endpointUrl, () => {
return
}),
)

const res = await fetch(endpointUrl, { method: 'POST' })

expect(res.status).toEqual(code)
})

it(`performs a ${code} request as-is if passthrough was returned from the resolver`, async () => {
const endpointUrl = httpServer.http.url(`/code/${code}`)
server.use(
http.post<ResponseBody>(endpointUrl, () => {
return passthrough()
}),
)

const res = await fetch(endpointUrl, { method: 'POST' })

expect(res.status).toEqual(code)
})
}

0 comments on commit ed09722

Please sign in to comment.