Skip to content

Add comprehensive input validation and error handling across all API controllers#528

Open
devin-ai-integration[bot] wants to merge 11 commits intomasterfrom
devin/1775708587-input-validation-complete
Open

Add comprehensive input validation and error handling across all API controllers#528
devin-ai-integration[bot] wants to merge 11 commits intomasterfrom
devin/1775708587-input-validation-complete

Conversation

@devin-ai-integration
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot commented Apr 9, 2026

Summary

Comprehensive input validation and structured error handling added across all REST API controllers. This PR integrates work from 5 parallel child sessions:

  • @Size constraints on all DTO/Param classes (NewArticleParam, UpdateArticleParam, RegisterParam, UpdateUserParam, LoginParam, NewCommentParam) to enforce field length limits
  • @Validated + param validation on all 7 controllers — @NotBlank on path variables, @Min/@Max on pagination query params (offset >= 0, 1 <= limit <= 100)
  • Enhanced CustomizeExceptionHandler with explicit handlers for ResourceNotFoundException (404), NoAuthorizationException (403), malformed JSON (422), and a generic Exception fallback (500 with logging) — all returning structured {"errors": {...}} bodies
  • Removed @ResponseStatus from ResourceNotFoundException and NoAuthorizationException (status now controlled by the handler)
  • Fixed unsafe authorization.split(" ")[1] in CurrentUserApi — replaced with extractToken() that validates the "Token " prefix and throws InvalidAuthenticationException("Authorization header must use Token scheme") on malformed headers
  • New tests covering negative offset, excessive/zero limit, overly long comment body, short password, malformed JSON, and malformed Authorization header
  • gradle.properties added with JDK 17 --add-exports args for Spotless compatibility

Updates since last revision

Addressed Devin Review feedback:

  • Added logger.error("Unhandled exception", ex) to the catch-all Exception handler so unhandled errors are logged with stack traces before returning 500
  • Added a String message constructor to InvalidAuthenticationException and updated extractToken to throw with "Authorization header must use Token scheme" instead of the misleading "invalid email or password"

Review & Testing Checklist for Human

  • Generic Exception catch-all handler (CustomizeExceptionHandler.java:151): This catches all unhandled exceptions, which may intercept Spring's built-in handling for HttpRequestMethodNotSupportedException (405), MissingServletRequestParameterException (400), etc. Verify these still return correct status codes, or consider narrowing the catch-all.
  • getParam helper change (CustomizeExceptionHandler.java:165): Logic changed from splits.length == 1 to splits.length <= 2. Verify existing ConstraintViolationException error responses still format field names correctly — this affects how @Validated parameter violations display in the errors body.
  • Non-deterministic test ordering: should_show_error_message_for_blank_username asserts on errors.username[0], but @NotBlank and @Size(min=1) now both fire on empty input — error ordering is not guaranteed. This test passed locally but may be flaky.
  • @NotBlank on path variables (e.g., @NotBlank @PathVariable("slug") String slug): For controllers with path-based routing like /articles/{slug}, a blank slug means a different URL entirely. Verify this validation actually triggers in practice vs. Spring returning 404 before it's reached.

Suggested Test Plan

  1. Run ./gradlew clean test — all 75 tests should pass
  2. Boot the app (./gradlew bootRun) and manually test:
    • GET /articles?offset=-1 → 422
    • GET /articles?limit=0 → 422
    • POST /users with {"user":{"email":"a@b.com","username":"x","password":"short"}} → 422
    • POST /users with malformed JSON body → 422
    • GET /user with Authorization: BadHeader → 422 with "Authorization header must use Token scheme"
    • GET /articles/nonexistent-slug → 404 with {"errors":{"resource":["not found"]}}
    • DELETE /articles/{slug} without auth → 403 with structured body
    • POST /invalid-endpoint → verify it still returns 405 (not 500 from the catch-all)
  3. Run the full suite 3–5 times to check for flaky test ordering issues

Notes

  • The DefaultJwtServiceTest.java diff is a Spotless auto-format change only (line wrapping), no logic change.
  • Child PRs: #522, #523, #524, #525, #526 — can be closed once this integrated PR is merged.

Link to Devin session: https://app.devin.ai/sessions/96594bc364384ae292afa201e2f9d351
Requested by: @tobydrinkall


Open with Devin

devin-ai-integration bot and others added 9 commits April 9, 2026 04:06
- NewArticleParam: @SiZe on title(255), description(255), body(65535)
- UpdateArticleParam: @SiZe on title(255), description(255), body(65535)
- RegisterParam: @SiZe on email(1-255), username(1-20), password(8-72)
- UpdateUserParam: @SiZe on email(255), username(20), password(72), bio(65535), image(512)
- LoginParam: @SiZe on email(255), password(72)
- NewCommentParam: @SiZe on body(65535)

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
Replace unsafe authorization.split(" ")[1] calls with a safe
extractToken() helper that validates the header starts with "Token "
before extracting the token value. Throws InvalidAuthenticationException
for malformed Authorization headers instead of ArrayIndexOutOfBoundsException.

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
- Add @validated class-level annotation to all 7 API controllers
- Add @notblank to path variables in ArticleApi, ArticleFavoriteApi, CommentsApi, ProfileApi
- Add @Min/@max to offset/limit query params in ArticlesApi
- Run spotlessJavaApply for Google Java Format compliance

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
- Add ResourceNotFoundException handler (404 with structured error body)
- Add NoAuthorizationException handler (403 with structured error body)
- Override handleHttpMessageNotReadable for malformed JSON (422)
- Add generic Exception fallback handler (500)
- Remove @ResponseStatus annotations from ResourceNotFoundException and NoAuthorizationException
- Add HttpMessageNotReadableException import

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
When @validated is on the controller class, constraint violation property
paths have 2 segments (e.g., article.slug) instead of 3. The previous
code used Arrays.copyOfRange(splits, 2, length) which returned empty
string for 2-segment paths. Now returns the last segment for paths
with 1 or 2 segments.

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
…ollers' into devin/1775708081-validation-tests
- ArticlesApiTest: negative offset, excessive limit, zero limit return 422
- CommentsApiTest: overly long comment body (>65535 chars) returns 422
- UsersApiTest: short password (<8 chars) and malformed JSON return 422
- CurrentUserApiTest: malformed Authorization header returns 422
- Apply Spotless formatting to DefaultJwtServiceTest
- Add gradle.properties with JDK 17 JVM args for Spotless compatibility

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
@devin-ai-integration
Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

… error message

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Toby Drinkall <toby.drinkall@cognition.ai>
@devin-ai-integration
Copy link
Copy Markdown
Author

End-to-End Test Results

Ran Spring Boot app locally (./gradlew bootRun) against clean SQLite DB, tested all validation and error handling endpoints via curl.

Session: https://app.devin.ai/sessions/96594bc364384ae292afa201e2f9d351

Validation & Error Handling Tests (12/12 passed)
# Test Status
1 GET /articles?offset=-1 → 422, "must be greater than or equal to 0" PASSED
2 GET /articles?limit=0 → 422, "must be greater than or equal to 1" PASSED
3 GET /articles?limit=101 → 422, "must be less than or equal to 100" PASSED
4 POST /users short password → 422, "size must be between 8 and 72" PASSED
5 POST /users malformed JSON → 422, {"errors":{"body":["malformed request body"]}} PASSED
6 GET /user bad auth header → 401 (blocked by security filter) PASSED
6a GET /user with Bearer <valid_jwt> → 422, "Authorization header must use Token scheme" PASSED
6b GET /user with Token <valid_jwt> → 200 with user data (positive control) PASSED
7 GET /user no auth header → 401 (blocked by security filter) PASSED
8 POST /users valid registration → 201 with JWT token (positive control) PASSED
9 GET /articles/nonexistent-slug → 404, {"errors":{"resource":["not found"]}} PASSED
10 GET /articles?offset=0&limit=10 → 200 with articles array (positive control) PASSED
Key Evidence

Pagination validation (@Min/@Max):

$ curl -s 'localhost:8080/articles?offset=-1'
HTTP 422: {"errors":{"offset":["must be greater than or equal to 0"]}}

$ curl -s 'localhost:8080/articles?limit=0'
HTTP 422: {"errors":{"limit":["must be greater than or equal to 1"]}}

$ curl -s 'localhost:8080/articles?limit=101'
HTTP 422: {"errors":{"limit":["must be less than or equal to 100"]}}

Password size constraint (@Size(min=8, max=72)):

$ curl -s -X POST localhost:8080/users -d '{"user":{"email":"t@t.com","username":"t","password":"short"}}'
HTTP 422: {"errors":{"password":["size must be between 8 and 72"]}}

Auth header scheme validation (extractToken fix):

$ curl -s localhost:8080/user -H "Authorization: Bearer <valid_jwt>"
HTTP 422: {"message":"Authorization header must use Token scheme"}

404 structured response:

$ curl -s localhost:8080/articles/nonexistent-slug-12345
HTTP 404: {"errors":{"resource":["not found"]}}
Note on Auth Header Tests

Tests 6 & 7 return 401 from Spring Security's JwtTokenFilter (which intercepts before the controller). The extractToken fix in CurrentUserApi is exercised when a valid JWT arrives with the wrong scheme (Test 6a: Bearer instead of Token), which correctly returns 422 with "Authorization header must use Token scheme".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant