dotnet / dotnet/aspnetcore

pauseCircuit on page lifecycle api event does not finish

Open
#63,639 7 comments 0 reactions 0 assignees View on GitHub
area-blazor
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Describe the bug

#### Server log when pauseCircuit on `freeze`

```json
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitRegistry","Message":"Pausing circuit with id Iw5R2Vb8ddM4WBokpbmrHm8VpUizf2bAFIxclHaVUts from connection IayowWUkJ5qQ9OuEYZ5VTg.","TraceID":"009d7187fdc7025597acd0507fa07939","SpanID":"6bacc7c38cc28e4f","Timestamp":"2025-09-11T10:55:04.9146347Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Infrastructure.PersistentStateValueProvider","Message":"Persisting value for storage key 'GY+utGHxVFNiiAOlJajMCoTIQigGKM0Y4/Dx8UZ1sOI=' of type 'IEnumerable`1' from component 'SearchPage' for property 'Selected'","TraceID":"009d7187fdc7025597acd0507fa07939","SpanID":"6bacc7c38cc28e4f","Timestamp":"2025-09-11T10:55:04.9149307Z"}
```

and restore
```json
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Invoking instance method 'DispatchEventAsync' on instance '1' with callback id '6'.","TraceID":"7beee1ba41e003f3e053f432fe97773f","SpanID":"abd3a238dd2a1e80","Timestamp":"2025-09-11T10:55:27.0238394Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer","Message":"Sending render batch 26 of size 120 bytes to client IayowWUkJ5qQ9OuEYZ5VTg.","TraceID":"7beee1ba41e003f3e053f432fe97773f","SpanID":"abd3a238dd2a1e80","Timestamp":"2025-09-11T10:55:27.0244261Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime","Message":"Invocation of 'DispatchEventAsync' on reference '1' with callback id '6' completed successfully.","TraceID":"7beee1ba41e003f3e053f432fe97773f","SpanID":"abd3a238dd2a1e80","Timestamp":"2025-09-11T10:55:27.0245470Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Disposing circuit 'Iw5R2Vb8ddM4WBokpbmrHm8VpUizf2bAFIxclHaVUts' started.","TraceID":"009d7187fdc7025597acd0507fa07939","SpanID":"6bacc7c38cc28e4f","Timestamp":"2025-09-11T10:55:27.0370825Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Circuit id 'Iw5R2Vb8ddM4WBokpbmrHm8VpUizf2bAFIxclHaVUts' disconnected from connection 'IayowWUkJ5qQ9OuEYZ5VTg'.","TraceID":"009d7187fdc7025597acd0507fa07939","SpanID":"6bacc7c38cc28e4f","Timestamp":"2025-09-11T10:55:27.0371209Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Closing circuit with id 'Iw5R2Vb8ddM4WBokpbmrHm8VpUizf2bAFIxclHaVUts'.","TraceID":"009d7187fdc7025597acd0507fa07939","SpanID":"6bacc7c38cc28e4f","Timestamp":"2025-09-11T10:55:27.0371309Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Disposing circuit 'Iw5R2Vb8ddM4WBokpbmrHm8VpUizf2bAFIxclHaVUts' succeeded.","TraceID":"009d7187fdc7025597acd0507fa07939","SpanID":"6bacc7c38cc28e4f","Timestamp":"2025-09-11T10:55:27.0830157Z"}
```

Client side, the event handler for `freeze` does not log the line after `Blazor.pauseCircuit`, which probably means the freeze event does not support async methods.

The following wake event then has a race condition with the `freeze`, which causes it to fail to resume because server thinks it didn't finish the pause, and then the freeze handler finish disconnecting again leaving the UI in a disconnected state with no knowledge that it is disconnected and the UI components doesnt show up.

### Expected Behavior

#### Server log when pauseCircuit on `hidden`
```json
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitRegistry","Message":"Pausing circuit with id rw0uZzqhEng17WeRf43GqzibcV7GhLpigjdUMv-kdP8 from connection 64giwj_2dFGG4073iJ1exg.","TraceID":"281958c54cd13f65595c1da70c07e61e","SpanID":"d18c619dba6be120","Timestamp":"2025-09-11T10:52:15.9959922Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Infrastructure.PersistentStateValueProvider","Message":"Persisting value for storage key 'GY+utGHxVFNiiAOlJajMCoTIQigGKM0Y4/Dx8UZ1sOI=' of type 'IEnumerable`1' from component 'SearchPage' for property 'Selected'","TraceID":"281958c54cd13f65595c1da70c07e61e","SpanID":"d18c619dba6be120","Timestamp":"2025-09-11T10:52:15.9962899Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Disposing circuit 'rw0uZzqhEng17WeRf43GqzibcV7GhLpigjdUMv-kdP8' started.","TraceID":"281958c54cd13f65595c1da70c07e61e","SpanID":"d18c619dba6be120","Timestamp":"2025-09-11T10:52:15.9995095Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitRegistry","Message":"Attempting to disconnect circuit with id rw0uZzqhEng17WeRf43GqzibcV7GhLpigjdUMv-kdP8 from connection 64giwj_2dFGG4073iJ1exg.","TraceID":"5ec521ceaf61172490060163321f1fe4","SpanID":"020424fbaea106b2","Timestamp":"2025-09-11T10:52:15.9995096Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitRegistry","Message":"Failed to disconnect circuit with id rw0uZzqhEng17WeRf43GqzibcV7GhLpigjdUMv-kdP8. The circuit is not active.","TraceID":"5ec521ceaf61172490060163321f1fe4","SpanID":"020424fbaea106b2","Timestamp":"2025-09-11T10:52:15.9995346Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Circuit id 'rw0uZzqhEng17WeRf43GqzibcV7GhLpigjdUMv-kdP8' disconnected from connection '64giwj_2dFGG4073iJ1exg'.","TraceID":"281958c54cd13f65595c1da70c07e61e","SpanID":"d18c619dba6be120","Timestamp":"2025-09-11T10:52:15.9995395Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Closing circuit with id 'rw0uZzqhEng17WeRf43GqzibcV7GhLpigjdUMv-kdP8'.","TraceID":"281958c54cd13f65595c1da70c07e61e","SpanID":"d18c619dba6be120","Timestamp":"2025-09-11T10:52:15.9995545Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Disposing circuit 'rw0uZzqhEng17WeRf43GqzibcV7GhLpigjdUMv-kdP8' succeeded.","TraceID":"281958c54cd13f65595c1da70c07e61e","SpanID":"d18c619dba6be120","Timestamp":"2025-09-11T10:52:16.0664999Z"}
```
and restore
```json
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime","Message":"Begin invoke JS interop '2': 'Blazor._internal.attachWebRendererInterop'","TraceID":"c331c81212c969cb21bca865eded5727","SpanID":"8ccaf6accb8e1a2e","Timestamp":"2025-09-11T10:52:35.4308771Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitFactory","Message":"Created circuit Tdg9ZV8_AclJ0k-Jj35jz5OJstNfmWhm1HTwk268MNA for connection BTVsppE-fYyxQU0MpzaiSw","TraceID":"c331c81212c969cb21bca865eded5727","SpanID":"8ccaf6accb8e1a2e","Timestamp":"2025-09-11T10:52:35.4309989Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Circuit initialization started.","TraceID":"c331c81212c969cb21bca865eded5727","SpanID":"8ccaf6accb8e1a2e","Timestamp":"2025-09-11T10:52:35.4310368Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Circuit initialization succeeded.","TraceID":"c331c81212c969cb21bca865eded5727","SpanID":"8ccaf6accb8e1a2e","Timestamp":"2025-09-11T10:52:35.4310490Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"The JS interop call with callback id '2' succeeded.","TraceID":"9f752067b68960217aa575452ca58acd","SpanID":"60491be37e61649b","Timestamp":"2025-09-11T10:52:35.4331201Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Update root components started.","TraceID":"2087fd11ccabf3f18ae9432008455821","SpanID":"748fa3f4e57b81b4","Timestamp":"2025-09-11T10:52:35.4346757Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Opening circuit with id 'Tdg9ZV8_AclJ0k-Jj35jz5OJstNfmWhm1HTwk268MNA'.","TraceID":"2087fd11ccabf3f18ae9432008455821","SpanID":"748fa3f4e57b81b4","Timestamp":"2025-09-11T10:52:35.4348509Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost","Message":"Circuit id 'Tdg9ZV8_AclJ0k-Jj35jz5OJstNfmWhm1HTwk268MNA' connected using connection 'BTVsppE-fYyxQU0MpzaiSw'.","TraceID":"2087fd11ccabf3f18ae9432008455821","SpanID":"748fa3f4e57b81b4","Timestamp":"2025-09-11T10:52:35.4348813Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer","Message":"Sending render batch 2 of size 897 bytes to client BTVsppE-fYyxQU0MpzaiSw.","TraceID":"2087fd11ccabf3f18ae9432008455821","SpanID":"748fa3f4e57b81b4","Timestamp":"2025-09-11T10:52:35.4350999Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer","Message":"Sending render batch 3 of size 37180 bytes to client BTVsppE-fYyxQU0MpzaiSw.","TraceID":"2087fd11ccabf3f18ae9432008455821","SpanID":"748fa3f4e57b81b4","Timestamp":"2025-09-11T10:52:35.4376567Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer","Message":"Sending render batch 4 of size 188975 bytes to client BTVsppE-fYyxQU0MpzaiSw.","TraceID":"2087fd11ccabf3f18ae9432008455821","SpanID":"748fa3f4e57b81b4","Timestamp":"2025-09-11T10:52:35.4427346Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer","Message":"Sending render batch 5 of size 2287 bytes to client BTVsppE-fYyxQU0MpzaiSw.","TraceID":"2087fd11ccabf3f18ae9432008455821","SpanID":"748fa3f4e57b81b4","Timestamp":"2025-09-11T10:52:35.4431672Z"}
{"LogLevel":"dbug","Category":"Microsoft.AspNetCore.Components.Infrastructure.PersistentStateValueProvider","Message":"Restored value from persistent state for storage key 'GY+utGHxVFNiiAOlJajMCoTIQigGKM0Y4/Dx8UZ1sOI=' of type 'IEnumerable`1' for component 'null' for property 'Selected'","TraceID":"2087fd11ccabf3f18ae9432008455821","SpanID":"748fa3f4e57b81b4","Timestamp":"2025-09-11T10:52:35.4433408Z"}
```

### Steps To Reproduce

This will use `freeze` on browsers that support page lifecycle API (ie chrome) and `hidden` on others (ie safari and firefox).
Replace `ReconnectModal.razor.js` in new dotnet 10 blazor project.
```js
// ---------------- UI wiring (Blazor reconnect modal) ----------------
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);

const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);

const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resumeClick);

function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed" || event.detail.state === "rejected") {
location.reload();
}
}

let inRetry = false;

async function retry() {
if (inRetry) return;

try {
// Blazor reconnect contract:
// - true => success (reconnected)
// - false => reached server but circuit unknown -> try resumeCircuit()
// - throw => couldn't reach server
console.log("Attempting to reconnect");
const successful = await Blazor.reconnect();
if (!successful) {
console.log("reconnect failed - Attempting to resume");
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
console.log("resume failed - reloading");
location.reload();
} else {
console.log("resume success");
reconnectModal.close();
}
} else {
console.log("reconnect success");
}
} catch {
// Server currently unreachable → retry when user brings tab to foreground
console.log("Failed to reconnect");
} finally {
inRetry = false;
}
}
async function resumeClick() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) location.reload();
} catch {
location.reload();
}
}

// --------------- Session actions used by the state machine ---------------
async function suspend(reason) {
console.log(`Pausing circuit`);
await Blazor.pauseCircuit(); //<-- problem here
console.log(`Paused circuit`); //<-- never shown
}

async function resumeFromSuspend(nextState) {
console.log(`Resume from suspend`);
await retry();
}

async function drop() {
console.log(`Discarding session`);
await Blazor.disconnect();
}

// ---------------- Guideline state machine (active/passive/hidden/frozen/terminated) ----------------
const getState = () => {
if (document.visibilityState === 'hidden') return 'hidden';
if (document.hasFocus()) return 'active';
return 'passive';
};

let state = getState();
let wasSuspended = false;
const supportsFreeze = 'onfreeze' in document;
const opts = { capture: true };

const stateChange = (nextState) => {
const prevState = state;
if (nextState === prevState) return;

console.log(`State change: ${prevState} >>> ${nextState}`);

// --- Suspend ---
if (supportsFreeze) {
if (nextState === 'frozen' && !wasSuspended) {
wasSuspended = true;
suspend('freeze');
}
} else {
if (nextState === 'hidden' && !wasSuspended) {
wasSuspended = true;
suspend('hidden');
}
}

// --- Wake (back to active/passive) ---
if (wasSuspended && (nextState === 'active' || nextState === 'passive')) {
wasSuspended = false;
resumeFromSuspend(nextState);
}

// --- Drop (leaving/unload) ---
if (nextState === 'terminated') {
drop();
}

state = nextState;
};

// Lifecycle listeners following your guideline
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach((type) => {
window.addEventListener(type, () => stateChange(getState()), opts);
});

// freeze → always 'frozen' (preferred suspend trigger if supported)
window.addEventListener('freeze', () => {
stateChange('frozen');
}, opts);

// pagehide → 'frozen' (bfcache) or 'terminated' (leaving/unload)
window.addEventListener('pagehide', (event) => {
stateChange(event.persisted ? 'frozen' : 'terminated');
}, opts);
```

### Exceptions (if any)

_No response_

### .NET Version

10.0.100-rc.1.25451.107

### Anything else?

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.