[payments] Match Link rel types case-insensitively in manifest download
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 순서로 진행했습니다.
- 회귀 테스트 2개(mixed-case rel 단독 / 다른 relation type과 혼재)를 먼저 작성하고, 수정 전에 실제로 실패하는 것을 확인했습니다 — 매니페스트 다운로드가 시작되지 않아 테스트 픽스처의
CHECK_EQ(downloads_.size(), 1u)가 터지며 CRASHED. 이것이 버그 재현 증거입니다. - 수정 후
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.txtbaseline 제도: 버그를 고치면 "알려진 실패" 기록과 불일치해 오히려 테스트가 실패로 표시되며, 수정 CL에서 baseline을 함께 갱신/삭제해야 합니다. 리뷰어가 코드만 보지 않고 "테스트 인프라가 이 회귀를 막을 수 있는가"까지 검증한다는 것도 인상적이었습니다. - 봇 로그와 로컬 트리가 어긋나면 fetch 신선도부터 의심할 것.
- payments 컴포넌트의 초기 리뷰는 개인 지정이 아니라
chrome-payments-reviews@google.com알리아스로 요청하는 것이 팀 관례입니다.