Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = 'com.flexcodelabs'
version = '0.0.67'
version = '0.0.68'
description = 'Flextuma App'

java {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ public ResponseEntity<Object> handleConstraintViolationException(ConstraintViola
public ResponseEntity<Object> handleDatabaseError(DataIntegrityViolationException ex) {
Throwable rootCause = ex.getRootCause();
String detail = (rootCause != null) ? rootCause.getMessage() : ex.getMessage();
return buildResponse(sanitizeDatabaseError(detail), getResponseStatus(detail, HttpStatus.BAD_REQUEST), ex);
// Classify off the sanitized message, not the raw one: engine-specific wording (e.g. MySQL's
// "Duplicate entry ... for key ..." vs Postgres's "... already exists") only reliably normalizes
// to a wording getResponseStatus() recognizes ("X already exists") after sanitizeDatabaseError.
String sanitized = sanitizeDatabaseError(detail);
return buildResponse(sanitized, getResponseStatus(sanitized, HttpStatus.BAD_REQUEST), ex);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
Expand Down Expand Up @@ -307,23 +311,28 @@ private ResponseEntity<Object> buildResponse(String message, HttpStatus status)
return buildResponse(message, status, null);
}

// Callers pass the status the exception actually carries (or, for DataIntegrityViolationException's
// raw DB error text, one already inferred via getResponseStatus() before calling in) -- it must be
// used as-is here. Re-inferring from the message text on top of that (as this used to do) meant any
// explicit status whose message happened to contain a word like "invalid" or "missing" -- e.g. the
// 401/403 "Invalid username or password" from a failed login -- got silently downgraded to 400, which
// also defeated UNLOGGED_STATUSES below (401/403 are meant to be routine and not error-logged, but
// arriving here already remapped to 400 they'd get logged anyway).
private ResponseEntity<Object> buildResponse(String message, HttpStatus status, Throwable ex) {
HttpStatus finalStatus = getResponseStatus(message, status);

Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("error", finalStatus.getReasonPhrase());
body.put("error", status.getReasonPhrase());
body.put("message", message != null ? capitalize(message) : "No message available");

if (!UNLOGGED_STATUSES.contains(finalStatus)) {
if (!UNLOGGED_STATUSES.contains(status)) {
if (ex != null) {
log.error("Request failed with {}: {}", finalStatus, message, ex);
log.error("Request failed with {}: {}", status, message, ex);
} else {
log.error("Request failed with {}: {}", finalStatus, message);
log.error("Request failed with {}: {}", status, message);
}
}

return new ResponseEntity<>(body, finalStatus);
return new ResponseEntity<>(body, status);
}

private String capitalize(String str) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public void throwResponseStatus() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Bad request error");
}

@GetMapping("/test/response-status-forbidden-with-sniffable-message")
public void throwResponseStatusForbiddenWithSniffableMessage() {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Invalid username or password");
}

@GetMapping("/test/general")
public void throwGeneral() throws Exception {
throw new Exception("General error");
Expand Down Expand Up @@ -132,6 +137,16 @@ void handleResponseStatusException_shouldReturnCorrectStatusAndMessage() throws
.andExpect(jsonPath("$.message").value("Bad request error"));
}

@Test
void handleResponseStatusException_shouldNotDowngradeStatusBasedOnMessageWording() throws Exception {
// Regression test: the exception's own status (here FORBIDDEN) must win even though the
// message contains "invalid", which getResponseStatus() would otherwise read as BAD_REQUEST.
mockMvc.perform(get("/test/response-status-forbidden-with-sniffable-message")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.message").value("Invalid username or password"));
}

@Test
void handleGeneralException_shouldReturnInternalServerError() throws Exception {
mockMvc.perform(get("/test/general")
Expand Down