@@ -69,11 +69,91 @@ def exception_to_http_response(request: "Request[UserT, AuthT, StateT]", exc: "E
6969 return cast ("Response[Any]" , create_exception_response (request , exc ))
7070 if request .app .debug :
7171 return cast ("Response[Any]" , create_debug_response (request , exc ))
72- detail = str (exc .__cause__ ) if exc .__cause__ is not None else str (exc )
73- return cast ("Response[Any]" , create_exception_response (request , InternalServerException (detail = detail )))
72+ # Production (non-debug, non-HTTPException): never embed raw exception text.
73+ # Debug rendering is already returned above by create_debug_response.
74+ return cast ("Response[Any]" , create_exception_response (request , InternalServerException ()))
7475 return create_inertia_exception_response (request , exc )
7576
7677
78+ def _exception_detail_for_response (request : "Request[Any, Any, Any]" , exc : Exception ) -> Any :
79+ if isinstance (exc , HTTPException ):
80+ return exc .detail
81+ if request .app .debug :
82+ return str (exc )
83+ return "Internal Server Error"
84+
85+
86+ def _exception_extra (exc : Exception ) -> Any :
87+ if not isinstance (exc , HTTPException ):
88+ return None
89+ try :
90+ return exc .extra # pyright: ignore[reportUnknownMemberType]
91+ except AttributeError :
92+ return None
93+
94+
95+ def _get_inertia_plugin (request : "Request[Any, Any, Any]" ) -> "InertiaPlugin | None" :
96+ try :
97+ return request .app .plugins .get ("InertiaPlugin" )
98+ except KeyError :
99+ return None
100+
101+
102+ def _store_field_errors (request : "Request[Any, Any, Any]" , extras : Any , detail : Any ) -> None :
103+ if not extras or not isinstance (extras , (list , tuple )) or len (extras ) < 1 : # pyright: ignore[reportUnknownArgumentType]
104+ return
105+ first_extra = extras [0 ] # pyright: ignore[reportUnknownVariableType]
106+ if not isinstance (first_extra , dict ):
107+ return
108+ message : dict [str , str ] = cast ("dict[str, str]" , first_extra )
109+ key_value = message .get ("key" )
110+ default_field = f"root.{ key_value } " if key_value is not None else "root"
111+ error_detail = str (message .get ("message" , detail ) or detail )
112+ match = FIELD_ERR_RE .search (error_detail )
113+ field = match .group (1 ) if match else default_field
114+ error (request , field , error_detail or str (detail ))
115+
116+
117+ def _create_exception_page_response (
118+ * ,
119+ content : dict [str , Any ],
120+ preferred_type : MediaType ,
121+ route_component : str | None ,
122+ is_inertia : bool ,
123+ status_code : int ,
124+ ) -> "Response[Any]" :
125+ if is_inertia and route_component is None :
126+ return Response [Any ](content = content , media_type = MediaType .JSON , status_code = status_code )
127+ return InertiaResponse [Any ](media_type = preferred_type , content = content , status_code = status_code )
128+
129+
130+ def _append_error_query (redirect_to : str , detail : Any ) -> str :
131+ parsed = urlparse (redirect_to )
132+ error_param = f"error={ quote (str (detail ), safe = '' )} "
133+ query = f"{ parsed .query } &{ error_param } " if parsed .query else error_param
134+ return urlunparse (parsed ._replace (query = query ))
135+
136+
137+ def _create_unauthorized_response (
138+ request : "Request[Any, Any, Any]" ,
139+ * ,
140+ detail : Any ,
141+ flash_succeeded : bool ,
142+ inertia_plugin : "InertiaPlugin" ,
143+ status_code : int ,
144+ exc : Exception ,
145+ ) -> "Response[Any] | None" :
146+ is_unauthorized = status_code == HTTP_401_UNAUTHORIZED or isinstance (exc , NotAuthorizedException )
147+ redirect_to_login = inertia_plugin .config .redirect_unauthorized_to
148+ if not is_unauthorized or redirect_to_login is None :
149+ return None
150+ if request .url .path != redirect_to_login :
151+ if not flash_succeeded and detail :
152+ redirect_to_login = _append_error_query (redirect_to_login , detail )
153+ return InertiaRedirect (request , redirect_to = redirect_to_login )
154+ return InertiaBack (request )
155+
156+
77157def create_inertia_exception_response (request : "Request[UserT, AuthT, StateT]" , exc : "Exception" ) -> "Response[Any]" :
78158 """Create the inertia exception response.
79159
@@ -95,23 +175,14 @@ def create_inertia_exception_response(request: "Request[UserT, AuthT, StateT]",
95175 """
96176 is_inertia_header = request .headers .get ("x-inertia" , "" ).lower () == "true"
97177 is_inertia = request .is_inertia if isinstance (request , InertiaRequest ) else is_inertia_header
178+ route_component = request .inertia .route_component if isinstance (request , InertiaRequest ) else None
98179
99180 status_code = exc .status_code if isinstance (exc , HTTPException ) else HTTP_500_INTERNAL_SERVER_ERROR
100181 preferred_type = MediaType .HTML if not is_inertia else MediaType .JSON
101- detail = exc .detail if isinstance (exc , HTTPException ) else str (exc )
102- extras : Any = None
103- if isinstance (exc , HTTPException ):
104- try :
105- extras = exc .extra # pyright: ignore[reportUnknownMemberType]
106- except AttributeError :
107- extras = None
182+ detail = _exception_detail_for_response (request , exc )
183+ extras = _exception_extra (exc )
108184 content : dict [str , Any ] = {"status_code" : status_code , "message" : detail }
109-
110- inertia_plugin : "InertiaPlugin | None"
111- try :
112- inertia_plugin = request .app .plugins .get ("InertiaPlugin" )
113- except KeyError :
114- inertia_plugin = None
185+ inertia_plugin = _get_inertia_plugin (request )
115186
116187 if extras :
117188 content .update ({"extra" : extras })
@@ -120,46 +191,45 @@ def create_inertia_exception_response(request: "Request[UserT, AuthT, StateT]",
120191 if detail :
121192 flash_succeeded = flash (request , detail , category = "error" )
122193
123- if extras and isinstance (extras , (list , tuple )) and len (extras ) >= 1 : # pyright: ignore[reportUnknownArgumentType]
124- first_extra = extras [0 ] # pyright: ignore[reportUnknownVariableType]
125- if isinstance (first_extra , dict ):
126- message : dict [str , str ] = cast ("dict[str, str]" , first_extra )
127- key_value = message .get ("key" )
128- default_field = f"root.{ key_value } " if key_value is not None else "root"
129- error_detail = str (message .get ("message" , detail ) or detail )
130- match = FIELD_ERR_RE .search (error_detail )
131- field = match .group (1 ) if match else default_field
132- error (request , field , error_detail or detail )
194+ _store_field_errors (request , extras , detail )
133195
134196 if status_code in {HTTP_422_UNPROCESSABLE_ENTITY , HTTP_400_BAD_REQUEST } or isinstance (
135197 exc , PermissionDeniedException
136198 ):
137199 return InertiaBack (request )
138200
139201 if inertia_plugin is None :
140- return InertiaResponse [Any ](media_type = preferred_type , content = content , status_code = status_code )
202+ return _create_exception_page_response (
203+ content = content ,
204+ preferred_type = preferred_type ,
205+ route_component = route_component ,
206+ is_inertia = is_inertia ,
207+ status_code = status_code ,
208+ )
141209
142- is_unauthorized = status_code == HTTP_401_UNAUTHORIZED or isinstance (exc , NotAuthorizedException )
143- redirect_to_login = inertia_plugin .config .redirect_unauthorized_to
144- if is_unauthorized and redirect_to_login is not None :
145- if request .url .path != redirect_to_login :
146- # If flash failed (no session), pass error message via query param
147- if not flash_succeeded and detail :
148- parsed = urlparse (redirect_to_login )
149- error_param = f"error={ quote (detail , safe = '' )} "
150- query = f"{ parsed .query } &{ error_param } " if parsed .query else error_param
151- redirect_to_login = urlunparse (parsed ._replace (query = query ))
152- return InertiaRedirect (request , redirect_to = redirect_to_login )
153- # Already on login page - redirect back so Inertia processes flash messages
154- # (Inertia.js shows 4xx responses in a modal instead of updating page state)
155- return InertiaBack (request )
210+ unauthorized_response = _create_unauthorized_response (
211+ request ,
212+ detail = detail ,
213+ flash_succeeded = flash_succeeded ,
214+ inertia_plugin = inertia_plugin ,
215+ status_code = status_code ,
216+ exc = exc ,
217+ )
218+ if unauthorized_response is not None :
219+ return unauthorized_response
156220
157221 if status_code in {HTTP_404_NOT_FOUND , HTTP_405_METHOD_NOT_ALLOWED } and (
158222 inertia_plugin .config .redirect_404 is not None and request .url .path != inertia_plugin .config .redirect_404
159223 ):
160224 return InertiaRedirect (request , redirect_to = inertia_plugin .config .redirect_404 )
161225
162- return InertiaResponse [Any ](media_type = preferred_type , content = content , status_code = status_code )
226+ return _create_exception_page_response (
227+ content = content ,
228+ preferred_type = preferred_type ,
229+ route_component = route_component ,
230+ is_inertia = is_inertia ,
231+ status_code = status_code ,
232+ )
163233
164234
165235def _register_exception_handlers ( # pyright: ignore[reportUnusedFunction]
0 commit comments