mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-26 23:44:17 +02:00
fix: handle re.error from malformed URI templates in build_regex (#3501)
This commit is contained in:
parent
d5d5c45be3
commit
5ff64ce21b
2 changed files with 70 additions and 3 deletions
|
|
@ -44,13 +44,16 @@ def extract_query_params(uri_template: str) -> set[str]:
|
|||
return set()
|
||||
|
||||
|
||||
def build_regex(template: str) -> re.Pattern:
|
||||
def build_regex(template: str) -> re.Pattern[str] | None:
|
||||
"""Build regex pattern for URI template, handling RFC 6570 syntax.
|
||||
|
||||
Supports:
|
||||
- `{var}` - simple path parameter
|
||||
- `{var*}` - wildcard path parameter (captures multiple segments)
|
||||
- `{?var1,var2}` - query parameters (ignored in path matching)
|
||||
|
||||
Returns None if the template produces an invalid regex (e.g. parameter
|
||||
names with hyphens, leading digits, or duplicates from a remote server).
|
||||
"""
|
||||
# Remove query parameter syntax for path matching
|
||||
template_without_query = re.sub(r"\{\?[^}]+\}", "", template)
|
||||
|
|
@ -67,7 +70,10 @@ def build_regex(template: str) -> re.Pattern:
|
|||
pattern += f"(?P<{name}>[^/]+)"
|
||||
else:
|
||||
pattern += re.escape(part)
|
||||
return re.compile(f"^{pattern}$")
|
||||
try:
|
||||
return re.compile(f"^{pattern}$")
|
||||
except re.error:
|
||||
return None
|
||||
|
||||
|
||||
def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
|
||||
|
|
@ -82,6 +88,8 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
|
|||
|
||||
# Match path parameters
|
||||
regex = build_regex(uri_template)
|
||||
if regex is None:
|
||||
return None
|
||||
match = regex.match(uri_path)
|
||||
if not match:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from pydantic import BaseModel
|
|||
from fastmcp import Context, FastMCP
|
||||
from fastmcp.resources import ResourceTemplate
|
||||
from fastmcp.resources.function_resource import FunctionResource
|
||||
from fastmcp.resources.template import match_uri_template
|
||||
from fastmcp.resources.template import build_regex, match_uri_template
|
||||
|
||||
|
||||
class TestResourceTemplate:
|
||||
|
|
@ -747,3 +747,62 @@ class TestContextHandling:
|
|||
# read() returns the raw value
|
||||
result = await resource.read()
|
||||
assert result == "item: 42"
|
||||
|
||||
|
||||
class TestMalformedURITemplates:
|
||||
"""Test that malformed URI templates from remote servers don't crash."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"template",
|
||||
[
|
||||
"test://{bad-name}/path",
|
||||
"test://{hyphen-param}/{other-param}/path",
|
||||
"test://{1leading}/path",
|
||||
"test://{123}/path",
|
||||
],
|
||||
ids=[
|
||||
"hyphen_in_name",
|
||||
"multiple_hyphens",
|
||||
"leading_digit",
|
||||
"all_digits",
|
||||
],
|
||||
)
|
||||
def test_build_regex_returns_none_for_invalid_group_names(self, template: str):
|
||||
assert build_regex(template) is None
|
||||
|
||||
def test_build_regex_returns_none_for_duplicate_group_names(self):
|
||||
assert build_regex("test://{a}/{a}/path") is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"template",
|
||||
[
|
||||
"test://{bad-name}/path",
|
||||
"test://{a}/{a}/path",
|
||||
"test://{1leading}/path",
|
||||
],
|
||||
ids=[
|
||||
"hyphen_in_name",
|
||||
"duplicate_groups",
|
||||
"leading_digit",
|
||||
],
|
||||
)
|
||||
def test_match_uri_template_returns_none_for_malformed_templates(
|
||||
self, template: str
|
||||
):
|
||||
assert match_uri_template("test://anything/path", template) is None
|
||||
|
||||
def test_resource_template_matches_returns_none_for_malformed_template(self):
|
||||
template = ResourceTemplate(
|
||||
uri_template="test://{bad-name}/path",
|
||||
name="test",
|
||||
parameters={},
|
||||
)
|
||||
assert template.matches("test://anything/path") is None
|
||||
|
||||
def test_build_regex_still_works_for_valid_templates(self):
|
||||
regex = build_regex("test://{name}/{id}")
|
||||
assert regex is not None
|
||||
match = regex.match("test://foo/123")
|
||||
assert match is not None
|
||||
assert match.group("name") == "foo"
|
||||
assert match.group("id") == "123"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue