From 4067db3ecaf36b75330524fa7e4b3a568badbdd4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:00:29 +0000 Subject: [PATCH] Add ResourceNotFoundException handler for structured error responses Add explicit exception handlers for ResourceNotFoundException in both REST and GraphQL exception handlers to return structured error messages instead of raw 404 responses. - CustomizeExceptionHandler: returns {"message": "Resource not found"} with 404 - GraphQLCustomizeExceptionHandler: returns NOT_FOUND typed GraphQL error - ProfileApiTest: add tests for follow, unfollow, and get profile with nonexistent users Addresses MDD-78 Co-Authored-By: unknown <> --- .../exception/CustomizeExceptionHandler.java | 16 +++++++ .../GraphQLCustomizeExceptionHandler.java | 9 ++++ .../java/io/spring/api/ProfileApiTest.java | 43 ++++++++++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java b/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java index ade3ff4ff..4ce5a4255 100644 --- a/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java +++ b/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java @@ -21,6 +21,10 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +/** + * Global exception handler for REST API controllers. + */ + @RestControllerAdvice public class CustomizeExceptionHandler extends ResponseEntityExceptionHandler { @@ -59,6 +63,18 @@ public ResponseEntity handleInvalidAuthentication( }); } + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity handleResourceNotFound( + ResourceNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body( + new HashMap() { + { + put("message", "Resource not found"); + } + }); + } + @Override protected ResponseEntity handleMethodArgumentNotValid( MethodArgumentNotValidException e, diff --git a/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java b/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java index bf4768b3b..0919389f8 100644 --- a/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java +++ b/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java @@ -9,6 +9,7 @@ import graphql.execution.DataFetcherExceptionHandlerResult; import io.spring.api.exception.FieldErrorResource; import io.spring.api.exception.InvalidAuthenticationException; +import io.spring.api.exception.ResourceNotFoundException; import io.spring.graphql.types.Error; import io.spring.graphql.types.ErrorItem; import java.util.ArrayList; @@ -38,6 +39,14 @@ public DataFetcherExceptionHandlerResult onException( .path(handlerParameters.getPath()) .build(); return DataFetcherExceptionHandlerResult.newResult().error(graphqlError).build(); + } else if (handlerParameters.getException() instanceof ResourceNotFoundException) { + GraphQLError graphqlError = + TypedGraphQLError.newBuilder() + .errorType(ErrorType.NOT_FOUND) + .message("Resource not found") + .path(handlerParameters.getPath()) + .build(); + return DataFetcherExceptionHandlerResult.newResult().error(graphqlError).build(); } else if (handlerParameters.getException() instanceof ConstraintViolationException) { List errors = new ArrayList<>(); for (ConstraintViolation violation : diff --git a/src/test/java/io/spring/api/ProfileApiTest.java b/src/test/java/io/spring/api/ProfileApiTest.java index f32091ecd..09985970f 100644 --- a/src/test/java/io/spring/api/ProfileApiTest.java +++ b/src/test/java/io/spring/api/ProfileApiTest.java @@ -8,6 +8,7 @@ import io.restassured.module.mockmvc.RestAssuredMockMvc; import io.spring.JacksonCustomizations; +import io.spring.api.exception.CustomizeExceptionHandler; import io.spring.api.security.WebSecurityConfig; import io.spring.application.ProfileQueryService; import io.spring.application.data.ProfileData; @@ -23,7 +24,7 @@ import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(ProfileApi.class) -@Import({WebSecurityConfig.class, JacksonCustomizations.class}) +@Import({WebSecurityConfig.class, JacksonCustomizations.class, CustomizeExceptionHandler.class}) public class ProfileApiTest extends TestWithCurrentUser { private User anotherUser; @@ -93,4 +94,44 @@ public void should_unfollow_user_success() throws Exception { verify(userRepository).removeRelation(eq(followRelation)); } + + @Test + public void should_get_404_when_follow_nonexistent_user() throws Exception { + when(userRepository.findByUsername(eq("nonexistent"))) + .thenReturn(Optional.empty()); + given() + .header("Authorization", "Token " + token) + .when() + .post("/profiles/{username}/follow", "nonexistent") + .prettyPeek() + .then() + .statusCode(404) + .body("message", equalTo("Resource not found")); + } + + @Test + public void should_get_404_when_unfollow_nonexistent_user() throws Exception { + when(userRepository.findByUsername(eq("nonexistent"))) + .thenReturn(Optional.empty()); + given() + .header("Authorization", "Token " + token) + .when() + .delete("/profiles/{username}/follow", "nonexistent") + .prettyPeek() + .then() + .statusCode(404) + .body("message", equalTo("Resource not found")); + } + + @Test + public void should_get_404_when_get_nonexistent_profile() throws Exception { + when(profileQueryService.findByUsername(eq("nonexistent"), eq(null))) + .thenReturn(Optional.empty()); + RestAssuredMockMvc.when() + .get("/profiles/{username}", "nonexistent") + .prettyPeek() + .then() + .statusCode(404) + .body("message", equalTo("Resource not found")); + } }