Found while vendoring the complete AdCP 3.1.1 request-signing vector set (#975). Reproduction is a canonicalization.json case, currently strict-xfail.
Problem
A URL with a trailing ? and an empty query loses the ? during canonicalization.
| case |
input |
expected @target-uri |
actual |
trailing-empty-query-preserved |
https://seller.example.com/p? |
https://seller.example.com/p? |
https://seller.example.com/p |
Cause
canonicalize_target_uri() round-trips through urlsplit / urlunsplit:
return urlunsplit((scheme, netloc, path, parts.query, ""))
urlsplit maps both /p? and /p to query == "", and urlunsplit emits no ? for an empty query. The distinction between "empty query" and "no query" is erased, so the two URLs canonicalize identically.
Why it matters
RFC 9421 §2.2.2 derives @target-uri from the absolute request target, where /p? and /p are distinct. A counterparty that preserves the trailing ? computes a different signature base than the SDK, and the signature fails to verify with no diagnostic beyond request_signature_invalid — the failure mode canonicalization.json exists to catch early.
Narrow, but it is a real divergence and the fix is contained: preserve the ? when the original URL had one, e.g. by checking for "?" in url after the authority rather than relying on urlunsplit to round-trip it.
Found while vendoring the complete AdCP 3.1.1 request-signing vector set (#975). Reproduction is a
canonicalization.jsoncase, currently strict-xfail.Problem
A URL with a trailing
?and an empty query loses the?during canonicalization.@target-uritrailing-empty-query-preservedhttps://seller.example.com/p?https://seller.example.com/p?https://seller.example.com/pCause
canonicalize_target_uri()round-trips throughurlsplit/urlunsplit:urlsplitmaps both/p?and/ptoquery == "", andurlunsplitemits no?for an empty query. The distinction between "empty query" and "no query" is erased, so the two URLs canonicalize identically.Why it matters
RFC 9421 §2.2.2 derives
@target-urifrom the absolute request target, where/p?and/pare distinct. A counterparty that preserves the trailing?computes a different signature base than the SDK, and the signature fails to verify with no diagnostic beyondrequest_signature_invalid— the failure modecanonicalization.jsonexists to catch early.Narrow, but it is a real divergence and the fix is contained: preserve the
?when the original URL had one, e.g. by checking for"?" in urlafter the authority rather than relying onurlunsplitto round-trip it.