mirror of
https://github.com/hanzoai/msw.git
synced 2026-09-24 17:00:34 +00:00
feat: send request reference within the RESPONSE event (#2510)
This commit is contained in:
committed by
GitHub
parent
d805290e8b
commit
cbbb126876
@@ -266,6 +266,7 @@
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/node": "~18.19.28",
|
||||
"@types/serviceworker": "^0.0.136",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||
"@typescript-eslint/parser": "^8.8.1",
|
||||
"@web/dev-server": "^0.4.6",
|
||||
|
||||
Generated
+8
@@ -90,6 +90,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ~18.19.28
|
||||
version: 18.19.28
|
||||
'@types/serviceworker':
|
||||
specifier: ^0.0.136
|
||||
version: 0.0.136
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^8.8.1
|
||||
version: 8.16.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])
|
||||
@@ -1463,6 +1466,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-5lqryjGIegGNpkOcCfVg7j+v+aB128pEUpffB9mqUAX3sFq38mjtKTTRtAq2oLtt5Wg3Ex0G9EB52ha0qJfbsQ==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==}
|
||||
|
||||
@@ -6432,6 +6438,8 @@ snapshots:
|
||||
'@types/mime': 4.0.0
|
||||
'@types/node': 18.19.28
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
@@ -36,12 +36,13 @@ export interface ServiceWorkerIncomingRequest extends RequestWithoutMethods {
|
||||
body?: ArrayBuffer | null
|
||||
}
|
||||
|
||||
type ServiceWorkerIncomingResponse = Pick<
|
||||
Response,
|
||||
'type' | 'ok' | 'status' | 'statusText' | 'body' | 'headers' | 'redirected'
|
||||
> & {
|
||||
requestId: string
|
||||
type ServiceWorkerIncomingResponse = {
|
||||
isMockedResponse: boolean
|
||||
request: ServiceWorkerIncomingRequest
|
||||
response: Pick<
|
||||
Response,
|
||||
'type' | 'ok' | 'status' | 'statusText' | 'body' | 'headers' | 'redirected'
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,8 +88,7 @@ export interface SetupWorkerInternalContext {
|
||||
startOptions: RequiredDeep<StartOptions>
|
||||
worker: ServiceWorker | null
|
||||
registration: ServiceWorkerRegistration | null
|
||||
getRequestHandlers(): Array<RequestHandler | WebSocketHandler>
|
||||
requests: Map<string, Request>
|
||||
getRequestHandlers: () => Array<RequestHandler | WebSocketHandler>
|
||||
emitter: Emitter<LifeCycleEventsMap>
|
||||
keepAliveInterval?: number
|
||||
workerChannel: {
|
||||
|
||||
@@ -66,7 +66,6 @@ export class SetupWorkerApi
|
||||
return this.handlersController.currentHandlers()
|
||||
},
|
||||
registration: null,
|
||||
requests: new Map(),
|
||||
emitter: this.emitter,
|
||||
workerChannel: {
|
||||
on: (eventType, callback) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ServiceWorkerMessage,
|
||||
WorkerChannel,
|
||||
} from './utils/createMessageChannel'
|
||||
import { parseWorkerRequest } from '../../utils/parseWorkerRequest'
|
||||
import { deserializeRequest } from '../../utils/deserializeRequest'
|
||||
import { RequestHandler } from '~/core/handlers/RequestHandler'
|
||||
import { handleRequest } from '~/core/utils/handleRequest'
|
||||
import { RequiredDeep } from '~/core/typeUtils'
|
||||
@@ -29,16 +29,15 @@ export const createRequestListener = (
|
||||
const messageChannel = new WorkerChannel(event.ports[0])
|
||||
|
||||
const requestId = message.payload.id
|
||||
const request = parseWorkerRequest(message.payload)
|
||||
const request = deserializeRequest(message.payload)
|
||||
const requestCloneForLogs = request.clone()
|
||||
|
||||
// Make this the first requets clone before the
|
||||
// Make this the first request clone before the
|
||||
// request resolution pipeline even starts.
|
||||
// Store the clone in cache so the first matching
|
||||
// request handler would skip the cloning phase.
|
||||
const requestClone = request.clone()
|
||||
RequestHandler.cache.set(request, requestClone)
|
||||
context.requests.set(requestId, requestClone)
|
||||
|
||||
try {
|
||||
await handleRequest(
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
SetupWorkerInternalContext,
|
||||
} from '../glossary'
|
||||
import type { ServiceWorkerMessage } from './utils/createMessageChannel'
|
||||
import { deserializeRequest } from '../../utils/deserializeRequest'
|
||||
|
||||
export function createResponseListener(context: SetupWorkerInternalContext) {
|
||||
return (
|
||||
@@ -14,12 +15,7 @@ export function createResponseListener(context: SetupWorkerInternalContext) {
|
||||
>,
|
||||
) => {
|
||||
const { payload: responseJson } = message
|
||||
|
||||
// Get the Request instance reference stored in the
|
||||
// request listener.
|
||||
const { requestId } = responseJson
|
||||
const request = context.requests.get(requestId)!
|
||||
context.requests.delete(requestId)
|
||||
const request = deserializeRequest(responseJson.request)
|
||||
|
||||
/**
|
||||
* CORS requests with `mode: "no-cors"` result in "opaque" responses.
|
||||
@@ -28,12 +24,12 @@ export function createResponseListener(context: SetupWorkerInternalContext) {
|
||||
* @see https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
|
||||
* @see https://github.com/mswjs/msw/issues/529
|
||||
*/
|
||||
if (responseJson.type?.includes('opaque')) {
|
||||
if (responseJson.response.type?.includes('opaque')) {
|
||||
return
|
||||
}
|
||||
|
||||
const response =
|
||||
responseJson.status === 0
|
||||
responseJson.response.status === 0
|
||||
? Response.error()
|
||||
: new FetchResponse(
|
||||
/**
|
||||
@@ -42,8 +38,8 @@ export function createResponseListener(context: SetupWorkerInternalContext) {
|
||||
* throw when passed a non-null body, so ensure it's null here
|
||||
* for those codes
|
||||
*/
|
||||
FetchResponse.isResponseWithBody(responseJson.status)
|
||||
? responseJson.body
|
||||
FetchResponse.isResponseWithBody(responseJson.response.status)
|
||||
? responseJson.response.body
|
||||
: null,
|
||||
{
|
||||
...responseJson,
|
||||
@@ -59,9 +55,9 @@ export function createResponseListener(context: SetupWorkerInternalContext) {
|
||||
context.emitter.emit(
|
||||
responseJson.isMockedResponse ? 'response:mocked' : 'response:bypass',
|
||||
{
|
||||
response,
|
||||
requestId: responseJson.request.id,
|
||||
request,
|
||||
requestId: responseJson.requestId,
|
||||
response,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { ServiceWorkerIncomingRequest } from '../setupWorker/glossary'
|
||||
* Converts a given request received from the Service Worker
|
||||
* into a Fetch `Request` instance.
|
||||
*/
|
||||
export function parseWorkerRequest(
|
||||
incomingRequest: ServiceWorkerIncomingRequest,
|
||||
export function deserializeRequest(
|
||||
serializedRequest: ServiceWorkerIncomingRequest,
|
||||
): Request {
|
||||
return new Request(incomingRequest.url, {
|
||||
...incomingRequest,
|
||||
body: pruneGetRequestBody(incomingRequest),
|
||||
return new Request(serializedRequest.url, {
|
||||
...serializedRequest,
|
||||
body: pruneGetRequestBody(serializedRequest),
|
||||
})
|
||||
}
|
||||
+88
-51
@@ -5,7 +5,6 @@
|
||||
* Mock Service Worker.
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '<PACKAGE_VERSION>'
|
||||
@@ -13,16 +12,16 @@ const INTEGRITY_CHECKSUM = '<INTEGRITY_CHECKSUM>'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
self.addEventListener('install', function () {
|
||||
addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener('message', async function (event) {
|
||||
const clientId = event.source.id
|
||||
addEventListener('message', async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, 'id')
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
@@ -94,17 +93,18 @@ self.addEventListener('message', async function (event) {
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const { request } = event
|
||||
|
||||
addEventListener('fetch', function (event) {
|
||||
// Bypass navigation requests.
|
||||
if (request.mode === 'navigate') {
|
||||
if (event.request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||
if (
|
||||
event.request.cache === 'only-if-cached' &&
|
||||
event.request.mode !== 'same-origin'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,48 +115,62 @@ self.addEventListener('fetch', function (event) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID.
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId))
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {string} requestId
|
||||
*/
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const requestCloneForEvents = event.request.clone()
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
;(async function () {
|
||||
const responseClone = response.clone()
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
request: {
|
||||
id: requestId,
|
||||
...serializedRequest,
|
||||
},
|
||||
response: {
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
body: responseClone.body,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
body: responseClone.body,
|
||||
},
|
||||
},
|
||||
[responseClone.body],
|
||||
)
|
||||
})()
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Resolve the main client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
/**
|
||||
* Resolve the main client for the given event.
|
||||
* Client that issues a request doesn't necessarily equal the client
|
||||
* that registered the worker. It's with the latter the worker should
|
||||
* communicate with during the response resolving phase.
|
||||
* @param {FetchEvent} event
|
||||
* @returns {Promise<Client | undefined>}
|
||||
*/
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
@@ -184,12 +198,16 @@ async function resolveMainClient(event) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {Client | undefined} client
|
||||
* @param {string} requestId
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = request.clone()
|
||||
const requestClone = event.request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
@@ -230,29 +248,17 @@ async function getResponse(event, client, requestId) {
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const requestBuffer = await request.arrayBuffer()
|
||||
const serializedRequest = await serializeRequest(event.request)
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: requestBuffer,
|
||||
keepalive: request.keepalive,
|
||||
...serializedRequest,
|
||||
},
|
||||
},
|
||||
[requestBuffer],
|
||||
[serializedRequest.body],
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
@@ -268,6 +274,12 @@ async function getResponse(event, client, requestId) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {any} message
|
||||
* @param {Array<Transferable>} transferrables
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
@@ -280,14 +292,18 @@ function sendToClient(client, message, transferrables = []) {
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(
|
||||
message,
|
||||
[channel.port2].concat(transferrables.filter(Boolean)),
|
||||
)
|
||||
client.postMessage(message, [
|
||||
channel.port2,
|
||||
...transferrables.filter(Boolean),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
async function respondWithMock(response) {
|
||||
/**
|
||||
* @param {Response} response
|
||||
* @returns {Response}
|
||||
*/
|
||||
function respondWithMock(response) {
|
||||
// Setting response status code to 0 is a no-op.
|
||||
// However, when responding with a "Response.error()", the produced Response
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
@@ -305,3 +321,24 @@ async function respondWithMock(response) {
|
||||
|
||||
return mockedResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
*/
|
||||
async function serializeRequest(request) {
|
||||
return {
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.arrayBuffer(),
|
||||
keepalive: request.keepalive,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"include": ["./mockServiceWorker.js"],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"lib": ["esnext"],
|
||||
"types": ["@types/serviceworker"]
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,8 @@ test('replaces all handlers with the explicit next runtime handlers upon reset',
|
||||
}),
|
||||
)
|
||||
|
||||
// Once reset with explicit next requets handlers,
|
||||
// replaces all present requets handlers with those.
|
||||
// Once reset with explicit next request handlers,
|
||||
// replaces all present request handlers with those.
|
||||
server.resetHandlers(
|
||||
http.get('https://test.mswjs.io/products', () => {
|
||||
return HttpResponse.json([1, 2, 3])
|
||||
|
||||
@@ -107,8 +107,12 @@ test('does not error on request which handler implicitly returns no mocked respo
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('ignores common static assets when using the "error" strategy', async () => {
|
||||
await fetch('https://example.com/styles/main.css')
|
||||
test(
|
||||
'ignores common static assets when using the "error" strategy',
|
||||
{ timeout: 8000 },
|
||||
async () => {
|
||||
await fetch('https://example.com/styles/main.css')
|
||||
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
{
|
||||
"path": "./src/tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./src/tsconfig.worker.json"
|
||||
},
|
||||
|
||||
// Tests.
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user