What

  • Two paths: controller exceptions vs pre-controller (filters/interceptors) exceptions.
  • Single response shape via GlobalExceptionHandler for both paths.

Flow (who catches what):

  1. Controller throws → handled by @ControllerAdvice methods.
  2. Filter/interceptor throws → not handled by @ControllerAdvice unless you forward via HandlerExceptionResolver.

Why

  • Consistency: avoid container default error pages/messages.
  • Control: send structured GenericResponse with proper HTTP codes.

How

  • Global handler (GlobalExceptionHandler):
@ControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(UnAuthorizedExceptionThrowable::class)
    fun handleUnauthorized(ex: UnAuthorizedExceptionThrowable, req: WebRequest): ResponseEntity<GenericResponse<Nothing>> {
        val body = GenericResponse<Nothing>(
            responseType = ResponseType.FAIL,
            message = ex.errorMessage,
            code = ex.code,
            type = ex.errorMessage
        )
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body)
    }

    @ExceptionHandler(Exception::class)
    fun handleGeneric(ex: Exception, req: WebRequest) =
        ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
            GenericResponse<Nothing>(
                responseType = ResponseType.ERROR,
                message = ex.message ?: "Internal server error",
                code = HttpStatus.INTERNAL_SERVER_ERROR.value(),
                type = "Internal server error"
            )
        )
}

Explanation:

  • Centralizes error shape with GenericResponse and correct HTTP status codes.

  • Add more handlers for Forbidden, BadRequest, TooManyRequests etc. to return precise codes consistently (see your GlobalExceptionHandler).

  • Route filter errors to the handler (JwtAuthenticationFilter):

override fun doFilterInternal(req: HttpServletRequest, res: HttpServletResponse, chain: FilterChain) {
    try {
        val token = jwtUtilMaster.getJwtAuth(req, mustReturnTokenOrElseCrash = true)
        if (token == null) {
            handlerExceptionResolver.resolveException(
                req, res, null, Exception("JWT token is required for this endpoint")
            )
            return
        }
        // ... authenticate and continue ...
        chain.doFilter(req, res)
    } catch (e: Exception) {
        handlerExceptionResolver.resolveException(req, res, null, e)
    }
}

Explanation:

  • Filters run before controllers; exceptions here bypass @ControllerAdvice by default.

  • handlerExceptionResolver.resolveException(...) forwards the error into your global handler so clients always see the same response format.

  • Prefer resolving (not writing to the response yourself) to avoid double-commit errors.

  • Use RuntimeException-based custom errors:

class UnAuthorizedException(
    val errorMessage: String = "Unauthorized access",
    val code: Int = 401
) : RuntimeException(errorMessage)

class UnAuthorizedExceptionThrowable(
    val errorMessage: String = "Unauthorized access",
    val code: Int = KeywordsAndConstants.TOKEN_NOT_VALID
) : RuntimeException(errorMessage)

Explanation:

  • Extending RuntimeException matches Spring’s expectation and keeps stack propagation simple.

  • Include a code and message to map directly to HTTP status and to your GenericResponse body.

  • Rules

    • Throw inside controllers → handled by @ControllerAdvice automatically.
    • Throw inside filters/interceptors → call handlerExceptionResolver.resolveException(...) to forward to the global handler.
    • Prefer project custom exceptions to set correct HTTP codes and messages centrally.
    • Do not write to HttpServletResponse before resolving; let the handler choose the status/body.