neondatabase/neon · warning · ApiError

failed to parse {param_name}

Error message

failed to parse {param_name}

What it means

Returned as HTTP 400 BadRequest by parse_request_param in neon's http-utils when the path parameter exists but T::from_str fails on its value. The generic is instantiated per endpoint with types like TenantId/TimelineId (UUID-parsing newtypes), NodeId, or unsigned integers, so this is the client-facing 'malformed identifier in the URL' error.

Source

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

pub fn get_request_param<'a>(
    request: &'a Request<Body>,
    param_name: &str,
) -> Result<&'a str, ApiError> {
    match request.param(param_name) {
        Some(arg) => Ok(arg),
        None => Err(ApiError::BadRequest(anyhow!(
            "no {param_name} specified in path param",
        ))),
    }
}

pub fn parse_request_param<T: FromStr>(
    request: &Request<Body>,
    param_name: &str,
) -> Result<T, ApiError> {
    match get_request_param(request, param_name)?.parse() {
        Ok(v) => Ok(v),
        Err(_) => Err(ApiError::BadRequest(anyhow!(
            "failed to parse {param_name}",
        ))),
    }
}

pub fn get_query_param<'a>(
    request: &'a Request<Body>,
    param_name: &str,
) -> Result<Option<Cow<'a, str>>, ApiError> {
    let query = match request.uri().query() {
        Some(q) => q,
        None => return Ok(None),
    };
    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();

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Check the exact value you put in that URL segment against the expected type (canonical UUID for tenant/timeline IDs, non-negative integer for numeric params)
  2. Validate IDs client-side before building the URL (UUID parse for tenant/timeline)
  3. If you truly have a non-UUID identifier, first resolve it to the tenant's ID via the listing endpoints, then use the ID in the path

Example fix

# before
curl http://localhost:9898/v1/tenant/my-tenant/timeline   # 400 failed to parse tenant_id

# after
curl http://localhost:9898/v1/tenant/3d982a9f-8b0f-4c2a-b1a6-1f2b6c9ab7ee/timeline
Defensive patterns

Strategy: validation

Validate before calling

// Validate IDs before building the URL:
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function tenantUrl(base, tenantId) {
  if (!UUID_RE.test(tenantId)) throw new Error(`invalid tenant id: ${tenantId}`);
  return `${base}/v1/tenant/${tenantId}`;
}

Type guard

function isUuid(s) {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(s));
}

Try / catch

// Map the 400 to a clearer client-side message naming the param:
const resp = await fetch(url);
if (resp.status === 400) {
  const msg = await resp.text();
  const m = msg.match(/failed to parse (\w+)/);
  throw new Error(m ? `malformed ${m[1]} in URL path` : msg);
}

Prevention

When it happens

Trigger: GET /v1/tenant/not-a-uuid/timeline — the :tenant_id segment fails TenantId::from_str; also negative or non-numeric values where u32/u64 is expected (e.g. /v1/node/-1), or a UUID with wrong hyphenation/length.

Common situations: Clients passing internal integer IDs where a UUID is required; truncated or whitespace-padded IDs from env vars; copy-paste of timeline IDs missing characters; using a tenant name instead of its ID in the URL.

Understand the failure class

Related errors


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