개발/AI

[디버깅] google-genai SDK TypeError 마스킹 APIError 노출

jykim23 2026. 6. 25. 20:45
반응형

부제: 에러 메시지가 진짜 원인을 가리고 있을 때

LLM 호출이 가끔 실패하는데, 로그에는 이런 메시지만 남았다.

'ClientResponse' object is not subscriptable

이 메시지로는 아무것도 알 수 없었다. 쿼터가 초과된 건지(429), 서버가 죽은 건지(503), 타임아웃인지(504), 모델 글리치인지 판별이 안 됐다. 진짜 원인이 이 메시지 뒤에 가려져 있었다.

증상

추적해 보니 SDK 안에서 일어난 일이었다.

google-genai의 응답 객체에는 본문을 파싱하는 json 프로퍼티가 있는데, 스트리밍 경로에서 내부 값이 리스트가 아닐 때 [0] 인덱싱을 시도하다 TypeError를 던졌다. 문제는 이 TypeError가 터지는 위치다.

상위 라이브러리(langchain)는 에러 응답을 해석하려고 그 json을 읽는데, 바로 거기서 TypeError가 나 버린다. 그래서 원래 전달돼야 할 APIError(code·status·message 같은 진짜 정보)는 이 엉뚱한 TypeError에 덮여서 호출자까지 오지 못했다.

즉, 에러를 해석하려는 코드가 에러 때문에 또 죽으면서, 정작 원인 코드를 잃어버린 상황이었다.

정리한 방식

두 군데를 손봤다.

1. SDK의 문제 지점을 기동 시 패치 리스트가 아닌 경우엔 인덱싱을 시도하지 말고 None을 돌려주도록 그 프로퍼티를 갈아끼웠다. 그러면 상위 라이브러리의 try/except가 정상적으로 fallback을 타고, 원본 APIError가 호출자까지 그대로 전파된다. 서버가 켜질 때 한 번 적용하고, 로그로 적용 여부를 남겼다.

# startup 시 1회 적용 (의사 코드)
def apply_sdk_patches():
    # 응답 본문이 list가 아니면 [0] 인덱싱 대신 None 반환
    HttpResponse.json = _safe_json
    logger.info("SDK patches applied")

2. 잡은 김에 진짜 에러를 구조화해서 로깅 LLM 호출을 감싼 자리에서 APIError를 잡아 code·status·message·details를 남기고 다시 던졌다(re-raise). 다음에 같은 일이 생기면 status code가 바로 로그에 보인다.

try:
    response = await llm.ainvoke(messages, config=config)
except Exception as e:
    _log_llm_api_error(e, attempt=attempt)   # code/status/message 구조화 기록
    raise

결과

APIError(429, RESOURCE_EXHAUSTED)를 주입해서 확인했다. 이제 로그에 이렇게 찍힌다.

Gemini APIError surfaced code=429 status=RESOURCE_EXHAUSTED

가짜 TypeError 대신 진짜 status code가 보이고, 호출자까지 정상적으로 전파된다.

정리

  • 에러를 해석하는 경로 자체가 또 다른 에러를 던지면, 원인이 통째로 가려질 수 있다
  • 라이브러리 버그라도, 그 지점만 기동 시 패치해서 우회할 수 있다
  • 잡은 에러는 그냥 삼키지 말고 code·status를 구조화해 남기고 다시 던진다
  • "원인 불명"으로 보이는 에러는 메시지를 의심하기 전에, 그 메시지가 어디서 만들어졌는지부터 본다

에러 메시지를 그대로 믿지 않고 어디서 만들어졌는지 따라가 보니, 원인은 처음부터 거기 있었다.

반응형