Skip to content

Commit ffa2ee1

Browse files
authored
Merge pull request #589 from jlowin/content-length
Ensure content-length is always stripped from client headers
2 parents 569798c + f8b52e7 commit ffa2ee1

File tree

5 files changed

+65
-74
lines changed

5 files changed

+65
-74
lines changed

docs/patterns/http-requests.mdx

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ from fastmcp import FastMCP
2323
from fastmcp.server.dependencies import get_http_request
2424
from starlette.requests import Request
2525

26-
mcp = FastMCP(name="HTTPRequestDemo")
26+
mcp = FastMCP(name="HTTP Request Demo")
2727

2828
@mcp.tool()
2929
async def user_agent_info() -> dict:
@@ -48,32 +48,40 @@ This approach works anywhere within a request's execution flow, not just within
4848
2. You're calling nested functions that need HTTP request data
4949
3. You're working with middleware or other request processing code
5050

51-
## Important Notes
52-
53-
- HTTP requests are only available when FastMCP is running as part of a web application
54-
- Accessing the HTTP request outside of a web request context will raise a `RuntimeError`
55-
- The `get_http_request()` function returns a standard [Starlette Request](https://www.starlette.io/requests/) object
51+
## Accessing HTTP Headers Only
5652

57-
## Common Use Cases
53+
If you only need request headers and want to avoid potential errors, you can use the `get_http_headers()` helper:
5854

59-
### Accessing Request Headers
55+
```python {2}
56+
from fastmcp import FastMCP
57+
from fastmcp.server.dependencies import get_http_headers
6058

61-
```python
62-
from fastmcp.server.dependencies import get_http_request
59+
mcp = FastMCP(name="Headers Demo")
6360

6461
@mcp.tool()
65-
async def get_auth_info() -> dict:
66-
"""Get authentication information from request headers."""
67-
request = get_http_request()
62+
async def safe_header_info() -> dict:
63+
"""Safely get header information without raising errors."""
64+
# Get headers (returns empty dict if no request context)
65+
headers = get_http_headers()
6866

6967
# Get authorization header
70-
auth_header = request.headers.get("authorization", "")
71-
72-
# Check for Bearer token
68+
auth_header = headers.get("authorization", "")
7369
is_bearer = auth_header.startswith("Bearer ")
7470

7571
return {
72+
"user_agent": headers.get("user-agent", "Unknown"),
73+
"content_type": headers.get("content-type", "Unknown"),
7674
"has_auth": bool(auth_header),
77-
"auth_type": "Bearer" if is_bearer else "Other" if auth_header else "None"
75+
"auth_type": "Bearer" if is_bearer else "Other" if auth_header else "None",
76+
"headers_count": len(headers)
7877
}
7978
```
79+
80+
By default, `get_http_headers()` excludes problematic headers like `content-length`. To include all headers, use `get_http_headers(include_all=True)`.
81+
82+
## Important Notes
83+
84+
- HTTP requests are only available when FastMCP is running as part of a web application
85+
- Accessing the HTTP request with `get_http_request()` outside of a web request context will raise a `RuntimeError`
86+
- The `get_http_headers()` function **never raises errors** - it returns an empty dict when no request context is available
87+
- The `get_http_request()` function returns a standard [Starlette Request](https://www.starlette.io/requests/) object

src/fastmcp/client/transports.py

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from typing_extensions import Unpack
2626

2727
from fastmcp.server import FastMCP as FastMCPServer
28-
from fastmcp.server.dependencies import get_http_request
28+
from fastmcp.server.dependencies import get_http_headers
2929
from fastmcp.server.server import FastMCP
3030
from fastmcp.utilities.logging import get_logger
3131
from fastmcp.utilities.mcp_config import MCPConfig, infer_transport_type_from_url
@@ -35,11 +35,6 @@
3535

3636
logger = get_logger(__name__)
3737

38-
# these headers, when forwarded to the remote server, can cause issues
39-
EXCLUDE_HEADERS = {
40-
"content-length",
41-
}
42-
4338

4439
class SessionKwargs(TypedDict, total=False):
4540
"""Keyword arguments for the MCP ClientSession constructor."""
@@ -138,23 +133,12 @@ def __init__(
138133
async def connect_session(
139134
self, **session_kwargs: Unpack[SessionKwargs]
140135
) -> AsyncIterator[ClientSession]:
141-
client_kwargs: dict[str, Any] = {
142-
"headers": self.headers,
143-
}
136+
client_kwargs: dict[str, Any] = {}
144137

145138
# load headers from an active HTTP request, if available. This will only be true
146139
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
147140
# need to be forwarded to the remote server.
148-
try:
149-
active_request = get_http_request()
150-
for name, value in active_request.headers.items():
151-
name = name.lower()
152-
if name not in self.headers and name not in {
153-
h.lower() for h in EXCLUDE_HEADERS
154-
}:
155-
client_kwargs["headers"][name] = str(value)
156-
except RuntimeError:
157-
client_kwargs["headers"] = self.headers
141+
client_kwargs["headers"] = get_http_headers() | self.headers
158142

159143
# sse_read_timeout has a default value set, so we can't pass None without overriding it
160144
# instead we simply leave the kwarg out if it's not provided
@@ -201,25 +185,12 @@ def __init__(
201185
async def connect_session(
202186
self, **session_kwargs: Unpack[SessionKwargs]
203187
) -> AsyncIterator[ClientSession]:
204-
client_kwargs: dict[str, Any] = {
205-
"headers": self.headers,
206-
}
188+
client_kwargs: dict[str, Any] = {}
207189

208190
# load headers from an active HTTP request, if available. This will only be true
209191
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
210192
# need to be forwarded to the remote server.
211-
try:
212-
active_request = get_http_request()
213-
for name, value in active_request.headers.items():
214-
name = name.lower()
215-
if name not in self.headers and name not in {
216-
h.lower() for h in EXCLUDE_HEADERS
217-
}:
218-
client_kwargs["headers"][name] = str(value)
219-
220-
except RuntimeError:
221-
client_kwargs["headers"] = self.headers
222-
print(client_kwargs)
193+
client_kwargs["headers"] = get_http_headers() | self.headers
223194

224195
# sse_read_timeout has a default value set, so we can't pass None without overriding it
225196
# instead we simply leave the kwarg out if it's not provided

src/fastmcp/server/dependencies.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,35 @@ def get_http_request() -> Request:
3333
if request is None:
3434
raise RuntimeError("No active HTTP request found.")
3535
return request
36+
37+
38+
def get_http_headers(include_all: bool = False) -> dict[str, str]:
39+
"""
40+
Extract headers from the current HTTP request if available.
41+
42+
Never raises an exception, even if there is no active HTTP request (in which case
43+
an empty dict is returned).
44+
45+
By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients.
46+
If `include_all` is True, all headers are returned.
47+
"""
48+
if include_all:
49+
exclude_headers = set()
50+
else:
51+
exclude_headers = {"content-length"}
52+
53+
# ensure all lowercase!
54+
# (just in case)
55+
exclude_headers = {h.lower() for h in exclude_headers}
56+
57+
headers = {}
58+
59+
try:
60+
request = get_http_request()
61+
for name, value in request.headers.items():
62+
lower_name = name.lower()
63+
if lower_name not in exclude_headers:
64+
headers[lower_name] = str(value)
65+
return headers
66+
except RuntimeError:
67+
return {}

src/fastmcp/server/openapi.py

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from fastmcp.exceptions import ToolError
2020
from fastmcp.resources import Resource, ResourceTemplate
21-
from fastmcp.server.dependencies import get_http_request
21+
from fastmcp.server.dependencies import get_http_headers
2222
from fastmcp.server.server import FastMCP
2323
from fastmcp.tools.tool import Tool, _convert_to_content
2424
from fastmcp.utilities import openapi
@@ -60,25 +60,6 @@ def _slugify(text: str) -> str:
6060
return slug
6161

6262

63-
def _get_mcp_client_headers() -> dict[str, str]:
64-
"""
65-
Extract headers from the current MCP client HTTP request if available.
66-
67-
These headers will take precedence over OpenAPI-defined headers when both are present.
68-
69-
Returns:
70-
Dictionary of header name-value pairs (lowercased names), or empty dict if no HTTP request is active.
71-
"""
72-
try:
73-
http_request = get_http_request()
74-
return {
75-
name.lower(): str(value) for name, value in http_request.headers.items()
76-
}
77-
except RuntimeError:
78-
# No active HTTP request (e.g., STDIO transport), return empty dict
79-
return {}
80-
81-
8263
# Type definitions for the mapping functions
8364
RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
8465
ComponentFn = Callable[
@@ -423,7 +404,7 @@ async def _execute_request(self, *args, **kwargs):
423404
headers.update(openapi_headers)
424405

425406
# Add headers from the current MCP client HTTP request (these take precedence)
426-
mcp_headers = _get_mcp_client_headers()
407+
mcp_headers = get_http_headers()
427408
headers.update(mcp_headers)
428409

429410
# Prepare request body
@@ -574,7 +555,7 @@ async def read(self) -> str | bytes:
574555

575556
# Prepare headers from MCP client request if available
576557
headers = {}
577-
mcp_headers = _get_mcp_client_headers()
558+
mcp_headers = get_http_headers()
578559
headers.update(mcp_headers)
579560

580561
response = await self._client.request(

tests/client/test_openapi.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ async def test_client_headers_proxy(self, proxy_server: str):
185185
Test that client headers are passed through the proxy to the remove server.
186186
"""
187187
async with Client(transport=StreamableHttpTransport(proxy_server)) as client:
188-
await client.ping()
189188
result = await client.read_resource("resource://get_headers_headers_get")
190189
assert isinstance(result[0], TextResourceContents)
191190
headers = json.loads(result[0].text)

0 commit comments

Comments
 (0)