Skip to content

[FEATURE][PLUGINS]: Propagate PluginResult.http_headers to HTTP responses #3576

@crivetimihai

Description

@crivetimihai

🧭 Type of Feature

  • Enhancement to existing functionality

🧭 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

  1. PluginExecutor.execute(): Merge http_headers from each plugin result into a combined headers dict (last-write-wins or priority-based)
  2. Service layer: Return aggregated headers alongside the response payload
  3. 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.

Metadata

Metadata

Assignees

Labels

COULDP3: Nice-to-have features with minimal impact if left out; included if time permitsapiREST API Related itemenhancementNew feature or request
No fields configured for Feature.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions