{"record":{"id":"bcc14580af070421","repo":"block/buzz","slug":"invalid-relay-url-raw-error","errorCode":null,"errorMessage":"invalid relay URL {raw:?}: {error}","messagePattern":"invalid relay URL (.+?): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"desktop/src-tauri/src/mesh_llm/transport_policy.rs","lineNumber":64,"sourceCode":"pub(super) fn sdk_iroh_relay_config(mode: IrohRelayMode) -> (bool, Vec<String>) {\n    match mode {\n        IrohRelayMode::Disabled => (true, Vec::new()),\n        IrohRelayMode::Default => (\n            false,\n            MESH_LLM_DEFAULT_RELAYS\n                .iter()\n                .map(|url| (*url).to_string())\n                .collect(),\n        ),\n        IrohRelayMode::Custom(urls) => {\n            (false, urls.into_iter().map(|url| url.to_string()).collect())\n        }\n    }\n}\n\nfn parse_configured_relay_url(raw: &str) -> anyhow::Result<RelayUrl> {\n    let parsed = url::Url::parse(raw)\n        .map_err(|error| anyhow::anyhow!(\"invalid relay URL {raw:?}: {error}\"))?;\n    let secure = parsed.scheme() == \"https\";\n    let local_http = parsed.scheme() == \"http\"\n        && parsed.host().is_some_and(|host| match host {\n            url::Host::Domain(domain) => domain.eq_ignore_ascii_case(\"localhost\"),\n            url::Host::Ipv4(ip) => ip.is_loopback(),\n            url::Host::Ipv6(ip) => ip.is_loopback(),\n        });\n    if !secure && !local_http {\n        anyhow::bail!(\n            \"relay URL {raw:?} must use https (http is allowed only for loopback development)\"\n        );\n    }\n    if !parsed.username().is_empty()\n        || parsed.password().is_some()\n        || parsed.query().is_some()\n        || parsed.fragment().is_some()\n        || !matches!(parsed.path(), \"\" | \"/\")\n    {","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/block/buzz/blob/6c35e82bd50f4ad6587554eeb429e7378d474ba7/desktop/src-tauri/src/mesh_llm/transport_policy.rs#L46-L82","documentation":"parse_configured_relay_url in desktop/src-tauri/src/mesh_llm/transport_policy.rs:62-89 validates each Iroh relay URL configured via the MESH_IROH_RELAYS environment variable before building the mesh transport. It first runs the raw string through url::Url::parse; when that fails it wraps the url::ParseError in an anyhow error with this message, embedding the offending raw value and the parser's reason (e.g. RelativeUrlWithoutBase, EmptyHost, InvalidPort). It fails at the very first stage of a three-stage check (URL parseability, scheme security, origin-only shape), so this specific error means the string was not a parseable absolute URL at all, rather than a valid URL rejected by policy.","triggerScenarios":"iroh_relay_mode() reads the MESH_IROH_RELAYS env var, splits it on commas, and calls parse_configured_relay_url per entry; url::Url::parse(raw) returns Err whenever an entry is not an absolute URL — missing scheme (e.g. \"myrelay.example.com\" without \"https://\"), a typo'd scheme (\"htps://...\"), an empty entry that survives trimming (\",,\"), stray whitespace inside a token, a bare IPv6 literal unbracketed, or any relative path like \"/relay\". The first malformed entry aborts the whole collect::<anyhow::Result<Vec<_>>>() with this error.","commonSituations":"Operators hand-editing the MESH_IROH_RELAYS env var forget the scheme prefix (writing relay.example.com instead of https://relay.example.com), copy a URL with a trailing comma or spaces, paste a URL containing a fragment or query that also trips later checks, or leave a stray comma in a comma-separated list; CI/dev containers inherit a partially-set or empty-list value of the variable.","solutions":["Read the {raw:?} in the message to find the exact offending entry, then correct that comma-separated entry in MESH_IROH_RELAYS to a full absolute URL such as https://relay.example.com.","Ensure every entry includes an explicit scheme (https, or http only for loopback hosts like localhost/127.0.0.1/::1).","Remove stray commas, whitespace, quotes, query strings, fragments, and credentials from the list; keep each entry a bare origin like https://host:port.","If no custom relays are needed, unset MESH_IROH_RELAYS entirely (or set it to 0/default) to fall back to the built-in default relay set.","Re-run and confirm startup passes; if it still fails, the message will move to the scheme or origin-only checks, indicating the URL now parses but violates policy."],"exampleFix":"// before (MESH_IROH_RELAYS)\nMESH_IROH_RELAYS=relay.example.com,https://backup.example.com\n\n// after\nMESH_IROH_RELAYS=https://relay.example.com,https://backup.example.com","handlingStrategy":"validation","validationCode":"// Rust-side guard before reading MESH_IROH_RELAYS entries into transport config\nfn validate_relay_list(raw_env: &str) -> Result<(), String> {\n    for entry in raw_env.split(',').map(str::trim).filter(|e| !e.is_empty()) {\n        match url::Url::parse(entry) {\n            Ok(u) if matches!(u.scheme(), \"https\")\n                || (u.scheme() == \"http\" && u.host_str().is_some_and(|h| h == \"localhost\" || h == \"127.0.0.1\")) => {}\n            Ok(u) => return Err(format!(\"{entry:?}: scheme {:?} not allowed\", u.scheme())),\n            Err(e) => return Err(format!(\"{entry:?}: {e}\")),\n        }\n    }\n    Ok(())\n}","typeGuard":"fn is_origin_only_url(raw: &str) -> bool {\n    url::Url::parse(raw).map(|u| {\n        u.username().is_empty()\n            && u.password().is_none()\n            && u.query().is_none()\n            && u.fragment().is_none()\n            && matches!(u.path(), \"\" | \"/\")\n    }).unwrap_or(false)\n}","tryCatchPattern":"// anyhow-based caller: propagate with context instead of unwrapping\nlet relays = std::env::var(\"MESH_IROH_RELAYS\").ok();\nlet mode = iroh_relay_mode().with_context(|| {\n    format!(\"invalid MESH_IROH_RELAYS (raw={relays:?}); expected comma-separated absolute https URLs\")\n})?;","preventionTips":["Always include the scheme in relay URLs: https://host — never a bare hostname.","Keep MESH_IROH_RELAYS a clean comma-separated origin list: no spaces, trailing commas, query strings, fragments, or credentials.","Smoke-test the env value with a one-liner before deploy: parse each comma-split entry with url::Url::parse.","Unset the variable entirely when defaults suffice rather than setting it to an empty or placeholder value.","Document the accepted forms (https anywhere, http only for loopback) next to the deployment config."],"tags":["config","url-parsing","rust","env-var","networking"],"backgroundTag":"invalid-url-format","analyzedSha":"6c35e82bd50f4ad6587554eeb429e7378d474ba7","analyzedAt":"2026-09-13T09:13:27.080Z","contentChangedAt":"2026-09-13T09:13:27.080Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}