Skip to content
This repository was archived by the owner on Jan 11, 2026. It is now read-only.

[FEATURE] ExternalApiExceptionresposeBody 필드 추가 및 에러 핸들링 추가#36

Merged
msk226 merged 1 commit intodevelopfrom
SPOT-306/refactor
Aug 21, 2025
Merged

[FEATURE] ExternalApiExceptionresposeBody 필드 추가 및 에러 핸들링 추가#36
msk226 merged 1 commit intodevelopfrom
SPOT-306/refactor

Conversation

@msk226
Copy link
Member

@msk226 msk226 commented Aug 21, 2025

#️⃣ 연관된 이슈


🔎 작업 내용

  • ExternalApiExceptionresposeBody 필드 추가
  • ExternalApiException 에러 핸들링을 통한 처리

📷 스크린샷 (선택)

작업한 결과물에 대한 간단한 스크린샷을 올려주세요.


💬리뷰 요구사항 (선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요.

Summary by CodeRabbit

  • New Features
    • Added dedicated handling for external API failures, returning a clear BAD_GATEWAY (502) response with a consistent error code and message.
    • When available, upstream error details are included in the response to aid troubleshooting.
  • Chores
    • Minor internal cleanups to improve stability and consistency of error handling (no impact on existing behaviors).

@msk226 msk226 self-assigned this Aug 21, 2025
@msk226 msk226 added the 👍 feature New feature or request label Aug 21, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

Adds a new external API error status, introduces ExternalApiException response body support, updates Feign executor to capture and propagate the body, and adds an exception handler that returns 502 with the external response body.

Changes

Cohort / File(s) Summary of Changes
Error codes
src/main/java/com/example/spot/common/api/code/status/ErrorStatus.java
Added _EXTERNAL_API_ERROR (BAD_GATEWAY, COMMON4018); whitespace tweak; fixed enum terminators.
Exception handling
src/main/java/com/example/spot/common/api/exception/ExceptionAdvice.java, src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java
Added handler for ExternalApiException returning 502 and body; ExternalApiException now stores responseBody, new getter; constructor updated to include body.
Feign executor integration
src/main/java/com/example/spot/common/infrastructure/feign/SafeFeignExecutor.java
On Feign errors, captures response body (contentUTF8) and passes to new ExternalApiException constructor.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant C as Client
    participant Ctrl as Controller/Service
    participant Feign as SafeFeignExecutor
    participant Ext as External API
    participant Adv as ExceptionAdvice
    participant Sen as Sentry

    C->>Ctrl: Request triggering external call
    Ctrl->>Feign: execute(...)
    Feign->>Ext: HTTP request
    Ext-->>Feign: Error response (e.g., 4xx/5xx + body)
    Note over Feign: FeignException thrown
    Feign->>Feign: Extract body (contentUTF8)
    Feign-->>Ctrl: throw ExternalApiException(msg, body, cause)
    Ctrl-->>Adv: Exception propagates
    Adv->>Sen: capture(exception)
    Adv-->>C: 502 BAD_GATEWAY + ApiResponse(body, _EXTERNAL_API_ERROR)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A packet hops, then hits a gate,
I twitch my ears—an error’s fate.
We catch the tale the server wrote,
And send it back, a careful note.
502 winds softly sigh—
A bunny logs, then hops on by. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SPOT-306/refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@msk226 msk226 merged commit a96d425 into develop Aug 21, 2025
1 of 2 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java (2)

5-5: Storing raw external response: consider size/PII guard

Persisting entire external response bodies can be large and may contain PII/secrets. Even if you don’t log it, it’s later returned to clients via the advice. Recommend sanitizing and truncating at the capture site (see SafeFeignExecutor comment) or defaulting to a bounded, sanitized representation here.


7-10: ExternalApiException constructor: rename parameters, widen cause type, and null-safe responseBody

Only one instantiation of the current three-arg constructor remains (SafeFeignExecutor.java:19–21), so renaming parameters and accepting Throwable won’t break any call sites. Applying the suggested refactor will improve clarity and guard against null response bodies.

Affected location:

  • src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java

Apply this diff:

--- a/src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java
+++ b/src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java
@@ -7,10 +7,11 @@
     /**
      * Constructs a new ExternalApiException.
-     * @param s the detail message
-     * @param body the raw response body
-     * @param e the underlying exception
+     * @param message the detail message
+     * @param responseBody the raw response body (empty string if null)
+     * @param cause the underlying throwable cause
      */
-    public ExternalApiException(String s, String body, Exception e) {
-        super(s, e);
-        this.responseBody = body;
+    public ExternalApiException(String message, String responseBody, Throwable cause) {
+        super(message, cause);
+        this.responseBody = (responseBody == null) ? "" : responseBody;
     }
src/main/java/com/example/spot/common/infrastructure/feign/SafeFeignExecutor.java (1)

25-37: Masking patterns: consider precompiled Pattern and centralizing sanitizer

Repeated replaceAll compiles regexes each call. For hot paths, precompile Patterns or move the sanitizer to a shared utility to reuse in both Feign and advice. Not blocking.

src/main/java/com/example/spot/common/api/code/status/ErrorStatus.java (1)

204-206: Double semicolon after enum constants is unusual

;; compiles but is noise. Prefer a single semicolon for readability.

Apply this diff:

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

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between eee7fcc and 4241fa1.

📒 Files selected for processing (4)
  • src/main/java/com/example/spot/common/api/code/status/ErrorStatus.java (3 hunks)
  • src/main/java/com/example/spot/common/api/exception/ExceptionAdvice.java (8 hunks)
  • src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java (1 hunks)
  • src/main/java/com/example/spot/common/infrastructure/feign/SafeFeignExecutor.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/example/spot/common/api/exception/ExceptionAdvice.java (1)
src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java (1)
  • ExternalApiException (3-15)
🔇 Additional comments (6)
src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java (1)

12-13: Accessor LGTM

Getter is straightforward and final field ensures immutability.

src/main/java/com/example/spot/common/api/code/status/ErrorStatus.java (2)

33-34: Good addition: dedicated external API error with 502 mapping

_EXTERNAL_API_ERROR aligns with the new advice and communicates intent clearly.


75-75: Whitespace-only tweak

No behavioral impact.

src/main/java/com/example/spot/common/api/exception/ExceptionAdvice.java (3)

5-5: Import of ExternalApiException is correct and scoped

Matches the new handler usage.


25-27: Doc/format-only changes

Javadoc spacing updates look fine; nothing to do.

Also applies to: 36-38, 49-51, 62-67


68-92: Constraint violation response shaping LGTM

Collecting messages and returning 400 with a structured list is consistent with other handlers.

Comment on lines +115 to +129
@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);
}
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());
}

Comment on lines +17 to 21
String body = e.contentUTF8();
String masked = mask(message);
throw new ExternalApiException(
"Feign API 호출 실패(" + e.status() + "): " + masked, e
"Feign API 호출 실패(" + e.status() + "): " + masked, body, e
);
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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

👍 feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant