Skip to content

Commit e4448e3

Browse files
committed
feat: update error handling follows RFC 9457
1 parent 9adbcbc commit e4448e3

5 files changed

Lines changed: 46 additions & 61 deletions

File tree

apps/backend/src/main/kotlin/org/tobynguyen/solitar/controller/ForwardController.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.tobynguyen.solitar.controller
22

3+
import jakarta.validation.Valid
34
import org.springframework.http.ResponseEntity
45
import org.springframework.web.bind.annotation.PostMapping
56
import org.springframework.web.bind.annotation.RequestBody
@@ -13,7 +14,7 @@ import org.tobynguyen.solitar.service.UrlService
1314
@RequestMapping("/forward")
1415
class ForwardController(private val urlService: UrlService) {
1516
@PostMapping
16-
fun forwardUrl(@RequestBody body: UrlForwardDto): ResponseEntity<UrlForwardResponseDto> {
17+
fun forwardUrl(@RequestBody @Valid body: UrlForwardDto): ResponseEntity<UrlForwardResponseDto> {
1718
val result = urlService.getOriginalUrl(body)
1819

1920
return ResponseEntity.ok(result)
Lines changed: 32 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,53 @@
11
package org.tobynguyen.solitar.exception
22

3-
import jakarta.servlet.http.HttpServletRequest
3+
import org.springframework.http.HttpHeaders
44
import org.springframework.http.HttpStatus
5+
import org.springframework.http.HttpStatusCode
6+
import org.springframework.http.ProblemDetail
57
import org.springframework.http.ResponseEntity
68
import org.springframework.web.bind.MethodArgumentNotValidException
79
import org.springframework.web.bind.annotation.ExceptionHandler
8-
import org.springframework.web.bind.annotation.ResponseStatus
910
import org.springframework.web.bind.annotation.RestControllerAdvice
10-
import org.tobynguyen.solitar.model.dto.ErrorResponse
11+
import org.springframework.web.context.request.WebRequest
12+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
1113

1214
@RestControllerAdvice
13-
class UrlExceptionHandler {
14-
15-
@ExceptionHandler(MethodArgumentNotValidException::class)
16-
@ResponseStatus(HttpStatus.BAD_REQUEST)
17-
fun onValidationFailed(e: MethodArgumentNotValidException): ResponseEntity<Map<String, Any>> {
18-
val map = buildMap {
19-
e.bindingResult.fieldErrors.forEach {
20-
put(it.field, it.defaultMessage ?: "Validation failed")
15+
class UrlExceptionHandler : ResponseEntityExceptionHandler() {
16+
17+
override fun handleMethodArgumentNotValid(
18+
ex: MethodArgumentNotValidException,
19+
headers: HttpHeaders,
20+
status: HttpStatusCode,
21+
request: WebRequest,
22+
): ResponseEntity<Any> {
23+
val invalidParams =
24+
ex.bindingResult.fieldErrors.map {
25+
mapOf("name" to it.field, "reason" to (it.defaultMessage ?: "Validation failed"))
2126
}
22-
}
2327

24-
return ResponseEntity.badRequest().body(map)
28+
val problemDetail =
29+
ProblemDetail.forStatusAndDetail(
30+
HttpStatus.BAD_REQUEST,
31+
"The request contained invalid data. Please check the 'invalid_params' array.",
32+
)
33+
.apply { setProperty("invalid_params", invalidParams) }
34+
35+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail)
2536
}
2637

2738
@ExceptionHandler(UrlNotFoundException::class)
28-
@ResponseStatus(HttpStatus.NOT_FOUND)
29-
fun onUrlNotFound(e: UrlNotFoundException, request: HttpServletRequest) =
30-
ErrorResponse(
31-
status = HttpStatus.NOT_FOUND.value(),
32-
error = HttpStatus.NOT_FOUND.reasonPhrase,
33-
message = e.message,
34-
path = request.requestURI,
35-
)
39+
fun onUrlNotFound(e: UrlNotFoundException) =
40+
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.message)
3641

3742
@ExceptionHandler(UrlExpiredException::class)
38-
@ResponseStatus(HttpStatus.GONE)
39-
fun onUrlExpired(e: UrlExpiredException, request: HttpServletRequest) =
40-
ErrorResponse(
41-
status = HttpStatus.GONE.value(),
42-
error = HttpStatus.GONE.reasonPhrase,
43-
message = e.message,
44-
path = request.requestURI,
45-
)
43+
fun onUrlExpired(e: UrlExpiredException) =
44+
ProblemDetail.forStatusAndDetail(HttpStatus.GONE, e.message)
4645

4746
@ExceptionHandler(UrlDisabledException::class)
48-
@ResponseStatus(HttpStatus.FORBIDDEN)
49-
fun onUrlDisabled(e: UrlDisabledException, request: HttpServletRequest) =
50-
ErrorResponse(
51-
status = HttpStatus.FORBIDDEN.value(),
52-
error = HttpStatus.FORBIDDEN.reasonPhrase,
53-
message = e.message,
54-
path = request.requestURI,
55-
)
47+
fun onUrlDisabled(e: UrlDisabledException) =
48+
ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, e.message)
5649

5750
@ExceptionHandler(UrlShortCodeConflictedException::class)
58-
@ResponseStatus(HttpStatus.CONFLICT)
59-
fun onUrlShortCodeConflicted(e: UrlShortCodeConflictedException, request: HttpServletRequest) =
60-
ErrorResponse(
61-
status = HttpStatus.CONFLICT.value(),
62-
error = HttpStatus.CONFLICT.reasonPhrase,
63-
message = e.message,
64-
path = request.requestURI,
65-
)
51+
fun onUrlShortCodeConflicted(e: UrlShortCodeConflictedException) =
52+
ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, e.message)
6653
}

apps/backend/src/main/kotlin/org/tobynguyen/solitar/model/dto/ErrorResponse.kt

Lines changed: 0 additions & 11 deletions
This file was deleted.

apps/backend/src/main/kotlin/org/tobynguyen/solitar/model/dto/UrlDto.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@ data class UrlCreateDto(
2525
@field:Size(message = "Password must be at least 3 characters", min = 3)
2626
@field:Size(message = "Password cannot exceed 255 characters", max = 255)
2727
val password: String?,
28-
) {}
28+
)
2929

30-
data class UrlResponseDto(val originalUrl: String, val shortCode: String) {}
30+
data class UrlResponseDto(val originalUrl: String, val shortCode: String)
3131

32-
data class UrlForwardResponseDto(val originalUrl: String) {}
32+
data class UrlForwardResponseDto(val originalUrl: String)
3333

34-
data class UrlForwardDto(val shortCode: String, val password: String?)
34+
data class UrlForwardDto(
35+
@field:NotBlank(message = "Short code is required") val shortCode: String,
36+
@field:Size(message = "Password must be at least 3 characters", min = 3)
37+
@field:Size(message = "Password cannot exceed 255 characters", max = 255)
38+
val password: String?,
39+
)

apps/backend/src/main/resources/application.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ spring:
33
name: solitar
44
main:
55
banner-mode: off
6+
mvc:
7+
problemdetails:
8+
enabled: true
69
datasource:
710
url: ${DATABASE_URL}
811
username: ${DATABASE_USERNAME}

0 commit comments

Comments
 (0)