a-b-street/abstreet · warning · anyhow::Error

window.history.replace_state failed

Error message

window.history.replace_state failed

What it means

With the History object in hand, update_url calls replace_state_with_url to swap the URL without a page refresh. A JS exception from replaceState (commonly SecurityError) becomes this error via the 'window.history.replace_state failed' fallback string.

Solutions

  1. Ensure the transformed URL stays same-origin (keep path/query changes only, no host change)
  2. Inspect the transformed URL string for accidental absolute/cross-origin prefixes
  3. Serve over http(s) instead of file://
  4. Catch the error and log instead of failing the whole operation

Example fix

// before: transform that yields a cross-origin URL
let new_url = format!("https://other-host.com{}", path);
// after: keep it same-origin
let new_url = format!("{}?zoom={}", path, zoom);
Defensive patterns

Strategy: validation

Validate before calling

// ensure the new URL is same-origin before calling replaceState
let base = window.location().origin().unwrap_or_default();
anyhow::ensure!(new_url.starts_with(&base) || new_url.starts_with('/'), "new URL must stay same-origin: {}", new_url);

Try / catch

if let Err(e) = history.replace_state_with_url(&JsValue::NULL, "", Some(&new_url)) {
    log::warn!("replaceState failed: {:?}; staying on current URL", e.as_string());
}

Prevention

When it happens

Trigger: history.replace_state_with_url throws: the new_url is not same-origin with the current document (SecurityError), or the browser refuses the state/URL argument.

Common situations: Transform producing a cross-origin URL (absolute URL on another host), serving from file:// where URL manipulation is restricted, sandboxed iframes.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13). Data as JSON: /api/errors/2c8801cead5cb15f. Report an issue: GitHub.

Appendix: source

Thrown at widgetry/src/tools/url.rs:127

        let url = window.location().href().map_err(|err| {
            anyhow!(err
                .as_string()
                .unwrap_or("window.location.href failed".to_string()))
        })?;
        let new_url = (transform)(url);

        // Setting window.location.href may seem like the obvious thing to do, but that actually
        // refreshes the page. This method just changes the URL and doesn't mess up history. See
        // https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API.
        let history = window.history().map_err(|err| {
            anyhow!(err
                .as_string()
                .unwrap_or("window.history failed".to_string()))
        })?;
        history
            .replace_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&new_url))
            .map_err(|err| {
                anyhow!(err
                    .as_string()
                    .unwrap_or("window.history.replace_state failed".to_string()))
            })?;
    }
    Ok(())
}

fn change_url_free_param(url: String, free_param: &str) -> String {
    // The URL parsing crates I checked had lots of dependencies and didn't even expose such a nice
    // API for doing this anyway.
    let url_parts = url.split('?').collect::<Vec<_>>();
    if url_parts.len() == 1 {
        return format!("{}?{}", url, free_param);
    }
    let mut query_params = String::new();
    let mut found_free = false;
    let mut first = true;
    for x in url_parts[1].split('&') {

View on GitHub (pinned to 0964f29315)