Skip to content

Fixes #3925: Implemented the content_types field functionality for PluginCondition#4005

Merged
ja8zyjits merged 2 commits into3955-bug-plugin-condition-evaluation-system-requires-comprehensive-overhaulfrom
3925-bug-pluginconditioncontent_types-field-is-defined-but-not-implemented
Apr 7, 2026
Merged

Fixes #3925: Implemented the content_types field functionality for PluginCondition#4005
ja8zyjits merged 2 commits into3955-bug-plugin-condition-evaluation-system-requires-comprehensive-overhaulfrom
3925-bug-pluginconditioncontent_types-field-is-defined-but-not-implemented

Conversation

@ja8zyjits
Copy link
Copy Markdown
Member

🐛 Bug-fix PR

Before opening this PR please:

  1. make lint - passes ruff, mypy, pylint
  2. make test - all unit + integration tests green ✅
  3. make coverage - ≥ 80 % ✅
  4. make docker docker-run-ssl or make podman podman-run-ssl
  5. Update relevant documentation. ✅
  6. Tested with sqlite and postgres + redis. ⏳
  7. Manual regression no longer fails. Ensure the UI and /version work correctly. ✅

📌 Summary

Implements the content_types field functionality for PluginCondition to enable plugins to execute conditionally based on HTTP Content-Type headers. This field was defined in the model but had no implementation, making it a non-functional configuration option that could lead to unexpected plugin behavior.

Fixes: #3925

🔁 Reproduction Steps

  1. Configure a plugin with content_types condition:

    plugins:
      - name: "TestPlugin"
        conditions:
          - content_types: ["application/json", "text/plain"]
  2. Send requests with different Content-Type headers

  3. Before fix: Plugin executes regardless of content type (field silently ignored)

  4. After fix: Plugin only executes when Content-Type matches configured types

🐞 Root Cause

The PluginCondition.content_types field was defined in mcpgateway/plugins/framework/models.py (line 212) but never implemented in the conditional matching logic:

  • matches() function in utils.py only checked server_ids, tenant_ids, user_patterns
  • payload_matches() function handled tools, prompts, resources, agents but not content_types
  • No mechanism existed to extract or pass Content-Type from HTTP requests to plugin context
  • No tests validated content type matching behavior

This was a silent failure - the field was accepted in configuration but had no effect.

💡 Fix Description

Core Implementation

  1. Added content_type field to GlobalContext (models.py):

    • Stores the normalized Content-Type from HTTP request headers
    • Optional field with permissive default (None = execute plugin)
  2. Implemented normalize_content_type() helper (utils.py):

    • Case-insensitive matching (APPLICATION/JSONapplication/json)
    • Strips charset and parameters (application/json; charset=utf-8application/json)
    • Handles edge cases (None, empty strings, whitespace)
  3. Updated matches() function (utils.py):

    • Added content type checking logic
    • Uses OR logic for multiple content types (match any)
    • Integrates with existing AND logic for combined conditions
  4. Extracted Content-Type at 7 instantiation sites:

    • auth.py (2 locations)
    • tool_service.py (3 locations)
    • http_auth_middleware.py (1 location)
    • rbac.py (1 location)

Testing

Created comprehensive test suite (test_content_type_matching.py) with 21 tests covering:

  • Exact matching, case-insensitive matching, charset parameter stripping
  • Multiple content types (OR logic), combined conditions (AND logic)
  • Permissive defaults (None content_type, empty conditions)
  • Edge cases (empty strings, whitespace, malformed headers)

Documentation

  • Architecture guide (docs/docs/architecture/plugins.md): Added "Content Type Filtering" section with examples, security notes, and performance benefits
  • User guide (docs/docs/using/plugins/index.md): Enhanced "Conditional Execution" section with practical examples

🧪 Verification

Check Command Status
Lint suite make lint ✅ Pass
Unit tests make test ✅ Pass (21 new + 1076 existing)
Coverage ≥ 80 % make coverage ✅ Pass
Manual regression no longer fails steps / screenshots ✅ Pass

Test Results:

  • All 21 new unit tests passing
  • All 1076 existing plugin framework tests passing
  • Doctests validated for models.py and utils.py

📐 MCP Compliance (if relevant)

  • Matches current MCP spec (no MCP protocol changes)
  • No breaking change to MCP clients (backward compatible)

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • No secrets/credentials committed
  • Comprehensive unit tests added
  • Documentation updated (architecture + user guide)
  • Backward compatible (existing configs without content_types continue to work)
  • Permissive defaults (None content_type = execute plugin)

🎯 Key Behaviors

  • Case-insensitive: APPLICATION/JSON matches application/json
  • Parameter stripping: application/json; charset=utf-8 matches application/json
  • Permissive default: When content_type is None or content_types not specified, plugin executes
  • Combined conditions: Uses AND logic with other conditions (server_ids, tenant_ids, etc.)
  • Multiple types: Uses OR logic (plugin executes if any content type matches)

📝 Example Configuration

plugins:
  - name: "JsonValidator"
    kind: "plugins.validation.json_validator.JsonValidator"
    hooks: ["tool_pre_invoke"]
    mode: "enforce"
    priority: 50
    conditions:
      - content_types: ["application/json"]
        server_ids: ["production-api"]
    config:
      strict_schema: true

## Summary

Implemented the `content_types` field functionality for `PluginCondition` to enable plugins to execute conditionally based on HTTP Content-Type headers.

## Changes

### Core Implementation
- **models.py**: Added `content_type: Optional[str]` field to `GlobalContext`
- **utils.py**:
  - Added `normalize_content_type()` helper for case-insensitive matching with charset/parameter stripping
  - Updated `matches()` function to check `content_types` conditions
- **auth.py**: Extract Content-Type from request headers (2 locations)
- **tool_service.py**: Extract Content-Type from request headers (3 locations)
- **http_auth_middleware.py**: Extract Content-Type from request headers (1 location)
- **rbac.py**: Extract Content-Type from request headers (1 location)

### Testing
- **test_content_type_matching.py**: Added 21 comprehensive unit tests covering:
  - Exact matching, case-insensitive matching, charset parameter stripping
  - Multiple content types (OR logic), combined conditions (AND logic)
  - Permissive defaults (None content_type, empty conditions)
  - Edge cases (empty strings, whitespace, malformed headers)

### Documentation
- **plugins.md**: Added "Content Type Filtering" section with examples, security notes, and performance benefits
- **index.md**: Enhanced "Conditional Execution" section with practical content_types examples

Signed-off-by: Jitesh Nair <jiteshnair@ibm.com>
@ja8zyjits
Copy link
Copy Markdown
Member Author

ja8zyjits commented Apr 7, 2026

Verified E2E testing:

config.yaml
image

Tested with QR code server existing in our directory
uvx qr_code_server/ --transport http --host localhost

Called the tool with this configuration
image

The generated QR code has yikes as output

Failing case with this configuration in tool call
image

The output is crap

@msureshkumar88
Copy link
Copy Markdown
Collaborator

🔍 Comprehensive Code Review — PR #4005

Fixes #3925: Implemented the content_types field functionality for PluginCondition by @ja8zyjits

✅ Issue Coverage Assessment

All 4 acceptance criteria met:

  • ✅ Field implemented in conditional matching logic
  • ✅ Content-Type extraction added at 7 instantiation points
  • ✅ Comprehensive test suite with 21 tests
  • ✅ Documentation updated in architecture and user guides

🚨 Required Changes Before Merge

1. CRITICAL: Add Type Guard to Prevent Crashes

File: mcpgateway/plugins/framework/utils.py:206

Issue: Function assumes string input but could receive None/int/other types, causing AttributeError.

Fix Required:

def normalize_content_type(content_type: str) -> str:
    """Extract base content type without parameters."""
    if not isinstance(content_type, str):  # ADD THIS
        return ''                           # ADD THIS
    return content_type.split(";")[0].strip().lower()

Severity: Medium (crashes plugin evaluation)
Effort: Trivial (1 line)


2. CRITICAL: Add Input Validation to Prevent DoS

File: mcpgateway/plugins/framework/models.py:1417

Issue: Content-Type headers stored without validation. Attacker could send 10MB header causing memory exhaustion.

Fix Required:

from pydantic import field_validator

class GlobalContext(BaseModel):
    # ... existing fields ...
    content_type: Optional[str] = None
    
    @field_validator('content_type')
    def validate_content_type(cls, v):
        if v is None:
            return v
        if len(v) > 200:
            raise ValueError("Content-Type header too long")
        if not v.isprintable():
            raise ValueError("Content-Type contains invalid characters")
        return v

Severity: Medium (DoS potential, CWE-20)
Effort: Low (10-15 lines)


3. HIGH: Performance Optimization Required

File: mcpgateway/plugins/framework/utils.py:259

Issue: Content-types normalized on every request (1000s/sec). Creates unnecessary CPU overhead.

Fix Required:

# In PluginCondition model
@field_validator('content_types')
def normalize_content_types(cls, v):
    """Pre-normalize content types during initialization."""
    if v:
        return [ct.split(";")[0].strip().lower() for ct in v]
    return v

# Then in matches() function, remove normalization:
if condition.content_types and context.content_type:
    normalized_request = normalize_content_type(context.content_type)
    # Use pre-normalized condition.content_types directly
    if normalized_request not in condition.content_types:
        return False

Severity: Medium (15-20% performance gain possible)
Effort: Low (10-15 lines)


4. MEDIUM: Remove Unintentional Comment

File: mcpgateway/services/sso_service.py:1460

Issue: Standalone # Standard comment appears to be leftover from refactoring.

Fix Required: Delete line 1460

Severity: Low (code cleanliness)
Effort: Trivial (1 line deletion)


5. MEDIUM: Enhance Docstring Examples

File: mcpgateway/plugins/framework/utils.py:187

Issue: Docstring doesn't demonstrate edge case handling.

Fix Required: Add examples:

def normalize_content_type(content_type: str) -> str:
    """Extract base content type without parameters.
    
    Examples:
        >>> normalize_content_type('application/json; charset=utf-8')
        'application/json'
        >>> normalize_content_type('TEXT/PLAIN')
        'text/plain'
        >>> normalize_content_type('')  # ADD THIS
        ''                               # ADD THIS
        >>> normalize_content_type('   ')  # ADD THIS
        ''                                  # ADD THIS
    """

Severity: Low (documentation quality)
Effort: Trivial (2 lines)


📊 Additional Findings Summary

Performance Opportunities (3 items)

  • ⚠️ Repeated normalization (addressed in # 3 above) — 15-20% performance gain
  • 🔵 Redundant header lookups — Content-Type extracted at 7 locations independently
  • 🔵 String operations in hot path — Consider LRU cache for common content types

Security Gaps (4 items)

  • ⚠️ Missing input validation (addressed in # 2 above) — DoS potential
  • ⚠️ Type confusion (addressed in # 1 above) — Crash risk
  • 🔵 Permissive defaults — Consider strict mode option for security plugins
  • 🔵 No audit logging — Content-type filtering decisions not logged

Code Quality (3 items)

  • 🔵 Duplicated extraction logic — 7 locations extract content-type independently
  • 🔵 Duplicated context creation — GlobalContext creation repeated 5 times
  • 🔵 Type aliases — Consider ContentType/NormalizedContentType type aliases

Implementation Gaps (4 items)

  • 🔵 No wildcard supportapplication/* patterns not supported
  • 🔵 No negation support — Cannot specify exclude_content_types
  • 🔵 No config validation — Invalid MIME types accepted at load time
  • 🔵 No metrics — Cannot track content-type filtering effectiveness

Testing Gaps (6 items)

  • 🔵 Missing edge cases — Malformed/unicode/long content-types not tested
  • 🔵 No performance tests — Normalization/matching performance not benchmarked
  • 🔵 No deny-path tests — Plugin prevention not explicitly tested
  • 🔵 No doctest validation — Docstring examples not validated

📈 Overall Assessment

Area Rating Notes
Issue Completion 🟢 Complete All 4 acceptance criteria met
Security Risk 🟡 Medium Input validation needed (items # 1, # 2)
Performance 🟡 Needs Work Optimization required (item # 3)
Code Quality 🟢 Good Minor cleanup needed (items # 4, # 5)
Test Coverage 🟡 Partial Unit tests excellent, integration/E2E missing

🎯 Recommendation: Request Changes

This PR successfully implements the content_types feature and addresses issue #3925. However, 5 required changes must be addressed before merge:

  1. ⚠️ Critical: Add type guard (prevents crashes)
  2. ⚠️ Critical: Add input validation (prevents DoS)
  3. ⚠️ High: Optimize performance (15-20% gain)
  4. 🔵 Medium: Remove unintentional comment
  5. 🔵 Medium: Enhance docstring examples

Additional recommendations for follow-up PRs:

  • Create integration tests for HTTP → Plugin flow validation
  • Add wildcard pattern matching support (application/*)
  • Extract duplicated content-type extraction logic
  • Add performance benchmarks

Estimated effort to address required changes: 2-3 hours


Legend: 🔴 Critical | ⚠️ High | 🔵 Medium | 🟢 Low

@ja8zyjits ja8zyjits requested a review from msureshkumar88 April 7, 2026 13:10
@ja8zyjits
Copy link
Copy Markdown
Member Author

Hi @msureshkumar88 ,
I have updated the code based on your suggestions.
Please review again.

@ja8zyjits ja8zyjits requested a review from gcgoncalves April 7, 2026 15:10
@ja8zyjits
Copy link
Copy Markdown
Member Author

Merging it now, will have this as part of PR to the main branch soon.

@ja8zyjits ja8zyjits merged commit 03bbdc6 into 3955-bug-plugin-condition-evaluation-system-requires-comprehensive-overhaul Apr 7, 2026
1 check passed
@ja8zyjits ja8zyjits deleted the 3925-bug-pluginconditioncontent_types-field-is-defined-but-not-implemented branch April 7, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants