[payments] Fail manifest download on multiple manifest Link headers

IN REVIEW2026components/paymentspayment-method-manifestwpt
2026. 9. 4.jmsmg 프로필 이미지jmsmg

결제 방식 식별자(PMI) 응답에 rel="payment-method-manifest" Link 헤더가 둘 이상 오면 매니페스트 다운로드를 실패시키도록 고쳤습니다(crbug.com/545843242). 직전 기여인 8336867의 쌍둥이 이슈로, 같은 함수를 다루면서 그때 배운 WPT baseline 처리를 처음부터 CL에 포함한 것이 이번의 차이입니다.

문제 설명

  • issue url: https://crbug.com/545843242
  • Payment Method Manifest 스펙의 "fetch payment method manifest" 알고리즘은 응답에서 rel="payment-method-manifest"인 link 헤더를 모두 추출한 뒤 그 개수가 1이 아니면 실패하도록 규정합니다.
  • 그러나 PaymentManifestDownloader::OnURLLoaderCompleteInternal()SplitLinkHeader() 루프 안에서 첫 매칭을 찾자마자 URL 검증과 다운로드를 시작하고 return 했습니다. 뒤에 매니페스트 링크가 더 있어도 세지 않으므로, 서버가 서로 다른 매니페스트를 가리키는 링크를 여러 개 보내도 브라우저가 임의로 첫 번째를 따라갔습니다.
  • 이 동작은 이미 WPT에 기록되어 있었습니다. external/wpt/payment-method-manifest/link-header-selection.https.window.js의 서브테스트 5개 중 "Multiple rel=payment-method-manifest link headers cause fetch to abort" 하나만 실패하고 있었고, 그 실패가 -expected.txt baseline에 "알려진 실패"로 체크인되어 있었습니다.

해결 내용

루프의 책임을 "수집"으로 좁히고, 개수 판정을 루프 밖으로 꺼냈습니다.

+  std::vector<GURL> manifest_urls;
   for (const auto& value : link_header_util::SplitLinkHeader(link_header)) {
     ...
     if (std::ranges::contains(rel_parts, "payment-method-manifest")) {
-      GURL payment_method_manifest_url = final_url.Resolve(*link_url);
-      // ... 검증 후 InitiateDownload(...); return;
+      manifest_urls.push_back(final_url.Resolve(*link_url));
     }
   }
+
+  if (manifest_urls.empty()) { /* 기존 kNoLinkHeader 에러 */ }
+
+  if (manifest_urls.size() > 1) {
+    // "If manifest_links's size is not 1, then return failure."
+    // https://w3c.github.io/payment-method-manifest/#fetch-pmm
+    RespondWithError(errors::kMultipleLinkHeaders, final_url, *log_,
+                     std::move(download->callback));
+    return;
+  }
  1. 개수는 rel 기준으로 셉니다. URL 유효성 검사(HTTPS 여부, same-origin)는 스펙 순서대로 링크가 하나로 확정된 뒤에만 수행하므로, 유효하지 않은 URL이 섞여 있어도 "여러 개"라는 사실이 먼저 보고됩니다.
  2. 새 에러 문자열 errors::kMultipleLinkHeadersnative_error_strings에 기존 kNoLinkHeader 옆에 추가했습니다. 두 에러 모두 RespondWithError()$1을 URL로 치환하는 같은 경로를 씁니다.
  3. 링크가 정확히 하나일 때의 동작(URL 검증 → same-origin 검사 → InitiateDownload)은 들여쓰기만 바뀌고 그대로입니다.
  4. 낡은 WPT baseline을 삭제했습니다. 유일한 [FAIL] 항목이 바로 이 버그였으므로, 수정 후에는 테스트 전체가 통과해 baseline이 오히려 불일치를 만듭니다.

테스트 방법

TDD로 진행했고, 이번에는 "빨강"을 로그로 명확히 남겼습니다.

  1. 회귀 테스트 3개를 먼저 작성했습니다.
    • MultipleManifestLinksAreFailure: 매니페스트 링크 2개 → 새 에러 메시지 기대
    • MultipleManifestLinksWithDifferentCaseAreFailure: rel=Payment-Method-Manifest + rel=payment-method-manifest → 대소문자만 다른 중복도 2개로 셈 (직전 CL 8336867과 연결되는 케이스)
    • ManifestLinkAmongOtherLinks: stylesheet, next 사이에 매니페스트 링크 1개 → 정상 진행
  2. 수정 전 실행: 앞의 두 개가 FAILED(첫 링크로 다운로드가 시작되어 에러 콜백이 오지 않음), 세 번째는 통과. 버그 재현과 회귀 방지 테스트가 각각 제 역할을 한다는 것을 확인했습니다.
  3. 수정 후: 새 테스트 3개 통과, *ManifestDownloader*:*PaymentManifestParser* 필터 140/140 통과.

빌드 전략 메모

직전 CL이 16시간 풀빌드를 유발한 경험(브랜치 base가 다르면 mtime이 바뀌어 5만 스텝 재빌드)이 있어, 이번에는 브랜치를 빌드 트리와 같은 옛 base에서 따고 직전에 머지된 8336867의 파일 두 개만 git checkout <sha> -- <files>로 가져와 TEMP 커밋으로 얹었습니다. 업로드 직전 git rebase origin/main을 하자 그 TEMP 커밋은 patch contents already upstream으로 자동 drop되어 단일 커밋만 남았습니다. 덕분에 증분 빌드 1분으로 검증을 마쳤습니다.

또 하나, 테스트 바이너리를 에이전트 셸에서 직접 실행하면 Check failed: !IsProcessBackgrounded()로 전부 크래시합니다. tmux 세션에서 실행해야 정상 동작합니다.

배운 점

  • 스펙의 알고리즘 순서가 곧 코드 구조입니다. "모두 추출 → 개수 검사 → 하나를 따라감"이라는 문장을 그대로 옮기니 조기 return이 사라지고 함수가 오히려 읽기 쉬워졌습니다.
  • WPT baseline은 미해결 버그 목록이기도 합니다. -expected.txt에 남은 [FAIL] 항목은 "아직 안 고친 것"의 기록이므로, 다음 이슈를 찾는 단서로도 쓸 수 있습니다. 이번 CL의 근거 자체가 그 baseline이었습니다.
  • 인접 이슈는 별도 CL로 쪼개되 순서를 지키면 이득입니다. 8336867에서 같은 함수를 이미 다뤄봤기 때문에 코드·테스트 픽스처·리뷰 관례를 그대로 재사용할 수 있었고, 먼저 머지된 덕분에 충돌도 없었습니다.

참고 자료