{"record":{"id":"ba597e2b4d5c6da6","repo":"PrefectHQ/fastmcp","slug":"uri-template-parameters-seen-normalized-and","errorCode":null,"errorMessage":"URI template parameters '{seen[normalized]}' and '{raw_name}' both normalize to '{normalized}'. Use one or the other, not both.","messagePattern":"URI template parameters '(.+?)' and '(.+?)' both normalize to '(.+?)'\\. Use one or the other, not both\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/resources/template.py","lineNumber":473,"sourceCode":"            if param.kind == inspect.Parameter.VAR_POSITIONAL:\n                raise ValueError(\n                    \"Functions with *args are not supported as resource templates\"\n                )\n\n        # Extract path and query parameters from URI template.\n        # Allow hyphens in names and normalize to underscores so they\n        # match Python function parameter names.\n        raw_path_params = set(re.findall(r\"{([\\w-]+)(?:\\*)?}\", uri_template))\n        raw_query_params = extract_query_params(uri_template)\n\n        # Detect collisions: two raw param names that normalize to the\n        # same Python identifier (e.g. {user-id} and {user_id}).\n        all_raw = raw_path_params | raw_query_params\n        seen: dict[str, str] = {}\n        for raw_name in sorted(all_raw):\n            normalized = raw_name.replace(\"-\", \"_\")\n            if normalized in seen:\n                raise ValueError(\n                    f\"URI template parameters '{seen[normalized]}' and \"\n                    f\"'{raw_name}' both normalize to '{normalized}'. \"\n                    f\"Use one or the other, not both.\"\n                )\n            seen[normalized] = raw_name\n\n        path_params = {p.replace(\"-\", \"_\") for p in raw_path_params}\n        query_params = {p.replace(\"-\", \"_\") for p in raw_query_params}\n        all_uri_params = path_params | query_params\n\n        if not all_uri_params:\n            raise ValueError(\"URI template must contain at least one parameter\")\n\n        # Use wrapper to get user-facing parameters (excludes injected params)\n        from fastmcp.server.dependencies import (\n            transform_context_annotations,\n            without_injected_parameters,\n        )","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/resources/template.py#L455-L491","documentation":"URI template parameters are normalized (hyphens become underscores) so they match Python identifiers. If two raw parameters like {user-id} and {user_id} normalize to the same Python name, the binding would be ambiguous, so from_function raises at registration. RFC 6570 allows hyphens in names but Python functions cannot use them, hence the normalization collision check.","triggerScenarios":"Registering a template whose URI contains both {x-y} and {x_y} (in path and/or query parts of the template), e.g. uri_template=\"api://{user-id}?ref={user_id}\".","commonSituations":"Hand-written URI templates mixing kebab-case and snake_case for the same concept; concatenating template strings from different sources; copying a URL path style into a template alongside Python-style params.","solutions":["Remove the duplicate parameter and use one spelling consistently","Rename one of the two parameters so they normalize to distinct identifiers","Use hyphenated spelling in the URI and underscored name in the function (that mapping is supported)"],"exampleFix":"// before\nuri_template=\"report://{user-id}?sort={user_id}\"\n// after\nuri_template=\"report://{user_id}?sort={sort_key}\"","handlingStrategy":"validation","validationCode":"import re\ndef check_no_collisions(uri_template):\n    raw = set(re.findall(r\"\\{([^}]+)\\}\", uri_template))\n    seen = {}\n    for r in raw:\n        n = r.replace(\"-\", \"_\")\n        if n in seen:\n            raise ValueError(f\"{seen[n]} and {r} both normalize to {n}\")\n        seen[n] = r","typeGuard":"def uri_params_unique(uri_template) -> bool:\n    import re\n    raw = re.findall(r\"\\{([^}]+)\\}\", uri_template)\n    norms = [r.replace(\"-\", \"_\") for r in raw]\n    return len(norms) == len(set(norms))","tryCatchPattern":"try:\n    tpl = FunctionResourceTemplate.from_function(fn, uri_template=uri)\nexcept ValueError as e:\n    if \"both normalize to\" in str(e):\n        raise ValueError(f\"Fix duplicate URI params in {uri!r}\") from e\n    raise","preventionTips":["Pick one spelling convention (snake_case) per template","Lint URI templates for duplicate placeholders before registering","Keep URI templates as named constants, not ad-hoc strings"],"tags":["uri-template","validation","naming"],"backgroundTag":"uri-template-parameter-collision","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}