Skip to content
This repository was archived by the owner on Jan 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public enum ErrorStatus implements BaseErrorCode {
_RSA_ERROR(HttpStatus.BAD_REQUEST, "COMMON4015", "RSA 에러가 발생했습니다."),
_NOT_ADMIN(HttpStatus.FORBIDDEN, "COMMON4016", "관리자 권한이 없습니다."),
_INVALID_ENUM_VALUE(HttpStatus.BAD_REQUEST, "COMMON4017", "유효하지 않은 Enum 값입니다."),
_EXTERNAL_API_ERROR(HttpStatus.BAD_GATEWAY, "COMMON4018", "외부 API 호출 중 오류가 발생했습니다."),

// 네이버 소셜 로그인 관련 에러
_NAVER_SIGN_IN_INTEGRATION_FAILED(HttpStatus.UNAUTHORIZED, "NAVER4001", "네이버 로그인 연동에 실패하였습니다."),
_NAVER_ACCESS_TOKEN_ISSUANCE_FAILED(HttpStatus.UNAUTHORIZED, "NAVER4002", "네이버 액세스 토큰 발급에 실패하였습니다."),
Expand Down Expand Up @@ -70,7 +72,7 @@ public enum ErrorStatus implements BaseErrorCode {
_STUDY_OWNER_ONLY_CAN_WITHDRAW(HttpStatus.FORBIDDEN, "STUDY4007", "스터디장만 해당 API를 통해 스터디를 탈퇴할 수 있습니다."),
_STUDY_NOT_RECRUITING(HttpStatus.BAD_REQUEST, "STUDY4007", "스터디 모집기한이 아닙니다."),
_STUDY_APPLICANT_NOT_FOUND(HttpStatus.NOT_FOUND, "STUDY4009", "처리를 기다리는 스터디 신청을 찾을 수 없습니다."),
_STUDY_APPLY_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "STUDY4010","스터디 신청이 이미 처리된 회원입니다."),
_STUDY_APPLY_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "STUDY4010", "스터디 신청이 이미 처리된 회원입니다."),
_STUDY_OWNER_CANNOT_APPLY(HttpStatus.BAD_REQUEST, "STUDY4011", "스터디장은 스터디에 신청할 수 없습니다."),
_STUDY_IS_FULL(HttpStatus.BAD_REQUEST, "STUDY4012", "스터디 인원이 가득 찼습니다."),
_ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS(HttpStatus.FORBIDDEN, "STUDY4013", "스터디장만 신청자 목록에 접근할 수 있습니다."),
Expand Down Expand Up @@ -199,9 +201,8 @@ public enum ErrorStatus implements BaseErrorCode {
_STUDY_TODO_IS_NOT_BELONG_TO_STUDY(HttpStatus.BAD_REQUEST, "TODO4003", "해당 투두 리스트가 해당 스터디에 속해있지 않습니다."),
_ONLY_STUDY_MEMBER_CAN_ACCESS_TODO_LIST(HttpStatus.FORBIDDEN, "TODO4004", "스터디 멤버만 투두 리스트에 접근할 수 있습니다."),
_TODO_LIST_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "SCHEDULE4004", "일정을 조회하려는 멤버가 스터디에 가입되지 않았습니다."),
_STUDY_TODO_NULL(HttpStatus.BAD_REQUEST, "TODO4005", "투두 리스트 아이디가 입력되지 않았습니다."),;

;
_STUDY_TODO_NULL(HttpStatus.BAD_REQUEST, "TODO4005", "투두 리스트 아이디가 입력되지 않았습니다."),
;;


private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.example.spot.common.api.ApiResponse;
import com.example.spot.common.api.code.status.ErrorStatus;
import com.example.spot.common.api.exception.base.ExternalApiException;
import io.sentry.Sentry;
import jakarta.validation.ConstraintViolationException;
import java.util.List;
Expand All @@ -22,6 +23,7 @@ public class ExceptionAdvice {

/**
* GeneralException 처리
*
* @param exception GeneralException
* @return ApiResponse - GeneralException
*/
Expand All @@ -33,6 +35,7 @@ public ApiResponse<ErrorStatus> baseExceptionHandle(GeneralException exception)

/**
* Exception 처리
*
* @param exception Exception
* @return ApiResponse - INTERNAL_SERVER_ERROR
*/
Expand All @@ -44,6 +47,7 @@ public ApiResponse<ErrorStatus> exceptionHandle(Exception exception) {

/**
* MethodArgumentTypeMismatchException 처리 - 잘못된 값 입력
*
* @param ex MethodArgumentTypeMismatchException
* @return ApiResponse - BAD_VALUE_REQUEST
*/
Expand All @@ -56,17 +60,20 @@ public ApiResponse handleMethodArgumentTypeMismatchException(MethodArgumentTypeM

/**
* ConstraintViolationException 처리
*
* @param exception ConstraintViolationException 객체
* @return 클라이언트에게 반환할 ApiResponse 객체
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<List<String>>> handleConstraintViolationException(ConstraintViolationException exception) {
public ResponseEntity<ApiResponse<List<String>>> handleConstraintViolationException(
ConstraintViolationException exception) {
// 모든 필드 오류 메시지를 수집
List<String> errors = exception.getConstraintViolations()
.stream()
// 각 ConstraintViolation에서 필드 경로와 메시지를 포맷하여 수집
.map(constraintViolation -> String.format("'%s': %s ", constraintViolation.getPropertyPath(), constraintViolation.getMessage()))
.collect(Collectors.toList());
.stream()
// 각 ConstraintViolation에서 필드 경로와 메시지를 포맷하여 수집
.map(constraintViolation -> String.format("'%s': %s ", constraintViolation.getPropertyPath(),
constraintViolation.getMessage()))
.collect(Collectors.toList());

// 모든 에러 메시지를 하나의 문자열로 결합
String errorMessage = String.join(", ", errors);
Expand All @@ -75,9 +82,9 @@ public ResponseEntity<ApiResponse<List<String>>> handleConstraintViolationExcept

// ApiResponse 객체를 생성하여 오류 정보를 포함
ApiResponse<List<String>> response = ApiResponse.onFailure(
ErrorStatus._BAD_REQUEST.getCode(), // HTTP 상태 코드
ErrorStatus._BAD_REQUEST.getMessage(), // 오류 메시지
errors // 오류 목록
ErrorStatus._BAD_REQUEST.getCode(), // HTTP 상태 코드
ErrorStatus._BAD_REQUEST.getMessage(), // 오류 메시지
errors // 오류 목록
);

// BAD_REQUEST 상태와 함께 ApiResponse 반환
Expand All @@ -86,7 +93,8 @@ public ResponseEntity<ApiResponse<List<String>>> handleConstraintViolationExcept


@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<List<String>>> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
public ResponseEntity<ApiResponse<List<String>>> handleMethodArgumentNotValidException(
MethodArgumentNotValidException exception) {
List<String> errors = exception.getBindingResult()
.getFieldErrors()
.stream()
Expand All @@ -104,6 +112,22 @@ public ResponseEntity<ApiResponse<List<String>>> handleMethodArgumentNotValidExc
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(ExternalApiException.class)
public ResponseEntity<ApiResponse<String>> handleExternalApiException(ExternalApiException exception) {
String errorMessage = String.format("외부 API 호출 실패: %s", exception.getMessage());
log.error("ExternalApiException. error message: {}", errorMessage, exception);

captureException(exception);

ApiResponse<String> response = ApiResponse.onFailure(
ErrorStatus._EXTERNAL_API_ERROR.getCode(),
ErrorStatus._EXTERNAL_API_ERROR.getMessage(),
exception.getResponseBody()
);

return new ResponseEntity<>(response, HttpStatus.BAD_GATEWAY);
}
Comment on lines +115 to +129
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Returning raw external response body to clients risks PII/secrets leakage

The handler currently returns exception.getResponseBody() directly. If the upstream body contains tokens, cookies, emails, or stack traces, they will be exposed to clients. Given the sanitizer already exists in SafeFeignExecutor, either ensure that executor stores a sanitized/truncated body (preferred; see executor comment), or sanitize here before responding.

Option A (preferred): rely on sanitized body from executor; then only ensure null-safety/truncation here:

-        ApiResponse<String> response = ApiResponse.onFailure(
-                ErrorStatus._EXTERNAL_API_ERROR.getCode(),
-                ErrorStatus._EXTERNAL_API_ERROR.getMessage(),
-                exception.getResponseBody()
-        );
+        String body = exception.getResponseBody();
+        String safeBody = (body == null) ? "" : (body.length() > 2048 ? body.substring(0, 2048) : body);
+        ApiResponse<String> response = ApiResponse.onFailure(
+                ErrorStatus._EXTERNAL_API_ERROR.getCode(),
+                ErrorStatus._EXTERNAL_API_ERROR.getMessage(),
+                safeBody
+        );

Option B (alternate): derive HTTP status from the enum for consistency:

-        return new ResponseEntity<>(response, HttpStatus.BAD_GATEWAY);
+        return new ResponseEntity<>(response, ErrorStatus._EXTERNAL_API_ERROR.getHttpStatus());

I can add unit tests for:

  • SafeFeignExecutor: masks Authorization/Cookie and truncates long bodies.
  • ExceptionAdvice: returns 502 and a sanitized payload.
    Would you like me to push a test commit?
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ExceptionHandler(ExternalApiException.class)
public ResponseEntity<ApiResponse<String>> handleExternalApiException(ExternalApiException exception) {
String errorMessage = String.format("외부 API 호출 실패: %s", exception.getMessage());
log.error("ExternalApiException. error message: {}", errorMessage, exception);
captureException(exception);
ApiResponse<String> response = ApiResponse.onFailure(
ErrorStatus._EXTERNAL_API_ERROR.getCode(),
ErrorStatus._EXTERNAL_API_ERROR.getMessage(),
exception.getResponseBody()
);
return new ResponseEntity<>(response, HttpStatus.BAD_GATEWAY);
}
@ExceptionHandler(ExternalApiException.class)
public ResponseEntity<ApiResponse<String>> handleExternalApiException(ExternalApiException exception) {
String errorMessage = String.format("외부 API 호출 실패: %s", exception.getMessage());
log.error("ExternalApiException. error message: {}", errorMessage, exception);
captureException(exception);
String body = exception.getResponseBody();
String safeBody = (body == null)
? ""
: (body.length() > 2048
? body.substring(0, 2048)
: body);
ApiResponse<String> response = ApiResponse.onFailure(
ErrorStatus._EXTERNAL_API_ERROR.getCode(),
ErrorStatus._EXTERNAL_API_ERROR.getMessage(),
safeBody
);
return new ResponseEntity<>(response, ErrorStatus._EXTERNAL_API_ERROR.getHttpStatus());
}


private static String validateGender(FieldError fieldError) {
String field = fieldError.getField();
String message = fieldError.getDefaultMessage();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.example.spot.common.api.exception.base;

public class ExternalApiException extends RuntimeException {
public ExternalApiException(String s, Exception e) {

private final String responseBody;

public ExternalApiException(String s, String body, Exception e) {
super(s, e);
this.responseBody = body;
}

public String getResponseBody() {
return responseBody;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ public static <T> T run(Supplier<T> call) {
return call.get();
} catch (FeignException e) {
String message = e.getMessage() != null ? e.getMessage() : "";
String body = e.contentUTF8();
String masked = mask(message);
throw new ExternalApiException(
"Feign API 호출 실패(" + e.status() + "): " + masked, e
"Feign API 호출 실패(" + e.status() + "): " + masked, body, e
);
Comment on lines +17 to 21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not propagate raw external bodies to clients; sanitize + truncate first

You’re passing e.contentUTF8() straight into ExternalApiException, which the advice returns to clients. That can leak tokens/PII or huge payloads. Sanitize with the existing mask and cap length before propagating.

Apply this diff to sanitize and bound the body:

-            String body = e.contentUTF8();
+            String body = e.contentUTF8();
+            String safeBody = mask(truncate(body, 2048)); // prevent PII leakage and huge payloads
             String masked = mask(message);
             throw new ExternalApiException(
-                    "Feign API 호출 실패(" + e.status() + "): " + masked, body, e
+                    "Feign API 호출 실패(" + e.status() + "): " + masked, safeBody, e
             );

Add this helper in the class (outside the selected range):

private static String truncate(String s, int max) {
    if (s == null) return "";
    return s.length() <= max ? s : s.substring(0, max);
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/spot/common/infrastructure/feign/SafeFeignExecutor.java
around lines 17-21, the code currently passes e.contentUTF8() directly into
ExternalApiException; instead, sanitize and truncate the body before
propagating: add the provided private static truncate(String s, int max) helper
method somewhere in the class (outside the shown range), then replace the direct
body usage with a sanitized version that first masks the content (using the
existing mask method) and then truncates it to a safe max length (e.g., 1000
chars) and pass that masked+truncated string into ExternalApiException while
keeping the original exception as the cause.

}
}
Expand Down