Skip to content

Commit c2baa3f

Browse files
authored
Merge branch 'main' into tatu/2285-errorv1-removal-17
2 parents 3577391 + bb2d944 commit c2baa3f

File tree

4 files changed

+179
-218
lines changed

4 files changed

+179
-218
lines changed
Lines changed: 175 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
package io.stargate.sgv2.jsonapi.exception.mappers;
22

3+
import static io.stargate.sgv2.jsonapi.util.ClassUtils.classSimpleName;
4+
35
import com.fasterxml.jackson.core.JsonParseException;
46
import com.fasterxml.jackson.core.exc.StreamConstraintsException;
7+
import com.fasterxml.jackson.databind.JsonMappingException;
58
import com.fasterxml.jackson.databind.JsonNode;
69
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
10+
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
711
import io.stargate.sgv2.jsonapi.api.model.command.CollectionCommand;
812
import io.stargate.sgv2.jsonapi.api.model.command.CommandResult;
13+
import io.stargate.sgv2.jsonapi.api.model.command.table.definition.datatype.ColumnDesc;
914
import io.stargate.sgv2.jsonapi.api.model.command.tracing.RequestTracing;
15+
import io.stargate.sgv2.jsonapi.config.constants.ErrorConstants;
1016
import io.stargate.sgv2.jsonapi.exception.*;
1117
import jakarta.validation.ConstraintViolation;
1218
import 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;
1721
import java.util.Map;
22+
import java.util.Optional;
23+
import java.util.concurrent.TimeoutException;
24+
import java.util.stream.Collectors;
1825
import org.jboss.resteasy.reactive.RestResponse;
1926
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
2027
import org.slf4j.Logger;
@@ -23,13 +30,18 @@
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
*/
2739
public 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

Comments
 (0)