[payments] Match Link rel types case-insensitively in manifest download

MERGED2026components/paymentspayment-method-manifest
2026. 9. 1.jmsmg 프로필 이미지jmsmg

PaymentManifestDownloader가 HTTP Link 헤더에서 payment method manifest 관계를 찾을 때 rel 값을 대소문자 구분 없이 비교하도록 고쳤습니다(crbug.com/545645933). RFC 8288 위반을 스펙 대조로 직접 발굴한 버그이며, 리뷰 과정에서 WPT(Web Platform Tests)가 이 버그를 실제로 잡아내는지 리뷰어가 CQ로 검증하는 흥미로운 과정이 있었습니다.

문제 설명

  • issue url: https://crbug.com/545645933
  • 웹 결제에서 서버는 응답 헤더로 매니페스트 위치를 알릴 수 있습니다: Link: <manifest.json>; rel="payment-method-manifest"
  • RFC 8288 section 2.1은 link relation type을 대소문자 구분 없이 비교하도록 규정합니다.
  • 그러나 PaymentManifestDownloader::OnURLLoaderCompleteInternal()은 rel 값을 소문자 리터럴 "payment-method-manifest"와 그대로 비교합니다. 서버가 rel="Payment-Method-Manifest"를 보내면 표준상 유효한 응답인데도 링크가 없는 것으로 처리되어 결제가 실패합니다.
  • 근본 원인은 공용 파서의 계약에 있습니다. components/link_header_util은 파라미터 이름(rel=)만 소문자로 정규화하고 은 원문 그대로 넘기므로, 값의 정규화 책임은 호출자에게 있습니다.

해결 내용

rel 값을 relation type으로 분리하기 전에 base::ToLowerASCII()로 정규화했습니다. 값 전체를 소문자화한 뒤 공백으로 분리하므로 rel="prefetch PAYMENT-METHOD-MANIFEST"처럼 여러 relation type이 섞인 경우도 함께 처리됩니다.

-    std::vector<std::string> rel_parts =
-        base::SplitString(rel->second.value_or(""), HTTP_LWS,
-                          base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
+    // Link relation types are case-insensitive (RFC 8288 section 2.1).
+    std::vector<std::string> rel_parts = base::SplitString(
+        base::ToLowerASCII(rel->second.value_or("")), HTTP_LWS,
+        base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

범위를 좁게 유지하기 위해 인접 이슈 545843242(Link 헤더가 정확히 하나여야 한다는 검증)는 이 CL에서 의도적으로 제외했습니다.

테스트 방법

TDD 순서로 진행했습니다.

  1. 회귀 테스트 2개(mixed-case rel 단독 / 다른 relation type과 혼재)를 먼저 작성하고, 수정 전에 실제로 실패하는 것을 확인했습니다 — 매니페스트 다운로드가 시작되지 않아 테스트 픽스처의 CHECK_EQ(downloads_.size(), 1u)가 터지며 CRASHED. 이것이 버그 재현 증거입니다.
  2. 수정 후 PaymentMethodManifestDownloaderTest 전체(39개)와 *ManifestDownloader* 필터 전체가 통과했습니다.

리뷰와 WPT baseline

payments OWNER(Darwin Yang)가 당일 +1을 주고 2차 리뷰어(Stephen McGruer)를 추가했는데, McGruer가 흥미로운 검증을 했습니다. "이 수정이 들어가면 WPT가 '실패'하기 시작할 것"이라 예측하고 직접 CQ를 돌린 것입니다.

Chromium은 통과하지 못하는 WPT의 실패 출력을 -expected.txt baseline으로 체크인해 "알려진 실패"로 관리합니다. link-header-parsing.https.window-expected.txt의 유일한 [FAIL] 엔트리가 바로 이 버그(case-insensitive 서브테스트)였고, 수정으로 전체 통과("All subtests passed")가 되자 낡은 baseline과 불일치해 CQ가 빨간불이 됐습니다 — 예측 적중이며, WPT가 이 버그를 잡아낼 수 있음을 증명한 실험이었습니다. 후속 패치셋에서 해당 baseline을 삭제해 해결했습니다.

조사 중 함정도 하나 있었습니다. 봇이 말하는 테스트 파일이 로컬 트리에 없었는데, 원인은 git fetch가 조용히 실패해 로컬 origin/main이 며칠 뒤처져 있던 것이었습니다. 최신 WPT 임포트가 개별 테스트들을 하나의 파일로 통합해놓은 상태였습니다.

배운 점

  • 파서/프로토콜 코드는 스펙 대조로 버그를 찾기 좋은 영역입니다. 대소문자 처리, 공백 처리, 개수 제한 같은 MUST 조항과 구현을 대조하면 되고, 스펙 인용 한 줄이 곧 수정의 정당성이 됩니다.
  • 공용 파서를 쓸 때는 정규화 계약을 확인해야 합니다. "파서가 알아서 해주겠지"가 아니라, 무엇을 정규화하고 무엇을 원문으로 넘기는지가 계약입니다.
  • WPT -expected.txt baseline 제도: 버그를 고치면 "알려진 실패" 기록과 불일치해 오히려 테스트가 실패로 표시되며, 수정 CL에서 baseline을 함께 갱신/삭제해야 합니다. 리뷰어가 코드만 보지 않고 "테스트 인프라가 이 회귀를 막을 수 있는가"까지 검증한다는 것도 인상적이었습니다.
  • 봇 로그와 로컬 트리가 어긋나면 fetch 신선도부터 의심할 것.
  • payments 컴포넌트의 초기 리뷰는 개인 지정이 아니라 chrome-payments-reviews@google.com 알리아스로 요청하는 것이 팀 관례입니다.

참고 자료