httpclient 모듈 완료 검증 — lane 실행과 실패 진단 revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5 generatedAt=2026-08-30T05:40:58+00:00 === 1. :check 는 archive-hygiene 게이트에서 실패한다 (소스와 무관) === $ ./gradlew :adapter:outbound:httpclient:check → BUILD FAILED 원인: build/libs 에 이전 리비전 JAR 이 남아 있음 $ ls src/adapter/outbound/httpclient/build/libs httpclient-0.0.1+0137263441f6.jar httpclient-0.0.1+a24ece9cf797.jar httpclient-0.0.1+e98b56eb03ec.jar exit=0 게이트 정의: src/gradle/archive-hygiene.gradle — 저장소가 remedy 태스크 cleanStaleTraceableJars 를 제공한다. 이 분석은 사용자 워크스페이스의 빌드 산출물을 삭제하지 않았다. === 2. 다섯 lane 개별 실행 === $ ./gradlew :adapter:outbound:httpclient:test :…:httpClientStableContractTest :…:httpClientSecurityTest :…:httpClientBlockHoundTest :…:spring62ApiSurfaceScan GRADLE_EXIT=1 — :test 에서 283 tests completed, 3 failed === 3. JUnit 집계 === test: classes=61 tests=283 failures=3 errors=0 skipped=0 httpClientStableContractTest: classes=7 tests=24 failures=0 errors=0 skipped=0 httpClientSecurityTest: classes=2 tests=9 failures=0 errors=0 skipped=0 httpClientBlockHoundTest: classes=1 tests=3 failures=0 errors=0 skipped=0 spring62ApiSurfaceScan: classes=1 tests=6 failures=0 errors=0 skipped=0 === 4. 실패 3건의 정확한 내용 === anUntrustedAuthorityIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError: expected: TLS_HANDSHAKE but was: CONNECT anExpiredCertificateIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError: expected: TLS_HANDSHAKE but was: CONNECT aHostnameMismatchIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError: expected: TLS_HANDSHAKE but was: CONNECT === 5. 원인: 원인 사슬을 바깥부터 훑고 CONNECT 분기가 TLS 분기보다 앞에 있다 === $ sed -n '30,42p' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java @Override public TransportFailure classify(Throwable failure, AttemptStage lastObservedStage) { // The whole cause chain is inspected, not just the outermost throwable: Spring wraps engine // exceptions, and a wrapped ConnectException still proves the request was never sent. Matching // only the outer type would downgrade a provable NOT_SENT to an ambiguous SENT_NO_RESPONSE. for (Throwable cause : chain(failure)) { TransportFailure recognized = recognize(cause, lastObservedStage); if (recognized != null) { return recognized; } } return conservative(lastObservedStage, FailureCategory.UNKNOWN, "TRANSPORT_FAILURE"); exit=0 $ sed -n '58,73p' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java if (cause instanceof ConnectTimeoutException || cause instanceof HttpHostConnectException || cause instanceof ConnectException || cause instanceof NoRouteToHostException) { return TransportFailure.notSent( AttemptStage.CONNECT, FailureCategory.CONNECT, "CONNECT_FAILED"); } if (cause instanceof SSLPeerUnverifiedException || cause instanceof CertificateException) { return TransportFailure.notSent( AttemptStage.TLS_HANDSHAKE, FailureCategory.TLS_PERMANENT, "TLS_TRUST_FAILED"); } if (cause instanceof SSLHandshakeException) { return TransportFailure.notSent( AttemptStage.TLS_HANDSHAKE, FailureCategory.TLS_PERMANENT, "TLS_HANDSHAKE_FAILED"); } if (cause instanceof SSLException exit=0 --- 재시도 결과 대비 --- $ grep -n 'case CONNECT\|case TLS_TRANSIENT\|permanent()' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/DefaultRetryEligibilityEngine.java 37: if (context.failureCategory().permanent()) { 107: case CONNECT -> RetryAllowed.of("CONNECT"); 109: case TLS_TRANSIENT -> RetryAllowed.of("TLS_TRANSIENT"); exit=0 $ grep -n 'TLS_PERMANENT' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/FailureCategory.java 18: TLS_PERMANENT, 38: || this == TLS_PERMANENT exit=0 === 6. 작업 트리 === $ git status --short | wc -l 0 exit=0