44import json
55import logging
66from functools import partial
7- from typing import Any , Callable , Optional , Union
7+ from typing import Any , Callable , Optional , Union , get_origin
88
99from fastapi import FastAPI , HTTPException , status
1010from fastapi .responses import StreamingResponse
@@ -65,13 +65,33 @@ def log_func_and_body(func: Callable, body: Optional[str] = None) -> None:
6565def failure_category_of (error : BaseException ) -> Optional [str ]:
6666 """Return the error's failure_category only when it is a plain string.
6767
68- Any other value would fail response-model validation inside an exception
69- handler, replacing the sanitized error body with a raw 500.
68+ Runs while an exception handler is building the sanitized response, so a
69+ non-string value — or an attribute access that itself raises — is treated
70+ as absent rather than allowed to replace that response with a raw 500.
7071 """
71- category = getattr (error , "failure_category" , None )
72+ try :
73+ category = getattr (error , "failure_category" , None )
74+ except Exception :
75+ return None
7276 return category if isinstance (category , str ) else None
7377
7478
79+ def status_code_of (error : BaseException ) -> int :
80+ """Return the error's status_code only when it is a usable integer.
81+
82+ Same contract as failure_category_of: runs inside exception handlers, so
83+ anything other than a plain int falls back to 500 instead of failing
84+ response-model validation.
85+ """
86+ try :
87+ status_code = getattr (error , "status_code" , None )
88+ except Exception :
89+ return status .HTTP_500_INTERNAL_SERVER_ERROR
90+ if isinstance (status_code , int ) and not isinstance (status_code , bool ):
91+ return status_code
92+ return status .HTTP_500_INTERNAL_SERVER_ERROR
93+
94+
7595async def invoke_func (func : Callable , kwargs : Optional [dict [str , Any ]] = None ) -> Any :
7696 kwargs = kwargs or {}
7797 if inspect .iscoroutinefunction (func ):
@@ -82,11 +102,14 @@ async def invoke_func(func: Callable, kwargs: Optional[dict[str, Any]] = None) -
82102
83103def check_precheck_func (precheck_func : Callable ):
84104 sig = inspect .signature (precheck_func )
85- inputs = sig .parameters .values ()
105+ inputs = list ( sig .parameters .values () )
86106 outputs = sig .return_annotation
87107 if len (inputs ) == 1 :
88108 i = inputs [0 ]
89- if i .name != "usage" or i .annotation is list :
109+ annotation_is_list = (
110+ i .annotation is sig .empty or i .annotation is list or get_origin (i .annotation ) is list
111+ )
112+ if i .name != "usage" or not annotation_is_list :
90113 raise ValueError ("the only input available for precheck is usage which must be a list" )
91114 if outputs not in [None , sig .empty ]:
92115 raise ValueError (f"no output should exist for precheck function, found: { outputs } " )
@@ -208,8 +231,7 @@ async def _stream_response():
208231 filedata_meta = filedata_meta_model .model_validate (
209232 filedata_meta .model_dump ()
210233 ),
211- status_code = getattr (e , "status_code" , None )
212- or status .HTTP_500_INTERNAL_SERVER_ERROR ,
234+ status_code = status_code_of (e ),
213235 status_code_text = f"[{ e .__class__ .__name__ } ] { e } " ,
214236 failure_category = failure_category_of (e ),
215237 ).model_dump_json ()
@@ -236,9 +258,9 @@ async def _stream_response():
236258 message_channels = message_channels ,
237259 filedata_meta = filedata_meta_model .model_validate (filedata_meta .model_dump ()),
238260 status_code = exc .status_code ,
239- status_code_text = json . dumps ( exc .detail )
240- if isinstance (exc .detail , dict )
241- else exc .detail ,
261+ status_code_text = exc .detail
262+ if isinstance (exc .detail , str )
263+ else json . dumps ( exc .detail , default = str ) ,
242264 failure_category = failure_category_of (exc ),
243265 file_data = request_dict .get ("file_data" , None ),
244266 )
@@ -262,8 +284,7 @@ async def _stream_response():
262284 usage = usage ,
263285 message_channels = message_channels ,
264286 filedata_meta = filedata_meta_model .model_validate (filedata_meta .model_dump ()),
265- status_code = getattr (invoke_error , "status_code" , None )
266- or status .HTTP_500_INTERNAL_SERVER_ERROR ,
287+ status_code = status_code_of (invoke_error ),
267288 status_code_text = f"[{ invoke_error .__class__ .__name__ } ] { invoke_error } " ,
268289 failure_category = failure_category_of (invoke_error ),
269290 file_data = request_dict .get ("file_data" , None ),
0 commit comments