Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
11 changes: 9 additions & 2 deletions src/agents/specialized/code_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def _load_templates(self) -> dict[str, str]:
return {
"fastapi_endpoint": textwrap.dedent(
"""
import logging

logger = logging.getLogger(__name__)

@app.post("/api/v1/{endpoint_name}")
async def {function_name}({parameters}):
\"\"\"
Expand All @@ -42,8 +46,11 @@ async def {function_name}({parameters}):
}}
except ValidationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
except Exception:
logger.exception("Generated endpoint failed")
raise HTTPException(
status_code=500, detail="Internal server error"
)
"""
),
"rest_api": textwrap.dedent(
Expand Down
2 changes: 1 addition & 1 deletion src/youtube_extension/backend/cloud_ai_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ async def analyze_video(request: VideoAnalysisRequest):
# Must precede CloudAIError: RateLimitError subclasses it, so catching
# the base first would shadow this handler and return a 503 instead.
logger.warning(f"Rate limit exceeded: {e}")
raise HTTPException(status_code=429, detail=f"Rate limit exceeded: {str(e)}")
raise HTTPException(status_code=429, detail="Rate limit exceeded")
except ConfigurationError as e:
# Must precede CloudAIError (same subclassing reason) so configuration
# failures reach this sanitized 500 rather than the dynamic 503 below.
Expand Down
81 changes: 60 additions & 21 deletions src/youtube_extension/backend/cloud_api_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,42 @@
router = APIRouter()


def _client_safe_error(error_message: Any) -> Optional[str]:
"""Replace persisted diagnostics with a stable client-safe message."""
return "Internal server error" if error_message else None


def _sanitize_error_list(value: Any) -> Any:
"""Replace scalar diagnostics while preserving structured error records."""
if isinstance(value, list):
return [_sanitize_error_list(item) for item in value]
if isinstance(value, tuple):
return tuple(_sanitize_error_list(item) for item in value)
if isinstance(value, dict):
return _sanitize_response_errors(value)
return None if value is None else "Internal server error"


def _sanitize_response_errors(value: Any) -> Any:
"""Copy a response tree while replacing persisted diagnostic error values."""
if isinstance(value, dict):
return {
key: (
_client_safe_error(item)
if key in {"error", "error_message"}
else _sanitize_error_list(item)
if key == "errors"
else _sanitize_response_errors(item)
)
for key, item in value.items()
}
if isinstance(value, list):
return [_sanitize_response_errors(item) for item in value]
if isinstance(value, tuple):
return tuple(_sanitize_response_errors(item) for item in value)
return value



# Pydantic models for API requests/responses
class CloudVideoProcessingRequest(BaseModel):
Expand Down Expand Up @@ -128,12 +164,12 @@ async def process_video_cloud(
video_url=result.video_url,
success=result.success,
status='completed' if result.success else 'failed',
metadata=result.metadata,
transcript=result.transcript,
ai_analysis=result.ai_analysis,
metadata=_sanitize_response_errors(result.metadata),
transcript=_sanitize_response_errors(result.transcript),
ai_analysis=_sanitize_response_errors(result.ai_analysis),
processing_time=result.processing_time,
from_cache=result.from_cache,
error=result.error_message,
error=_client_safe_error(result.error_message),
)

except Exception as e:
Expand Down Expand Up @@ -297,7 +333,7 @@ async def get_video_status(video_id: str):
created_at=state.created_at,
updated_at=state.updated_at,
processing_time=state.processing_time,
error_message=state.error_message,
error_message=_client_safe_error(state.error_message),
)

except HTTPException:
Expand Down Expand Up @@ -329,13 +365,13 @@ async def get_video_result(video_id: str):
"video_url": state.video_url,
"status": state.status,
"current_stage": state.current_stage,
"metadata": state.metadata,
"transcript": state.transcript,
"ai_analysis": state.ai_analysis,
"metadata": _sanitize_response_errors(state.metadata),
"transcript": _sanitize_response_errors(state.transcript),
"ai_analysis": _sanitize_response_errors(state.ai_analysis),
"processing_time": state.processing_time,
"created_at": state.created_at,
"updated_at": state.updated_at,
"error_message": state.error_message,
"error_message": _client_safe_error(state.error_message),
}

except HTTPException:
Expand All @@ -362,11 +398,11 @@ async def get_queue_stats():
"timestamp": datetime.now(timezone.utc).isoformat(),
}

except Exception as e:
logger.error(f"Error getting queue stats: {e}")
except Exception:
logger.error("Error getting queue stats", exc_info=True)
return {
"success": False,
"error": str(e),
"error": "Internal server error",
"timestamp": datetime.now(timezone.utc).isoformat(),
}

Expand All @@ -389,10 +425,11 @@ async def get_cloud_status():
"status": "operational",
"enabled": True,
}
except Exception as e:
except Exception:
logger.error("firestore status check failed", exc_info=True)
status["services"]["firestore"] = {
"status": "error",
"error": str(e),
"error": "Service unavailable",
}
status["overall_status"] = "degraded"

Expand All @@ -405,10 +442,11 @@ async def get_cloud_status():
"enabled": True,
"queue_stats": stats,
}
except Exception as e:
except Exception:
logger.error("cloud tasks status check failed", exc_info=True)
status["services"]["cloud_tasks"] = {
"status": "error",
"error": str(e),
"error": "Service unavailable",
}
status["overall_status"] = "degraded"

Expand All @@ -419,20 +457,21 @@ async def get_cloud_status():
"status": "operational",
"enabled": True,
}
except Exception as e:
except Exception:
logger.error("vertex AI status check failed", exc_info=True)
status["services"]["vertex_ai"] = {
"status": "error",
"error": str(e),
"error": "Service unavailable",
}
status["overall_status"] = "degraded"

return status

except Exception as e:
logger.error(f"Error getting cloud status: {e}")
except Exception:
logger.error("Error getting cloud status", exc_info=True)
return {
"overall_status": "error",
"error": str(e),
"error": "Internal server error",
"timestamp": datetime.now(timezone.utc).isoformat(),
}

Expand Down
93 changes: 68 additions & 25 deletions src/youtube_extension/backend/real_api_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,45 @@
# Configure logging
logger = logging.getLogger(__name__)

_PUBLIC_PROCESSING_ERROR = "Video processing failed"


def _sanitize_public_error(value: Any) -> Optional[str]:
"""Replace persisted processor diagnostics with a stable public message."""
return None if value is None else _PUBLIC_PROCESSING_ERROR


def _sanitize_error_list(value: Any) -> Any:
"""Replace scalar diagnostics while preserving structured error records."""
if isinstance(value, list):
return [_sanitize_error_list(item) for item in value]
if isinstance(value, tuple):
return tuple(_sanitize_error_list(item) for item in value)
if isinstance(value, dict):
return _sanitize_response_errors(value)
return None if value is None else _PUBLIC_PROCESSING_ERROR


def _sanitize_response_errors(value: Any) -> Any:
"""Copy a response tree while replacing persisted diagnostic error values."""
if isinstance(value, dict):
return {
key: (
_sanitize_public_error(item)
if key in {"error", "error_message"}
else _sanitize_error_list(item)
if key == "errors"
else _sanitize_response_errors(item)
)
for key, item in value.items()
}
if isinstance(value, list):
return [_sanitize_response_errors(item) for item in value]
if isinstance(value, tuple):
return tuple(_sanitize_response_errors(item) for item in value)
return value


# Distinguishes "no cache entry" from any entry content. The read helper returns
# raw bytes today, but its historical contract returned parsed JSON, where a file
# holding the literal ``null`` yields ``None`` -- a plain ``None`` return conflates
Expand Down Expand Up @@ -246,22 +285,23 @@ async def process_video_real_api(request: VideoProcessingRequest, background_tas
video_url=request.video_url,
force_refresh=request.force_refresh
)
safe_result = _sanitize_response_errors(result)

# Track metrics
processing_time = (datetime.now(timezone.utc) - start_time).total_seconds()

# Format response
response = VideoAnalysisResponse(
video_id=result.get('video_id', ''),
video_id=safe_result.get('video_id', ''),
video_url=request.video_url,
success=result.get('success', False),
metadata=result.get('metadata'),
transcript=result.get('transcript'),
ai_analysis=result.get('ai_analysis'),
cost_breakdown=result.get('cost_breakdown'),
success=safe_result.get('success', False),
metadata=safe_result.get('metadata'),
transcript=safe_result.get('transcript'),
ai_analysis=safe_result.get('ai_analysis'),
cost_breakdown=safe_result.get('cost_breakdown'),
processing_time=processing_time,
cached=result.get('cached', False),
error=result.get('error')
cached=safe_result.get('cached', False),
error=_sanitize_public_error(safe_result.get('error'))
)

logger.info(f"✅ Real API processing completed: {result.get('video_id')} - ${result.get('cost_breakdown', {}).get('total_cost', 0):.4f}")
Expand Down Expand Up @@ -319,7 +359,7 @@ async def batch_process_videos(request: BatchProcessingRequest):
max_concurrent=request.max_concurrent
)

return result
return _sanitize_response_errors(result)

except HTTPException:
# Preserve explicit 4xx responses (e.g. the 400 batch-size guard above).
Expand All @@ -343,8 +383,8 @@ async def get_processed_videos_list():
# JSON file per cached video. That is unbounded blocking I/O which
# would otherwise stall the event loop for every concurrent request,
# so it runs in a worker thread.
return await asyncio.to_thread(
_collect_processed_videos_sync, processor.cache_dir
return _sanitize_response_errors(
await asyncio.to_thread(_collect_processed_videos_sync, processor.cache_dir)
)

except Exception as e:
Expand Down Expand Up @@ -381,7 +421,10 @@ async def get_video_analysis(video_id: str):
# Returning a Response skips FastAPI's jsonable_encoder/json.dumps
# round-trip, which would otherwise re-serialise the payload on
# the event loop in proportion to its size.
return Response(content=video_data, media_type="application/json")
return Response(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The CWE-209 response sanitization (JSON parse + recursive tree-walk + re-serialize) runs synchronously on the asyncio event loop in get_video_analysis and get_processed_videos_list, reintroducing the exact size-proportional loop stall the surrounding worker-thread offload was designed to prevent.

Fix on Vercel

content=json.dumps(_sanitize_response_errors(json.loads(video_data))),
media_type="application/json",
)

except HTTPException:
raise
Expand All @@ -401,10 +444,10 @@ async def get_cost_dashboard():
dashboard = await cost_monitor.get_cost_dashboard()
return dashboard

except Exception as e:
logger.error(f"Error getting cost dashboard: {e}")
except Exception:
logger.error("Error getting cost dashboard", exc_info=True)
return {
"error": str(e),
"error": "Internal server error",
"timestamp": datetime.now(timezone.utc).isoformat()
}

Expand All @@ -425,10 +468,10 @@ async def get_usage_analytics(days: int = 7):

except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting usage analytics: {e}")
except Exception:
logger.error("Error getting usage analytics", exc_info=True)
return {
"error": str(e),
"error": "Internal server error",
"timestamp": datetime.now(timezone.utc).isoformat()
}

Expand All @@ -441,10 +484,10 @@ async def get_optimization_recommendations():
recommendations = await cost_monitor.optimize_api_usage()
return recommendations

except Exception as e:
logger.error(f"Error getting optimization recommendations: {e}")
except Exception:
logger.error("Error getting optimization recommendations", exc_info=True)
return {
"error": str(e),
"error": "Internal server error",
"recommendations": [],
"timestamp": datetime.now(timezone.utc).isoformat()
}
Expand Down Expand Up @@ -472,7 +515,7 @@ async def get_service_status():
return {
"overall_status": "operational" if processor_status.get('service_status') == 'operational' else "degraded",
"timestamp": datetime.now(timezone.utc).isoformat(),
"processor": processor_status,
"processor": _sanitize_response_errors(processor_status),
"cost_monitoring": {
"status": "operational",
"today_cost": cost_dashboard.get('today_summary', {}).get('total_cost', 0.0),
Expand All @@ -490,11 +533,11 @@ async def get_service_status():
"version": "2.0.0-real-api-integration"
}

except Exception as e:
logger.error(f"Error getting service status: {e}")
except Exception:
logger.error("Error getting service status", exc_info=True)
return {
"overall_status": "error",
"error": str(e),
"error": "Internal server error",
"timestamp": datetime.now(timezone.utc).isoformat()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,10 +578,12 @@ async def validate_video_url(self, url: str) -> tuple[bool, str, str]:

return True, video_id, "Video is public and accessible"

except ValueError as e:
return False, "", f"Invalid URL format: {e}"
except Exception as e:
return False, "", f"Video validation failed: {e}"
except ValueError:
logger.warning("Video URL validation failed: invalid URL format", exc_info=True)
return False, "", "Invalid URL format"
except Exception:
logger.error("Video URL validation failed", exc_info=True)
return False, "", "Video validation failed"

async def close(self):
"""Close the HTTP client"""
Expand Down
Loading