neondatabase/neon · error · ApiError

param {param_name} specified more than once

Error message

param {param_name} specified more than once

What it means

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).

Source

Thrown at libs/http-utils/src/request.rs:61

    };
    let values = url::form_urlencoded::parse(query.as_bytes())
        .filter_map(|(k, v)| if k == param_name { Some(v) } else { None })
        // we call .next() twice below. If it's None the first time, .fuse() ensures it's None afterwards
        .fuse();

    // Work around an issue with Alloy's pyroscope scrape where the "seconds"
    // parameter is added several times. https://github.com/grafana/alloy/issues/3026
    // TODO: revert after Alloy is fixed.
    let value1 = values
        .map(Ok)
        .reduce(|acc, i| {
            match acc {
                Err(_) => acc,

                // It's okay to have duplicates as along as they have the same value.
                Ok(ref a) if a == &i.unwrap() => acc,

                _ => Err(ApiError::BadRequest(anyhow!(
                    "param {param_name} specified more than once"
                ))),
            }
        })
        .transpose()?;
    // if values.next().is_some() {
    //     return Err(ApiError::BadRequest(anyhow!(
    //         "param {param_name} specified more than once"
    //     )));
    // }

    Ok(value1)
}

pub fn must_get_query_param<'a>(
    request: &'a Request<Body>,
    param_name: &str,
) -> Result<Cow<'a, str>, ApiError> {

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Send the parameter exactly once, or make every occurrence identical (?a=1&a=1 is allowed)
  2. Log the full incoming request URI and fix the client/URL-builder component that appends the parameter a second time
  3. If a middleware/proxy injects the duplicate, rename your parameter or strip the injected copy at the gateway
  4. If you genuinely need multi-value semantics, switch the handler off get_query_param to a multi-value-aware parser

Example fix

# before
GET /v1/metrics?seconds=60&seconds=120   -> 400 param seconds specified more than once

# after (single value, or identical duplicates)
GET /v1/metrics?seconds=60
GET /v1/metrics?seconds=60&seconds=60   -> accepted
Defensive patterns

Strategy: validation

Validate before calling

use hyper::{Body, Request};
use url::form_urlencoded;

/// Fails fast with a clear message when `param_name` appears twice with different values.
pub fn check_no_conflicting_query_param(
    request: &Request<Body>,
    param_name: &str,
) -> Result<(), String> {
    let Some(query) = request.uri().query() else {
        return Ok(());
    };
    let mut values = form_urlencoded::parse(query.as_bytes())
        .filter(|(k, _)| k == param_name)
        .map(|(_, v)| v.into_owned());
    let Some(first) = values.next() else {
        return Ok(());
    };
    if values.any(|v| v != first) {
        return Err(format!(
            "param {param_name} specified more than once with different values; first was {first:?}"
        ));
    }
    Ok(())
}

Try / catch

match get_query_param(request, "seconds") {
    Err(ApiError::BadRequest(e)) if e.to_string().contains("specified more than once") => {
        // 400 to the client, but log the full URI to find who duplicates the param
        tracing::warn!(uri = %request.uri(), "conflicting duplicate query param");
        return Err(ApiError::BadRequest(e));
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/442a9f6c93c3b879. Report an issue: GitHub.