Skip to content

Commit eb45e7a

Browse files
authored
fix: do not treat static asset requests as unhandled by default (#2440)
1 parent d0729ae commit eb45e7a

File tree

8 files changed

+156
-49
lines changed

8 files changed

+156
-49
lines changed

src/core/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export * from './HttpResponse'
6262
export * from './delay'
6363
export { bypass } from './bypass'
6464
export { passthrough } from './passthrough'
65+
export { isCommonAssetRequest } from './isCommonAssetRequest'
6566

6667
// Validate environmental globals before executing any code.
6768
// This ensures that the library gives user-friendly errors

src/core/isCommonAssetRequest.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Determines if the given request is a static asset request.
3+
* Useful when deciding which unhandled requests to ignore.
4+
* @note Despite being ignored, you can still intercept and mock
5+
* static assets by creating request handlers for them.
6+
*
7+
* @example
8+
* import { isCommonAssetRequest } from 'msw'
9+
*
10+
* await worker.start({
11+
* onUnhandledRequest(request, print) {
12+
* if (!isCommonAssetRequest(request)) {
13+
* print.warning()
14+
* }
15+
* }
16+
* })
17+
*/
18+
export function isCommonAssetRequest(request: Request): boolean {
19+
const url = new URL(request.url)
20+
21+
// Ignore certain protocols.
22+
if (url.protocol === 'file:') {
23+
return true
24+
}
25+
26+
// Ignore static assets hosts.
27+
if (/(fonts\.googleapis\.com)/.test(url.hostname)) {
28+
return true
29+
}
30+
31+
// Ignore node modules served over HTTP.
32+
if (/node_modules/.test(url.pathname)) {
33+
return true
34+
}
35+
36+
// Ignore internal Vite requests, like "/@vite/client".
37+
if (url.pathname.includes('@vite')) {
38+
return true
39+
}
40+
41+
// Ignore common static assets.
42+
return /\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(
43+
url.pathname,
44+
)
45+
}

src/core/utils/request/onUnhandledRequest.test.ts

+65-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
/**
2-
* @vitest-environment jsdom
3-
*/
1+
// @vitest-environment jsdom
42
import {
53
onUnhandledRequest,
64
UnhandledRequestCallback,
@@ -90,7 +88,7 @@ test('supports the "error" request strategy', async () => {
9088
})
9189

9290
test('supports a custom callback function', async () => {
93-
const callback = vi.fn<Parameters<UnhandledRequestCallback>>((request) => {
91+
const callback = vi.fn<UnhandledRequestCallback>((request) => {
9492
console.warn(`callback: ${request.method} ${request.url}`)
9593
})
9694
const request = new Request(new URL('/user', 'http://localhost:3000'))
@@ -109,12 +107,10 @@ test('supports a custom callback function', async () => {
109107
})
110108

111109
test('supports calling default strategies from the custom callback function', async () => {
112-
const callback = vi.fn<Parameters<UnhandledRequestCallback>>(
113-
(request, print) => {
114-
// Call the default "error" strategy.
115-
print.error()
116-
},
117-
)
110+
const callback = vi.fn<UnhandledRequestCallback>((request, print) => {
111+
// Call the default "error" strategy.
112+
print.error()
113+
})
118114
const request = new Request(new URL('http://localhost/api'))
119115
await expect(onUnhandledRequest(request, callback)).rejects.toThrow(
120116
`[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.`,
@@ -171,3 +167,62 @@ test('prints with an absolute URL and search params', async () => {
171167
fixtures.warningWithoutSuggestions(`https://mswjs.io/api?foo=boo`),
172168
)
173169
})
170+
171+
test('ignores common static assets when using the "warn" strategy', async () => {
172+
await Promise.allSettled([
173+
onUnhandledRequest(
174+
new Request(new URL('https://example.com/main.css')),
175+
'warn',
176+
),
177+
onUnhandledRequest(
178+
new Request(new URL('https://example.com/index.mjs')),
179+
'warn',
180+
),
181+
onUnhandledRequest(
182+
new Request(new URL('https://example.com/node_modules/abc-123')),
183+
'warn',
184+
),
185+
onUnhandledRequest(
186+
new Request(new URL('https://fonts.googleapis.com/some-font')),
187+
'warn',
188+
),
189+
])
190+
191+
expect(console.warn).not.toHaveBeenCalled()
192+
})
193+
194+
test('ignores common static assets when using the "error" strategy', async () => {
195+
await Promise.allSettled([
196+
onUnhandledRequest(
197+
new Request(new URL('https://example.com/main.css')),
198+
'error',
199+
),
200+
onUnhandledRequest(
201+
new Request(new URL('https://example.com/index.mjs')),
202+
'error',
203+
),
204+
onUnhandledRequest(
205+
new Request(new URL('https://example.com/node_modules/abc-123')),
206+
'error',
207+
),
208+
onUnhandledRequest(
209+
new Request(new URL('https://fonts.googleapis.com/some-font')),
210+
'error',
211+
),
212+
])
213+
214+
expect(console.error).not.toHaveBeenCalled()
215+
})
216+
217+
test('exposes common static assets to the explicit callback', async () => {
218+
let callbackRequest!: Request
219+
await onUnhandledRequest(
220+
new Request(new URL('https://example.com/main.css')),
221+
(request) => {
222+
callbackRequest = request
223+
},
224+
)
225+
226+
expect(callbackRequest).toBeInstanceOf(Request)
227+
expect(callbackRequest.url).toBe('https://example.com/main.css')
228+
})

src/core/utils/request/onUnhandledRequest.ts

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { toPublicUrl } from './toPublicUrl'
22
import { InternalError, devUtils } from '../internal/devUtils'
3+
import { isCommonAssetRequest } from '../../isCommonAssetRequest'
34

45
export interface UnhandledRequestPrint {
56
warning(): void
@@ -71,15 +72,10 @@ export async function onUnhandledRequest(
7172
return
7273
}
7374

74-
/**
75-
* @note Ignore "file://" requests.
76-
* Those often are an implementation detail of modern tooling
77-
* that fetches modules via HTTP. Developers don't issue those
78-
* requests and so they mustn't be warned about them.
79-
*/
80-
if (url.protocol === 'file:') {
81-
return
75+
// Ignore common static asset requests when using a built-in strategy.
76+
// There's a slight overhead here because this utility will create a request URL
77+
// instance again despite us having done so previously in this function.
78+
if (!isCommonAssetRequest(request)) {
79+
applyStrategy(strategy)
8280
}
83-
84-
applyStrategy(strategy)
8581
}

test/browser/msw-api/setup-worker/start/on-unhandled-request/error.mocks.ts

-14
This file was deleted.

test/browser/msw-api/setup-worker/start/on-unhandled-request/warn.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,19 @@ test('does not warn on request which handler implicitly returns no mocked respon
9191
]),
9292
)
9393
})
94+
95+
test('ignores common static assets when using the "warn" strategy', async ({
96+
loadExample,
97+
spyOnConsole,
98+
page,
99+
}) => {
100+
const consoleSpy = spyOnConsole()
101+
await loadExample(require.resolve('./warn.mocks.ts'))
102+
103+
// This request will error so perform it accordingly.
104+
await page.evaluate(() => {
105+
return fetch('https://example.com/styles/main.css').catch(() => null)
106+
})
107+
108+
expect(consoleSpy.get('warning')).toBeUndefined()
109+
})

test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ beforeEach(() => {
4545
})
4646

4747
afterEach(() => {
48-
vi.resetAllMocks()
48+
vi.clearAllMocks()
4949
})
5050

5151
afterAll(async () => {
@@ -54,7 +54,7 @@ afterAll(async () => {
5454
await httpServer.close()
5555
})
5656

57-
test('errors on unhandled request when using the "error" value', async () => {
57+
test('errors on unhandled request when using the "error" strategy', async () => {
5858
const endpointUrl = httpServer.http.url('/')
5959
const makeRequest = () => {
6060
return fetch(endpointUrl)
@@ -105,3 +105,9 @@ test('does not error on request which handler implicitly returns no mocked respo
105105

106106
expect(console.error).not.toHaveBeenCalled()
107107
})
108+
109+
test('ignores common static assets when using the "error" strategy', async () => {
110+
await fetch('https://example.com/styles/main.css')
111+
112+
expect(console.error).not.toHaveBeenCalled()
113+
})
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,36 @@
1-
/**
2-
* @vitest-environment node
3-
*/
1+
// @vitest-environment node
42
import { setupServer } from 'msw/node'
5-
import { HttpResponse, http } from 'msw'
63

7-
const server = setupServer(
8-
http.get('https://test.mswjs.io/user', () => {
9-
return HttpResponse.json({ firstName: 'John' })
10-
}),
11-
)
4+
const server = setupServer()
125

136
beforeAll(() => {
147
server.listen({ onUnhandledRequest: 'warn' })
158
vi.spyOn(global.console, 'warn').mockImplementation(() => void 0)
169
})
1710

11+
afterEach(() => {
12+
vi.clearAllMocks()
13+
})
14+
1815
afterAll(() => {
1916
server.close()
2017
vi.restoreAllMocks()
2118
})
2219

23-
test('warns on unhandled request when using the "warn" value', async () => {
24-
const res = await fetch('https://test.mswjs.io')
20+
test('warns on unhandled request when using the "warn" strategy', async () => {
21+
await fetch('https://test.mswjs.io/user')
2522

26-
expect(res).toHaveProperty('status', 404)
2723
expect(console.warn).toBeCalledWith(`\
2824
[MSW] Warning: intercepted a request without a matching request handler:
2925
30-
• GET https://test.mswjs.io/
26+
• GET https://test.mswjs.io/user
3127
3228
If you still wish to intercept this unhandled request, please create a request handler for it.
3329
Read more: https://mswjs.io/docs/getting-started/mocks`)
3430
})
31+
32+
test('ignores common static assets when using the "warn" strategy', async () => {
33+
await fetch('https://example.com/styles/main.css')
34+
35+
expect(console.warn).not.toHaveBeenCalled()
36+
})

0 commit comments

Comments
 (0)