Skip to content

Commit 52ade43

Browse files
committed
1) Fixed typo
2) Made the headers and status_code follow RFC 9110 3) Fixed corresponding tests Signed-off-by: Jitesh Nair <jiteshnair@ibm.com>
1 parent 0d3812d commit 52ade43

File tree

4 files changed

+110
-16
lines changed

4 files changed

+110
-16
lines changed

mcpgateway/main.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from functools import lru_cache
3434
import hashlib
3535
import html
36+
import re
3637
import sys
3738
from typing import Any, AsyncIterator, Dict, List, Optional, Union
3839
from urllib.parse import urlparse, urlunparse
@@ -87,7 +88,7 @@
8788
from mcpgateway.middleware.validation_middleware import ValidationMiddleware
8889
from mcpgateway.observability import init_telemetry
8990
from mcpgateway.plugins.framework import PluginError, PluginManager, PluginViolationError
90-
from mcpgateway.plugins.framework.constants import PLUGIN_VIOLATION_CODE_MAPPING, PluginViolationCode
91+
from mcpgateway.plugins.framework.constants import PLUGIN_VIOLATION_CODE_MAPPING, PluginViolationCode, VALID_HTTP_STATUS_CODES
9192
from mcpgateway.routers.server_well_known import router as server_well_known_router
9293
from mcpgateway.routers.well_known import router as well_known_router
9394
from mcpgateway.schemas import (
@@ -1445,6 +1446,51 @@ async def database_exception_handler(_request: Request, exc: IntegrityError):
14451446
return ORJSONResponse(status_code=409, content=ErrorFormatter.format_database_error(exc))
14461447

14471448

1449+
def _validate_http_headers(headers: dict[str, str]) -> Optional[dict[str, str]]:
1450+
"""Validate headers according to RFC 9110.
1451+
1452+
Args:
1453+
headers: dict of headers
1454+
1455+
Returns:
1456+
Optional[dict[str, str]]: dictionary of valid headers
1457+
1458+
Rules enforced:
1459+
- Header name must match RFC 9110 'token'.
1460+
- No whitespace before colon (enforced by dictionary usage).
1461+
- Header value must not contain CTL characters (0x00–0x1F, 0x7F).
1462+
"""
1463+
1464+
# RFC 9110 'token' definition:
1465+
# token = 1*tchar
1466+
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
1467+
# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
1468+
# / DIGIT / ALPHA
1469+
header_key = re.compile(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$")
1470+
validated: dict[str, str] = {}
1471+
for key, value in headers.items():
1472+
# Validate header name (RFC 9110)
1473+
if not re.match(header_key, key):
1474+
logger.warning(f"Invalid header name: {key}")
1475+
continue
1476+
# Validate header value (no CRLF)
1477+
if "\r" in value or "\n" in value:
1478+
logger.warning(f"Header value contains CRLF: {key}")
1479+
continue
1480+
# RFC 9110: Reject CTLs (0x00–0x1F, 0x7F). Allow SP (0x20) and HTAB (0x09).
1481+
# Further structure (quoted-string, lists, parameters) is left to higher-level parsers.
1482+
valid = True
1483+
for ch in value:
1484+
code = ord(ch)
1485+
if (0 <= code <= 31 or code == 127) and code not in (9, 32):
1486+
valid = False
1487+
break
1488+
if not valid:
1489+
continue
1490+
validated[key] = value
1491+
return validated if validated else None
1492+
1493+
14481494
@app.exception_handler(PluginViolationError)
14491495
async def plugin_violation_exception_handler(_request: Request, exc: PluginViolationError):
14501496
"""Handle plugins violations globally.
@@ -1506,7 +1552,7 @@ async def plugin_violation_exception_handler(_request: Request, exc: PluginViola
15061552

15071553
# Use HTTP status code from violation if present (e.g., 429 for rate limiting)
15081554
http_status = exc.violation.http_status_code if exc.violation.http_status_code else None
1509-
if http_status and not 400 <= http_status <= 599:
1555+
if http_status and not VALID_HTTP_STATUS_CODES.get(http_status):
15101556
logger.warning(f"Invalid HTTP status code {http_status} from violation, defaulting to 200")
15111557
http_status = None
15121558
if not http_status:
@@ -1524,7 +1570,9 @@ async def plugin_violation_exception_handler(_request: Request, exc: PluginViola
15241570

15251571
response = ORJSONResponse(status_code=http_status, content={"error": json_rpc_error.model_dump()})
15261572
if headers:
1527-
response.headers.update(headers)
1573+
validatated_headers = _validate_http_headers(headers)
1574+
if validatated_headers:
1575+
response.headers.update(validatated_headers)
15281576
return response
15291577

15301578

mcpgateway/plugins/framework/constants.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
# Standard
1212

13+
# Standard
1314
from dataclasses import dataclass
1415
from types import MappingProxyType
1516
from typing import Mapping
@@ -54,7 +55,7 @@ class PluginViolationCode:
5455
"""
5556
Plugin violation codes as an immutable dataclass object.
5657
57-
Provide Maping for violation codes to their corresponding HTTP status codes for proper error responses.
58+
Provide Mapping for violation codes to their corresponding HTTP status codes for proper error responses.
5859
"""
5960

6061
code: int
@@ -89,3 +90,48 @@ class PluginViolationCode:
8990
"PROCESSING_ERROR": PluginViolationCode(500, "PROCESSING_ERROR", "Used when processing encounters an error"),
9091
}
9192
)
93+
94+
VALID_HTTP_STATUS_CODES: dict[int, str] = { # RFC 9110
95+
# 4xx — Client Error
96+
400: "Bad Request",
97+
401: "Unauthorized",
98+
402: "Payment Required",
99+
403: "Forbidden",
100+
404: "Not Found",
101+
405: "Method Not Allowed",
102+
406: "Not Acceptable",
103+
407: "Proxy Authentication Required",
104+
408: "Request Timeout",
105+
409: "Conflict",
106+
410: "Gone",
107+
411: "Length Required",
108+
412: "Precondition Failed",
109+
413: "Content Too Large", # (was "Payload Too Large" before RFC 9110)
110+
414: "URI Too Long",
111+
415: "Unsupported Media Type",
112+
416: "Range Not Satisfiable",
113+
417: "Expectation Failed",
114+
418: "(Unused)",
115+
421: "Misdirected Request",
116+
422: "Unprocessable Content", # (was "Unprocessable Entity")
117+
423: "Locked",
118+
424: "Failed Dependency",
119+
425: "Too Early",
120+
426: "Upgrade Required",
121+
428: "Precondition Required",
122+
429: "Too Many Requests",
123+
431: "Request Header Fields Too Large",
124+
451: "Unavailable For Legal Reasons",
125+
# 5xx — Server Error
126+
500: "Internal Server Error",
127+
501: "Not Implemented",
128+
502: "Bad Gateway",
129+
503: "Service Unavailable",
130+
504: "Gateway Timeout",
131+
505: "HTTP Version Not Supported",
132+
506: "Variant Also Negotiates",
133+
507: "Insufficient Storage",
134+
508: "Loop Detected",
135+
510: "Not Extended",
136+
511: "Network Authentication Required",
137+
}

plugins/rate_limiter/rate_limiter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def _select_most_restrictive(
207207
"limited": True,
208208
"remaining": remaining,
209209
"reset_in": retry_after,
210-
"dimensions": [m for _, _, _, m in allowed_dims],
210+
"dimensions": {"allowed": [m for _, _, _, m in allowed_dims]},
211211
}
212212
return True, limit, remaining, reset_ts, aggregated_meta
213213

tests/unit/mcpgateway/test_main.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3383,7 +3383,7 @@ def test_plugin_violation_invalid_http_status_code_below_range(self):
33833383
assert content["error"]["code"] == -32602
33843384

33853385
def test_plugin_violation_invalid_http_status_code_above_range(self):
3386-
"""Test that invalid HTTP status code above 599 defaults to None and uses mapping."""
3386+
"""Test that invalid HTTP status code above 511 defaults to None and uses mapping."""
33873387
# Standard
33883388
import asyncio
33893389

@@ -3396,7 +3396,7 @@ def test_plugin_violation_invalid_http_status_code_above_range(self):
33963396
reason="Invalid status",
33973397
description="Status code above valid range",
33983398
code="RATE_LIMIT", # Has mapping to 429
3399-
http_status_code=600, # Invalid: above 599
3399+
http_status_code=512 # Invalid: above 511
34003400
)
34013401
exc = PluginViolationError(message="Invalid status", violation=violation)
34023402

@@ -3421,7 +3421,7 @@ def test_plugin_violation_invalid_http_status_code_no_mapping_fallback(self):
34213421
reason="Invalid status",
34223422
description="Status code invalid, no mapping",
34233423
code="UNKNOWN_CODE", # Not in mapping
3424-
http_status_code=1000, # Invalid: way above 599
3424+
http_status_code=1000, # Invalid: way above 511
34253425
)
34263426
exc = PluginViolationError(message="Invalid status", violation=violation)
34273427

@@ -3433,7 +3433,7 @@ def test_plugin_violation_invalid_http_status_code_no_mapping_fallback(self):
34333433
assert content["error"]["code"] == -32602
34343434

34353435
def test_plugin_violation_valid_http_status_code_edge_cases(self):
3436-
"""Test that valid edge case HTTP status codes (400, 599) are accepted."""
3436+
"""Test that valid edge case HTTP status codes (400, 511) are accepted."""
34373437
# Standard
34383438
import asyncio
34393439

@@ -3453,16 +3453,16 @@ def test_plugin_violation_valid_http_status_code_edge_cases(self):
34533453
result_400 = asyncio.run(plugin_violation_exception_handler(None, exc_400))
34543454
assert result_400.status_code == 400
34553455

3456-
# Test upper boundary (599)
3457-
violation_599 = PluginViolation(
3456+
# Test upper boundary (511)
3457+
violation_511 = PluginViolation(
34583458
reason="Network error",
3459-
description="Valid status 599",
3459+
description="Valid status 511",
34603460
code="ERROR",
3461-
http_status_code=599, # Valid: exactly 599
3461+
http_status_code=511, # Valid: exactly 511
34623462
)
3463-
exc_599 = PluginViolationError(message="Status 599", violation=violation_599)
3464-
result_599 = asyncio.run(plugin_violation_exception_handler(None, exc_599))
3465-
assert result_599.status_code == 599
3463+
exc_511 = PluginViolationError(message="Status 511", violation=violation_511)
3464+
result_511 = asyncio.run(plugin_violation_exception_handler(None, exc_511))
3465+
assert result_511.status_code == 511
34663466

34673467
def test_plugin_exception_handler_with_full_error(self):
34683468
"""Test plugin_exception_handler with complete error details."""

0 commit comments

Comments
 (0)