ws.link("ws://*") handlers never fire in Chromium/Firefox since 2.12.9 (wildcard host percent-encoded by new URL)
Prerequisites
- I confirm my issue is not in the FAQ
- I confirm my issue is not one of the known issues
- I confirm my issue is not a question
- I have searched the existing issues
Environment check
- I'm using the latest
mswversion - I'm using TypeScript >= 4.7
Browsers
Chromium (Chrome), Firefox — WebKit is unaffected.
Reproduction repository
Minimal standalone repro (one HTML file, one module, no framework): attached below inline.
Reproduction steps
- Register a WebSocket handler with a wildcard host:
ws.link("ws://*"). await worker.start().- Open a WebSocket to any host, e.g.
new WebSocket("ws://localhost:5000/socket.io/?EIO=4&transport=websocket").
Current behavior
The connection listener never fires in Chromium or Firefox. It fires correctly in WebKit.
Interception itself is working in all three engines — globalThis.WebSocket is replaced (verified
cross-realm against a blank iframe's pristine constructor), and the connection is intercepted. Only
the handler match fails, so the connection falls through to passthrough and errors.
This is a regression. Bisected:
| msw | Chromium | WebKit |
|---|---|---|
| 2.6.0 / 2.10.0 / 2.12.0 / 2.12.7 | works | works |
| 2.12.8 | works | works |
| 2.12.9 | broken | works |
| 2.12.10 / 2.12.14 / 2.13.0 / 2.15.0 | broken | works |
2.12.9 contains a single change: ws: support relative urls in ws.link (#2661, 2f1d23c).
Cause
WebSocketHandler#parse now routes the handler URL through #resolveWebSocketUrl unless it is a
RegExp or starts with *:
const resolvedHandlerUrl =
this.url instanceof RegExp || this.url.startsWith('*')
? this.url
: this.#resolveWebSocketUrl(this.url, args.resolutionContext?.baseUrl)"ws://*" does not start with * — it starts with ws:// — so it is resolved through new URL().
Engines disagree on how a wildcard host is normalised:
new URL("ws://*").href
// Chromium: "ws://%2A/" ← wildcard percent-encoded
// Firefox: "ws://%2A/"
// WebKit: "ws://*/" ← wildcard preservedSo on Chromium/Firefox the handler's predicate becomes ws://%2A, which cannot match any real host.
WebKit keeps * and continues to match, which is why a WebKit-only test suite stays green.
The startsWith('*') guard protects patterns like */path, but not a wildcard that appears in the
host after a scheme.
Expected behavior
ws.link("ws://*") matches any host on every engine, as it did up to 2.12.8.
Suggested fix
Decode the wildcard back after resolution, which keeps the relative-URL support added in #2661:
// in #resolveWebSocketUrl, alongside the existing trailing-slash strip
return resolvedUrl.replace(/%2A/gi, '*').replace(/\/$/, '')Widening the guard to this.url.includes('*') would also fix the symptom, but would skip resolution
for relative patterns containing a wildcard (/socket.io/*), which #2661 intends to resolve against
the base URL.
I have not run this against the msw test suite — happy to open a PR if the approach looks right.
Repro source
main.js:
import { ws } from "msw"
import { setupWorker } from "msw/browser"
const link = ws.link("ws://*")
let fired = false
const handler = link.addEventListener("connection", () => {
fired = true
console.log("HANDLER FIRED")
})
const worker = setupWorker(handler)
await worker.start({ quiet: true })
// Cross-realm check that interception is in place at all.
const frame = document.createElement("iframe")
document.body.appendChild(frame)
console.log("WebSocket replaced:", globalThis.WebSocket !== frame.contentWindow.WebSocket)
frame.remove()
console.log('URL("ws://*").href =', new URL("ws://*").href)
new WebSocket("ws://localhost:5000/socket.io/?EIO=4&transport=websocket")
setTimeout(() => console.log(fired ? "handler fired" : "handler NEVER fired"), 4000)localhost:5000 is deliberately a host that does not exist, so any successful connection can only
have come from the mock.
Source: mswjs/msw