내가 개발한 Whatnow 서버에서는 비동기적으로 슬랙으로 예외를 전송하고 있습니다.
ContentCachingRequest, ContentResponseWrapper 를 적용하여 body 를 래핑하여 다음 객체로 건내주면 된다고 생각했어요.
자 어떻게 적용하는지는 Document에 나와있습니다.
간단하게 적용해보았습니다. ContentCacheFilter를 만들어보겠습니다
@Component
class HttpContentCacheFilter : OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain,
) {
val wrappingRequest = ContentCachingRequestWrapper(request)
val wrappingResponse = ContentCachingResponseWrapper(response)
chain.doFilter(wrappingRequest, wrappingResponse)
wrappingResponse.copyBodyToResponse()
}
}
HttpServletRequest, Response 를 ContentCachingResponse로 래핑해서 사용하게 해줍니다.
이렇게 Wrapping 된 request, response 를 GlobalExceptionHandler 에서 사용해볼 수 있겠습니다.
@ExceptionHandler(Exception::class)
private fun handleException(
e: Exception,
request: HttpServletRequest?,
): ResponseEntity<ErrorResponse?> {
logger.error("Exception", e)
val cachingRequest = request as ContentCachingRequestWrapper
val url = UriComponentsBuilder.fromHttpRequest(ServletServerHttpRequest(request))
.build()
.toUriString()
val internalServerError = INTERNAL_SERVER_ERROR
val errorResponse = ErrorResponse(
internalServerError.status,
internalServerError.code,
internalServerError.reason,
url,
)
slackProvider.execute(cachingRequest, e, userId)
return ResponseEntity.status(HttpStatus.valueOf(internalServerError.status))
.body<ErrorResponse>(errorResponse)
}
override 해야하기 때문에 HttpServletRequest를 ContentCachingRequestWrapper 로 다시 캐스팅 해야합니다.
최종적으로 에러 핸들러에서 넘어오는 HttpRequest는 ContentCachingRequestWrapper 타입입니다.
그런데...
java.lang.ClassCastException:
class org.springframework.web.filter.ForwardedHeaderFilter$ForwardedHeaderExtractingRequest
cannot be cast to class org.springframework.web.util.ContentCachingRequestWrapper
왜 슬랙 알림이 안나갈까... 고민하다가..
이런 에러가 발생하는데 잘 보면 ForwardHeaderFilter 가 앞에서 먼저 동작하게 되면서 캐스팅 오류가 발생하면서 슬랙 에러 알림이 안날라가더라구요.. 이것도 모르고 계속 그냥 문제가 없네.. 생각하고 있었어요.
디버그를 계속 찍어보니까 Filter 순서가 안맞는 것이???
실제로 등록된 걸 보면 ServletFilter 가 먼저 originalChain 이 먼저 실행되고 그 뒤에 additionalFilters 가 실행이 되는데
img : FilterChainProxy$VirtualFilterChain
내부 additionalFilters에 등록된 필터에서는 ForwardedHeaderFilter 필터가 HttpContentCacheFilter 보다 앞에오게하면 된다고 생각했는데 당최 내가 선언적으로 filterConfig에 내가 작성한 순서대로 순서가 지정되지 않습니다.
그래서 이 스택오버 플로우의 의견를 보면 따로
https://stackoverflow.com/questions/25957879/filter-order-in-spring-boot
그래서 ServletFilterConfig 를 따로 설정을 해주는 작업을 해봤습니다.
@Configuration
@Profile("prod", "staging", "dev")
class ServletFilterConfig(
val forwardedHeaderFilter: ForwardedHeaderFilter,
val httpContentCacheFilter: HttpContentCacheFilter,
) : WebMvcConfigurer {
@Bean
fun securityFilterChain(
@Qualifier(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
securityFilter: Filter?,
): FilterRegistrationBean<Filter> {
val registration: FilterRegistrationBean<Filter> = FilterRegistrationBean(securityFilter)
registration.order = Int.MAX_VALUE - 3
registration.setName(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
return registration
}
@Bean
fun setResourceUrlEncodingFilter(): FilterRegistrationBean<ResourceUrlEncodingFilter> {
val registrationBean: FilterRegistrationBean<ResourceUrlEncodingFilter> = FilterRegistrationBean(ResourceUrlEncodingFilter())
registrationBean.order = Int.MAX_VALUE - 2
return registrationBean
}
@Bean
fun setForwardedHeaderFilterOrder(): FilterRegistrationBean<ForwardedHeaderFilter> {
val registrationBean: FilterRegistrationBean<ForwardedHeaderFilter> = FilterRegistrationBean(forwardedHeaderFilter)
registrationBean.order = Int.MAX_VALUE - 1
return registrationBean
}
@Bean
fun setHttpContentCacheFilterOrder(): FilterRegistrationBean<HttpContentCacheFilter> {
val registrationBean: FilterRegistrationBean<HttpContentCacheFilter> = FilterRegistrationBean(httpContentCacheFilter)
registrationBean.order = Int.MAX_VALUE
return registrationBean
}
}
ForwardedHeaderFilter를 HttpContentCacheFilte의 앞으로 설정해주었습니다.
그랬더니 성공!!
근데 Security에 FilterConfig 로 설정해주면 좋을텐데 굳이 별도의 클래스를 나눠서 등록해야하나? 싶긴 한데 개선해볼 수 있을 것 같다.
프록시 환경에서는 필터 등록 순서가 달라질 수도 있다는 점을 알게 되었다.
reference
spring 프록시 환경에서 HttpContentCache적용