{"record":{"id":"3e4d941df102a674","repo":"risingwavelabs/risingwave","slug":"normalized-url-isn-t-the-same-as-the-original-one","errorCode":null,"errorMessage":"normalized URL isn't the same as the original one","messagePattern":"normalized URL isn't the same as the original one","errorType":"http","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/meta/dashboard/src/proxy.rs","lineNumber":80,"sourceCode":"    uri: Uri,\n    cache: Arc<Mutex<HashMap<String, CachedResponse>>>,\n) -> anyhow::Result<Response> {\n    let mut path = uri.path().to_owned();\n    if path.ends_with('/') {\n        path += \"index.html\";\n    }\n\n    if let Some(resp) = cache.lock().unwrap().get(&path) {\n        return Ok(resp.clone().into_response());\n    }\n\n    let url_str = format!(\n        \"https://raw.githubusercontent.com/risingwavelabs/risingwave/dashboard-artifact{}\",\n        path\n    );\n    let url = Url::parse(&url_str)?;\n    if url.to_string() != url_str {\n        return Err(anyhow!(\"normalized URL isn't the same as the original one\"));\n    }\n\n    tracing::info!(\"dashboard service: proxying {}\", url);\n\n    let content = reqwest::get(url.clone()).await?;\n\n    let resp = CachedResponse {\n        code: content.status(),\n        headers: content.headers().clone(),\n        body: content.bytes().await?,\n        uri: url,\n    };\n\n    cache.lock().unwrap().insert(path, resp.clone());\n\n    Ok(resp.into_response())\n}\n","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/meta/dashboard/src/proxy.rs#L62-L98","documentation":"The meta dashboard HTTP proxy builds a URL on https://raw.githubusercontent.com/risingwavelabs/risingwave/dashboard-artifact plus the request path, parses it with url::Url, and then asserts that re-serializing the parsed URL yields the exact original string. If Url normalization changed the string (encoding, dot-segment removal, etc.), it refuses the request with this anyhow error as a safety check against unexpected/malicious paths.","triggerScenarios":"A GET to the dashboard proxy endpoint whose path, after being appended to the base URL, normalizes differently — e.g. paths containing characters needing percent-encoding, `..` dot segments, empty segments, or a path that escapes the artifact prefix.","commonSituations":"Requesting a dashboard artifact file whose name contains special characters or encoded sequences; crafted URLs probing path traversal; a version of the `url` crate with different normalization rules; proxying to a branch/artifact path that no longer exists and redirects/normalizes oddly.","solutions":["Check the requested path for invalid characters, `..` segments, or percent-encoding that triggers normalization; request a plain artifact path instead.","Verify the dashboard/proxy client URL matches the artifact file names published in the risingwave repo's dashboard-artifact branch.","If the path is legitimate but normalized (e.g. contains spaces), URL-encode it client-side in a way that survives round-tripping, or fix the artifact file name.","If you hit this as an internal invariant, ensure the `url` crate version is consistent (update Cargo.lock) and compare url.to_string() with the constructed string when debugging."],"exampleFix":"// before: path with characters that get normalized\nGET /dashboard/v2.0/some dir/index.html\n// after: properly percent-encoded, round-trip-stable path\nGET /dashboard/v2.0/some%20dir/index.html","handlingStrategy":"validation","validationCode":"// Client side: send only round-trip-stable, percent-encoded paths\nconst path = encodeURI(rawPath).replaceAll('..', '');\nif (!path.startsWith('/') || path.includes('//')) {\n  throw new Error('invalid artifact path: ' + rawPath);\n}\nfetch(`/probe/artifact${path}`);","typeGuard":"fn is_round_trip_stable(path: &str, base: &str) -> bool {\n    match Url::parse(&format!(\"{}{}\", base, path)) {\n        Ok(url) => url.as_str() == &format!(\"{}{}\", base, path),\n        Err(_) => false,\n    }\n}","tryCatchPattern":"// Rust: handle the anyhow error from the proxy handler\nmatch proxy(path).await {\n    Ok(resp) => resp,\n    Err(e) if e.to_string().contains(\"normalized URL\") => {\n        StatusCode::BAD_REQUEST // reject malformed artifact paths\n    }\n    Err(e) => { tracing::error!(error = ?e, \"proxy failed\"); StatusCode::INTERNAL_SERVER_ERROR }\n}","preventionTips":["Only request artifact paths with plain ASCII file names as published in the dashboard-artifact branch.","Percent-encode paths client-side and avoid `..`, `//`, and trailing slashes.","Treat this error as a signal of a malicious/invalid probe path — reject with 400, never retry as-is.","Pin the `url` crate version so parse/serialize normalization behavior stays consistent."],"tags":["http","url","proxy","rust"],"backgroundTag":"invalid-url-format","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}