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
- 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)
- Validate IDs client-side before building the URL (UUID parse for tenant/timeline)
- 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
- Parse-check all IDs (UUID/int) client-side before constructing paths
- Use typed ID wrappers (TenantId/TimelineId) in your client code, not raw strings
- Resolve names to IDs via listing endpoints instead of guessing path values
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- no {param_name} specified in path param
- missing request body
- Failed to parse json request: {e}
- invalid format {format}
- duration must be 1-60 secs
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/c21a294169c47eb2.
Report an issue: GitHub.