wordpress-mobile / wordpress-mobile/GutenbergKit

Offline indicator latches permanently when the initial connectivity probe fails

Open
#525 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
29
Forks
6
Avg merge
1d 9h
Merged PRs (30d)
41

Description

Summary

OfflineIndicator runs a one-shot connectivity probe on mount when navigator.onLine === true. If that probe rejects — most plausibly because the WP REST root for a Jetpack-tunneled site takes longer than the hard 5s timeout to respond on first hit — setIsConnected(false) latches and the "Working Offline" bar stays up indefinitely. The only path back to connected is a browser online event, which is fired exclusively on a real offline → online transition. If navigator.onLine never went to false, the event never fires, the probe never re-runs, and the editor displays "Working Offline" while the network is perfectly fine.

Repro

  1. Editor on a Jetpack-connected self-hosted site whose JP tunnel happens to cold-start slowly (or any condition where the first HEAD to siteApiRoot exceeds 5s).

  2. Open the editor. "Working Offline" bar appears.

  3. From DevTools attached to the WebView:

    ({navigatorOnLine: navigator.onLine, probeUrl: window.GBKit.siteApiRoot})
    

    {navigatorOnLine: true, probeUrl: "https://<site>/wp-json/"}. The browser thinks it's online; the URL is correct.

  4. Manually re-run the probe with the same parameters as probeConnectivity():

    await fetch(window.GBKit.siteApiRoot, {method:'HEAD', mode:'no-cors', cache:'no-store', signal: AbortSignal.timeout(5000)});
    

    → resolves in ~1.6s. Connectivity is fine now; it wasn't at mount.

  5. Force a re-probe:

    window.dispatchEvent(new Event('online'));
    

    → bar disappears immediately. Confirms the state is latched, not the network.

Verified on org.wordpress.gutenbergkit:android:v0.17.2 in the WordPress-Android jetpack.wpmt.co test site editor.

Root Cause

src/components/offline-indicator/index.jsx, useNetworkConnectivity:

useEffect( () => {
    const abortController = new AbortController();

    if ( navigator.onLine ) {
        probeConnectivity().then( ( connected ) => {
            if ( ! abortController.signal.aborted ) {
                setIsConnected( connected );
            }
        } );
    }

    const handleOnline = async () => { … };       // only fires on offline → online
    const handleOffline = () => setIsConnected( false );

    window.addEventListener( 'online', handleOnline );
    window.addEventListener( 'offline', handleOffline );
    …
}, [] );

A single probe drives the only setIsConnected(true) path on mount. Recovery only happens via online, which Chrome dispatches strictly on a real network-interface transition. navigator.onLine staying true the whole time means there's nothing to dispatch.

The 5s timeout is treated as hard authority. A cold REST root or a momentary DNS lag is enough to flip the state and there's no path back.

Suggested Fix

Several non-exclusive options:

  1. Soft initial timeout / retry-once before latching. If the first probe rejects with TimeoutError, retry once after a short delay (1–2s) before committing setIsConnected(false). Cheap and catches the common cold-start case.

  2. Periodic re-probe while latched offline. A modest backoff timer (e.g. 5s → 15s → 30s capped) that only runs while isConnected === false self-heals the latch without trusting the browser event. No cost while online.

  3. Re-probe on user-driven signals. visibilitychange returning to visible is a strong "the user just looked at the editor" cue; pageshow, focus on the WebView, or a debounced input/scroll handler would catch the cases where the user has clearly resumed activity. Cheap and well-bounded.

  4. Probe interpretation: distinguish "couldn't reach API root" from "no network at all." If the probe rejects with a network error vs. a timeout vs. an HTTP-style error, those imply different recovery strategies. A TypeError from fetch (no network interface) is much more likely to require an online event. A TimeoutError is much more likely to require retry.

(1) alone would have prevented this incident. (2) is the most robust against future regressions of this shape.

Affected

  • All consumers of OfflineIndicator (i.e., every site loaded in GutenbergKit). Worst case where the indicator persists is sites with a non-trivial cold-path to siteApiRoot — Jetpack-connected self-hosted sites in particular.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/components/offline-indicator/index.jsx at useNetworkConnectivity and inspect how the initial probe, online event, and offline event update the indicator. Reproduce an initial probe failure while navigator.onLine remains true, then verify that the chosen recovery behavior clears the "Working Offline" bar without requiring an online event.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.