Skip to content

헤드리스 홉 목록을 받아 상품 문서·차단 판정을 extractor 가 맡음 - #67

Open
m-a-king wants to merge 2 commits into
mainfrom
feat/render-hop-trail
Open

헤드리스 홉 목록을 받아 상품 문서·차단 판정을 extractor 가 맡음#67
m-a-king wants to merge 2 commits into
mainfrom
feat/render-hop-trail

Conversation

@m-a-king

@m-a-king m-a-king commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Situation

  • 2026-09-07 prod 에서 에이블리 공유링크 2건이 NO_EXTRACTABLE_CONTENT 로 FAILED 됐다. renderer 가 3,188자 셸을 돌려줬고 셸 게이트가 그대로 확정 실패로 닫았다.
  • 원인은 renderer 가 "어느 문서가 상품 페이지인가" 를 스스로 판단하다 오판한 것이다(renderer PR 참조). 에이블리는 서버 본문이 늘 3KB 셸이고 상품은 하이드레이션 뒤 DOM 에만 있다.

Task

  • renderer 계약이 "최선의 HTML 한 장 + verdict" 에서 "거쳐 간 홉 목록(url·status·headers·body·dom)" 으로 바뀐다. 어느 홉의 어느 문서를 파이프라인에 태울지, 차단인지의 판단을 extractor 가 맡는다.
  • renderer 배포는 수동이라 뒤처질 수 있으므로 구계약 응답도 읽어야 한다.

Action

홉 해석 (HeadlessProductLinkExtractor)

  • 후보 순서: 마지막 홉부터 거슬러, 같은 홉 안에서는 dom 다음 body. 홈 피드·앱 유도로 튕긴 경우 상품은 앞 홉에 있고, 같은 홉에서는 하이드레이션된 dom 이 알차다.
  • 차단 판정: status 401·403·405·429·490 또는 title 의 챌린지 마커(HeadlessBlockSignal). renderer 의 _block_reason 을 그대로 옮긴 것이고, 이제 홉 단위로 본다. 챌린지 홉을 지나 상품 홉에 도달했으면 차단이 아니다.
  • 채택: 구조화 데이터가 잡히는 첫 후보. 없으면 마지막 홉의 dom 한 장을 기존 파이프라인(셸 게이트 → LLM)에 넘긴다. LLM 은 한 번만 부른다.
상황 결과
후보 중 구조화 데이터 있음 그 문서로 via=structured
후보는 있으나 구조화 없음 마지막 홉 dom 으로 셸 게이트 또는 LLM
모든 홉이 차단 신호 HEADLESS_BLOCKED(일시, 기존과 동일)
홉은 있으나 HTML 전부 빈값, 또는 홉 없음 HEADLESS_UPSTREAM(일시)

wire (HttpHeadlessRenderer)

  • HeadlessRendererPageContent 대신 List<RenderedHop> 을 돌려준다. 가지치기는 후보를 만들 때 PageContent.of 가 한다.
  • 매 홉 url 을 SSRF 가드에 통과시킨다. 어느 홉이든 내부망이면 렌더 전체를 거부한다. 형식 위반 url 은 원본 link 로 폴백한다(기존 final_url 규칙과 같다).
  • 구계약(html·final_url·status) 응답은 홉 하나(dom=html)로 읽는다. renderer 가 먼저든 나중이든 배포 순서와 무관하게 동작한다.
  • 요청에서 include_html 을 뺐다.

범위 밖

  • 차단 status 세분화(접근 거부·챌린지·CAPTCHA·레이트리밋·상품 없음)는 infra 에러 코드 카탈로그가 additive 로 먼저 늘어야 해서 이번 PR 에 넣지 않았다. 404·410 은 지금처럼 셸 게이트로 흘러 NO_EXTRACTABLE_CONTENT 가 된다.

Result

  • 홉 해석 8케이스·wire 12케이스를 단위로 고정했다. 에이블리 실패 케이스는 "body 셸 + dom 상품" 홉으로 재현해 LLM 없이 via=structured 로 닫히는 것을 확인했다.
  • 로컬 전체 테스트에서 ExtractionErrorCodeCatalogTest 2건이 실패했는데 워크트리에 shared-infra/ 카탈로그가 설치되지 않은 환경 문제다(CI 는 infra 를 체크아웃한다). 이 변경과 무관.
  • 배포 순서: 이 PR 먼저, renderer 나중.

연관 이슈

- renderer 계약이 "최선의 HTML 한 장 + verdict" 에서 "거쳐 간 홉 목록(url·status·headers·body·dom)" 으로 바뀐다. 2026-09-07 prod 에이블리 공유링크 실패는 renderer 가 상품 페이지 여부를 스스로 판단하다 오판해 3KB 셸 본문을 준 것이었고, 그 판단을 이쪽으로 옮긴다
- 후보는 마지막 홉부터 거슬러 dom → body 순. 홈 피드로 튕긴 경우 상품은 앞 홉에 있고, 에이블리는 body 가 셸이고 상품은 하이드레이션된 dom 에만 있다. 구조화 데이터가 잡히는 첫 후보를 쓰고 없으면 마지막 홉 dom 으로 기존 파이프라인(셸 게이트 → LLM)을 탄다. LLM 은 한 번만
- 차단 판정(status 401·403·405·429·490, 챌린지 title)은 renderer 의 목록을 그대로 옮겨 홉 단위로 본다(HeadlessBlockSignal). 모든 홉이 차단이어야 HEADLESS_BLOCKED, 챌린지를 지나 상품 홉에 도달했으면 차단이 아니다
- 매 홉 url 을 SSRF 가드에 통과시킨다. 어느 홉이든 내부망이면 렌더 전체 거부
- renderer 배포가 수동이라 뒤처질 수 있어 구계약(html·final_url) 응답을 홉 하나로 읽는 호환을 둔다. 이 PR 을 먼저 배포한다
- 차단 status 세분화(404·410 은 상품 없음)는 infra 에러 코드 계약이 먼저라 범위 밖
@m-a-king m-a-king added the feat 외부 가시적 새 기능 label Sep 8, 2026
@m-a-king m-a-king self-assigned this Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: ab18c169-2639-4fb5-b8c7-f6e281c9e94a


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.

- 후보 순서를 "모든 홉의 dom 을 마지막부터, 그다음 모든 홉의 body" 로 바꾼다. 앞 홉 dom 이 마지막 홉 body 보다 먼저여야 홈 피드 body 의 사이트 공통 JSON-LD 에 밀리지 않는다. body 가 dom 과 같으면 건너뛴다
- 모든 후보를 미리 파싱해 리스트로 들던 것을 루프 안 지연 파싱으로 바꾼다. 구조화 데이터가 잡히면 즉시 끝내고, 그 결과를 파이프라인 오버로드로 넘겨 같은 Document 를 두 번 파싱하지 않는다
- 차단 status 뒤에 실린 온전한 구조화 데이터는 그대로 쓴다(봇 방어는 어떤 status 로도 위장한다). 차단 신호는 LLM 후보에서만 빼고, 아무것도 못 뽑았을 때 HEADLESS_BLOCKED/UPSTREAM 을 가르는 데만 쓴다. 본문 없는 403 도 홉 단위로 차단으로 센다
- LLM fallback 은 마지막 홉 dom 이 아니라 LLM 에 넘길 것이 있는 첫 후보다. 마지막 홉이 앱 유도 셸이고 앞 홉에 텍스트가 있으면 앞 홉으로 간다
- 차단 신호에 cf-mitigated: challenge 헤더와 Cloudflare 계열 title 마커를 더한다. headers 필드가 이제 읽힌다. HeadlessBlockSignal 은 형제 분류기(EmptyShellDetector·LlmInputGate)와 같은 extraction 패키지의 package-private 으로 옮긴다
- 구계약 호환: verdict=BLOCK 은 html 이 실려 와도 HEADLESS_BLOCKED, status 만 있는 응답도 홉 하나로 읽어 차단 분류가 된다. 제거 조건(renderer #34 배포)을 주석에 남긴다
- Locale.ROOT, URL 마스킹 Pattern 상수화, 성공 로그의 항상 null 인 error 제거
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 외부 가시적 새 기능

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant