{"record":{"id":"442a9f6c93c3b879","repo":"neondatabase/neon","slug":"param-param-name-specified-more-than-once","errorCode":null,"errorMessage":"param {param_name} specified more than once","messagePattern":"param (.+?) specified more than once","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"libs/http-utils/src/request.rs","lineNumber":61,"sourceCode":"    };\n    let values = url::form_urlencoded::parse(query.as_bytes())\n        .filter_map(|(k, v)| if k == param_name { Some(v) } else { None })\n        // we call .next() twice below. If it's None the first time, .fuse() ensures it's None afterwards\n        .fuse();\n\n    // Work around an issue with Alloy's pyroscope scrape where the \"seconds\"\n    // parameter is added several times. https://github.com/grafana/alloy/issues/3026\n    // TODO: revert after Alloy is fixed.\n    let value1 = values\n        .map(Ok)\n        .reduce(|acc, i| {\n            match acc {\n                Err(_) => acc,\n\n                // It's okay to have duplicates as along as they have the same value.\n                Ok(ref a) if a == &i.unwrap() => acc,\n\n                _ => Err(ApiError::BadRequest(anyhow!(\n                    \"param {param_name} specified more than once\"\n                ))),\n            }\n        })\n        .transpose()?;\n    // if values.next().is_some() {\n    //     return Err(ApiError::BadRequest(anyhow!(\n    //         \"param {param_name} specified more than once\"\n    //     )));\n    // }\n\n    Ok(value1)\n}\n\npub fn must_get_query_param<'a>(\n    request: &'a Request<Body>,\n    param_name: &str,\n) -> Result<Cow<'a, str>, ApiError> {","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/http-utils/src/request.rs#L43-L79","documentation":"Thrown by get_query_param in http-utils when a query parameter appears more than once in the query string with DIFFERENT values. The reduce-based logic deliberately tolerates repeated occurrences that carry the same value -- a workaround for Grafana Alloy's pyroscope scraper duplicating the 'seconds' parameter (grafana/alloy#3026) -- but conflicting duplicates produce ApiError::BadRequest (HTTP 400).","triggerScenarios":"Any request to a handler built with get_query_param / must_get_query_param / parse_query_param whose query string repeats a parameter with conflicting values, e.g. 'GET /v1/tenant_config?limit=10&limit=50'. Same-value duplicates like '?seconds=30&seconds=30' are accepted and do not raise.","commonSituations":"Scrapers or SDKs that append their own copy of a parameter the caller already set (the Alloy pyroscope case); hand-built URLs that string-concatenate extra filters; API gateways or sidecars that inject a duplicate parameter with a different value; clients that send both a default and an explicit value.","solutions":["Send the parameter exactly once, or make every occurrence identical (?a=1&a=1 is allowed)","Log the full incoming request URI and fix the client/URL-builder component that appends the parameter a second time","If a middleware/proxy injects the duplicate, rename your parameter or strip the injected copy at the gateway","If you genuinely need multi-value semantics, switch the handler off get_query_param to a multi-value-aware parser"],"exampleFix":"# before\nGET /v1/metrics?seconds=60&seconds=120   -> 400 param seconds specified more than once\n\n# after (single value, or identical duplicates)\nGET /v1/metrics?seconds=60\nGET /v1/metrics?seconds=60&seconds=60   -> accepted","handlingStrategy":"validation","validationCode":"use hyper::{Body, Request};\nuse url::form_urlencoded;\n\n/// Fails fast with a clear message when `param_name` appears twice with different values.\npub fn check_no_conflicting_query_param(\n    request: &Request<Body>,\n    param_name: &str,\n) -> Result<(), String> {\n    let Some(query) = request.uri().query() else {\n        return Ok(());\n    };\n    let mut values = form_urlencoded::parse(query.as_bytes())\n        .filter(|(k, _)| k == param_name)\n        .map(|(_, v)| v.into_owned());\n    let Some(first) = values.next() else {\n        return Ok(());\n    };\n    if values.any(|v| v != first) {\n        return Err(format!(\n            \"param {param_name} specified more than once with different values; first was {first:?}\"\n        ));\n    }\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"match get_query_param(request, \"seconds\") {\n    Err(ApiError::BadRequest(e)) if e.to_string().contains(\"specified more than once\") => {\n        // 400 to the client, but log the full URI to find who duplicates the param\n        tracing::warn!(uri = %request.uri(), \"conflicting duplicate query param\");\n        return Err(ApiError::BadRequest(e));\n    }\n    other => other?,\n}","preventionTips":["In URL builders, use a map/set keyed by parameter name instead of pushing (key, value) pairs repeatedly","Log the final outgoing URI in client wrappers so injected duplicates are visible immediately","When integrating scrapers (Alloy/Grafana), pin versions without the duplicate-params bug or normalize the query at the gateway","Write a handler test that sends the same param twice with equal values (must pass) and with different values (must 400)"],"tags":["rust","http","query-params","validation","bad-request"],"backgroundTag":"duplicate-query-parameter","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}