Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/vite-dev-server/src/node/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,10 @@ export function createServer(
claimOnActivate: true,
debug: createDebugger('vrowzer:svc-worker-server')!,
})
const honoFetchHandler = handle(middlewares)
// NOTE(kazupon): Without options, Hono's Service Worker adapter fetches a 404 response again
// from the network, so requests for missing preview files reached the host server. Requests
// within the base path belong to the virtual project, so answer them with the 404, as Vite does.
const honoFetchHandler = handle(middlewares, {})
const fetchHandler = (event: FetchEvent) => {
// Leave requests outside the virtual project unanswered so the browser
// performs the fetch with the controlled client's native semantics.
Expand Down
2 changes: 1 addition & 1 deletion packages/vrowzer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const vrowzer = Vrowzer()

The runtime `serviceWorkerScope` remains available for compatibility and for builds without the plugin. If both values are provided, they must match or `Vrowzer()` throws before registration. Without either value, the scope defaults to `/`. The scope does not set the preview URL; that is the role of `basePath`.

The scope selects which pages the Service Worker controls, not which request URLs it receives from those pages. Vrowzer only responds to same-origin HTTP(S) requests within `basePath`. Cross-origin requests and same-origin requests outside `basePath` are left to the browser's native network path.
The scope selects which pages the Service Worker controls, not which request URLs it receives from those pages. Vrowzer only responds to same-origin HTTP(S) requests within `basePath`. Cross-origin requests and same-origin requests outside `basePath` are left to the browser's native network path. Requests within `basePath` that the virtual project cannot serve get a 404 response from the Service Worker; they are not sent to the host server.

`serviceWorkerVersion` identifies the Service Worker version expected by the controller and reported by the worker. When `@vrowzer/vite-plugin` is used, configure the version on the plugin and omit the runtime option:

Expand Down
64 changes: 64 additions & 0 deletions packages/vrowzer/integration/vrowzer.integration-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ let crossOriginAssetServerUrl: string
let pageConsoleLogs: string[] = []
let delayedServiceWorkerRequests = 0
let hmrClientProbeId = 0
// Requests within the preview base path that reached the host server
const hostPreviewRequests: string[] = []

function delayServiceWorkerResponse(delay: number): Plugin {
return {
Expand Down Expand Up @@ -78,6 +80,26 @@ function serveHostFetchProbe(): Plugin {
}
}

/**
* Records the requests within the preview base path that reach the host server. It runs before
* the guard of `@vrowzer/vite-plugin`, which answers such requests.
*/
function recordHostPreviewRequests(): Plugin {
return {
name: 'vrowzer:test-record-host-preview-requests',
enforce: 'pre',
configurePreviewServer(server) {
server.middlewares.use((request, _response, next) => {
const pathname = new URL(request.url ?? '/', 'http://localhost').pathname
if (pathname.startsWith('/__preview__/')) {
hostPreviewRequests.push(`${request.method} ${pathname}`)
}
next()
})
}
}
}

async function closeHttpServer(server: HttpServer | undefined): Promise<void> {
if (!server?.listening) {
return
Expand Down Expand Up @@ -398,6 +420,7 @@ beforeAll(async () => {
server = await preview({
root: PLAYGROUND_DIR,
plugins: [
recordHostPreviewRequests(),
serveHostFetchProbe(),
...(SERVICE_WORKER_RESPONSE_DELAY > 0
? [delayServiceWorkerResponse(SERVICE_WORKER_RESPONSE_DELAY)]
Expand Down Expand Up @@ -1252,6 +1275,21 @@ if (import.meta.hot) {
expect(report.message).toContain('500')
})

test('reports a missing preview HTML once with 404', async () => {
await prepareFixtures()
// The SPA fallback answers other missing paths with index.html, and removing index.html would
// break the other sessions. The fallback skips /favicon.ico, so its request ends in a 404.
await mountSession('html-missing', '/__preview__/favicon.ico')

const report = await waitForLoadError('html-missing')

expect(report).toMatchObject({ id: 'html-missing', stage: 'html', status: 404 })
// A page that reloads the preview, such as the host's "Waiting for Service Worker..." page,
// would report the error again every second
await new Promise(resolve => setTimeout(resolve, 2500))
expect(await readLoadErrors('html-missing')).toHaveLength(1)
})

test('reports a rejected fetch of the preview HTML', async () => {
await prepareFixtures()
await mountSession('html-fetch', UNREACHABLE_URL)
Expand Down Expand Up @@ -1442,6 +1480,32 @@ if (import.meta.hot) {
expect(response?.status()).toBe(200)
expect(response?.fromServiceWorker()).toBe(true)
})

test('answers missing files within basePath with 404 without asking the host', async () => {
const hostRequestCount = hostPreviewRequests.length

const responses = await page.evaluate(async () => {
const read = async (path: string, init?: RequestInit) => {
const response = await fetch(path, init)
return { status: response.status, body: await response.text() }
}
return {
narrowAccept: await read('/__preview__/missing-files/data.json', {
headers: { Accept: 'application/json' }
}),
post: await read('/__preview__/missing-files/data.json', { method: 'POST', body: '{}' }),
favicon: await read('/__preview__/favicon.ico')
}
})

// The SPA fallback does not answer these requests, so they end in the 404 handler
expect(responses).toEqual({
narrowAccept: { status: 404, body: '' },
post: { status: 404, body: '' },
favicon: { status: 404, body: '' }
})
expect(hostPreviewRequests.slice(hostRequestCount)).toEqual([])
})
})

describe('error responses', () => {
Expand Down
Loading