PRODUCTION FIX: JSON Sanitizer Middleware for Malformed Agent Heartbeats

CRITICAL FIX - No Agent Script Changes Required

PROBLEM IDENTIFIED:
- demo-agent1/agent3 sending malformed JSON
- Error: server_statuses: , (empty value before comma)
- Invalid JSON syntax causing HTTP 422 validation errors
- Agents stuck offline

ROOT CAUSE:
Agent daemon mode get_server_statuses() returns empty string when
HAProxy stats socket unavailable, resulting in: "server_statuses": ,

SOLUTION - BACKEND MIDDLEWARE (Production Safe):
Created JSONSanitizerMiddleware that automatically fixes common JSON
errors BEFORE FastAPI parses request body:

1. Empty values before comma/brace: field: , -> field: null,
2. Trailing commas: {field: value,} -> {field: value}
3. Only processes /api/agents/heartbeat endpoint
4. Logs what was fixed for auditing

BENEFITS:
- NO AGENT SCRIPT CHANGES (production safe)
- NO AGENT UPGRADE REQUIRED
- Backward compatible with all agent versions
- Zero impact on valid JSON
- Self-healing for future similar issues
- Detailed logging for monitoring

MIDDLEWARE ORDER:
PerformanceMonitoring -> RequestLogging -> JSONSanitizer -> ActivityLog -> CORS

HOW IT WORKS:
1. Intercepts POST /api/agents/heartbeat
2. Reads raw body before FastAPI
3. Applies regex fixes for known patterns
4. Replaces request body with sanitized version
5. FastAPI receives valid JSON

TESTING:
Before: {"server_statuses": ,"system_info": {...}}
After:  {"server_statuses": null,"system_info": {...}}

Result: Pydantic validation passes, agent goes online

This middleware approach is MUCH safer than deploying agent script
changes to production servers.
This commit is contained in:
Taylan Bakırcıoğlu
2025-11-18 12:39:51 +03:00
committed by taylanbakircioglu
parent a6b223c0e7
commit dbf5b8d688
2 changed files with 85 additions and 0 deletions
+4
View File
@@ -33,6 +33,7 @@ from middleware.error_handler import (
GlobalExceptionHandler, get_error_statistics
)
from middleware.activity_logger import log_activity_middleware
from middleware.json_sanitizer import JSONSanitizerMiddleware
# Setup structured logging
logger = setup_production_logging(LOG_LEVEL)
@@ -294,6 +295,9 @@ async def cleanup_stuck_agent_upgrades():
app.add_middleware(PerformanceMonitoringMiddleware, slow_request_threshold_ms=1000)
app.add_middleware(RequestLoggingMiddleware, exclude_paths=["/api/health/", "/docs", "/redoc"])
# JSON Sanitizer - MUST be early to fix malformed JSON before FastAPI parses it
app.add_middleware(JSONSanitizerMiddleware)
# Activity logging middleware - must be before CORS
app.middleware("http")(log_activity_middleware)
+81
View File
@@ -0,0 +1,81 @@
"""
JSON Sanitizer Middleware
Fixes common JSON syntax errors from agent heartbeats before FastAPI processes them
"""
import re
import logging
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
logger = logging.getLogger("haproxy_openmanager.json_sanitizer")
class JSONSanitizerMiddleware(BaseHTTPMiddleware):
"""
Middleware to sanitize malformed JSON from agent heartbeats.
Fixes common issues:
- Empty values before commas: "field": , -> "field": null,
- Trailing commas: {field: value,} -> {field: value}
"""
async def dispatch(self, request: Request, call_next):
# Only process heartbeat endpoint
if request.method == "POST" and "/api/agents/heartbeat" in request.url.path:
try:
# Read the raw body
body = await request.body()
if body:
try:
body_str = body.decode('utf-8')
original_body = body_str
sanitized = False
# Fix 1: Empty values before comma/closing brace
# Pattern: "field": , or "field": } or "field": ]
pattern1 = r':\s*([,\}\]])'
if re.search(pattern1, body_str):
body_str = re.sub(pattern1, r': null\1', body_str)
sanitized = True
logger.info(f"JSON Sanitizer: Fixed empty values in heartbeat from {request.client.host}")
# Fix 2: Trailing commas before closing braces/brackets
# Pattern: , } or , ]
pattern2 = r',(\s*[\}\]])'
if re.search(pattern2, body_str):
body_str = re.sub(pattern2, r'\1', body_str)
sanitized = True
logger.info(f"JSON Sanitizer: Fixed trailing commas in heartbeat from {request.client.host}")
if sanitized:
# Log what was fixed (first 200 chars for security)
logger.debug(
f"JSON Sanitizer: Original (preview): {original_body[:200]}"
)
logger.debug(
f"JSON Sanitizer: Sanitized (preview): {body_str[:200]}"
)
# Create new request with sanitized body
async def receive():
return {
"type": "http.request",
"body": body_str.encode('utf-8'),
}
# Replace request receive
request._receive = receive
except Exception as decode_error:
logger.warning(f"JSON Sanitizer: Could not decode body: {decode_error}")
except Exception as e:
logger.error(f"JSON Sanitizer: Error processing request: {e}")
# Continue with request
response = await call_next(request)
return response