-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#143 응답 통일 - GlobalExceptionHandler 구체화 #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d9dfc17
eb8e29c
86ecbd9
9fc0b69
e3ab225
ab66799
75439b6
4e2f2c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,16 +16,38 @@ | ||||||||||
|
|
|||||||||||
|
|
|||||||||||
| import jakarta.servlet.http.HttpServletRequest; | |||||||||||
| import jakarta.validation.ConstraintViolationException; | |||||||||||
| import lombok.extern.slf4j.Slf4j; | |||||||||||
| import com.fasterxml.jackson.databind.exc.InvalidFormatException; | |||||||||||
| import org.springframework.http.converter.HttpMessageNotReadableException; | |||||||||||
| import org.springframework.web.HttpMediaTypeNotSupportedException; | |||||||||||
| import org.springframework.web.HttpRequestMethodNotSupportedException; | |||||||||||
| import org.springframework.web.bind.MissingServletRequestParameterException; | |||||||||||
| import org.springframework.web.method.annotation.HandlerMethodValidationException; | |||||||||||
| import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; | |||||||||||
| import org.springframework.web.servlet.resource.NoResourceFoundException; | |||||||||||
|
|
|||||||||||
| import java.util.HashMap; | |||||||||||
| import java.util.Map; | |||||||||||
| import java.util.stream.Collectors; | |||||||||||
|
|
|||||||||||
|
|
|||||||||||
| @Slf4j | |||||||||||
| @RestControllerAdvice | |||||||||||
| public class GlobalExceptionHandler { | |||||||||||
|
|
|||||||||||
| // NPE — 주로 DTO 검증 누락이나 비즈니스 로직에서 발생 | |||||||||||
| @ExceptionHandler(NullPointerException.class) | |||||||||||
| public ResponseEntity<ErrorResponse> handleNullPointerException(NullPointerException e, HttpServletRequest request) { | |||||||||||
| log.error("NullPointerException 발생 — 파일: {}, 라인: {}", | |||||||||||
| e.getStackTrace()[0].getFileName(), e.getStackTrace()[0].getLineNumber()); | |||||||||||
|
Comment on lines
+42
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
JVM(HotSpot)은 같은 위치에서 🛠️ 제안 수정- log.error("NullPointerException 발생 — 파일: {}, 라인: {}",
- e.getStackTrace()[0].getFileName(), e.getStackTrace()[0].getLineNumber());
+ StackTraceElement[] trace = e.getStackTrace();
+ if (trace.length > 0) {
+ log.error("NullPointerException 발생 — 파일: {}, 라인: {}",
+ trace[0].getFileName(), trace[0].getLineNumber());
+ } else {
+ log.error("NullPointerException 발생 — 스택트레이스 없음", e);
+ }🤖 Prompt for AI Agents |
|||||||||||
| log.error("에러가 발생한 지점 {}, {}", request.getMethod(), request.getRequestURI()); | |||||||||||
|
|
|||||||||||
| return ResponseEntity | |||||||||||
| .status(ErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus()) | |||||||||||
| .body(ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, request)); | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| // 처리되지 않은 모든 예외를 잡는 핸들러 | |||||||||||
| @ExceptionHandler(Exception.class) | |||||||||||
| public ResponseEntity<ErrorResponse> handleAllException(Exception e, HttpServletRequest request) { | |||||||||||
|
|
@@ -44,7 +66,7 @@ public ResponseEntity<ErrorResponse> handleAllException(Exception e, HttpServlet | ||||||||||
| public ResponseEntity<ErrorResponse> handleAppCustomException(AppException e, HttpServletRequest request) { | |||||||||||
| log.error("AppException 발생: {}", e.getErrorCode().getMessage()); | |||||||||||
| log.error("에러가 발생한 지점 {}, {}", request.getMethod(), request.getRequestURI()); | |||||||||||
| ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode(), request); | |||||||||||
| ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode(), request, e.getBind()); | |||||||||||
| return ResponseEntity | |||||||||||
| .status(e.getErrorCode().getHttpStatus()) | |||||||||||
| .body(errorResponse); | |||||||||||
|
|
@@ -53,17 +75,20 @@ public ResponseEntity<ErrorResponse> handleAppCustomException(AppException e, Ht | ||||||||||
| //@Valid 검사 실패(필수 파라미터가 null 또는 공백) 인 경우 예외를 잡는 핸들러 | |||||||||||
| @ExceptionHandler(MethodArgumentNotValidException.class) | |||||||||||
| public ResponseEntity<ErrorResponse> handleNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) { | |||||||||||
| // 예시 결과: "email: 이메일 형식이 아닙니다, password: 비밀번호는 필수입니다" | |||||||||||
| String errorMessage = e.getBindingResult().getFieldErrors().stream() | |||||||||||
| .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) | |||||||||||
| .collect(Collectors.joining(", ")); | |||||||||||
| Map<String, String> errors = e.getBindingResult().getFieldErrors().stream() | |||||||||||
| .collect(Collectors.toMap( | |||||||||||
| fieldError -> fieldError.getField(), | |||||||||||
| fieldError -> fieldError.getDefaultMessage(), | |||||||||||
| (existing, duplicate) -> existing | |||||||||||
| )); | |||||||||||
|
|
|||||||||||
| log.error("MethodArgumentNotValidException 발생: {}", errorMessage); | |||||||||||
| log.error("MethodArgumentNotValidException 발생: {}", errors); | |||||||||||
| log.error("에러가 발생한 지점 {}, {}", request.getMethod(), request.getRequestURI()); | |||||||||||
|
|
|||||||||||
| ErrorResponse errorResponse = ErrorResponse.of( | |||||||||||
| ErrorCode.INVALID_PARAMETER, | |||||||||||
| request | |||||||||||
| request, | |||||||||||
| errors | |||||||||||
| ); | |||||||||||
|
|
|||||||||||
| return ResponseEntity | |||||||||||
|
|
@@ -161,13 +186,181 @@ public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTyp | ||||||||||
| } | |||||||||||
|
|
|||||||||||
| // Enum 변환 실패가 아닌 일반적인 타입 매스매치(예: Long 타입에 문자열 입력)인 경우 | |||||||||||
| Map<String, String> errors = new HashMap<>(); | |||||||||||
| errors.put(e.getName(), e.getValue() != null ? e.getValue().toString() : "null"); | |||||||||||
|
|
|||||||||||
| ErrorResponse errorResponse = ErrorResponse.of( | |||||||||||
| ErrorCode.PARAMETER_MISMATCH, | |||||||||||
| request, | |||||||||||
| errors | |||||||||||
| ); | |||||||||||
|
|
|||||||||||
| return ResponseEntity | |||||||||||
| .status(ErrorCode.PARAMETER_MISMATCH.getHttpStatus()) | |||||||||||
| .body(errorResponse); | |||||||||||
| } | |||||||||||
|
|
|||||||||||
| // @Valid 쿼리 파라미터 유효성 검사 실패 | |||||||||||
| @ExceptionHandler(HandlerMethodValidationException.class) | |||||||||||
| public ResponseEntity<ErrorResponse> handleHandlerMethodValidationException( | |||||||||||
| HandlerMethodValidationException e, | |||||||||||
| HttpServletRequest request | |||||||||||
| ) { | |||||||||||
|
|
|||||||||||
| Map<String, String> errors = new HashMap<>(); | |||||||||||
| e.getParameterValidationResults().forEach(result -> | |||||||||||
| errors.put( | |||||||||||
| result.getMethodParameter().getParameterName(), | |||||||||||
| result.getResolvableErrors().get(0).getDefaultMessage() | |||||||||||
| ) | |||||||||||
| ); | |||||||||||
|
Comment on lines
+211
to
+216
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# -parameters 컴파일 플래그 사용 여부 확인
rg -nP -C3 '(\-parameters|parameters\s*=\s*true|compilerArgs)' --iglob '*.gradle*' --iglob 'pom.xml'Repository: WhereYouAd/WhereYouAd-Backend Length of output: 55 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Build files =="
find . -maxdepth 5 -type f \( -name "build.gradle" -o -name "build.gradle.kts" -o -name "pom.xml" \) | sed 's|^\./||'
echo
echo "== Search for -parameters / parameters=true / compilerArgs in build files =="
rg -n -i '(-parameters|\bparameters\s*=\s*true\b|compilerArgs)' --hidden --follow \
--iglob '*/build.gradle*' --iglob '*/pom.xml' || true
echo
echo "== Show GlobalExceptionHandler.java around lines 180-250 =="
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
if [ -f "$FILE" ]; then
nl -ba "$FILE" | sed -n '180,250p'
else
echo "File not found at expected path: $FILE"
echo "== Candidate files =="
fd -t f "GlobalExceptionHandler.java" .
fiRepository: WhereYouAd/WhereYouAd-Backend Length of output: 454 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Build files =="
mapfile -t BUILD_FILES < <(find . -type f \( -name "build.gradle" -o -name "build.gradle.kts" -o -name "pom.xml" \) -print | sed 's|^\./||')
printf "%s\n" "${BUILD_FILES[@]:-}"
echo
echo "== Search -parameters / parameters=true / compilerArgs in build files (explicit targets) =="
if [ "${`#BUILD_FILES`[@]}" -eq 0 ]; then
echo "No build files found."
else
rg -n -i '(-parameters|\bparameters\s*=\s*true\b|compilerArgs)' "${BUILD_FILES[@]}" || true
fi
echo
echo "== Show GlobalExceptionHandler.java around lines 180-250 (with line numbers) =="
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
if [ -f "$FILE" ]; then
awk 'NR>=180 && NR<=250 {printf "%d\t%s\n", NR, $0}' "$FILE"
else
echo "File not found at: $FILE"
echo "== Candidate files =="
fd -t f "GlobalExceptionHandler.java" .
fi
echo
echo "== Fallback: repo-wide search for -parameters / getParameterName usage =="
rg -n --hidden --follow '\-parameters' . || true
rg -n --hidden --follow 'getParameterName\(\)' src/main/java || true
rg -n --hidden --follow 'getResolvableErrors\(\)' src/main/java || trueRepository: WhereYouAd/WhereYouAd-Backend Length of output: 166 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Repo root listing =="
ls -la
echo
echo "== Build config candidates (build.gradle / build.gradle.kts / pom.xml) =="
find . -maxdepth 6 -type f \( -name "build.gradle" -o -name "build.gradle.kts" -o -name "pom.xml" \) -print | sed 's|^\./||' || true
echo
echo "== Search for -parameters / parameters=true / compilerArgs in build files =="
rg -n -i '(\-parameters|\bparameters\s*=\s*true\b|compilerArgs)' \
-g 'build.gradle*' -g 'pom.xml' . || true
echo
echo "== Locate GlobalExceptionHandler.java =="
FILE="$(fd -t f 'GlobalExceptionHandler\.java$' . | head -n 1 || true)"
if [ -z "$FILE" ]; then
echo "GlobalExceptionHandler.java not found."
exit 0
fi
echo "Found: $FILE"
echo
echo "== Show code around the reported lines (180-250) =="
awk 'NR>=180 && NR<=250 {printf "%d\t%s\n", NR, $0}' "$FILE"Repository: WhereYouAd/WhereYouAd-Backend Length of output: 4202 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== build.gradle: lines containing -parameters / compilerArgs / parameters=true =="
rg -n --hidden --follow '(-parameters|\bcompilerArgs\b|\bparameters\s*=\s*true\b)' build.gradle || true
echo
echo "== build.gradle: java/toolchain/compileJava related sections (best-effort) =="
rg -n --hidden --follow '(toolchain|compileJava|options\.compilerArgs|JavaCompile|tasks\.withType\(JavaCompile\)|jvmTarget)' build.gradle || true
echo
echo "== build.gradle: print entire compileJava-related blocks (up to ~200 lines around hits) =="
# Show a small context window around any compilerArgs hit; otherwise show near tasks.withType
if rg -n 'compilerArgs' build.gradle >/dev/null 2>&1; then
rg -n -C15 'compilerArgs' build.gradle
elif rg -n 'compileJava' build.gradle >/dev/null 2>&1; then
rg -n -C15 'compileJava' build.gradle
else
echo "(no obvious compileJava/compilerArgs blocks found)"
fiRepository: WhereYouAd/WhereYouAd-Backend Length of output: 393 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== build.gradle: search broader for parameter-name / compilerArgs settings =="
rg -n --hidden --follow 'parameters\s*=\s*true|-\s*parameters|parameterName|parameter names|compilerArgs|options\.compilerArgs|compileJava|tasks\.withType\(JavaCompile\)|withType\(JavaCompile\)|all\s*JavaCompile|jvmTarget|options\s*\{' build.gradle || true
echo
echo "== build.gradle: show java/toolchain + compileJava/tasks blocks (context) =="
rg -n --hidden --follow -C5 'toolchain|compileJava|withType\(JavaCompile\)|tasks\.' build.gradle || true
echo
echo "== Locate ErrorResponse class =="
ERRFILE="$(fd -t f 'ErrorResponse\.java$' src | head -n 1 || true)"
echo "Found: ${ERRFILE:-none}"
if [ -n "${ERRFILE:-}" ]; then
awk 'NR>=1 && NR<=220 {print NR "\t" $0}' "$ERRFILE" | sed -n '1,220p'
fi
echo
echo "== Locate ErrorResponse.of(...) usage/implementation if multiple constructors =="
rg -n --hidden --follow 'class ErrorResponse|static\s+ErrorResponse\s+of' src/main/java || trueRepository: WhereYouAd/WhereYouAd-Backend Length of output: 2935 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== build.gradle: search for any JavaCompile/options.compilerArgs/compilerArgs.add patterns =="
rg -n --hidden --follow -i '(options\.compilerArgs|compilerArgs(\.add)?|java\.toolchain|tasks\.withType\(JavaCompile\)|withType\(JavaCompile\)|\-parameters|\bparameters\s*=\s*true\b|parameter\s*names)' build.gradle || true
echo
echo "== Inspect BaseResponse (to see how errors field is serialized) =="
BASEFILE="$(fd -t f 'BaseResponse\.java$' src/main/java | head -n 1 || true)"
echo "Found: ${BASEFILE:-none}"
if [ -n "${BASEFILE:-}" ]; then
awk 'NR>=1 && NR<=220 {print NR "\t" $0}' "$BASEFILE"
fi
echo
echo "== Inspect any Jackson configuration affecting Map null keys =="
# Best-effort: search for ObjectMapper or JsonInclude settings
rg -n --hidden --follow -i '(ObjectMapper|JsonInclude|SerializationFeature|null\s*key|Map<|FAIL_ON|`@Json`' src/main/java || trueRepository: WhereYouAd/WhereYouAd-Backend Length of output: 965 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Search for Jackson/ObjectMapper customizations =="
rg -n --hidden --follow -i 'ObjectMapper|JsonInclude|Jackson2ObjectMapperBuilderCustomizer|SerializationFeature|FAIL_ON|`@Json`|`@JsonInclude`' src/main/java || true
echo
echo "== Search where getResolvableErrors() is used =="
rg -n --hidden --follow 'getResolvableErrors\(\)' src/main/java || true
echo
echo "== Search for HandlerMethodValidationException usage/overrides =="
rg -n --hidden --follow 'HandlerMethodValidationException' src/main/java || trueRepository: WhereYouAd/WhereYouAd-Backend Length of output: 16873 🌐 Web query:
💡 Result: In Spring Boot 3 (Spring Framework 6.x and 7.x), the HandlerMethodValidationException is thrown when method-level validation fails [1][2][3]. The behavior you described—null parameter names and potential issues with empty error lists—is tied to how Spring resolves method parameter names [3]. Parameter Names and the -parameters Flag Spring Framework relies on the Java reflection API to determine method parameter names. If the Java compiler is not invoked with the -parameters flag, these names are not stored in the compiled class files. Without this information, Spring may default to generic names like arg0, arg1, or return null, which causes getParameterName on a MethodParameter to return null [3][4][5]. To ensure correct parameter names are available at runtime for validation reporting, you must compile your code with the -parameters compiler flag [3]. Behavior of getResolvableErrors The getResolvableErrors method in ParameterValidationResult returns a list of MessageSourceResolvable objects [6][7]. This list will be empty if there are no validation errors for that specific parameter, but it should not be empty when a HandlerMethodValidationException is thrown, as the exception is only raised when errors exist [3]. If you are encountering an empty list in your exception handler, it may indicate you are iterating over parameters that did not cause the validation failure or that the structure of the result is being misinterpreted [3][4][5]. Recommended Handling Approach To correctly handle these exceptions and extract validation details: 1. Use getParameterValidationResults to access the results [1][8]. 2. For each ParameterValidationResult, check if it contains errors before accessing them [3]. 3. Ensure the -parameters flag is enabled in your build tool (e.g., Maven or Gradle) to avoid null parameter names [3]. Example snippet for your ExceptionHandler: Citations:
HandlerMethodValidationException 에러 추출 시 null/빈 값 방어 추가 필요 (211-216)
🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 무슨 오류인지 설명해줘 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
문제가 있는 코드 (Lines 211-216)e.getParameterValidationResults().forEach(result ->
errors.put(
result.getMethodParameter().getParameterName(), // ← 문제 1
result.getResolvableErrors().get(0).getDefaultMessage() // ← 문제 2
)
);🐛 문제 1:
|
|||||||||||
| 문제 | 발생 원인 | 결과 |
|---|---|---|
null 키 |
-parameters 컴파일 옵션 누락 |
JSON 직렬화 오류 또는 null 키 응답 |
get(0) |
에러 리스트가 비어 있는 경우 | IndexOutOfBoundsException → 500 에러 |
( ・ᴗ・ )🐇 ← 이 부분만 조금 고치면 훨씬 안전해져요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
InvalidFormatException 파싱 시 경계 조건을 확인해 주세요.
cause.getPath().get(0)은 path가 비어 있는 경우(예: 루트 레벨 변환 실패) IndexOutOfBoundsException을, cause.getValue().toString()은 값이 null일 때 NullPointerException을 던질 수 있어요. 드물지만 핸들러 안에서 터지면 통일된 응답 대신 500이 나가니, 가드를 한 줄 넣어두면 안전합니다.
🛠️ 방어 코드 예시
- String fieldName = cause.getPath().get(0).getFieldName();
-
- if (cause.getValue().toString().isEmpty()) {
+ String fieldName = cause.getPath().isEmpty() ? "unknown" : cause.getPath().get(0).getFieldName();
+ Object rawValue = cause.getValue();
+
+ if (rawValue == null || rawValue.toString().isEmpty()) {
errors.put(fieldName, "빈칸을 변환할 수 없습니다.");
} else {
- errors.put(fieldName, "'" + cause.getValue() + "'을(를) 변환할 수 없습니다.");
+ errors.put(fieldName, "'" + rawValue + "'을(를) 변환할 수 없습니다.");
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java`
around lines 290 - 298, When handling InvalidFormatException in
GlobalExceptionHandler, add guards around cause.getPath() and cause.getValue():
check if cause.getPath() is empty before calling get(0) and choose a safe
default fieldName (e.g., "value" or "root") when empty; check if
cause.getValue() is null before calling toString() and branch the error message
accordingly (null -> treat as empty or use a "'null'을(를) 변환할 수 없습니다." message).
Update the block that computes fieldName and the errors.put(...) logic so it
never calls get(0) on an empty path or toString() on a null value, preserving
the existing messages but using the safe defaults.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Spring Boot 3.x HttpMessageNotReadableException JSON parse error cause JsonParseException
💡 Result:
In Spring Boot 3.x, HttpMessageNotReadableException is a wrapper exception thrown by the framework when a registered HttpMessageConverter—typically the Jackson-based converter—fails to read or deserialize the incoming request body [1]. When the root cause is a JsonParseException (or other Jackson exceptions like JsonMappingException), it indicates that the provided request body is not valid JSON or does not conform to the expected structure of the target Java DTO [2][3]. Root Cause Analysis: The exception chain often contains the underlying cause within the nested exception [3]. You can access this via: - ex.getCause: The direct cause (e.g., JsonMappingException or JsonParseException) [1][3]. - ex.getMostSpecificCause: A convenient method (from the base class NestedRuntimeException) to reach the root of the error, such as the specific parsing issue identified by Jackson [3][4]. Common Triggers: 1. Malformed JSON (syntax errors) triggering a JsonParseException [5][3]. 2. Schema mismatches, such as missing required fields or type mismatches (e.g., sending a string to an integer field), often triggering a JsonMappingException or InvalidFormatException [2][6][3]. 3. Missing default constructors or inaccessible properties on the target DTO [6]. Recommended Handling Strategy: The most robust approach to handling this exception in Spring Boot 3.x is to use a @RestControllerAdvice combined with ResponseEntityExceptionHandler [7]. By overriding the handleHttpMessageNotReadable method, you can provide a cleaner, consistent API error response instead of the default stack trace: @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity handleHttpMessageNotReadable( HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { String errorMessage = "Malformed JSON request"; // Accessing the specific cause for a more helpful message Throwable mostSpecificCause = ex.getMostSpecificCause; if (mostSpecificCause!= null) { errorMessage = mostSpecificCause.getMessage; } // Return a custom error response DTO return handleExceptionInternal(ex, errorMessage, headers, status, request); } } This allows you to intercept the error before it propagates to the default error handling mechanism, ensuring that clients receive a structured 400 Bad Request response [5][8][4].
Citations:
- 1: https://docs.spring.io/spring-framework/docs/7.0.6/javadoc-api/org/springframework/http/converter/HttpMessageNotReadableException.html
- 2: https://oneuptime.com/blog/post/2025-12-22-httpmessagenotreadableexception-spring/view
- 3: Handle specific reasons of HttpMessageNotReadableException zalando/problem-spring-web#1
- 4: https://medium.com/@dasbabai2017/%EF%B8%8F-the-ultimate-guide-to-exception-handling-in-spring-boot-rest-apis-d4de5f830778
- 5: https://stackoverflow.com/questions/62666481/how-to-handle-org-springframework-http-converter-httpmessagenotreadableexceptio
- 6: spring boot admin throws JSON parse error, please help ! spring boot admin version is 1.5.7 codecentric/spring-boot-admin#892
- 7: https://stackoverflow.com/questions/75145749/exception-handlind-httpmessagenotreadableexception-on-springboot
- 8: https://docs.enterprise.spring.io/spring-boot/3.3/how-to/spring-mvc.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
echo "== Exists? =="; ls -l "$FILE"
echo
echo "== Lines 280-350 (context) =="
nl -ba "$FILE" | sed -n '280,350p'
echo
echo "== Search for JSON_PARSE_FAIL handling =="
rg -n "JSON_PARSE_FAIL|JSON parse error|HttpMessageNotReadableException" "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 314
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
echo "== Lines 280-350 (context) =="
awk 'NR>=280 && NR<=350 {printf "%d:%s\n", NR, $0}' "$FILE"
echo
echo "== Search for JSON_PARSE_FAIL / JSON parse error / HttpMessageNotReadableException =="
rg -n "JSON_PARSE_FAIL|JSON parse error|HttpMessageNotReadableException|getMessage\\(\\)" "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 5095
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
echo "== Imports (top 1-120) =="
awk 'NR>=1 && NR<=120 {print}' "$FILE"
echo
echo "== Check for JsonParseException usage =="
rg -n "JsonParseException|getMostSpecificCause|mostSpecificCause|JsonMappingException" "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 5649
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java"
echo "== Search for JsonParseException (anywhere in file) =="
rg -n "JsonParseException|getMostSpecificCause|getRootCause" "$FILE" || true
echo
echo "== Lines 200-360 (exception handler area) =="
awk 'NR>=200 && NR<=360 {print NR ":" $0}' "$FILE"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 7736
HTTP JSON 파싱 실패 분기를 e.getMessage() 문자열이 아닌 예외 원인 타입으로 처리하기
e.getMessage().contains("JSON parse error")는 Spring/Jackson 버전(또는 메시지 포맷 변경)에서 조용히 빗나가고, 실제로는 JSON 문법 오류인데 “Request Body 없음” 분기로 떨어질 수 있어요.- Spring Boot 3.x에서 malformed JSON이면
HttpMessageNotReadableException의 nested cause(가장 구체적인 원인)가com.fasterxml.jackson.core.JsonParseException으로 내려오는 편이라, 그 타입으로 분기하는 게 더 안전합니다. (이미InvalidFormatException분기처럼 원인 타입 기반으로 처리하고 있어요.)
🛠️ 제안
- if (e.getMessage() != null && e.getMessage().contains("JSON parse error")) {
+ if (e.getMostSpecificCause() instanceof com.fasterxml.jackson.core.JsonParseException) {
log.error("HttpMessageNotReadableException 발생: JSON 파싱 실패 - {}", e.getMessage());
log.error("에러가 발생한 지점 {}, {}", request.getMethod(), request.getRequestURI());
return ResponseEntity
.status(ErrorCode.JSON_PARSE_FAIL.getHttpStatus())
.body(ErrorResponse.of(ErrorCode.JSON_PARSE_FAIL, request));
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.java`
around lines 309 - 316, Replace the fragile string-match branch in
GlobalExceptionHandler's HttpMessageNotReadableException handling (the if that
checks e.getMessage().contains("JSON parse error")) with a check on the nested
cause type: traverse e.getCause() chain (or use the same root-cause helper used
for InvalidFormatException handling) and detect
com.fasterxml.jackson.core.JsonParseException (or its Jackson supertype) to
identify malformed JSON; when that cause is found, log the error and request
like the current block and return ErrorCode.JSON_PARSE_FAIL, keeping the
existing logging and response behavior but based on the cause type rather than
the message text.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
무슨 오류인지 설명해줘
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kingmingyu 쉽게 설명해 드릴게요! 😊
🔍 문제가 된 코드 (현재)
if (e.getMessage() != null && e.getMessage().contains("JSON parse error")) {
// JSON 문법 오류 처리
}❓ 왜 문제인가?
e.getMessage()의 문자열에 의존하고 있기 때문이에요.
Spring/Jackson이 malformed JSON(문법적으로 잘못된 JSON)을 파싱하다 실패하면 HttpMessageNotReadableException을 던지는데, 이 예외의 메시지가 "JSON parse error: ..."라는 형태로 내려와요.
그런데 이 메시지 포맷은 공식 API가 아니에요. Spring Boot나 Jackson 버전이 업그레이드되면 메시지 형태가 달라질 수도 있어요.
💥 어떤 상황이 생기냐면?
예를 들어 프론트에서 이런 잘못된 JSON을 보냈다고 해봐요:
{ "name": "홍길동" ← 닫는 괄호 없음!- 현재 코드:
e.getMessage()에"JSON parse error"문자열이 없어지면 → 이 조건을 통과 못 하고 → 맨 아래 "Request Body 없음" 분기로 떨어짐 - 결과: 실제로는 JSON 문법 오류인데, 잘못된 에러 코드(
NOT_FOUND_REQUEST_BODY)로 응답하게 됨 ❌
✅ 더 안전한 방법 (제안)
예외의 타입으로 판단하는 거예요. 이미 InvalidFormatException을 instanceof로 체크하는 것처럼요:
// 변경 전 (문자열 의존 - 취약)
if (e.getMessage() != null && e.getMessage().contains("JSON parse error")) {
// 변경 후 (타입 기반 - 안전)
if (e.getMostSpecificCause() instanceof com.fasterxml.jackson.core.JsonParseException) {JsonParseException은 Jackson이 JSON 문법 자체가 깨졌을 때 던지는 정식 예외 클래스라, 버전이 바뀌어도 신뢰할 수 있어요.
요약하면: 문자열("JSON parse error")은 언제든 바뀔 수 있는 불안정한 기준이고, 예외 타입(JsonParseException)은 훨씬 안정적인 기준이에요! 🐇
Uh oh!
There was an error while loading. Please reload this page.