Description
URL.join() decodes percent-encoded characters in the base URL's path when merging a relative reference (RFC 3986 §5.3, the branch used when the base path does not end with /). The merge is built from self.parts, which are already percent-decoded, so an encoded delimiter like %2F is turned into a real / and the path structure is corrupted.
Reproduction
from yarl import URL
print(URL("http://x/a%2Fb/c").join(URL("d"))) # http://x/a/b/d
print(URL("http://x/a%20b/c").join(URL("d"))) # http://x/a b/d
Expected vs actual
|
result |
| expected |
http://x/a%2Fb/d (the a%2Fb segment is preserved) |
| actual |
http://x/a/b/d (%2F decoded into a separator -> the single segment splits into two) |
%20 is likewise decoded into a literal space.
Root cause
The RFC-3986 merge branch in URL.join constructs the merged path from self.parts[:-1]. parts runs the segments through UNQUOTER (decoded), so feeding them back into a path mixes decoded text with the still-encoded relative path. The raw/encoded segments (raw_parts) should be used for the merge so the original encoding round-trips.
(Distinct from #896, which is RFC-correct last-segment replacement; this is specifically the loss of percent-encoding.)
Description
URL.join()decodes percent-encoded characters in the base URL's path when merging a relative reference (RFC 3986 §5.3, the branch used when the base path does not end with/). The merge is built fromself.parts, which are already percent-decoded, so an encoded delimiter like%2Fis turned into a real/and the path structure is corrupted.Reproduction
Expected vs actual
http://x/a%2Fb/d(thea%2Fbsegment is preserved)http://x/a/b/d(%2Fdecoded into a separator -> the single segment splits into two)%20is likewise decoded into a literal space.Root cause
The RFC-3986 merge branch in
URL.joinconstructs the merged path fromself.parts[:-1].partsruns the segments throughUNQUOTER(decoded), so feeding them back into a path mixes decoded text with the still-encoded relative path. The raw/encoded segments (raw_parts) should be used for the merge so the original encoding round-trips.(Distinct from #896, which is RFC-correct last-segment replacement; this is specifically the loss of percent-encoding.)