API in Service Worker
FetchEvent and request routing
Published
A FetchEvent is dispatched in the service worker for every navigation and subresource request made by a controlled client; calling event.respondWith() replaces the network response with any Response, and not calling it lets the request proceed to the network unchanged. The interface is available in Chrome 40, Edge 17, Firefox 44, and Safari 11.1 (BCD api.FetchEvent), with preloadResponse and handled added later and covered on their own entries.
Syntax
Section titled “Syntax”self.addEventListener('fetch', (event) => { event.respondWith(responsePromise); // optional: take over the response event.waitUntil(promise); // optional: keep the worker alive for side effects});respondWith() must be called synchronously inside the handler; after the handler returns, the browser has already decided whether to go to the network. waitUntil() can be called at any point while the event is still being handled, including from inside the respondWith() promise chain.
Members
Section titled “Members”The event exposes the request and the client identifiers; the two methods control the response and the worker’s lifetime.
| Member | Type | Description |
|---|---|---|
request |
Request |
The intercepted request. url, method, headers, mode, destination, and credentials are the properties used for routing. |
respondWith(r) |
void |
Takes Response or Promise<Response>. The promise’s settlement is the response the client receives. |
waitUntil(p) |
void |
Extends the event’s lifetime until p settles, so a cache write started after respondWith() is not cut off when the worker is terminated. |
clientId |
string |
Id of the client that made the request. Empty for a navigation, because the navigating client does not exist yet. |
resultingClientId |
string |
Id of the client a navigation will create. Empty for subresource requests. |
replacesClientId |
string |
Id of the client being replaced by a navigation (same-window navigations only). |
handled |
Promise<void> |
Resolves when the response has been handed to the client, or rejects if the event ended with a network error (BCD api.FetchEvent.handled). |
preloadResponse |
Promise<Response | undefined> |
The navigation-preload response, or undefined when preload did not run; see the NavigationPreloadManager entry. |
request.destination is the most reliable routing key: 'document' for navigations, 'script', 'style', 'image', 'font', and '' for fetch() and XMLHttpRequest calls. request.mode === 'navigate' is equivalent to destination === 'document' for top-level pages but also matches iframes, whose destination is 'iframe'.
Exceptions
Section titled “Exceptions”respondWith() throws synchronously; the response-side failures are delivered to the client as a network error, not as an exception in the worker.
InvalidStateError:respondWith()was called after the handler returned, or called a second time on the same event.- Network error to the client: the promise passed to
respondWith()rejects, or resolves to something that is not aResponse. Chromium logsThe FetchEvent for "<url>" resulted in a network error response: an object that was not a Response was passed to respondWith().in the worker console and the page seesTypeError: Failed to fetch(or an error page for navigations). - Network error to the client: the
Responsebody has already been used, for example because it was passed tocache.put()withoutclone(). TypeErrorfromnew Request(event.request, init)when the request is a navigation (mode: 'navigate') andinitis not empty; build a newRequestfromevent.request.urlinstead when a navigation request needs modified headers.
Examples
Section titled “Examples”The handlers below are written so that only the branch that matches calls respondWith(); every other request falls through to the network.
Routing by destination and URL prefix
Section titled “Routing by destination and URL prefix”Images get a cache-first helper, API calls bypass the cache, and everything else is left to the browser.
self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); if (url.origin !== self.location.origin) return; // third-party: do not intercept
if (event.request.destination === 'image') { event.respondWith(cacheFirst(event.request, 'images-v1')); } else if (url.pathname.startsWith('/api/')) { event.respondWith(fetch(event.request)); // explicit network-only }});
async function cacheFirst(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); if (cached) return cached; const response = await fetch(request); if (response.ok) cache.put(request, response.clone()); return response;}Returning early for other origins matters even for a network-only route: once respondWith() is called, the worker is responsible for the full response, including CORS behaviour and opaque responses.
Serving the app shell for navigations with waitUntil()
Section titled “Serving the app shell for navigations with waitUntil()”A single-page app answers every navigation from the cached shell and refreshes that shell in the background, keeping the worker alive for the refresh.
self.addEventListener('fetch', (event) => { if (event.request.mode !== 'navigate') return; event.respondWith( caches.match('/index.html').then((cached) => cached ?? fetch(event.request)) ); event.waitUntil( fetch('/index.html').then((fresh) => fresh.ok ? caches.open('shell-v1').then((c) => c.put('/index.html', fresh)) : undefined ).catch(() => {}) );});Without waitUntil() the browser may terminate the worker as soon as respondWith() settles, dropping the background refresh. The catch prevents an offline refresh from surfacing as an unhandled rejection.
Detecting service worker support and keeping fetches working without it
Section titled “Detecting service worker support and keeping fetches working without it”Pages served without a controlling worker (first visit, insecure origins, a hard reload) must still load. Register conditionally and do not make page code depend on interception.
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch((err) => { console.warn('Service worker registration failed; running without offline support', err); });}// Page code calls fetch('/api/items') the same way in both cases: the worker// is an optimisation layer, so the request works when no worker is in control.navigator.serviceWorker.controller is null until a worker controls the page, which is also the state after a hard reload (Shift+Reload) in Chrome and Firefox; code that reads it must handle null.
See also
Section titled “See also”- Service Workers specification: FetchEvent interface (w3c.github.io)
- FetchEvent: respondWith() method (developer.mozilla.org)
- FetchEvent (developer.mozilla.org)
- Caching strategies for service workers
- Cache API
- NavigationPreloadManager
- Offline fallback
Specifications
| Specification | Status |
|---|---|
| None. | |