Skip to content

Service worker swallows server-issued redirects, so ?farm= never reaches the login form #778

Description

@mforce

What happens

Login.tsx prefills the farm code from ?farm=<slug> (#535). A visitor who arrives at / and is redirected by the server to /login?farm=<slug> never gets that prefill, because the service worker answers the navigation before it reaches the network.

sw.js registers:

registerRoute(new NavigationRoute(
  createHandlerBoundToURL("/index.html"),
  { denylist: [/^\/api(?:[/?]|$)/i, /^\/health(?:[/?]|$)/i] }
))

Only /api and /health are denylisted, so every other navigation, including /, is served from the precached index.html. The request never leaves the browser, the server's 302 is never seen, and the SPA renders at / with no query string to read.

Reproduction

  1. In a normal window that has the app's service worker installed, navigate to the site root.
  2. Observe: 200, served from the service worker, URL stays at /, farm code field empty.
  3. Repeat in a private window (no service worker): the redirect is followed, the URL becomes /login?farm=<slug>, and the field is prefilled.

A hard reload does not change the outcome. Confirmed against 0.1.0.

Why it matters

Visitor Result
First visit ever, or private window redirect followed → prefilled from ?farm=
Returning, has signed in since the farm-code change prefilled from the remembered-code cache
Returning, service worker from an earlier build, never signed in with a farm code empty field

The third row is the gap. It self-heals after one successful sign-in, because rememberFarmCode then populates cluckwork.farmCodes and the cache path takes over. But it is exactly the "I don't know my farm code" case the prefill exists to solve, and for an upgraded install it is the first thing the user meets.

Worth noting theme-init.js reads the same parameter for per-farm branding (cluckwork.brand:<slug>, #586), so it is affected identically — an upgraded install gets the default palette where a fresh one gets the farm's.

Possible directions

No strong preference, and the tradeoffs are yours:

  1. Let root navigations reach the network. Add / to the NavigationRoute denylist, or exclude navigations carrying a query string. Costs an offline-capable root, which may matter for the PWA case.
  2. Give the SPA its own fallback. When there is no ?farm= and no remembered code, fall back to a configured default. Keeps the service worker as-is and removes the dependency on a redirect being observable at all.
  3. Treat it as working as intended and rely on the remembered-code cache, accepting the empty field on upgraded installs.

Option 2 seems the most robust from the outside, since it does not depend on how the parameter is delivered — but it needs a way to configure the default, which may not be wanted.

Activity

  1. added
    bugSomething isn't working
    severity:p3Defect: degraded or partial behaviour
    epic-1.6Phase 1.6 — Multi-farm tenancy
    priority:tier3Real product weight, real cost
    and removed
    epic-1.6Phase 1.6 — Multi-farm tenancy
    on Sep 12, 2026
  2. mforce commented on Sep 13, 2026

    @mforce
    OwnerAuthor

    Triaged in the 2026-09-13 issue cleanup. Kept open, labelled priority:tier3, and the stale epic-1.6 label removed — epic #530 closed today (Phase 1.6 is complete bar #556), so that label pointed at a closed parent.

    Why it stays open rather than being fixed now: this issue's "possible directions" section correctly says the tradeoff is the owner's, and it is a real one. Option 1 costs an offline-capable root, which is a PWA decision, not a bug fix — the app ships an installable PWA baseline (#142) and a service-worker guarantees check runs in CI (ci.yml, "Verify service-worker guarantees"). Choosing wrong here trades a narrow prefill gap for a broader offline regression.

    Why tier3 rather than higher: the affected population is narrow and the condition self-heals. It needs all of — a service worker from an earlier build, never having signed in with a farm code, and arriving at the root rather than a ?farm= link. One successful sign-in populates cluckwork.farmCodes and the cache path takes over permanently.

    Two things for whoever picks it up, so they are not re-derived:

    1. theme-init.js is affected identically and is easy to forget. It reads the same parameter for per-farm branding (cluckwork.brand:<slug>, SPA: per-farm brand palette, keyed by slug so it survives pre-paint #586), so an upgraded install gets the default palette where a fresh one gets the farm's. A fix that restores ?farm= for Login.tsx but not for the branding script fixes half the symptom.
    2. The reproduction needs a real installed service worker — a private window is the control, not the test. A hard reload does not change the outcome, which is the detail that makes this look like a server bug when it is a client one.

    Nothing about the analysis above changes; it is accurate and was verified against sw.js's NavigationRoute denylist, which excludes only /api and /health.

  3. added this to the Platform hardening milestone on Sep 13, 2026
  4. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    Closing as won't fix — the analysis in the issue is correct, the fix is not, and the reason is worth recording.

    Why the listed directions were all declined. Option 1 costs an offline-capable root, which is a PWA regression traded for a narrow prefill gap — the same reasoning as the 2026-09-13 triage. Option 2 needs a configured default farm code, and no such constant exists to configure: this deployment serves several farms (#530), so there is no single right answer to bake into a frontend build or to stamp from the edge. Option 3 is what the code already does.

    Two alternatives were examined after triage and also do not work, so they are recorded here rather than re-derived later:

    • A request header instead of a URL parameter. Browsers cannot attach a custom header to a navigation — only fetch/XHR can, which is why X-Cluckwork-Account works on /auth/refresh and cannot work here. Independently fatal: the service worker intercepts the navigation before the server sees the request, so a request header has no opportunity to influence the response. A URL parameter is a thing that travels there; the trip is the thing that does not happen.
    • A response header on the cached index.html. This one is mechanically viable — the worker could read it and rewrite the navigation — but the precached index.html is one file identical for every URL, so any header it carries says the same thing to every farm. It degenerates to option 2's missing constant.

    What the gap actually reduces to. The affected visitor is one whose service worker predates the farm-code feature, who has never signed in with a code, and who arrives at the root rather than at a ?farm= link. In that state the app cannot name the farm from anything on the device — which is exactly the information the missing redirect would have supplied. The remembered-codes roster is the only device-side source that works across all three of the issue's own rows, and it is already implemented (#535, #587). One successful sign-in ends the condition permanently.

    theme-init.js is affected identically, per triage point 1, and is left affected for the same reason: its pre-paint brand lookup already falls back to a roster of one, and the default palette is the documented cost of not being able to attribute a brand to a farm (the pre-#586 un-namespaced key, purged at startup rather than guessed at).

    Reopen if the population turns out to be wider than tier3 assumes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:frontendReact/Vite web clientbugSomething isn't workingpriority:tier3Real product weight, real costseverity:p3Defect: degraded or partial behaviour

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions