Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ dependencies = [
"typer>=0.16,<0.17",
"questionary>=2.0.1,<3",
"rich>=13.9.2,<14",
"fastapi>=0.115.0,<0.116",
"fastapi>=0.115.0", # upper bound removed — CVE-2025-62727 fix needs starlette>=0.49.1
"starlette>=0.49.1,<1.0.0", # CVE-2025-62727 (HIGH) — floor 0.49.1 patches vuln; cap <1.0.0 avoids BaseHTTPMiddleware streaming regression
"tornado>=6.5.5", # CVE-2026-31958 (HIGH) — floor pin ensures patched release
"uvicorn>=0.31.1",
"watchfiles>=0.24.0,<1.0",
"python-on-whales>=0.73.0,<0.74",
Expand Down
32 changes: 20 additions & 12 deletions src/agentex/lib/sdk/fastacp/base/base_acp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

import uvicorn
from fastapi import FastAPI, Request
from pydantic import TypeAdapter, ValidationError
from fastapi.responses import StreamingResponse
from starlette.middleware.base import BaseHTTPMiddleware
from pydantic import TypeAdapter, ValidationError
from starlette.types import ASGIApp, Receive, Scope, Send

from agentex.lib.types.acp import (
RPC_SYNC_METHODS,
Expand Down Expand Up @@ -43,17 +43,25 @@
task_message_update_adapter = TypeAdapter(TaskMessageUpdate)


class RequestIDMiddleware(BaseHTTPMiddleware):
"""Middleware to extract or generate request IDs and add them to logs and response headers"""
class RequestIDMiddleware:
"""Pure ASGI middleware to extract or generate request IDs and set them in the logging context.

Implemented as a pure ASGI middleware (rather than BaseHTTPMiddleware) so that it never
buffers the response body. BaseHTTPMiddleware's call_next() silently swallows
StreamingResponse bodies in several starlette versions, which caused message/send handlers
to return result=null through the Agentex server proxy.
"""

def __init__(self, app: ASGIApp) -> None:
self.app = app

async def dispatch(self, request: Request, call_next): # type: ignore[override]
# Extract request ID from header or generate a new one if there isn't one
request_id = request.headers.get("x-request-id") or uuid.uuid4().hex
# Store request ID in request state for access in handlers
ctx_var_request_id.set(request_id)
# Process request
response = await call_next(request)
return response
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
headers = dict(scope.get("headers", []))
raw_request_id = headers.get(b"x-request-id", b"")
request_id = raw_request_id.decode() if raw_request_id else uuid.uuid4().hex
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 decode() can raise UnicodeDecodeError on malformed headers

raw_request_id.decode() uses UTF-8 by default. If a client (or an upstream proxy) sends a non-UTF-8 byte sequence in the x-request-id header, this will raise an unhandled UnicodeDecodeError that propagates through the ASGI stack, causing a 500 for the request.

The HTTP/1.1 spec (RFC 7230) specifies that header field values are ISO-8859-1 / Latin-1, so decode('latin-1') is both spec-correct and will never raise an exception (every byte sequence is valid Latin-1).

Suggested change
request_id = raw_request_id.decode() if raw_request_id else uuid.uuid4().hex
request_id = raw_request_id.decode("latin-1") if raw_request_id else uuid.uuid4().hex
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agentex/lib/sdk/fastacp/base/base_acp_server.py
Line: 62

Comment:
**`decode()` can raise `UnicodeDecodeError` on malformed headers**

`raw_request_id.decode()` uses UTF-8 by default. If a client (or an upstream proxy) sends a non-UTF-8 byte sequence in the `x-request-id` header, this will raise an unhandled `UnicodeDecodeError` that propagates through the ASGI stack, causing a 500 for the request.

The HTTP/1.1 spec (RFC 7230) specifies that header field values are ISO-8859-1 / Latin-1, so `decode('latin-1')` is both spec-correct and will never raise an exception (every byte sequence is valid Latin-1).

```suggestion
            request_id = raw_request_id.decode("latin-1") if raw_request_id else uuid.uuid4().hex
```

How can I resolve this? If you propose a fix, please make it concise.

ctx_var_request_id.set(request_id)
await self.app(scope, receive, send)


class BaseACPServer(FastAPI):
Expand Down
93 changes: 73 additions & 20 deletions src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,20 @@ async def mock_handler(params):
"jsonrpc": "2.0",
"method": "event/send",
"params": {
"task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"},
"message": {
"type": "text",
"author": "user",
"content": "test message",
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task"},
"event": {
"id": "evt-1",
"agent_id": "test-agent-456",
"sequence_id": 1,
"task_id": "test-task",
},
},
"id": "test-1",
Expand Down Expand Up @@ -218,11 +227,20 @@ async def mock_handler(params):
"jsonrpc": "2.0",
"method": "event/send",
"params": {
"task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"},
"message": {
"type": "text",
"author": "user",
"content": "test message",
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task"},
"event": {
"id": "evt-1",
"agent_id": "test-agent-456",
"sequence_id": 1,
"task_id": "test-task",
},
},
# No ID = notification
Expand Down Expand Up @@ -256,7 +274,17 @@ async def mock_handler(params):
request = {
"jsonrpc": "2.0",
"method": "task/cancel",
"params": {"task_id": "test-task-123"},
"params": {
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task-123"},
},
"id": "test-request-1",
}

Expand All @@ -280,7 +308,7 @@ def test_send_message_synchronous_response(self, base_acp_server):
async def mock_execute_handler(params):
return {
"task_id": params.task.id,
"message_content": params.message.content,
"message_content": params.content.content,
"status": "executed_synchronously",
"custom_data": {"processed": True, "timestamp": "2024-01-01T12:00:00Z"},
}
Expand All @@ -291,8 +319,16 @@ async def mock_execute_handler(params):
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"task": {"id": "test-task-123", "agent_id": "test-agent", "status": "RUNNING"},
"message": {
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task-123"},
"content": {
"type": "text",
"author": "user",
"content": "Execute this task please",
Expand Down Expand Up @@ -340,7 +376,15 @@ async def mock_init_handler(params):
"jsonrpc": "2.0",
"method": "task/create",
"params": {
"task": {"id": "test-task-456", "agent_id": "test-agent", "status": "RUNNING"}
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task-456"},
},
"id": "test-init-1",
}
Expand Down Expand Up @@ -428,11 +472,20 @@ async def failing_handler(params):
"jsonrpc": "2.0",
"method": "event/send",
"params": {
"task": {"id": "test-task", "agent_id": "test-agent", "status": "RUNNING"},
"message": {
"type": "text",
"author": "user",
"content": "test message",
"agent": {
"id": "test-agent-456",
"name": "test-agent",
"description": "test agent",
"acp_type": "sync",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z",
},
"task": {"id": "test-task"},
"event": {
"id": "evt-1",
"agent_id": "test-agent-456",
"sequence_id": 1,
"task_id": "test-task",
},
},
"id": "test-1",
Expand Down
Loading
Loading