Skip to content

pauseCircuit on page lifecycle api event does not finish #63639

@davhdavh

Description

@davhdavh

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Server log when pauseCircuit on freeze

{"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

{"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

{"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

{"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.

// ---------------- 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions