tinyhumansai/openhuman · warning
secret request {} cancelled before user submit
Error message
secret request {} cancelled before user submit What it means
During MCP server install, secrets requested from the user are held in a process-local in-memory map keyed by SecretRef; await_fulfillment blocks on a oneshot receiver with a 5-minute budget (REQUEST_TIMEOUT = 300 s). This variant fires when the sender is dropped without a submit — per the source comment, usually because the GC sweep purged the entry (fulfilled-but-unused entries are purged after a 15-minute idle TTL) — and it is deliberately surfaced as a timeout-style error to keep callers simple. The entry is forgotten on this path, so the old SecretRef is dead.
Source
Thrown at src/openhuman/mcp/registry/setup.rs:155
entry.value = Some(value);
entry.last_touched = Instant::now();
if let Some(tx) = entry.waiter.take() {
let _ = tx.send(());
}
tracing::debug!("[mcp-setup] fulfilled ref={}", r.as_str());
true
}
/// Block on a freshly-minted request with the global timeout. On timeout
/// the entry is removed and `Err(_)` is returned.
pub async fn await_fulfillment(r: &SecretRef, rx: oneshot::Receiver<()>) -> anyhow::Result<()> {
match timeout(REQUEST_TIMEOUT, rx).await {
Ok(Ok(())) => Ok(()),
Ok(Err(_)) => {
// Sender dropped — usually means GC purged the entry. Surface
// as a timeout-style error to keep the caller simple.
let _ = forget(r).await;
anyhow::bail!("secret request {} cancelled before user submit", r.as_str())
}
Err(_) => {
let _ = forget(r).await;
anyhow::bail!(
"secret request {} timed out after {}s",
r.as_str(),
REQUEST_TIMEOUT.as_secs()
)
}
}
}
/// Resolve a `{KEY: SecretRef}` map into a `Vec<(KEY, VALUE)>`. Returns
/// `Err(_)` if any ref is unknown or not yet fulfilled — callers should
/// retry rather than partially-apply.
///
/// Touches the `last_touched` on every hit so iterative `test_connection`
/// calls reset the idle TTL.View on GitHub (pinned to 7491200858)
Solutions
- Retry the install — a fresh request mints a new SecretRef and re-prompts the user.
- Avoid concurrent installs of the same server; let the first flow finish or cancel it cleanly first.
- If it recurs, check logs for GC/purge activity around the abort to confirm the entry was collected rather than a bug dropping senders.
Defensive patterns
Strategy: retry
Try / catch
Catch the anyhow error and branch on the message: `cancelled before user submit` → re-run the request flow with a fresh SecretRef (the old ref was forgotten); `timed out` → re-prompt only if a user is actually present. Never retry the dead ref itself.
Prevention
- Complete secret prompts promptly — entries are GC'd after ~15 min idle.
- Serialize installs that need the same secrets; avoid duplicate concurrent requests.
- Run installs in a session where the prompt UI is visible.
When it happens
Trigger: await_fulfillment is waiting while the pending entry is removed from the map: the idle-GC sweep collects it, a duplicate/superseding request replaces it, or the process is tearing the registry down. Distinct from the timeout path: the wait ended early because the channel closed, not because 300 s elapsed.
Common situations: User abandons the credential prompt long enough for GC; two installs of the same server racing so the second registration drops the first's sender; slow UI leaving prompts pending past the GC horizon.
Related errors
- secret request {} timed out after {}s
- Socket not connected
- Invalid ${paramName}: ${value}. Must be a positive integer.
- Invalid ${paramName}: '${value}'. Must be a positive integer
- Invalid ${paramName}: ${String(value)}. Must be a positive i
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/5ca0aca00c8f2b00.
Report an issue: GitHub.