🧭 Type of Feature
🧭 Epic
Title: Propagate plugin HTTP headers on successful (non-violation) responses
Goal: Allow plugins to add informational HTTP headers (e.g., X-RateLimit-Remaining) to successful responses, not just violation responses.
Why now: PR #3449 added http_headers to both PluginViolation and PluginResult, but only the violation path is wired end-to-end. The successful-response path is a dead code gap discovered during review.
🙋♂️ User Story 1
As a: client consuming rate-limited API endpoints
I want: to receive X-RateLimit-Remaining and X-RateLimit-Reset headers on every successful response
So that: I can implement proactive backoff before hitting 429 errors
✅ Acceptance Criteria
Scenario: Successful request includes rate limit headers
Given a rate limiter plugin configured with "10/m" per user
When a user makes a successful tool call (under the limit)
Then the HTTP response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers
And the HTTP status code is 200
And no Retry-After header is present
Scenario: Headers reflect decreasing remaining count
Given a rate limiter plugin configured with "3/m" per user
When the user makes 3 successive successful calls
Then X-RateLimit-Remaining decreases from 2 to 1 to 0
📐 Current State
PluginResult.http_headers was added in #3449 and the rate limiter plugin already populates it on success:
# plugins/rate_limiter/rate_limiter.py (line 267-268)
headers = _make_headers(limit, remaining, reset_ts, retry_after, include_retry_after=False)
return PromptPrehookResult(metadata=meta, http_headers=headers)
However, the following components do not read or propagate PluginResult.http_headers:
| Component |
File |
Gap |
PluginExecutor.execute() |
mcpgateway/plugins/framework/manager.py |
Aggregates metadata but ignores http_headers |
PromptService |
mcpgateway/services/prompt_service.py |
Only reads modified_payload from plugin results |
ToolService |
mcpgateway/services/tool_service.py |
Only reads modified_payload from plugin results |
📐 Proposed Approach
PluginExecutor.execute(): Merge http_headers from each plugin result into a combined headers dict (last-write-wins or priority-based)
- Service layer: Return aggregated headers alongside the response payload
- RPC/HTTP handler: Apply headers to the
ORJSONResponse before returning
🔗 Related Issues
📓 Additional Context
The PluginViolation.http_headers path works correctly end-to-end (verified with E2E testing in #3449). This issue is specifically about the successful-response path where the field exists but is not consumed.
🧭 Type of Feature
🧭 Epic
Title: Propagate plugin HTTP headers on successful (non-violation) responses
Goal: Allow plugins to add informational HTTP headers (e.g.,
X-RateLimit-Remaining) to successful responses, not just violation responses.Why now: PR #3449 added
http_headersto bothPluginViolationandPluginResult, but only the violation path is wired end-to-end. The successful-response path is a dead code gap discovered during review.🙋♂️ User Story 1
As a: client consuming rate-limited API endpoints
I want: to receive
X-RateLimit-RemainingandX-RateLimit-Resetheaders on every successful responseSo that: I can implement proactive backoff before hitting 429 errors
✅ Acceptance Criteria
📐 Current State
PluginResult.http_headerswas added in #3449 and the rate limiter plugin already populates it on success:However, the following components do not read or propagate
PluginResult.http_headers:PluginExecutor.execute()mcpgateway/plugins/framework/manager.pymetadatabut ignoreshttp_headersPromptServicemcpgateway/services/prompt_service.pymodified_payloadfrom plugin resultsToolServicemcpgateway/services/tool_service.pymodified_payloadfrom plugin results📐 Proposed Approach
PluginExecutor.execute(): Mergehttp_headersfrom each plugin result into a combined headers dict (last-write-wins or priority-based)ORJSONResponsebefore returning🔗 Related Issues
http_headersfield (merged)📓 Additional Context
The
PluginViolation.http_headerspath works correctly end-to-end (verified with E2E testing in #3449). This issue is specifically about the successful-response path where the field exists but is not consumed.