Problem/Motivation
598e739208de28182f3329a2c23511f5c27489e5 introduced drupal_http_build_query(). For some reason, the choice was made to unencode slashes when encoding parameters:
// For better readability of paths in query strings, we decode slashes.
// @see drupal_encode_path()
$params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
This behavior carried forward into Drupal 8's UrlHelper::buildQuery().
While RFC 3986 actually addresses the possibility of leaving "slash" / unencoded (see RFC 3986 section 3.4 'Query', https://www.ietf.org/rfc/rfc3986.txt), PHP itself does natively encode it. So do the equivalent functions in a sampling of other languages:
PHP:
php > print urlencode('foo=bar/baz&stuff=things');
foo%3Dbar%2Fbaz%26stuff%3Dthings
Ruby:
irb(main):005:0> CGI.escape('foo=bar/baz&stuff=things')
=> "foo%3Dbar%2Fbaz%26stuff%3Dthings"
Python:
>>> import urllib
>>> params = {'foo': 'bar/baz', 'stuff': 'things'}
>>> urllib.urlencode(params)
'foo=bar%2Fbaz&stuff=things'
Javascript:
> encodeURIComponent('foo=bar/baz&stuff=things')
'foo%3Dbar%2Fbaz%26stuff%3Dthings'
I am sure there are counter-examples, but encoding the slash is pretty standard behavior.
Under many circumstances, Drupal's behavior here is strictly cosmetic, and possibly preferable for visibility, as the comment states. However, there are cases where there is a shared understanding of what urlencoding does is required, and that shared understanding should be respected.
For example, query parameters can be used to construct signed signatures via a shared secret to ensure that incoming requests to an API are from a trusted source. If one of those parameters contains a URL generated by Drupal, the parameter value will differ from what might be expected by another application. This would also affect the generated signature, and cause a mismatch when checking the signature.
Proposed resolution
Remove the substitution:
// For better readability of paths in query strings, we decode slashes.
$params[] = $key . '=' . rawurlencode($value);
Alternately I could see it as an additional option to class Url's options argument, to catch cases where users are explicitly relying on the slash being present unencoded in a parameter. But, that should be discussed by people that know core better than me.