[BUG] UI hangs on a blank skeleton forever when navigator.onLine is false (paused query blocks router creation)
[!WARNING] Before submitting a PR, please make sure that:
- A maintainer has triaged this issue and applied the
readylabel- This issue has no assignee
- No duplicate PR exists
PRs not meeting these requirements may be automatically closed.
MLflow version
- Affected: 3.10.0, 3.16.1 (latest at time of writing)
- Not affected: 3.9.0
System information
- OS: macOS 15 (arm64)
- Python: 3.12
- Browser: Chrome 152 / 153
- Note: the bug is browser- and OS-independent. It reproduces anywhere
navigator.onLineisfalse, which DevTools can simulate in one click.
Describe the problem
When navigator.onLine is false, the entire MLflow UI renders a loading skeleton forever. There is no error message, no console error, no network request, and no timeout — the page just never finishes loading.
This is not limited to genuinely offline users. Chrome keeps navigator.onLine as browser-process-wide state, and it can latch to false while the network is perfectly fine (we hit this with a VPN that churns tunnel interfaces). In that state every tab — including incognito windows, which share the process — shows a permanently blank MLflow UI, while fetch() to the very same tracking server returns 200 with real data from the same page's console. Restarting the browser is the only way out, and nothing in the UI hints at the cause.
Code to reproduce issue
pip install mlflow==3.16.1
mlflow server --host 127.0.0.1 --port 5000- Open
http://127.0.0.1:5000in Chrome. - DevTools → Network tab → set throttling to Offline.
- Reload the page.
Expected: an error state, an "offline" notice, or the UI rendering with defaults.
Actual: a permanent skeleton. #root contains only skeleton markup, innerText.length === 0. Console is clean. No requests are issued at all.
Root cause
useServerInfo() does not set networkMode, so it inherits React Query's default 'online'. With navigator.onLine === false the query is paused before queryFn ever runs and stays isLoading: true indefinitely:
fetchStatus: "paused", isPaused: true, isLoading: true, status: "loading"(read directly off the React fiber tree of a stuck page)
MlflowRouter then refuses to build the router at all:
// mlflow/server/js/src/MlflowRouter.tsx
const { workspacesEnabled, loading: featuresLoading } = useWorkspacesEnabled();
const hashRouter = useMemo(
() =>
// Don't create router while still loading features
featuresLoading ? null : createHashRouter([...]),
[routes, workspacesEnabled, featuresLoading],
);
// Show loading skeleton while determining if workspaces are enabled
if (featuresLoading || !hashRouter) {
return <LegacySkeleton />; // <-- never leaves this branch
}So one paused query takes down the whole application shell.
Notably this defeats error handling that is already written. fetchServerInfo is deliberately defensive — it catches every failure and falls back to a default:
// mlflow/server/js/src/experiment-tracking/hooks/useServerInfo.tsx
const DEFAULT_RESPONSE: ServerInfoResponse = { store_type: '', workspaces_enabled: false };
async function fetchServerInfo(): Promise<ServerInfoResponse> {
try {
const response = await fetch(getAjaxUrl('ajax-api/3.0/mlflow/server-info'), {...});
if (!response.ok) {
// If the endpoint doesn't exist or returns an error, return default
return DEFAULT_RESPONSE;
}
return response.json();
} catch {
// Network error or other failure - return default
return DEFAULT_RESPONSE;
}
}A React Query pause happens one layer above queryFn, so none of that runs. The intended graceful degradation never gets a chance.
Why 3.9.0 is not affected
app.tsx creates the query client the same way in both releases (new QueryClient(), no defaultOptions). The regression came from 3.10.0 adding ServerInfoProvider plus the featuresLoading gate in MlflowRouter. Before that, a paused query could not block the app shell.
Bisect, same browser and machine, MLflow served from 127.0.0.1:
| MLflow | renders? | #root innerText |
lazy chunks requested |
|---|---|---|---|
| 3.9.0 | yes | 378 | 3 |
| 3.10.0 | no | 0 | 0 |
| 3.16.1 | no | 0 | 0 |
Forcing navigator.onLine to true before the bundle executes, with nothing else changed, makes 3.10.0 render normally (innerText 0 → 1216, innerHTML 1009 → 37511). That isolates navigator.onLine as the single variable.
Suggested fix
Two small changes, either of which alone fixes the hang:
useServerInfo.tsx— addnetworkMode: 'always'to theuseQueryoptions. This query already treats every failure as "use defaults", so there is no reason for it to pause.MlflowRouter.tsx— do not hard-gate router creation onfeaturesLoading. Fall back toworkspacesEnabled: false, which is exactly whatDEFAULT_RESPONSEalready encodes.
A broader option, if maintainers prefer it: set networkMode: 'always' in defaultOptions.queries on the QueryClient in app.tsx. Every MLflow query targets the same origin that served the page, so if the page loaded the server is reachable; pausing on navigator.onLine buys nothing here. TanStack Query's own docs note that navigator.onLine is unreliable on desktop Chrome and recommend networkMode: 'always' for applications that are not offline-first.
Even if a paused query is considered correct behaviour when genuinely offline, rendering an unlabelled blank skeleton indefinitely is not — some error or offline state should surface.
I'm happy to open a PR for whichever direction you prefer.
What component(s) does this bug affect?
area/uiux: Front-end, user experience, plotting, JavaScript, JavaScript dev server
Source: mlflow/mlflow