Skip to content

Commit 700dafb

Browse files
unam98alh0409
andauthored
잘못된 형태의 요청이 500+Slack/Sentry 알림으로 새던 문제 수정 (#254)
prod Grafana 로그 확인 결과 최근 7일간 5xx 40건이 전부 정상 앱 트래픽이 아니라 Postman 등으로 잘못된 메서드/형식을 직접 찌른 요청(GET /api/auth, 잘못된 Content-Type의 POST /api/auth)과 취약점 스캐너 프로빙(.env, /etc/passwd 경로 접근 시도)이었다. 원래 405/415/406으로 응답해야 할 요청인데, ControllerExceptionAdvice의 범용 Exception 핸들러가 전부 500으로 뭉개고 Slack/Sentry 알림까지 매번 보내고 있었다 — 실제 장애가 아닌데 알림만 계속 쌓이는 구조. HttpRequestMethodNotSupportedException(405)/HttpMediaTypeNotSupportedException (415)/HttpMediaTypeNotAcceptableException(406) 전용 핸들러를 추가해 WARN 로그만 남기고 정상 상태 코드로 응답하도록 분리했다. 406은 본문(JSON)을 만들려는 시도 자체가 다시 협상 실패로 이어질 수 있어(실제 로그에서 "Failure in @ExceptionHandler" 이중 오류로 확인) 본문 없이 상태 코드만 응답한다. MockMvc로 수정 전 실제로 500이 나고, 수정 후 405/415가 나며 Slack 알림이 호출되지 않음을 재현 테스트로 검증했다. Co-authored-by: 나미 <dnska6657@gmail.com>
1 parent d8a8775 commit 700dafb

3 files changed

Lines changed: 77 additions & 0 deletions

File tree

‎src/main/java/org/runnect/server/common/advice/ControllerExceptionAdvice.java‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
import org.springframework.http.ResponseEntity;
1515
import org.springframework.stereotype.Component;
1616
import org.springframework.validation.FieldError;
17+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
18+
import org.springframework.web.HttpMediaTypeNotSupportedException;
19+
import org.springframework.web.HttpRequestMethodNotSupportedException;
1720
import org.springframework.web.bind.MethodArgumentNotValidException;
1821
import org.springframework.web.bind.MissingRequestHeaderException;
1922
import org.springframework.web.bind.MissingServletRequestParameterException;
@@ -66,6 +69,39 @@ protected ApiResponseDto handleMissingRequestParameterException(final MissingSer
6669

6770

6871

72+
/**
73+
* 405 / 415 / 406 — 클라이언트가 잘못된 형태로 요청한 경우 (서버 장애 아님)
74+
*
75+
* 실제 prod 로그를 보면 GET /api/auth(POST만 지원), 잘못된 Content-Type의 POST
76+
* 요청처럼 정상 앱 트래픽이라면 나올 수 없는 요청(Postman 등으로 직접 호출하거나
77+
* 취약점 스캐너가 찌른 것)이 계속 들어온다. 이런 경우까지 아래 범용 Exception
78+
* 핸들러가 500으로 뭉개고 Slack/Sentry로 매번 알림을 보내면, 실제 장애가 아닌데도
79+
* 알림만 계속 쌓인다. 정확한 상태 코드로 응답하고 로그만 WARN으로 남긴다.
80+
*/
81+
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
82+
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
83+
protected ApiResponseDto handleHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e, final HttpServletRequest request) {
84+
log.warn("[405] {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
85+
return ApiResponseDto.error(ErrorStatus.INVALID_HTTP_METHOD_EXCEPTION);
86+
}
87+
88+
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
89+
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
90+
protected ApiResponseDto handleHttpMediaTypeNotSupportedException(final HttpMediaTypeNotSupportedException e, final HttpServletRequest request) {
91+
log.warn("[415] {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
92+
return ApiResponseDto.error(ErrorStatus.UNSUPPORTED_MEDIA_TYPE_EXCEPTION);
93+
}
94+
95+
// 클라이언트가 Accept 헤더로 서버가 만들 수 없는 응답 형식을 요구한 경우.
96+
// 본문(JSON)을 굳이 만들려 하면 그 시도 자체가 다시 협상에 실패해 이중 오류로
97+
// 이어질 수 있어(handleException 안에서 실패 → DefaultHandlerExceptionResolver로
98+
// 전파되는 게 실제 로그에서 확인됨), 본문 없이 상태 코드만 응답한다.
99+
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
100+
protected ResponseEntity<Void> handleHttpMediaTypeNotAcceptableException(final HttpMediaTypeNotAcceptableException e, final HttpServletRequest request) {
101+
log.warn("[406] {} {} - {}", request.getMethod(), request.getRequestURI(), e.getMessage());
102+
return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build();
103+
}
104+
69105
/**
70106
* 500 Internal Server Error
71107
*

‎src/main/java/org/runnect/server/common/constant/ErrorStatus.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ public enum ErrorStatus {
6565
*/
6666
NOT_FOUND_USER_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다"),
6767
NOT_FOUND_MARATHON_PUBLIC_COURSE_EXCEPTION(HttpStatus.NOT_FOUND, "마라톤 코스가 존재하지 않습니다."),
68+
69+
/**
70+
* 405 METHOD NOT ALLOWED
71+
*/
72+
INVALID_HTTP_METHOD_EXCEPTION(HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP 메서드입니다."),
73+
74+
/**
75+
* 415 UNSUPPORTED MEDIA TYPE
76+
*/
77+
UNSUPPORTED_MEDIA_TYPE_EXCEPTION(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "지원하지 않는 요청 형식입니다."),
78+
6879
/**
6980
* 409 CONFLICT
7081
*/

‎src/test/java/org/runnect/server/auth/controller/AuthControllerTest.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,34 @@ class GetNewToken {
140140
.andExpect(status().isUnauthorized());
141141
}
142142
}
143+
144+
// prod Grafana 로그 확인 결과, 최근 7일간 5xx 40건이 전부 정상 트래픽이 아니라
145+
// Postman 등으로 잘못된 메서드/형식을 직접 찌른 요청(GET /api/auth, 잘못된
146+
// Content-Type의 POST /api/auth)이었다. 원래는 405/415로 응답해야 할 요청인데
147+
// ControllerExceptionAdvice의 범용 Exception 핸들러가 이걸 500으로 뭉개고
148+
// Slack/Sentry 알림까지 매번 보내고 있었다 — 진짜 장애가 아닌데 노이즈만 쌓이는 구조.
149+
@Nested
150+
@DisplayName("잘못된 형태의 요청 (클라이언트 오류, 서버 장애 아님)")
151+
class InvalidRequestShape {
152+
153+
@Test
154+
@DisplayName("POST만 지원하는 /api/auth에 GET으로 요청하면 405를 반환하고 Slack 알림을 보내지 않는다")
155+
void 지원하지_않는_메서드() throws Exception {
156+
mockMvc.perform(get("/api/auth"))
157+
.andExpect(status().isMethodNotAllowed());
158+
159+
BDDMockito.verifyNoInteractions(slackApi);
160+
}
161+
162+
@Test
163+
@DisplayName("JSON을 기대하는 /api/auth에 multipart로 요청하면 415를 반환하고 Slack 알림을 보내지 않는다")
164+
void 지원하지_않는_컨텐츠타입() throws Exception {
165+
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders
166+
.multipart("/api/auth")
167+
.file("token", "kakao-token".getBytes()))
168+
.andExpect(status().isUnsupportedMediaType());
169+
170+
BDDMockito.verifyNoInteractions(slackApi);
171+
}
172+
}
143173
}

0 commit comments

Comments
 (0)