Skip to content

[1.x] fix: Handle network connection loss in the frontend - #4843

Merged
imorland merged 3 commits into
flarum:1.xfrom
gianniguida:gg/feat-network-error-handling
Jul 29, 2026
Merged

[1.x] fix: Handle network connection loss in the frontend#4843
imorland merged 3 commits into
flarum:1.xfrom
gianniguida:gg/feat-network-error-handling

Conversation

@gianniguida

Copy link
Copy Markdown
Contributor

Problem

All frontend XHR goes through Application#request(). When a request fails at the network level — the user is offline, DNS fails, the origin is unreachable — the rejection carries status === 0, which requestErrorCatch() has no branch for. Users get the generic "Oops! Something went wrong. Please reload the page and try again." alert, which wrongly suggests a server-side bug and prompts bug reports for what is a client connectivity issue.

Core also makes no use of the browser's online/offline events, so users get no proactive indication that they've gone offline, and content that failed to load while offline stays missing until a full page reload.

This was discussed with @imorland and is contributed as three self-contained commits.

What this PR does

1. Classify network-level failures (status === 0)requestErrorCatch now shows a dedicated translated alert: an offline-specific message when navigator.onLine === false, otherwise a generic connection-problem message. The existing cross-origin special case is preserved inside the new branch. Parallel failures produce a single alert instead of one per request. Aborted requests cannot trigger this: Mithril (2.0.4) wraps xhr.abort and leaves aborted promises unsettled, so they never reach the error handler by construction.

2. online/offline events + automatic retry — going offline shows a persistent, dismissible alert; reconnecting clears all connection alerts and shows a brief confirmation. Additionally, a GET request that fails while the browser reports being offline is not rejected: its promise is held open, and the request is re-issued when the online event fires, settling the original promise with the retry's result. Content that failed to load while offline (post stream pages, discussion lists) appears automatically on reconnection with no changes to calling code. Only GETs qualify — writes keep failing fast, so nothing can be submitted twice. A network failure while the browser reports being online still rejects exactly as before, so no promise can wait for an online event that will never come.

3. Deduplicate deferred requests — deferred requests are keyed by method + URL + params; identical requests queued while offline are retried with a single request whose result settles every caller's promise. This matters in practice: e.g. FoF Horizon's admin stats widget polls a GET every 5 seconds, which would otherwise replay dozens of identical requests on reconnection.

New locale keys: core.lib.error.network_message, core.lib.error.offline_message, core.lib.connection_restored_message (English only).

Behavior notes

  • customErrorHandler and rejection propagation are unchanged for every status except the offline-GET deferral case; regression tests cover 422/500/cross-origin.
  • A survey of core, bundled extensions, and popular FoF extensions found no GET call sites whose errorHandler/.catch logic would be adversely affected by deferral — GET failure handling in the wild is loading-flag resets, which now correctly keep spinners visible until the content self-heals.

Testing

  • 22 new Jest unit tests (tests/unit/common/Application.test.ts): status-0 classification (online/offline/cross-origin), alert dedup, custom-error-handler contract, 422/500 regression guards, connectivity events, deferral + resolution on reconnect, re-deferral while still offline, retry dedup.
  • Manual smoke test on a local 1.x forum: offline request failure → single dedicated alert; reply while offline → alert + composer content preserved; reconnect → offline alert cleared, confirmation shown, deferred content loads without reload; 422/500 render as on 1.x.

Screenshot

image image

Necessity

  • Has the problem that is being solved here been clearly explained?
  • If applicable, have various options for solving this problem been considered?
  • For core PRs, does this need to be in core, or could it be in an extension?
  • Are we willing to maintain this for years / potentially forever?

Confirmed

  • Frontend changes: tested on a local Flarum installation.
  • Backend changes: tests are green (run composer test).
  • Core developer confirmed locally this works as intended.
  • Tests have been added, or are not appropriate here.

Required changes:

  • Related documentation PR: (Remove if irrelevant)

gianniguida and others added 3 commits July 27, 2026 14:16
Classify network-level request failures (status 0) in
Application#requestErrorCatch and show a dedicated translated alert
instead of the generic 'Oops! Something went wrong' message. While the
browser reports being offline, a more specific offline message is used;
failed cross-origin requests keep the existing cross-origin message.

Only one connection alert is shown at a time, even when several parallel
requests fail at once. Aborted requests cannot trigger the alert, as
Mithril leaves their promises unsettled.

Additionally, listen to the window online/offline events: going offline
shows a persistent dismissible alert, and once connectivity is restored
it is replaced by a brief confirmation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a GET request fails at the network level while the browser reports
being offline, hold its promise open instead of rejecting it and queue
the request. Once the online event fires, deferred requests are
re-issued and their original promises settled with the retry's result —
so content that failed to load while offline (discussion posts, lists,
blog articles) appears automatically on reconnection, without a page
reload or any changes to calling code.

Only GETs qualify: they are safe to repeat. Writes keep failing fast to
avoid duplicate submissions. A request that fails again while still
offline is re-deferred; a network-level failure while the browser
reports being online still rejects as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Key deferred requests by method, URL and params. Identical requests
queued while offline — e.g. a polling widget firing every few seconds —
are retried with a single request once connectivity is restored, and
every caller's promise is settled with that request's result. This
prevents a burst of duplicate requests on reconnection (fof/horizon's
admin stats widget polls a GET every 5 seconds by default).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gianniguida
gianniguida requested a review from a team as a code owner July 27, 2026 14:15
@imorland imorland changed the title Handle network connection loss in the frontend [1.x] fix: Handle network connection loss in the frontend Jul 27, 2026
@imorland imorland added this to the 1.8.18 milestone Jul 27, 2026
imorland pushed a commit that referenced this pull request Jul 29, 2026
* feat: handle network connection loss in the frontend

Port of the 1.x network-loss handling (#4843) to 2.x:

- Classify network-level request failures (status 0) in requestErrorCatch
  and show a dedicated translated alert (offline-specific when
  navigator.onLine is false; failed cross-origin requests keep their
  existing message). Parallel failures produce a single alert. Aborted
  requests cannot trigger this, as Mithril leaves their promises
  unsettled.
- Listen to the window online/offline events: going offline shows a
  persistent dismissible alert; reconnecting clears connection alerts
  and briefly confirms.
- Defer GET requests that fail while the browser is offline: their
  promises are held open and settled with the result of a retry once
  connectivity is restored, so content that failed to load appears
  automatically without a page reload. Writes keep failing fast.
  Identical deferred requests (e.g. from a polling widget) are keyed by
  method, URL and params, and retried with a single request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: retry lazy chunk loads that failed while offline

Code-split chunks load via script tags, not XHR, so the offline handling
in Application#request never sees them. A chunk that failed to load
while offline previously rejected (or was silently swallowed by its
caller): the reply/discussion composer would not open, and
DiscussionPage's PostStream import left the page stuck on its loading
skeleton with no retry — even after connectivity returned.

Since every chunk load funnels through ExportRegistry#loadChunk
(webpack's script loader is overridden with it), hold back failures
that occur while the browser reports being offline and retry once the
online event fires. The import() promise stays pending in the meantime,
so every caller — composer bodies, lazy routes, modals, the post
stream — recovers on its own, without changes to call sites.

Failures while the browser is online are reported unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: format Application.tsx with the repo's pinned prettier

* test: silence expected chunk-url warning in ExportRegistry tests

The tests never register chunks, so the registry legitimately warns
before falling back to the URL passed in. Spy it away, as the admin
Application test does for console.group/error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: assert the unregistered-chunk warning and URL fallback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@imorland
imorland merged commit 7478f5f into flarum:1.x Jul 29, 2026
409 of 456 checks passed
imorland pushed a commit to flarum/flarum-core that referenced this pull request Jul 30, 2026
* feat: handle network connection loss in the frontend

Port of the 1.x network-loss handling (flarum/framework#4843) to 2.x:

- Classify network-level request failures (status 0) in requestErrorCatch
  and show a dedicated translated alert (offline-specific when
  navigator.onLine is false; failed cross-origin requests keep their
  existing message). Parallel failures produce a single alert. Aborted
  requests cannot trigger this, as Mithril leaves their promises
  unsettled.
- Listen to the window online/offline events: going offline shows a
  persistent dismissible alert; reconnecting clears connection alerts
  and briefly confirms.
- Defer GET requests that fail while the browser is offline: their
  promises are held open and settled with the result of a retry once
  connectivity is restored, so content that failed to load appears
  automatically without a page reload. Writes keep failing fast.
  Identical deferred requests (e.g. from a polling widget) are keyed by
  method, URL and params, and retried with a single request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: retry lazy chunk loads that failed while offline

Code-split chunks load via script tags, not XHR, so the offline handling
in Application#request never sees them. A chunk that failed to load
while offline previously rejected (or was silently swallowed by its
caller): the reply/discussion composer would not open, and
DiscussionPage's PostStream import left the page stuck on its loading
skeleton with no retry — even after connectivity returned.

Since every chunk load funnels through ExportRegistry#loadChunk
(webpack's script loader is overridden with it), hold back failures
that occur while the browser reports being offline and retry once the
online event fires. The import() promise stays pending in the meantime,
so every caller — composer bodies, lazy routes, modals, the post
stream — recovers on its own, without changes to call sites.

Failures while the browser is online are reported unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: format Application.tsx with the repo's pinned prettier

* test: silence expected chunk-url warning in ExportRegistry tests

The tests never register chunks, so the registry legitimately warns
before falling back to the URL passed in. Spy it away, as the admin
Application test does for console.group/error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: assert the unregistered-chunk warning and URL fallback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants