InfrastructureExceptionHandler.java

package com.project.auth.config.web;

import com.project.auth.application.support.exception.CommonErrorCode;
import com.project.auth.application.support.logging.LogSanitizer;
import com.project.auth.domain.user.exception.DomainException;
import com.project.auth.infrastructure.support.exception.InfrastructureException;
import com.project.auth.presentation.support.exception.ApiErrorHttpStatusMapper;
import com.project.auth.presentation.support.response.ApiResult;
import com.project.auth.presentation.support.response.ApiResultFactory;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * InfrastructureErrorCode is kept as an internal classification for logs and alerting.
 * Client responses are intentionally normalized to COMMON-999 to avoid exposing internal dependency details.
 * This advice stays in bootstrap because moving it to presentation would create a
 * presentation -> infrastructure dependency and break the layer rule.
 */
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class InfrastructureExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(InfrastructureExceptionHandler.class);

    private final ApiResultFactory apiResultFactory;

    public InfrastructureExceptionHandler(ApiResultFactory apiResultFactory) {
        this.apiResultFactory = apiResultFactory;
    }

    @ExceptionHandler(InfrastructureException.class)
    public ResponseEntity<ApiResult<Void>> handleInfrastructureException(
            InfrastructureException exception,
            HttpServletRequest request
    ) {
        log.error(
                "Infrastructure failure. errorCode={} method={} requestPath={}",
                exception.getErrorCode().code(),
                request.getMethod(),
                LogSanitizer.requestPath(request.getRequestURI()),
                exception
        );

        return ResponseEntity.status(ApiErrorHttpStatusMapper.map(CommonErrorCode.INTERNAL_SERVER_ERROR))
                .body(apiResultFactory.failure(
                        CommonErrorCode.INTERNAL_SERVER_ERROR.code(),
                        CommonErrorCode.INTERNAL_SERVER_ERROR.message()
                ));
    }

    @ExceptionHandler(DomainException.class)
    public ResponseEntity<ApiResult<Void>> handleLeakedDomainException(
            DomainException exception,
            HttpServletRequest request
    ) {
        log.error(
                "Leaked domain exception. method={} requestPath={}",
                request.getMethod(),
                LogSanitizer.requestPath(request.getRequestURI()),
                exception
        );

        return ResponseEntity.status(ApiErrorHttpStatusMapper.map(CommonErrorCode.INTERNAL_SERVER_ERROR))
                .body(apiResultFactory.failure(
                        CommonErrorCode.INTERNAL_SERVER_ERROR.code(),
                        CommonErrorCode.INTERNAL_SERVER_ERROR.message()
                ));
    }
}