[payments] Fail manifest download on multiple manifest Link headers
IN REVIEW2026components/paymentspayment-method-manifestwpt
2026. 9. 4.
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.txtbaseline에 "알려진 실패"로 체크인되어 있었습니다.
해결 내용
루프의 책임을 "수집"으로 좁히고, 개수 판정을 루프 밖으로 꺼냈습니다.
+ 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;
+ }- 개수는 rel 기준으로 셉니다. URL 유효성 검사(HTTPS 여부, same-origin)는 스펙 순서대로 링크가 하나로 확정된 뒤에만 수행하므로, 유효하지 않은 URL이 섞여 있어도 "여러 개"라는 사실이 먼저 보고됩니다.
- 새 에러 문자열
errors::kMultipleLinkHeaders를native_error_strings에 기존kNoLinkHeader옆에 추가했습니다. 두 에러 모두RespondWithError()가$1을 URL로 치환하는 같은 경로를 씁니다. - 링크가 정확히 하나일 때의 동작(URL 검증 → same-origin 검사 →
InitiateDownload)은 들여쓰기만 바뀌고 그대로입니다. - 낡은 WPT baseline을 삭제했습니다. 유일한
[FAIL]항목이 바로 이 버그였으므로, 수정 후에는 테스트 전체가 통과해 baseline이 오히려 불일치를 만듭니다.
테스트 방법
TDD로 진행했고, 이번에는 "빨강"을 로그로 명확히 남겼습니다.
- 회귀 테스트 3개를 먼저 작성했습니다.
MultipleManifestLinksAreFailure: 매니페스트 링크 2개 → 새 에러 메시지 기대MultipleManifestLinksWithDifferentCaseAreFailure:rel=Payment-Method-Manifest+rel=payment-method-manifest→ 대소문자만 다른 중복도 2개로 셈 (직전 CL 8336867과 연결되는 케이스)ManifestLinkAmongOtherLinks:stylesheet,next사이에 매니페스트 링크 1개 → 정상 진행
- 수정 전 실행: 앞의 두 개가 FAILED(첫 링크로 다운로드가 시작되어 에러 콜백이 오지 않음), 세 번째는 통과. 버그 재현과 회귀 방지 테스트가 각각 제 역할을 한다는 것을 확인했습니다.
- 수정 후: 새 테스트 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에서 같은 함수를 이미 다뤄봤기 때문에 코드·테스트 픽스처·리뷰 관례를 그대로 재사용할 수 있었고, 먼저 머지된 덕분에 충돌도 없었습니다.