Files
llm-wiki/raw/errors/jdk-httpclient-dns-unresolvedaddress-connectexception-2026-06-11.md

4.7 KiB

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
error / jdk-httpclient-dns-unresolvedaddress-connectexception-2026-06-11 error-note raw
feature-outbound-http-client-baseline
ca-skeleton
error
ca-skeleton
jdk-httpclient
dns
error-classification
outbound-http
2026-06-11 resolved

error: jdk-httpclient-dns-unresolvedaddress-connectexception-2026-06-11

Layer: raw/errors/ — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.

Parent / 부모

증상 / Symptom

  • 에러 메시지 (구현 에이전트 보고 원문 발췌 — 테스트 red 단계):
    red: ./gradlew :adapter-outbound:test --tests '*.OutboundHttpClientTest' --console=plain
    → 4 FAILED (... t4 CONNECT_FAILED not DNS_FAILED ...)
    
  • 발생 컨텍스트: OutboundHttpClient.get("http://nonexistent-host-zzz.invalid", ...) 호출 시 OutboundHttpErrorMapperDEPENDENCY_DNS_FAILED 가 아니라 DEPENDENCY_CONNECT_FAILED 를 반환.
  • 발생 환경: local, JDK 21 (JdkClientHttpRequestFactory + java.net.http.HttpClient).
  • 재현 가능 여부: always.

재현 절차 / Reproduction

  1. JDK 21 HttpClient 기반 Spring RestClient 로 존재하지 않는 호스트(*.invalid)에 GET 요청.
  2. cause chain 을 단일 패스로 위에서부터 매칭하는 분류기(UnknownHostException|UnresolvedAddressException 규칙이 ConnectException 규칙보다 우선순위가 높아도, 체인 순서상 ConnectException 이 먼저 등장)를 통과.
  3. 기대: DEPENDENCY_DNS_FAILED.
  4. 실제: DEPENDENCY_CONNECT_FAILED — JDK 21 HttpClient 가 DNS 실패를 ConnectException(cause=ConnectException(cause=UnresolvedAddressException)) 으로 래핑하기 때문에, 체인을 바깥에서부터 한 번만 훑는 분류기는 바깥쪽 ConnectException 에서 먼저 멈춘다.

조사 단계 / Investigation log

  • 2026-06-11 — t4 red 관측 → 예외 cause chain 출력으로 ConnectException → ConnectException → UnresolvedAddressException 중첩 구조 확인 (JDK 21 로컬 검증).
  • 2026-06-11 — 분류 규칙 순서 조정만으로는 해결 불가(체인 등장 순서 문제) → ConnectException 매칭 시 잔여 서브 체인을 hasDnsCauseInChain() 으로 추가 스캔, DNS 근원 발견 시 DEPENDENCY_DNS_FAILED 우선 반환하도록 수정 → t4 green, 기존 mapper 단위테스트 19/19 회귀 없음.

근본 원인 / Root cause

  • 직접 원인: 분류기가 cause chain 에서 먼저 등장하는 예외 타입으로 결정 — 바깥 래퍼(ConnectException)가 안쪽 근원(UnresolvedAddressException)을 가림.
  • 근본 원인: JDK HttpClient 의 예외 래핑 구조(DNS 실패도 ConnectException 으로 노출)가 "타입 우선순위 = 체인 등장 순서" 가정과 충돌.
  • 트리거 조건: JDK 21 HttpClient + 미해석 호스트명. (needs-confirmation: 다른 JDK 버전/다른 ClientHttpRequestFactory 의 래핑 구조는 미검증.)

Sources / 근거

  • 로컬 검증: OutboundHttpClientTest.t4_unknown_host_* (JDK 21) — 수정 전 red / 수정 후 green. 외부 공식 문서 인용 없음 (JDK 예외 래핑 구조는 로컬 관측 기반).

해결 / Resolution

  • 적용한 조치: OutboundHttpErrorMapperConnectException 매칭 시 hasDnsCauseInChain() helper 로 서브 체인에서 UnknownHostException/UnresolvedAddressException 을 추가 탐색, 발견 시 DNS 분류 우선.
  • 검증 방법: ./gradlew :adapter-outbound:testOutboundHttpClientTest.t4 + OutboundHttpErrorMapperTest 19/19 PASS.
  • 잔여 위험: factory 교체(Apache/Jetty 등) 시 래핑 구조가 달라질 수 있음 — 계약 테스트가 회귀를 잡음.

회고 / Lessons

  • 빨리 감지하는 신호: "DNS 실패가 CONNECT_FAILED 로 잡힘" / 분류 테스트에서 인접 카테고리 오분류 → 예외 cause chain 전체를 덤프해 래핑 구조부터 확인.
  • 예방 체크리스트: 예외 분류기는 "타입 우선순위" 와 "체인 등장 순서" 를 분리해 설계 — 특정 근원(DNS)이 래퍼(connect)보다 우선해야 하면 서브 체인 스캔을 명시.
  • wiki 일반화 후보: "cause-chain 기반 예외 분류기의 우선순위 함정" (wiki/concepts 추출 후보).
  • 같은 red 라운드에서 발견된 인접 오분류: connect-refused 가 HttpConnectTimeoutException extends HttpTimeoutException 상속 때문에 TIMEOUT 으로 새는 문제 — 분류 규칙 순서(connect-timeout 을 read-timeout 보다 먼저)로 해결 (branch note §Cluster Errors 기록).