Skip to content

refactor: exception handling and logging standardization - #97

Merged
Yu-JeSeung merged 3 commits into
mainfrom
refactor/exception_handling_logging_standardization
Apr 28, 2026
Merged

refactor: exception handling and logging standardization#97
Yu-JeSeung merged 3 commits into
mainfrom
refactor/exception_handling_logging_standardization

Conversation

@Yu-JeSeung

@Yu-JeSeung Yu-JeSeung commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

관련 이슈

Close #91

🎯 배경

  • 예외처리 기능과 로깅처리 부분을 정규화할 필요가 있었습니다. 공통로직을 각 기능별로 구현했던 부분을 하나의 파일로 통일하였습니다.

🔍 주요 내용

  • core 폴더를 기준으로 공통 로깅/예외처리 체계 확립
  • auth 를 통한 JWT 공통화
  • textfilter 무지성 try/except 명시적 처리

변경 요약

예외 처리와 로깅을 core/로 통합해 표준화했습니다. JWT 처리와 서비스별 에러를 커스텀 예외로 통일하고, 무분별한 try/except를 제거해 명시적 오류 흐름과 구조화된 로그를 적용했습니다.

주요 변경점

  • core/exceptions: AppError 기반 예외 계층(예: BadRequestError, UnauthorizedError, ConfigurationError 등) 및 FastAPI 전역 예외 핸들러(register_exception_handlers) 추가
  • core/logging: 요청ID 기반 컨텍스트, RequestContextFilter, configure_logging/get_logger 및 요청 로깅 미들웨어(register_request_logging) 도입
  • core/auth.py: JWT 검증에서 fastapi.HTTPException 대신 ConfigurationError/UnauthorizedError 등 커스텀 예외 사용
  • text_filtering/*: 광범위한 try/except 제거, 입력 검증 실패는 BadRequestError로 명확화; stdout print → logger로 전환
  • image_analysis/ 및 LLM/OSS/service.py: 설정 검증 시 ConfigurationError 사용, print→logger 대체, OSS 호출 에러 로깅 개선
  • 앱 진입점(main.py, app_oss_main.py): 애플리케이션 시작 시 전역 로깅 구성·요청 로깅·예외 핸들러 등록; Dockerfile은 core 디렉토리 포함 등 빌드 복사 대상 확장

주의/리스크

  • 기존 코드가 새로운 커스텀 예외 타입에 대응하지 않으면 에러 흐름(응답 코드/페이로드)이 변경될 수 있음
  • 로깅 동작이 LOG_LEVEL 및 새 구성에 의존하므로 배포 환경에서 설정 확인 필요
  • 일부 엔드포인트에서 이전에 500으로 묶였던 예외가 핸들러에 의해 다른 상태/응답으로 바뀌어 클라이언트 호환성에 영향 가능

다음 액션

  • 호출부(다른 모듈/클라이언트)가 새로운 예외 타입을 올바르게 처리하는지 검토 및 수정
  • 통합 테스트 추가(예외 핸들링 경로, 요청ID 전파, 로그 포맷 검증)
  • 배포 전 LOG_LEVEL 및 예외/로깅 동작에 대한 문서화 및 운영 환경 점검

@Yu-JeSeung Yu-JeSeung self-assigned this Apr 27, 2026
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

이 PR은 전역 구조화 로깅과 요청 컨텍스트, 커스텀 예외 계층 및 중앙 예외 처리기를 도입하고 관련 서비스 코드에서 로그·예외 흐름을 적용하며 Docker 빌드가 여러 디렉터리를 이미지로 복사하도록 업데이트합니다.

Changes

Cohort / File(s) Summary
Docker Configuration
Dockerfile, Dockerfiles_oss
Dockerfile들이 core/, text_filtering/, image_analysis/ 등 디렉터리를 이미지에 복사하도록 변경; uvicorn 엔트리포인트 줄 끝 개행/공백 조정.
Exception Hierarchy
core/exceptions/base.py, core/exceptions/__init__.py
AppError 기반의 커스텀 예외 계층(여러 HTTP 의미 예외 포함) 추가 및 패키지 레벨 재내보내기(__all__).
Exception Handlers
core/exceptions/handlers.py
register_exception_handlers(app) 추가: AppError, HTTPException, RequestValidationError, 일반 예외를 일관된 JSON 응답으로 매핑하고 request_id 주입·로그 레벨 적용.
Logging Infrastructure
core/logging/config.py, core/logging/context.py, core/logging/middleware.py, core/logging/__init__.py
서비스별 로깅 구성(configure_logging/get_logger), 요청 스코프 컨텍스트(request_id), RequestContextFilter 및 요청 시작/완료/오류를 기록하는 미들웨어와 등록 유틸리티 추가.
Auth Updates
core/auth.py
JWT 검증에서 HTTPException 대신 ConfigurationError/UnauthorizedError를 도입하여 오류 타입을 변경하고 예외 체인을 유지.
LLM OSS Service & OSS App Init
LLM/OSS/service.py, app_oss_main.py
OSS 클라이언트/DB 풀 초기화·종료에 구조화된 로깅 추가; 구성 검증시 ConfigurationError 사용 및 예외 로깅 개선; 앱 생성 시 전역 로깅·요청 로깅·예외 핸들러 등록.
Main App Entrypoint
main.py
앱 시작 시 configure_logging("main-api") 호출 및 register_request_logging·register_exception_handlers 등록으로 글로벌 로깅·예외 처리 연결.
Text Filtering
text_filtering/service.py, text_filtering/text_filtering.py, text_filtering/text_filtering_rule.py
stdout print를 로거로 교체, 입력 strip 전처리 추가, 파싱 실패 등에 BadRequestError 발생 및 광범위한 예외 캡처 제거로 예외 명시적 전파.
Image Analysis
image_analysis/service.py
_require_spring_url() 런타임 검증으로 설정 오류를 ConfigurationError로 처리; 작업자 로그를 structured logging으로 전환; 오류경로에서 적절한 AppError 서브클래스 발생.
Miscellaneous
여러 파일
여러 모듈에 get_logger(__name__) 초기화 추가 및 파일 끝 개행/형식 수정.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Middleware as "Request Logging\nMiddleware"
    participant App as "FastAPI App\n(Routes/Services)"
    participant Handler as "Exception\nHandlers"
    participant Logger as "Logger\n(configured)"

    Client->>Middleware: HTTP Request (maybe X-Request-ID)
    Middleware->>Middleware: generate/extract X-Request-ID\nset request.state.request_id
    Middleware->>Logger: log request start (method, path, request_id)
    Middleware->>App: forward request

    alt Success
        App->>Logger: structured logs during processing
        App-->>Middleware: Response
        Middleware->>Logger: log completion (status, duration_ms, request_id)
        Middleware-->>Client: Response + X-Request-ID header
    else Error
        App-->>Handler: exception raised
        Handler->>Logger: log warning/error (inject request_id)
        Handler-->>Middleware: JSON error response (error, code, request_id)
        Middleware->>Logger: log failure (status, duration_ms, request_id)
        Middleware-->>Client: JSON error + X-Request-ID header
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 변경 사항의 핵심을 명확하게 요약합니다. 예외 처리와 로깅 표준화라는 주요 변경 사항을 정확히 반영합니다.
Description check ✅ Passed PR 설명에서 관련 이슈, 배경, 주요 내용이 포함되어 있으나 리뷰 소요 시간 항목이 누락되어 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/exception_handling_logging_standardization

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
main.py (1)

25-31: HTTPException(413)AppError 서브클래스로 통일하기 위해서는 먼저 PayloadTooLargeError 추가 필요 (Optional)

PR의 방향성("HTTPException을 AppError 서브클래스로 교체")에 맞춰 이 미들웨어의 HTTPException(status_code=413, detail="File too large")을 교체하는 것이 좋습니다. 다만 현재 core/exceptions/base.py에는 413 상태 코드용 PayloadTooLargeError 같은 서브클래스가 존재하지 않으므로, 먼저 다음과 같이 추가한 후:

class PayloadTooLargeError(AppError):
    def __init__(self, message: str, *, code: str = "payload_too_large") -> None:
        super().__init__(message, status_code=413, code=code)

그다음 미들웨어에서 이를 사용하면 됩니다. 참고로 core/exceptions/handlers.py에는 이미 @app.exception_handler(HTTPException) 핸들러가 등록되어 있으므로, 현재 코드도 동작상 문제는 없습니다. 다만 로그 추적 및 일관성 측면에서 명시적 AppError 서브클래스 사용을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main.py` around lines 25 - 31, Add a new AppError subclass named
PayloadTooLargeError (subclassing AppError, default status_code=413 and default
code "payload_too_large") and replace the HTTPException raised in the
limit_upload_size middleware with raising PayloadTooLargeError; locate the
middleware function limit_upload_size and the AppError base class to implement
the new class and then change the raise statement to use PayloadTooLargeError so
the app uses a consistent AppError subtype for 413 responses.
image_analysis/service.py (2)

137-174: locals().get("job_id") 패턴 정리 권장.

locals() 사전 조회는 변수 선언 시점에 따라 동작이 달라져 가독성과 정적 분석 친화성이 떨어집니다. try 진입 전 job_id: str | None = None으로 초기화한 뒤 직접 참조하면 의도가 명확해지고, 향후 리팩터 시 실수 가능성도 줄어듭니다.

♻️ 제안 수정안
-    try:
+    job_id: str | None = None
+    try:
         file_bytes = await file.read()
@@
     except Exception:
-        jid = locals().get("job_id")
-        if jid:
-            _job_store.pop(jid, None)
+        if job_id is not None:
+            _job_store.pop(job_id, None)
         _active_users.discard(user_id)
         raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@image_analysis/service.py` around lines 137 - 174, In
enqueue_timetable_analysis, avoid using locals().get("job_id"); instead declare
job_id: str | None = None before the outer try, assign to job_id where you
currently generate the UUID, and in the exception handler reference that job_id
variable to pop from _job_store and for any cleanup (also ensure
_active_users.discard(user_id) still runs); this makes intent explicit and
eliminates fragile locals() usage while keeping logic around _job_store,
_active_users, and job queue timeout unchanged.

29-55: _require_spring_url이 임포트 시점에 캐시된 상수를 검증합니다.

SPRING_TIMETABLE_URL은 모듈 임포트 시 한 번 읽혀 고정되므로(Line 29), _require_spring_url는 사실상 매 호출마다 동일한 정적 값만 검사합니다. AI 요약의 "런타임 URL 검증" 의도와 다르게, 런타임에 설정이 갱신되더라도 반영되지 않습니다. 의도가 단순한 부재 가드라면 현재로도 충분하지만, 헬퍼명/주석을 그에 맞게 정리하거나 헬퍼 내부에서 get_settings().spring_timetable_url을 다시 읽도록 변경하는 것이 더 일관됩니다.

♻️ 제안 수정안 (헬퍼에서 매번 settings 읽기)
-SPRING_TIMETABLE_URL = settings.spring_timetable_url
@@
-def _require_spring_url() -> str:
-    if not SPRING_TIMETABLE_URL:
-        raise ConfigurationError("SPRING_TIMETABLE_URL is required")
-    return SPRING_TIMETABLE_URL
+def _require_spring_url() -> str:
+    url = get_settings().spring_timetable_url
+    if not url:
+        raise ConfigurationError("SPRING_TIMETABLE_URL is required")
+    return url
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@image_analysis/service.py` around lines 29 - 55, The helper
_require_spring_url currently validates the module-level constant
SPRING_TIMETABLE_URL which is captured at import time; change it to read the
setting dynamically instead so runtime changes are respected: inside
_require_spring_url call get_settings().spring_timetable_url (instead of using
SPRING_TIMETABLE_URL), validate that value and raise ConfigurationError if
missing, and update any callers or docstring accordingly (or alternatively
rename the helper if you prefer it to be a one-time import guard); reference
symbols: _require_spring_url, SPRING_TIMETABLE_URL, get_settings(),
ConfigurationError.
core/exceptions/handlers.py (1)

77-86: logger.exception 호출의 exc_info=exc는 중복입니다.

logger.exception(...)은 기본적으로 exc_info=True로 동작하며 현재 처리 중인 예외를 자동으로 첨부합니다. 명시 전달이 잘못된 것은 아니지만 가독성을 위해 제거하거나, 명시적으로 첨부하고 싶다면 logger.error(..., exc_info=exc)로 통일하는 편이 의도가 더 명확합니다.

♻️ 제안 수정안
-        logger.exception("unhandled_exception path=%s", request.url.path, exc_info=exc)
+        logger.exception("unhandled_exception path=%s", request.url.path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/exceptions/handlers.py` around lines 77 - 86, The call inside
unhandled_exception_handler currently passes exc_info=exc to logger.exception
which is redundant; update the logging to either remove the explicit exc_info
and keep logger.exception("unhandled_exception path=%s", request.url.path) or
replace the call with logger.error("unhandled_exception path=%s",
request.url.path, exc_info=exc) to make the intent explicit; adjust only the
logging line (referencing unhandled_exception_handler, logger.exception,
logger.error and exc_info) so the exception info is attached cleanly and code
readability is improved.
core/logging/config.py (1)

24-37: 재호출 시 핸들러 레벨만 갱신되는 점은 좋습니다.

_CONFIGURED 플래그로 중복 핸들러 부착을 방지하되, 재호출 시 루트/핸들러 레벨은 새로 반영하도록 한 분기는 운영 중 로그 레벨 변경 시나리오에서 유용합니다. 다만 _SERVICE_NAME은 매 호출마다 덮어써지므로, 한 프로세스에서 서로 다른 서비스명으로 두 번 호출되면 마지막 값으로 통일된다는 점은 운용상 기억해두시면 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/logging/config.py` around lines 24 - 37, The code currently overwrites
the global _SERVICE_NAME on every configure_logging call which can cause
inconsistent service names if configure_logging is invoked multiple times;
change configure_logging so it only assigns _SERVICE_NAME when not already set
(e.g., if not _CONFIGURED or if _SERVICE_NAME is falsy) while keeping the
existing behavior of updating root/handler log levels on reconfiguration; update
the logic around _CONFIGURED and _SERVICE_NAME in configure_logging so that
_SERVICE_NAME is preserved after the first assignment but log levels are still
applied on subsequent calls.
core/exceptions/base.py (1)

4-55: 예외 계층 구조 설계가 명확합니다.

AppError를 베이스로 HTTP 상태 코드와 code 식별자를 갖는 서브클래스를 일관되게 정의했고, 키워드 전용 인자로 시그니처가 통일되어 있어 핸들러에서 일괄 처리하기 좋습니다. 특별한 결함은 없습니다.

선택적으로, 핸들러에서 응답 본문에 부가 컨텍스트(예: 검증 실패 필드, 외부 시스템 식별자 등)를 실어야 하는 경우를 대비해 AppError에 선택적 details: dict | None 필드를 추가해 두면, 추후 각 호출부에서 시그니처를 깨뜨리지 않고 확장할 수 있어 유용합니다. 현재 사용 범위에서 필요 없다면 무시하셔도 됩니다.

♻️ 선택적 확장 예시
 class AppError(Exception):
     def __init__(
         self,
         message: str,
         *,
         status_code: int = 400,
         code: str = "application_error",
+        details: dict | None = None,
     ) -> None:
         super().__init__(message)
         self.message = message
         self.status_code = status_code
         self.code = code
+        self.details = details
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/exceptions/base.py` around lines 4 - 55, Add an optional details: dict |
None field to AppError and propagate it through every subclass so callers can
supply extra context without breaking existing call sites: update
AppError.__init__ signature to include *, details: dict | None = None, set
self.details = details, and update each subclass constructor (BadRequestError,
UnauthorizedError, ForbiddenError, NotFoundError, ConflictError,
ServiceUnavailableError, GatewayTimeoutError, ConfigurationError) to accept the
same kw-only details: dict | None = None and pass details=details when calling
super().__init__ so the extra context is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/exceptions/handlers.py`:
- Around line 46-57: The HTTPException handler currently drops
HTTPException.headers when delegating to _error_response; update
http_exception_handler to pass exc.headers through (or an empty dict when None)
into the call to _error_response so response headers like WWW-Authenticate are
preserved; locate the async function http_exception_handler and modify its
return invocation of _error_response to accept and forward exc.headers
(merging/normalizing None to {} as needed).

In `@core/logging/middleware.py`:
- Around line 18-19: 클라이언트가 보낸 X-Request-ID 값을 검증하지 않아 로그 인젝션 위험이 있으므로
request.headers.get("X-Request-ID")로 받은 값에 대해 허용 문자와 길이 검증을 추가하고 유효하지 않으면 새
UUID로 대체하도록 변경하세요; 구체적으로 core/logging/middleware.py에서 request_id =
request.headers.get("X-Request-ID") 대신 수신값을 정규식(예: UUID 패턴 또는 영숫자/하이픈만 허용)과 최대
길이 검사로 검증한 후 유효하면 set_request_id(request_id)로 전달하고, 유효하지 않으면
set_request_id(str(uuid.uuid4()))를 호출하도록 구현해 _DEFAULT_FORMAT에 들어가는
%(request_id)s에 안전한 값만 노출되게 하세요.

In `@LLM/OSS/service.py`:
- Around line 122-129: The current _get_oss_client() raises ConfigurationError
but call_oss() swallows all exceptions in its broad try/except, preventing
register_exception_handlers from handling configuration faults; either (A) move
OSS settings validation out of _get_oss_client() into app startup
(lifespan/module import) so missing OSS_API_KEY/OSS_MODEL raises
ConfigurationError during boot, or (B) narrow call_oss()'s exception handling so
ConfigurationError (or AppError) is re-raised while only transient OpenAI call
failures are caught and fall back — update the call_oss() except block to
re-raise ConfigurationError (refer to _get_oss_client, call_oss,
ConfigurationError, register_exception_handlers) or add startup validation that
uses the same checks currently in _get_oss_client().

---

Nitpick comments:
In `@core/exceptions/base.py`:
- Around line 4-55: Add an optional details: dict | None field to AppError and
propagate it through every subclass so callers can supply extra context without
breaking existing call sites: update AppError.__init__ signature to include *,
details: dict | None = None, set self.details = details, and update each
subclass constructor (BadRequestError, UnauthorizedError, ForbiddenError,
NotFoundError, ConflictError, ServiceUnavailableError, GatewayTimeoutError,
ConfigurationError) to accept the same kw-only details: dict | None = None and
pass details=details when calling super().__init__ so the extra context is
preserved.

In `@core/exceptions/handlers.py`:
- Around line 77-86: The call inside unhandled_exception_handler currently
passes exc_info=exc to logger.exception which is redundant; update the logging
to either remove the explicit exc_info and keep
logger.exception("unhandled_exception path=%s", request.url.path) or replace the
call with logger.error("unhandled_exception path=%s", request.url.path,
exc_info=exc) to make the intent explicit; adjust only the logging line
(referencing unhandled_exception_handler, logger.exception, logger.error and
exc_info) so the exception info is attached cleanly and code readability is
improved.

In `@core/logging/config.py`:
- Around line 24-37: The code currently overwrites the global _SERVICE_NAME on
every configure_logging call which can cause inconsistent service names if
configure_logging is invoked multiple times; change configure_logging so it only
assigns _SERVICE_NAME when not already set (e.g., if not _CONFIGURED or if
_SERVICE_NAME is falsy) while keeping the existing behavior of updating
root/handler log levels on reconfiguration; update the logic around _CONFIGURED
and _SERVICE_NAME in configure_logging so that _SERVICE_NAME is preserved after
the first assignment but log levels are still applied on subsequent calls.

In `@image_analysis/service.py`:
- Around line 137-174: In enqueue_timetable_analysis, avoid using
locals().get("job_id"); instead declare job_id: str | None = None before the
outer try, assign to job_id where you currently generate the UUID, and in the
exception handler reference that job_id variable to pop from _job_store and for
any cleanup (also ensure _active_users.discard(user_id) still runs); this makes
intent explicit and eliminates fragile locals() usage while keeping logic around
_job_store, _active_users, and job queue timeout unchanged.
- Around line 29-55: The helper _require_spring_url currently validates the
module-level constant SPRING_TIMETABLE_URL which is captured at import time;
change it to read the setting dynamically instead so runtime changes are
respected: inside _require_spring_url call get_settings().spring_timetable_url
(instead of using SPRING_TIMETABLE_URL), validate that value and raise
ConfigurationError if missing, and update any callers or docstring accordingly
(or alternatively rename the helper if you prefer it to be a one-time import
guard); reference symbols: _require_spring_url, SPRING_TIMETABLE_URL,
get_settings(), ConfigurationError.

In `@main.py`:
- Around line 25-31: Add a new AppError subclass named PayloadTooLargeError
(subclassing AppError, default status_code=413 and default code
"payload_too_large") and replace the HTTPException raised in the
limit_upload_size middleware with raising PayloadTooLargeError; locate the
middleware function limit_upload_size and the AppError base class to implement
the new class and then change the raise statement to use PayloadTooLargeError so
the app uses a consistent AppError subtype for 413 responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7f8a1e2e-c0df-4f34-9e20-812b79ac356e

📥 Commits

Reviewing files that changed from the base of the PR and between d070944 and 5a0d2bd.

📒 Files selected for processing (17)
  • Dockerfile
  • Dockerfiles_oss
  • LLM/OSS/service.py
  • app_oss_main.py
  • core/auth.py
  • core/exceptions/__init__.py
  • core/exceptions/base.py
  • core/exceptions/handlers.py
  • core/logging/__init__.py
  • core/logging/config.py
  • core/logging/context.py
  • core/logging/middleware.py
  • image_analysis/service.py
  • main.py
  • text_filtering/service.py
  • text_filtering/text_filtering.py
  • text_filtering/text_filtering_rule.py

Comment thread core/exceptions/handlers.py Outdated
Comment on lines +18 to +19
request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
set_request_id(request_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

클라이언트가 보낸 X-Request-ID에 대한 검증이 없어 로그 인젝션 가능성이 있습니다.

request.headers.get("X-Request-ID") 값이 그대로 set_request_id로 들어가고 이후 _DEFAULT_FORMAT%(request_id)s를 통해 로그 라인에 직접 출력됩니다. 외부 클라이언트가 개행 문자나 제어 문자(\n, \r)를 포함한 헤더를 전송할 경우 위조된 로그 라인을 삽입할 수 있어, 추후 로그 분석/모니터링 신뢰도를 떨어뜨릴 수 있습니다. 길이 제한과 허용 문자(예: UUID 또는 영숫자/하이픈)만 통과시키고, 그 외에는 새 UUID로 대체하는 것을 권장합니다.

🛡️ 제안 수정안
+import re
+
+_REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
+
+
+def _safe_request_id(raw: str | None) -> str:
+    if raw and _REQUEST_ID_RE.match(raw):
+        return raw
+    return str(uuid.uuid4())
+
+
 def register_request_logging(app: FastAPI) -> None:
     `@app.middleware`("http")
     async def request_logging_middleware(request: Request, call_next):
-        request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
+        request_id = _safe_request_id(request.headers.get("X-Request-ID"))
         set_request_id(request_id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/logging/middleware.py` around lines 18 - 19, 클라이언트가 보낸 X-Request-ID 값을
검증하지 않아 로그 인젝션 위험이 있으므로 request.headers.get("X-Request-ID")로 받은 값에 대해 허용 문자와 길이
검증을 추가하고 유효하지 않으면 새 UUID로 대체하도록 변경하세요; 구체적으로 core/logging/middleware.py에서
request_id = request.headers.get("X-Request-ID") 대신 수신값을 정규식(예: UUID 패턴 또는
영숫자/하이픈만 허용)과 최대 길이 검사로 검증한 후 유효하면 set_request_id(request_id)로 전달하고, 유효하지 않으면
set_request_id(str(uuid.uuid4()))를 호출하도록 구현해 _DEFAULT_FORMAT에 들어가는
%(request_id)s에 안전한 값만 노출되게 하세요.

Comment thread LLM/OSS/service.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
core/exceptions/handlers.py (1)

86-95: logger.exceptionexc_info=exc 전달은 중복입니다.

logger.exception(...)은 기본적으로 exc_info=True로 동작하여 현재 처리 중인 예외의 스택 트레이스를 기록합니다. 명시적으로 exc_info=exc를 전달해도 동작은 비슷하지만, 의도가 모호해지므로 통상 둘 중 하나를 사용합니다.

♻️ 제안 수정안
-        logger.exception("unhandled_exception path=%s", request.url.path, exc_info=exc)
+        logger.exception("unhandled_exception path=%s", request.url.path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/exceptions/handlers.py` around lines 86 - 95, The logger.exception call
in unhandled_exception_handler redundantly passes exc_info=exc; logger.exception
already logs the current exception (exc_info=True) so remove the exc_info=exc
argument from the logger.exception("unhandled_exception path=%s",
request.url.path, ...) call (or alternatively change to logger.error(...,
exc_info=exc) if you intend to supply a specific exception), leaving a single
clear call in unhandled_exception_handler.
LLM/OSS/service.py (1)

142-156: _get_oss_client()을 두 번 호출하고 있어, 라인 143에서 얻은 client 변수가 사용되지 않습니다.

라인 143에서 client = _get_oss_client()로 클라이언트를 받아 ConfigurationError를 통과시키는 의도는 좋지만, 실제 호출은 라인 147에서 _get_oss_client()를 다시 부르고 있어 변수 client가 사용되지 않는 dead code가 되었습니다. 또한 두 번의 try 블록으로 분리할 필요 없이 ConfigurationError만 통과시키는 형태로 단순화할 수 있습니다.

♻️ 제안 수정안
-    try:
-        client = _get_oss_client()
-    except ConfigurationError:
-        raise
-    try:
-        response = _get_oss_client().chat.completions.create(
+    client = _get_oss_client()
+    try:
+        response = client.chat.completions.create(
             model=settings.oss_model,
             messages=messages,
             temperature=kwargs.get("temperature", 0.3),
             max_tokens=kwargs.get("max_tokens", 64),
         )
         return (response.choices[0].message.content or "").strip()
     except Exception:
         logger.warning("oss_call_failed", exc_info=True)
         return ""

이렇게 하면 _get_oss_client()에서 발생하는 ConfigurationError는 자연스럽게 호출자로 전파되고, 이후 OpenAI 호출 단계의 일시적 예외만 fallback으로 흡수됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LLM/OSS/service.py` around lines 142 - 156, The code calls _get_oss_client()
twice and never uses the first result (client), creating dead code and
unnecessary duplication; change the logic to call client = _get_oss_client()
once (letting ConfigurationError propagate) and then use that client for the
chat completion call (client.chat.completions.create(...)), combining the try
blocks so only the external call is wrapped in the broad exception handler that
logs via logger.warning("oss_call_failed", exc_info=True) and returns "" on
failure; remove the unused variable and duplicate call to clean up the flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/exceptions/handlers.py`:
- Around line 55-66: The exception handler is only registered for
fastapi.HTTPException and thus misses Starlette-raised HTTPExceptions (e.g.,
404/405); update the decorator registration to use
starlette.exceptions.HTTPException so the existing
http_exception_handler(request: Request, exc: HTTPException) (which calls
set_request_id(_request_id(request)), logs and returns _error_response(...,
extra_headers=exc.headers)) will catch both Starlette and FastAPI HTTPExceptions
and ensure unified `{error, code, request_id}` responses and X-Request-ID header
for routing errors.

---

Nitpick comments:
In `@core/exceptions/handlers.py`:
- Around line 86-95: The logger.exception call in unhandled_exception_handler
redundantly passes exc_info=exc; logger.exception already logs the current
exception (exc_info=True) so remove the exc_info=exc argument from the
logger.exception("unhandled_exception path=%s", request.url.path, ...) call (or
alternatively change to logger.error(..., exc_info=exc) if you intend to supply
a specific exception), leaving a single clear call in
unhandled_exception_handler.

In `@LLM/OSS/service.py`:
- Around line 142-156: The code calls _get_oss_client() twice and never uses the
first result (client), creating dead code and unnecessary duplication; change
the logic to call client = _get_oss_client() once (letting ConfigurationError
propagate) and then use that client for the chat completion call
(client.chat.completions.create(...)), combining the try blocks so only the
external call is wrapped in the broad exception handler that logs via
logger.warning("oss_call_failed", exc_info=True) and returns "" on failure;
remove the unused variable and duplicate call to clean up the flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7d2b9539-5e94-4a9f-8340-ba050bea3d34

📥 Commits

Reviewing files that changed from the base of the PR and between 5a0d2bd and 3c6c63c.

📒 Files selected for processing (2)
  • LLM/OSS/service.py
  • core/exceptions/handlers.py

Comment thread core/exceptions/handlers.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/exceptions/handlers.py (1)

28-30: X-Request-ID가 덮어써질 여지가 있습니다.

Line 28-30에서 extra_headers를 그대로 update하면, 외부에서 전달된 X-Request-ID가 내부 추적 ID를 덮어쓸 수 있습니다. 추적 무결성을 위해 X-Request-ID는 항상 내부 값을 유지하는 편이 안전합니다.

제안 패치
     headers = {"X-Request-ID": request_id}
     if extra_headers:
-        headers.update(extra_headers)
+        headers.update(
+            {k: v for k, v in extra_headers.items() if k.lower() != "x-request-id"}
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/exceptions/handlers.py` around lines 28 - 30, The current headers update
uses headers.update(extra_headers) which allows an external "X-Request-ID" to
overwrite the internal request_id; change the merge so the internal X-Request-ID
in headers (the variable request_id) is preserved — when incorporating
extra_headers (variable extra_headers) filter out any "X-Request-ID" key
(case-insensitively) before calling update or only copy keys that are not
"X-Request-ID" into headers to ensure the internal trace id in headers is never
replaced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/exceptions/handlers.py`:
- Around line 75-79: The current logger.warning call logs exc.errors(), which
may include raw user input; change it to log only the number of errors to avoid
exposing user data: replace the existing
logger.warning("request_validation_failed path=%s errors=%s", request.url.path,
exc.errors()) usage in the exception handler with a message that uses the error
count (e.g., compute count = len(exc.errors()) or similar) and include that
count instead of exc.errors() so only the error quantity is recorded.

---

Nitpick comments:
In `@core/exceptions/handlers.py`:
- Around line 28-30: The current headers update uses
headers.update(extra_headers) which allows an external "X-Request-ID" to
overwrite the internal request_id; change the merge so the internal X-Request-ID
in headers (the variable request_id) is preserved — when incorporating
extra_headers (variable extra_headers) filter out any "X-Request-ID" key
(case-insensitively) before calling update or only copy keys that are not
"X-Request-ID" into headers to ensure the internal trace id in headers is never
replaced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a43f0957-63d7-40a1-944b-3ec138d2c534

📥 Commits

Reviewing files that changed from the base of the PR and between 3c6c63c and efece73.

📒 Files selected for processing (1)
  • core/exceptions/handlers.py

Comment on lines +75 to +79
logger.warning(
"request_validation_failed path=%s errors=%s",
request.url.path,
exc.errors(),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

FastAPI + Pydantic v2에서 RequestValidationError.errors() 결과에 사용자가 보낸 input 값(원문 데이터)이 포함되는지 공식 문서/예제로 확인해 주세요.

💡 Result:

결론: FastAPI + Pydantic v2에서 RequestValidationError.errors 결과(각 error dict)에, 사용자가 보낸 원문 input 값이 포함됩니다. 또한 Pydantic v2의 ValidationError.errors 기본 동작에서도 include_input 기본값이 True라서 input이 포함됩니다. 근거 요약 1) FastAPI 문서(Handling Errors)에서 RequestValidationError가 invalid data로부터 받은 body(exc.body)를 별도로 사용할 수 있음을 명시합니다. 문서 예제는 exc.errors와 exc.body를 함께 내려주며, 이는 FastAPI의 RequestValidationError가 입력값을 가지고 있음을 보여줍니다. (다만 질문의 “errors 내부에 input이 포함되느냐”는 Pydantic v2의 errors 스펙으로 확인됩니다.) 2) Pydantic v2 공식 문서에서 errors의 ErrorDetails 딕셔너리에 input(“validation을 위해 제공된 input”) 필드가 있다고 명시합니다. 예시 출력에서도 error dict에 'input': ... 값이 포함되어 출력됩니다. 3) Pydantic v2 pydantic_core ValidationError.errors 시그니처 문서에서도 include_input 파라미터가 기본값 True이며, “각 에러의 input value 포함 여부”를 제어한다고 명시합니다. 따라서 FastAPI가 내부적으로 호출하는 Pydantic v2 errors 기본 설정에서는 input이 포함됩니다. 주의/실무 팁(보안 관점) - input 값이 에러 응답에 그대로 포함되면(기본 동작) 사용자 입력 자체를 로깅/노출할 수 있으므로, 필요하면 커스텀 exception handler에서 input을 제거(또는 include_input=False 사용)하는 방식을 고려해야 합니다. (FastAPI 문서 자체도 예시에서 errors를 가공하는 형태를 보여줍니다.)

Citations:


🏁 Script executed:

# Check if the file exists and read the relevant lines
file_path="core/exceptions/handlers.py"
if [ -f "$file_path" ]; then
    echo "=== File found, checking line count ==="
    wc -l "$file_path"
    echo ""
    echo "=== Reading lines 70-85 for context ==="
    sed -n '70,85p' "$file_path"
    echo ""
    echo "=== Full handler function context (lines 1-100) ==="
    head -100 "$file_path"
else
    echo "File not found. Searching for handlers.py"
    fd "handlers.py" 
fi

Repository: dongsooop/AI

Length of output: 3890


요청 검증 오류 로그에서 사용자 입력값 노출 방지 필요

exc.errors() 는 Pydantic v2의 기본 동작에서 사용자가 보낸 원문 데이터를 포함합니다. 운영 환경의 로그에 민감 정보가 기록되지 않도록 오류 개수만 로깅하세요.

제안 패치
-        logger.warning(
-            "request_validation_failed path=%s errors=%s",
-            request.url.path,
-            exc.errors(),
-        )
+        logger.warning(
+            "request_validation_failed path=%s error_count=%d",
+            request.url.path,
+            len(exc.errors()),
+        )
📝 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
logger.warning(
"request_validation_failed path=%s errors=%s",
request.url.path,
exc.errors(),
)
logger.warning(
"request_validation_failed path=%s error_count=%d",
request.url.path,
len(exc.errors()),
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/exceptions/handlers.py` around lines 75 - 79, The current logger.warning
call logs exc.errors(), which may include raw user input; change it to log only
the number of errors to avoid exposing user data: replace the existing
logger.warning("request_validation_failed path=%s errors=%s", request.url.path,
exc.errors()) usage in the exception handler with a message that uses the error
count (e.g., compute count = len(exc.errors()) or similar) and include that
count instead of exc.errors() so only the error quantity is recorded.

@Yu-JeSeung
Yu-JeSeung merged commit 86fbe5f into main Apr 28, 2026
1 check passed
@Yu-JeSeung
Yu-JeSeung deleted the refactor/exception_handling_logging_standardization branch April 28, 2026 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] AI 서비스 전체 코드 리팩토링 진행

1 participant