Peterase's picture
fix: remove Transfer-Encoding chunked (illegal in HTTP/2) from SSE response headers
3be9e60
import os
import uuid
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from fastapi.security import OAuth2PasswordBearer
from src.core.domain.schemas import ChatRequest, ChatResponse, SearchResponse
from src.core.use_cases.search_use_case import SearchUseCase
from src.core.use_cases.rag_chat_use_case import RagChatUseCase
from src.core.use_cases.agent_router_use_case import AgentRouterUseCase
from src.api.dependencies import get_search_use_case, get_rag_chat_use_case, get_agent_router_use_case
from src.core.security import get_current_user
from src.core.domain.db_models import User
from jose import jwt, JWTError
from src.core.config import settings
from src.infrastructure.database import get_db
from sqlalchemy.orm import Session
router = APIRouter()
# Optional bearer — doesn't raise if token is missing
_optional_bearer = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login", auto_error=False)
def get_optional_user(
token: Optional[str] = Depends(_optional_bearer),
db: Session = Depends(get_db)
) -> Optional[User]:
"""Returns the authenticated user or None for guests."""
if not token:
return None
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
if payload.get("type") != "access":
return None
email = payload.get("sub")
if not email:
return None
user = db.query(User).filter(User.email == email).first()
return user if user and user.is_active else None
except JWTError:
return None
@router.post("/search")
def direct_search(
request: ChatRequest,
search_use_case: SearchUseCase = Depends(get_search_use_case),
current_user: User = Depends(get_current_user)
):
"""Lightning-fast hybrid search bypassing the LLM."""
try:
results = search_use_case.execute(
query=request.query,
limit=request.top_k,
source_filter=request.source_filter,
language_filter=request.language_filter,
days_back=getattr(request, 'days_back', None)
)
hits = [{"content": r.content, "metadata": r.metadata, "score": r.score, "doc_id": r.doc_id} for r in results]
return {"results": hits, "query": request.query}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/chat/test", response_model=ChatResponse)
async def chat_test(
request: ChatRequest,
agent_router_use_case: AgentRouterUseCase = Depends(get_agent_router_use_case)
):
"""Test RAG chat endpoint without authentication for debugging"""
# Get endpoint from environment variable or use default
os.getenv("RAG_ENDPOINT", "/rag/chat/test")
result = await agent_router_use_case.execute_chat(request)
return result
@router.post("/chat/stream")
async def chat_with_rag_stream(
request: ChatRequest,
router_use_case: AgentRouterUseCase = Depends(get_agent_router_use_case),
current_user: Optional[User] = Depends(get_optional_user)
):
"""Streaming RAG chat. Works for both authenticated users and guests."""
try:
if current_user is None and not request.session_id:
request.session_id = f"guest_{uuid.uuid4().hex[:12]}"
user_id = current_user.id if current_user else None
# X-Accel-Buffering: no tells HF nginx NOT to buffer — fixes ERR_HTTP2_PROTOCOL_ERROR
# NOTE: Do NOT set Transfer-Encoding: chunked — it is prohibited in HTTP/2 (RFC 7540 §8.1.2.2)
headers = {
"X-Accel-Buffering": "no",
"Cache-Control": "no-cache, no-transform",
}
return StreamingResponse(
router_use_case.execute_stream(request, is_guest=(current_user is None), user_id=user_id),
media_type="text/event-stream",
headers=headers,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))