astrid-runtime/astrid · error · anyhow::Error
CORS origin {raw:?} doesn't parse as a URL: {e}
Error message
CORS origin {raw:?} doesn't parse as a URL: {e} What it means
validate_cors_origin parses each configured CORS origin as a URL and rejects values url::Url::parse cannot handle. Origins are byte-matched against Access-Control-Allow-Origin, so a malformed string would silently never match a real preflight; config validation fails fast instead.
Source
Thrown at crates/astrid-gateway/src/config.rs:189
tls.cert_path.display()
);
}
crate::tls::warn_if_key_is_too_open(&tls.key_path);
}
Ok(())
}
}
/// Validate a single CORS origin string. Origins MUST be of the form
/// `scheme://host[:port]` with no path, query, or fragment — that's
/// what the browser sends in `Origin:` and what the response's
/// `Access-Control-Allow-Origin:` is byte-matched against. A
/// `https://app.example/` (trailing slash) would silently fail to
/// match a real preflight; rejecting it here is what makes that
/// surfacable.
fn validate_cors_origin(raw: &str) -> anyhow::Result<()> {
let parsed = url::Url::parse(raw)
.map_err(|e| anyhow::anyhow!("CORS origin {raw:?} doesn't parse as a URL: {e}"))?;
match parsed.scheme() {
"http" | "https" => {},
other => anyhow::bail!(
"CORS origin {raw:?} uses scheme {other:?}; only http/https are valid for browser origins"
),
}
if parsed.host_str().is_none() {
anyhow::bail!("CORS origin {raw:?} has no host component");
}
// Browsers strip userinfo before sending `Origin:`, so a config
// entry with embedded credentials can never match a real
// preflight. Reject so operators don't silently misconfigure.
if !parsed.username().is_empty() || parsed.password().is_some() {
anyhow::bail!(
"CORS origin {raw:?} carries userinfo (user:password); browsers strip it before sending `Origin:` so this can never match"
);
}
if parsed.path() != "" && parsed.path() != "/" {View on GitHub (pinned to affd8760f4)
Solutions
- Fix the origin string in config to a fully-qualified absolute URL (scheme + host)
- Ensure the scheme is included: 'https://app.example.com' not 'app.example.com'
- Trim whitespace from the configured value before validation
- Re-run config validation to confirm all origins parse
Example fix
// before cors_origins = ["app.example.com", "https://dashboard.example.io"] // after cors_origins = ["https://app.example.com", "https://dashboard.example.io"]
Defensive patterns
Strategy: validation
Validate before calling
fn is_valid_cors_origin(raw: &str) -> bool {
match url::Url::parse(raw.trim()) {
Ok(u) => matches!(u.scheme(), "http" | "https"),
Err(_) => false,
}
}
// assert all origins pass before loading config:
// config.cors_origins.iter().all(|o| is_valid_cors_origin(o)) Prevention
- Always write origins as absolute URLs including scheme (https://...)
- Trim whitespace from config values sourced from env/files
- Run config validation in CI so bad origins fail before deploy
When it happens
Trigger: A CORS origin entry in config (validated via validate()) that is not a syntactically valid URL — e.g. missing scheme, spaces, stray characters — causing url::Url::parse to return a RelativeUrlWithoutBase or similar ParseError.
Common situations: Writing 'app.example.com' without https://; typos like 'https//app.example.com'; pasting origins with trailing whitespace; env-var interpolation producing an empty or partial value.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- signed source URL cannot contain path segments
- invalid value for {capsule_id}.{key}: expected one of {}, go
- existing [security.capsule_local_egress].{capsule_id} is not
- editor '{editor}' exited with non-zero status
- signed Distro member '{}' must resolve to a prebuilt .capsul
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/b47982477781cbbc.
Report an issue: GitHub.