33import asyncio
44import os
55import traceback
6- from io import BytesIO
76from typing import Annotated , Any
87
98from fastapi import APIRouter , Depends , File , HTTPException , Request , UploadFile , status
1413from authorization .middleware import authorize
1514from client import AsyncLlamaStackClientHolder
1615from configuration import configuration
16+ from constants import DEFAULT_MAX_FILE_UPLOAD_SIZE
1717from log import get_logger
1818from models .config import Action
1919from models .requests import (
2222 VectorStoreUpdateRequest ,
2323)
2424from models .responses import (
25+ AbstractErrorResponse ,
2526 FileResponse ,
2627 ForbiddenResponse ,
2728 InternalServerErrorResponse ,
6364
6465file_responses : dict [int | str , dict [str , Any ]] = {
6566 200 : FileResponse .openapi_response (),
67+ 400 : {"description" : "Bad Request - Invalid file upload" },
6668 401 : UnauthorizedResponse .openapi_response (
6769 examples = ["missing header" , "missing token" ]
6870 ),
@@ -434,22 +436,69 @@ async def create_file(
434436
435437 Raises:
436438 HTTPException:
437- - 400: Bad request (e.g., file too large)
439+ - 400: Bad request (e.g., file too large, invalid format )
438440 - 401: Authentication failed
439441 - 403: Authorization failed
440442 - 500: Lightspeed Stack configuration not loaded
441443 - 503: Unable to connect to Llama Stack
442444 """
443445 _ = auth
444- _ = request
445446
446447 check_configuration_loaded (configuration )
447448
449+ # Check Content-Length header BEFORE reading to prevent DoS via memory exhaustion
450+ content_length = request .headers .get ("content-length" )
451+ if content_length :
452+ try :
453+ size = int (content_length )
454+ if size > DEFAULT_MAX_FILE_UPLOAD_SIZE :
455+ response = AbstractErrorResponse (
456+ response = "File too large" ,
457+ cause = (
458+ f"File size { size } bytes exceeds maximum allowed "
459+ f"size of { DEFAULT_MAX_FILE_UPLOAD_SIZE } bytes "
460+ f"({ DEFAULT_MAX_FILE_UPLOAD_SIZE // (1024 * 1024 )} MB)"
461+ ),
462+ status_code = status .HTTP_400_BAD_REQUEST ,
463+ )
464+ raise HTTPException (** response .model_dump ())
465+ except ValueError :
466+ # Invalid Content-Length header, continue and validate after reading
467+ pass
468+
469+ # file.size attribute if available
470+ if hasattr (file , "size" ) and file .size is not None :
471+ if file .size > DEFAULT_MAX_FILE_UPLOAD_SIZE :
472+ response = AbstractErrorResponse (
473+ response = "File too large" ,
474+ cause = (
475+ f"File size { file .size } bytes exceeds maximum allowed "
476+ f"size of { DEFAULT_MAX_FILE_UPLOAD_SIZE } bytes "
477+ f"({ DEFAULT_MAX_FILE_UPLOAD_SIZE // (1024 * 1024 )} MB)"
478+ ),
479+ status_code = status .HTTP_400_BAD_REQUEST ,
480+ )
481+ raise HTTPException (** response .model_dump ())
482+
448483 try :
449484 client = AsyncLlamaStackClientHolder ().get_client ()
450485
451- # Read file content
486+ # Read file content once
452487 content = await file .read ()
488+
489+ # Verify actual size after reading
490+ if len (content ) > DEFAULT_MAX_FILE_UPLOAD_SIZE :
491+ response = AbstractErrorResponse (
492+ response = "File too large" ,
493+ cause = (
494+ f"File content size { len (content )} bytes exceeds maximum "
495+ f"allowed size of { DEFAULT_MAX_FILE_UPLOAD_SIZE } bytes "
496+ f"({ DEFAULT_MAX_FILE_UPLOAD_SIZE // (1024 * 1024 )} MB)"
497+ ),
498+ status_code = status .HTTP_400_BAD_REQUEST ,
499+ )
500+ raise HTTPException (** response .model_dump ())
501+
453502 filename = file .filename or "uploaded_file"
454503
455504 # Add .txt extension if no extension present
@@ -463,10 +512,12 @@ async def create_file(
463512 len (content ),
464513 )
465514
466- # Convert to BytesIO for Llama Stack client
467- # The client expects bytes, io.IOBase, PathLike, or a tuple
515+ # BytesIO wraps the bytes object for library client compatibility
516+ # Note: BytesIO doesn't copy the data, it creates a file-like view
517+ from io import BytesIO
518+
468519 file_bytes = BytesIO (content )
469- file_bytes .name = filename # Set the filename attribute
520+ file_bytes .name = filename
470521
471522 file_obj = await client .files .create (
472523 file = file_bytes ,
@@ -487,7 +538,13 @@ async def create_file(
487538 raise HTTPException (** response .model_dump ()) from e
488539 except BadRequestError as e :
489540 logger .error ("Bad request for file upload: %s" , e )
490- response = ServiceUnavailableResponse (backend_name = "Llama Stack" , cause = str (e ))
541+ # BadRequestError from Llama Stack indicates client error (e.g., file too large)
542+ # Map to 400 Bad Request, not 503 Service Unavailable
543+ response = AbstractErrorResponse (
544+ response = "Invalid file upload" ,
545+ cause = f"File upload rejected by Llama Stack: { str (e )} " ,
546+ status_code = status .HTTP_400_BAD_REQUEST ,
547+ )
491548 raise HTTPException (** response .model_dump ()) from e
492549 except Exception as e :
493550 full_trace = traceback .format_exc ()
@@ -572,9 +629,12 @@ async def add_file_to_vector_store(
572629 else :
573630 raise # Re-raise if not a lock error or max retries reached
574631 if not vs_file :
575- raise HTTPException (
576- status_code = 500 , detail = "Failed to create vector store file"
632+ # Use standard error response model for consistency
633+ response = InternalServerErrorResponse (
634+ response = "Failed to create vector store file" ,
635+ cause = "All retry attempts failed to create the vector store file" ,
577636 )
637+ raise HTTPException (** response .model_dump ())
578638 logger .info (
579639 "Vector store file created - ID: %s, status: %s, last_error: %s" ,
580640 vs_file .id ,
0 commit comments