11package io .stargate .sgv2 .jsonapi .exception .mappers ;
22
3+ import static io .stargate .sgv2 .jsonapi .util .ClassUtils .classSimpleName ;
4+
35import com .fasterxml .jackson .core .JsonParseException ;
46import com .fasterxml .jackson .core .exc .StreamConstraintsException ;
7+ import com .fasterxml .jackson .databind .JsonMappingException ;
58import com .fasterxml .jackson .databind .JsonNode ;
69import com .fasterxml .jackson .databind .exc .MismatchedInputException ;
10+ import com .fasterxml .jackson .databind .exc .UnrecognizedPropertyException ;
711import io .stargate .sgv2 .jsonapi .api .model .command .CollectionCommand ;
812import io .stargate .sgv2 .jsonapi .api .model .command .CommandResult ;
13+ import io .stargate .sgv2 .jsonapi .api .model .command .table .definition .datatype .ColumnDesc ;
914import io .stargate .sgv2 .jsonapi .api .model .command .tracing .RequestTracing ;
15+ import io .stargate .sgv2 .jsonapi .config .constants .ErrorConstants ;
1016import io .stargate .sgv2 .jsonapi .exception .*;
1117import jakarta .validation .ConstraintViolation ;
1218import jakarta .validation .ConstraintViolationException ;
13- import jakarta .ws .rs .NotAllowedException ;
14- import jakarta .ws .rs .NotFoundException ;
15- import jakarta .ws .rs .NotSupportedException ;
16- import jakarta .ws .rs .WebApplicationException ;
19+ import jakarta .ws .rs .*;
20+ import java .util .Collections ;
1721import java .util .Map ;
22+ import java .util .Optional ;
23+ import java .util .concurrent .TimeoutException ;
24+ import java .util .stream .Collectors ;
1825import org .jboss .resteasy .reactive .RestResponse ;
1926import org .jboss .resteasy .reactive .server .ServerExceptionMapper ;
2027import org .slf4j .Logger ;
2330/**
2431 * Exception mappers that are bound into the quarkus framework to handle exceptions raised form
2532 * there.
33+ *
34+ * <p>Using different functions with decorators to make it a bit clearer what is handled where.
35+ *
36+ * <p><b>NOTE:</b>Not exactly sure how quarkus if finding the mappers when subclasses are thrown, be
37+ * careful making changes specially with {@link #mapJacksonException(Throwable)}
2638 */
2739public class FrameworkExceptionMapper {
2840 private static final Logger LOGGER = LoggerFactory .getLogger (FrameworkExceptionMapper .class );
2941
3042 /**
3143 * Prefix used in constraint violation property paths that should be stripped out. <b>NOTE:</b>
32- * This name must match the name of the function in the ResourceHandeler , e.g. {@link
44+ * This name must match the name of the function in the ResourceHandler , e.g. {@link
3345 * io.stargate.sgv2.jsonapi.api.v1.CollectionResource#postCommand(CollectionCommand, String,
3446 * String)}
3547 */
@@ -47,121 +59,196 @@ public class FrameworkExceptionMapper {
4759 * <p>This could include ApiExceptions that we throw from inside our deserialization code called
4860 * from quarkus.
4961 */
50- @ ServerExceptionMapper
62+ @ ServerExceptionMapper ({ Throwable . class })
5163 public RestResponse <CommandResult > mapThrowable (Throwable throwable ) {
52- if (LOGGER .isDebugEnabled ()) {
53- LOGGER .debug ("mapThrowable() - mapping attached exception" , throwable );
54- }
5564
56- var mapped = ThrowableToErrorMapper . mapThrowable (throwable );
65+ var translated = translateThrowable (throwable );
5766 if (LOGGER .isDebugEnabled ()) {
58- LOGGER .debug ("mapThrowable() - mapped to attached exception" , mapped );
67+ LOGGER .debug (
68+ "mapThrowable() - mapped to attached exception throwable.class={}, throwable.message={}" ,
69+ classSimpleName (throwable ),
70+ throwable .getMessage (),
71+ translated );
5972 }
60- return CommandResult .statusOnlyBuilder (RequestTracing .NO_OP )
61- .addThrowable (mapped )
62- .build ()
63- .toRestResponse ();
73+
74+ return responseForException (translated );
6475 }
6576
6677 /**
6778 * Mapping for jackson parsing and mapping exceptions
6879 *
69- * <p>Uses the attribute because these two Jackson exceptions do not have a common parent.
80+ * <p><b>NOTE:</b> This needs to have the specific exception classes listed to be called by
81+ * quarkus. If not, the {@link com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException}
82+ * will not be handled.
7083 */
7184 @ ServerExceptionMapper ({JsonParseException .class , MismatchedInputException .class })
7285 public RestResponse <CommandResult > mapJacksonException (Throwable jacksonException ) {
7386
87+ var translated = translateThrowable (jacksonException );
7488 if (LOGGER .isDebugEnabled ()) {
75- LOGGER .debug ("mapJacksonException() - mapping attached exception" , jacksonException );
76- }
77-
78- /// TODO: Aaron - bring the handling for jackon errors into this class
79- var mapped = ThrowableToErrorMapper .mapThrowable (jacksonException );
80- if (LOGGER .isDebugEnabled ()) {
81- LOGGER .debug ("mapJacksonException() - mapped to attached exception" , mapped );
89+ LOGGER .debug (
90+ "mapJacksonException() - mapped to attached exception jacksonException.class={}, jacksonException.message={}" ,
91+ classSimpleName (jacksonException ),
92+ jacksonException .getMessage (),
93+ translated );
8294 }
83- return CommandResult .statusOnlyBuilder (RequestTracing .NO_OP )
84- .addThrowable (mapped )
85- .build ()
86- .toRestResponse ();
95+ return responseForException (translated );
8796 }
8897
8998 /**
9099 * Mapping for Jakarta WebApplicationException and its subtypes
91100 *
92101 * <p>
93102 */
94- @ ServerExceptionMapper
95- public RestResponse <CommandResult > mapJakartaException (WebApplicationException wae ) {
103+ @ ServerExceptionMapper ({WebApplicationException .class })
104+ public RestResponse <CommandResult > mapJakartaException (
105+ WebApplicationException webApplicationException ) {
96106
97107 // 06-Nov-2023, tatu: Let's dig the innermost root cause; needed f.ex for [jsonapi#448]
98108 // to get to StreamConstraintsException
99- Throwable toReport = wae ;
109+ Throwable toReport = webApplicationException ;
100110 while (toReport .getCause () != null ) {
101111 toReport = toReport .getCause ();
102112 }
103113
104- if (LOGGER .isDebugEnabled ()) {
114+ if (toReport != webApplicationException && LOGGER .isDebugEnabled ()) {
105115 LOGGER .debug (
106- "mapJakartaException() - wae='{}', translating attached exception" , wae , toReport );
116+ "mapJakartaException() - processing cause of original exception attached, webApplicationException.class={}, webApplicationException.message={}" ,
117+ classSimpleName (webApplicationException ),
118+ webApplicationException .getMessage (),
119+ toReport );
107120 }
108121
109- var resultBuilder = CommandResult .statusOnlyBuilder (RequestTracing .NO_OP );
110-
111- var restResponse =
112- switch (toReport ) {
113- case APIException ae -> // Already an APIException, nothing to do
114- resultBuilder .addThrowable (ae ).build ().toRestResponse ();
115- case JsonApiException jae -> resultBuilder .addThrowable (jae ).build ().toRestResponse ();
116- case StreamConstraintsException sce ->
117- resultBuilder
118- .addThrowable (
119- DocumentException .Code .SHRED_DOC_LIMIT_VIOLATION .get (
120- Map .of ("errorMessage" , sce .getMessage ())))
121- .build ()
122- .toRestResponse ();
123- case NotAllowedException nae -> responseForException (nae );
124- case NotFoundException nfe -> responseForException (nfe );
125- case NotSupportedException nse ->
126- resultBuilder
127- .addThrowable (RequestException .Code .UNSUPPORTED_CONTENT_TYPE .get ())
128- .build ()
129- .toRestResponse ();
130- default ->
131- resultBuilder
132- .addThrowable (ThrowableToErrorMapper .mapThrowable (toReport ))
133- .build ()
134- .toRestResponse ();
135- };
136-
122+ var translated = translateThrowable (toReport );
137123 if (LOGGER .isDebugEnabled ()) {
138124 LOGGER .debug (
139- "mapJakartaException() - returning restResponse.getStatusInfo()={}" ,
140- restResponse .getStatusInfo ());
125+ "mapJakartaException() - mapped to attached exception toReport.class={}, toReport.message={}" ,
126+ classSimpleName (toReport ),
127+ toReport .getMessage (),
128+ translated );
141129 }
142- return restResponse ;
130+ return responseForException ( translated ) ;
143131 }
144132
145- @ ServerExceptionMapper
133+ @ ServerExceptionMapper ({ ConstraintViolationException . class })
146134 public RestResponse <CommandResult > constraintViolationException (
147135 ConstraintViolationException exception ) {
148136
149- if (LOGGER .isDebugEnabled ()) {
150- LOGGER .debug ("constraintViolationException() - mapping attached exception" , exception );
151- }
152-
153137 var builder = CommandResult .statusOnlyBuilder (RequestTracing .NO_OP );
154138
155139 // this used to have a distinct() call, but it was doing it on all CommandError objects which
156140 // like the ApiException has a unique ID so had not affect.
141+ // LOGGING is in apiExceptionFor()
157142 exception .getConstraintViolations ().stream ()
158- .map (FrameworkExceptionMapper ::constraintException )
143+ .map (FrameworkExceptionMapper ::apiExceptionFor )
159144 .forEach (builder ::addThrowable );
160145
161146 return builder .build ().toRestResponse ();
162147 }
163148
164- private static APIException constraintException (ConstraintViolation <?> violation ) {
149+ private static RestResponse <CommandResult > responseForException (RuntimeException exception ) {
150+
151+ return switch (exception ) {
152+ case ClientErrorException cee -> RestResponse .status (cee .getResponse ().getStatus ());
153+ default ->
154+ CommandResult .statusOnlyBuilder (RequestTracing .NO_OP )
155+ .addThrowable (exception )
156+ .build ()
157+ .toRestResponse ();
158+ };
159+ }
160+
161+ /**
162+ * Translate a Throwable into an appropriate APIException or JsonApiException.
163+ *
164+ * <p>NOTES: this code is refactored from the old ThrowableToErrorMapper class, it is missing
165+ * specific handling befor below because I could not see how they triggered(amorton 26 jan 2026):
166+ *
167+ * <ul>
168+ * <li>{@link io.quarkus.security.UnauthorizedException}
169+ * <li>{@link java.util.concurrent.TimeoutException}
170+ * </ul>
171+ *
172+ * @param throwable
173+ * @return
174+ */
175+ public static RuntimeException translateThrowable (Throwable throwable ) {
176+ return switch (throwable ) {
177+ case APIException ae -> ae ;
178+ case JsonApiException jae -> jae ;
179+
180+ // ##########
181+ // WEIRD ONES FROM throwableToErrorMapper
182+ // TODO: AARON - why would this happen ? was in old ThrowableToErrorMapper
183+ case TimeoutException te -> ErrorCodeV1 .EMBEDDING_PROVIDER_TIMEOUT .toApiException ();
184+
185+ // ##########
186+ // # QUARKUS ERRORS
187+ // these are the client 4xx errors , no change means we return them as-is e.g. the 4XX code
188+ case NotAllowedException na -> na ;
189+ case NotFoundException nf -> nf ;
190+ case NotSupportedException nse -> RequestException .Code .UNSUPPORTED_CONTENT_TYPE .get ();
191+
192+ // ##########
193+ // JACKSON ERRORS
194+
195+ case StreamConstraintsException sce ->
196+ DocumentException .Code .SHRED_DOC_LIMIT_VIOLATION .get (
197+ Map .of ("errorMessage" , sce .getMessage ()));
198+
199+ // Low-level parsing problem? Actual BAD_REQUEST (400) since we could not process
200+ case JsonParseException jpe ->
201+ RequestException .Code .REQUEST_NOT_JSON .get (
202+ Map .of (ErrorConstants .TemplateVars .ERROR_MESSAGE , jpe .getMessage ()));
203+
204+ // Unrecognized property? (note: CommandObjectMapperHandler handles some cases)
205+ // 09-Oct-2025, tatu: Retain custom exception message, if set by us:
206+ case UnrecognizedPropertyException upe when ColumnDesc .class .equals (
207+ upe .getReferringClass ()) ->
208+ RequestException .Code .COMMAND_FIELD_UNKNOWN .get (
209+ Map .of ("field" , upe .getPropertyName (), "message" , upe .getOriginalMessage ()));
210+
211+ // otherwise rewrite to avoid Jackson-isms:
212+ case UnrecognizedPropertyException upe -> {
213+ var knownIds =
214+ Optional .ofNullable (upe .getKnownPropertyIds ()).orElse (Collections .emptyList ());
215+ var knownDesc =
216+ knownIds .stream ()
217+ .map (ob -> String .format ("'%s'" , ob .toString ()))
218+ .sorted ()
219+ .collect (Collectors .joining (", " ));
220+ yield RequestException .Code .COMMAND_FIELD_UNKNOWN .get (
221+ Map .of (
222+ "field" ,
223+ upe .getPropertyName (),
224+ "message" ,
225+ "not one of known fields (%s)" .formatted (knownDesc )));
226+ }
227+
228+ // NOTE: must be after the UnrecognizedPropertyException check
229+ // 09-Jan-2025, tatu: [data-api#1812] Not ideal but slightly better than before
230+ case JsonMappingException jme ->
231+ RequestException .Code .REQUEST_STRUCTURE_MISMATCH .get (
232+ Map .of ("errorMessage" , jme .getMessage ()));
233+
234+ // ##########
235+ // DEFAULT
236+ default -> {
237+ var e =
238+ ServerException .Code .UNEXPECTED_SERVER_ERROR .get (ErrorFormatters .errVars (throwable ));
239+ if (LOGGER .isErrorEnabled ()) {
240+ LOGGER .error (
241+ "translateThrowable() - Unrecognized exception translated to attached SERVER_UNHANDLED_ERROR. throwable.class={}, throwable.message={}" ,
242+ classSimpleName (throwable ),
243+ throwable .getMessage (),
244+ e );
245+ }
246+ yield e ;
247+ }
248+ };
249+ }
250+
251+ private static APIException apiExceptionFor (ConstraintViolation <?> violation ) {
165252
166253 // Let's remove useless "postCommand." prefix if seen
167254 // This comes from the name of the function on the ResourceHandler
@@ -170,14 +257,24 @@ private static APIException constraintException(ConstraintViolation<?> violation
170257 ? violation .getPropertyPath ().toString ().substring (PREFIX_POST_COMMAND .length ())
171258 : violation .getPropertyPath ().toString ();
172259
173- return RequestException .Code .COMMAND_FIELD_VALUE_INVALID .get (
174- Map .of (
175- "field" ,
176- propertyPath ,
177- "value" ,
178- prettyPrintConstraintValue (violation .getInvalidValue ()),
179- "message" ,
180- violation .getMessage ()));
260+ var exception =
261+ RequestException .Code .COMMAND_FIELD_VALUE_INVALID .get (
262+ Map .of (
263+ "field" ,
264+ propertyPath ,
265+ "value" ,
266+ prettyPrintConstraintValue (violation .getInvalidValue ()),
267+ "message" ,
268+ violation .getMessage ()));
269+
270+ if (LOGGER .isDebugEnabled ()) {
271+ LOGGER .debug (
272+ "apiExceptionFor() - mapped constraint to attached exception violation.getPropertyPath={}, violation.getMessage={}" ,
273+ violation .getPropertyPath (),
274+ violation .getMessage (),
275+ exception );
276+ }
277+ return exception ;
181278 }
182279
183280 private static String prettyPrintConstraintValue (Object rawValue ) {
@@ -200,8 +297,4 @@ private static String prettyPrintConstraintValue(Object rawValue) {
200297 MAX_VALUE_LENGTH_TO_INCLUDE );
201298 };
202299 }
203-
204- private static RestResponse <CommandResult > responseForException (WebApplicationException wae ) {
205- return RestResponse .status (wae .getResponse ().getStatus ());
206- }
207300}
0 commit comments