Skip to content

@toapi/worker: offline stale-fallback is unreachable — serveFromNetwork is not awaited #423

Description

@mismosmi

Summary

@toapi/worker@1.1.0 documents, as one of its three headline benefits:

Offline resilience — if the network is unavailable, an expired-but-present cache entry is served rather than failing.

That fallback cannot run. In packages/2-toapi-worker/src/handle-toapi-request.ts the expired-entry branch returns the promise instead of awaiting it, so the catch is dead code:

} else {
  // cached response is expired
  try {
    // try to serve from network
    return serveFromNetwork(req);   // <- not awaited
  } catch (error) {
    // probably network not available, serve old response
    errorLog(error);
    return cachedResponse;          // <- unreachable
  }
}

serveFromNetwork starts with await fetch(req), which rejects when there is no network. Because the promise is returned rather than awaited, that rejection escapes handleToapiRequest, reaches event.respondWith(), and the browser reports a network error — instead of serving the cached copy that is sitting right there.

return await serveFromNetwork(req) fixes it.

Why this is the common path, not an edge case

listenForInvalidations calls expireAll() every time the stream opens, which stamps expiresAt = Date.now() on every entry, including ones cached without a TTL (their expiresAt was null). That is documented and intended — "when the service worker connects to the invalidation stream, it automatically marks every cached entry as expired so the next access revalidates it".

The consequence is that after any service-worker restart, every cached entry takes the broken branch. So the practical behaviour is:

  • entries re-fetched since the worker last started → served from cache offline (they take the expiresAt === null path, which returns cachedResponse directly);
  • everything else → hard network error offline.

Since a service worker is killed after ~30s idle and restarted on the next event, "everything else" is most of the cache most of the time.

Reproduction

  1. Set up a worker with setupToapiWorker() and load a page that reads a tagged /api route.
  2. Confirm the response is in the tapi-cache Cache Storage bucket.
  3. Let the service worker be terminated (DevTools → Application → Service Workers → Stop, or just idle), so the next start re-opens the invalidation stream and runs expireAll().
  4. Go offline and reload.

Expected: the cached response is served (per the docs above).
Actual: the request fails with a network error; event.respondWith() received a rejected promise.

Version

@toapi/worker@1.1.0 — verified both in the published dist/handle-toapi-request.js and in the TypeScript source at main.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions