Exception handling #82

Merged
stne3960 merged 6 commits from feature/exception-handling into main 2026-02-02 11:18:37 +01:00
2 changed files with 89 additions and 0 deletions
Showing only changes of commit 0367923123 - Show all commits

View File

@ -32,6 +32,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId> <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- OpenAPI specification --> <!-- OpenAPI specification -->
<dependency> <dependency>

View File

@ -0,0 +1,85 @@
package se.su.dsv.studentportalen.bff.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Centralized exception handler for all REST controllers.
* Returns RFC 7807 Problem Details for all error responses.
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* Handles Jakarta Bean Validation errors (missing/invalid fields).
* Triggered when @Valid fails on request body.
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
Map<String, List<String>> violations = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.groupingBy(
FieldError::getField,
Collectors.mapping(
error -> error.getDefaultMessage() != null ? error.getDefaultMessage() : "Invalid value",
Collectors.toList()
)
));
LOG.warn("Validation failed: {}", violations);
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problem.setType(URI.create("studentportalen:validation-error"));
problem.setTitle("Validation failed");
problem.setDetail("One or more fields are invalid");
problem.setProperty("violations", violations);
return problem;
}
/**
* Handles business exceptions from the BFF.
* These include mapped errors from backend systems like Daisy.
*/
@ExceptionHandler(StudentportalenException.class)
public ProblemDetail handleBusinessError(StudentportalenException ex) {
LOG.warn("Business error: {} - {}", ex.getType(), ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatus(ex.getStatus());
problem.setType(URI.create(ex.getType()));
problem.setTitle(ex.getTitle());
problem.setDetail(ex.getMessage());
if (ex instanceof ValidationException ve && !ve.getViolations().isEmpty()) {
problem.setProperty("violations", ve.getViolations());
}
return problem;
}
/**
* Handles unexpected errors.
* Logs the full stack trace but returns a generic message to the client.
*/
@ExceptionHandler(Exception.class)
public ProblemDetail handleUnexpected(Exception ex) {
LOG.error("Unexpected error", ex);
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
problem.setType(URI.create("studentportalen:internal-error"));
problem.setTitle("An unexpected error occurred");
problem.setDetail("Please try again later");
return problem;
}
}