@@ -100,6 +100,13 @@ class URL:
100
100
url = httpx.URL("http://xn--fiqs8s.icom.museum")
101
101
assert url.raw_host == b"xn--fiqs8s.icom.museum"
102
102
103
+ * `url.port` is either None or an integer. URLs that include the default port for
104
+ "http", "https", "ws", "wss", and "ftp" schemes have their port normalized to `None`.
105
+
106
+ assert httpx.URL("http://example.com") == httpx.URL("http://example.com:80")
107
+ assert httpx.URL("http://example.com").port is None
108
+ assert httpx.URL("http://example.com:80").port is None
109
+
103
110
* `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work with
104
111
`url.username` and `url.password` instead, which handle the URL escaping.
105
112
@@ -144,6 +151,24 @@ def __init__(
144
151
f"Invalid type for url. Expected str or httpx.URL, got { type (url )} : { url !r} "
145
152
)
146
153
154
+ # Perform port normalization, following the WHATWG spec for default ports.
155
+ #
156
+ # See:
157
+ # * https://tools.ietf.org/html/rfc3986#section-3.2.3
158
+ # * https://url.spec.whatwg.org/#url-miscellaneous
159
+ # * https://url.spec.whatwg.org/#scheme-state
160
+ default_port = {
161
+ "ftp" : ":21" ,
162
+ "http" : ":80" ,
163
+ "https" : ":443" ,
164
+ "ws" : ":80" ,
165
+ "wss" : ":443" ,
166
+ }.get (self ._uri_reference .scheme , "" )
167
+ authority = self ._uri_reference .authority or ""
168
+ if default_port and authority .endswith (default_port ):
169
+ authority = authority [: - len (default_port )]
170
+ self ._uri_reference = self ._uri_reference .copy_with (authority = authority )
171
+
147
172
if kwargs :
148
173
self ._uri_reference = self .copy_with (** kwargs )._uri_reference
149
174
@@ -253,6 +278,15 @@ def raw_host(self) -> bytes:
253
278
def port (self ) -> typing .Optional [int ]:
254
279
"""
255
280
The URL port as an integer.
281
+
282
+ Note that the URL class performs port normalization as per the WHATWG spec.
283
+ Default ports for "http", "https", "ws", "wss", and "ftp" schemes are always
284
+ treated as `None`.
285
+
286
+ For example:
287
+
288
+ assert httpx.URL("http://www.example.com") == httpx.URL("http://www.example.com:80")
289
+ assert httpx.URL("http://www.example.com:80").port is None
256
290
"""
257
291
port = self ._uri_reference .port
258
292
return int (port ) if port else None
@@ -263,13 +297,8 @@ def netloc(self) -> bytes:
263
297
Either `<host>` or `<host>:<port>` as bytes.
264
298
Always normalized to lowercase, and IDNA encoded.
265
299
266
- The port component is not included if it is the default for an
267
- "http://" or "https://" URL.
268
-
269
300
This property may be used for generating the value of a request
270
301
"Host" header.
271
-
272
- See: https://tools.ietf.org/html/rfc3986#section-3.2.3
273
302
"""
274
303
host = self ._uri_reference .host or ""
275
304
port = self ._uri_reference .port
@@ -547,7 +576,7 @@ def __hash__(self) -> int:
547
576
return hash (str (self ))
548
577
549
578
def __eq__ (self , other : typing .Any ) -> bool :
550
- return isinstance (other , (URL , str )) and str (self ) == str (other )
579
+ return isinstance (other , (URL , str )) and str (self ) == str (URL ( other ) )
551
580
552
581
def __str__ (self ) -> str :
553
582
return self ._uri_reference .unsplit ()
@@ -1099,11 +1128,7 @@ def _prepare(self, default_headers: typing.Dict[str, str]) -> None:
1099
1128
)
1100
1129
1101
1130
if not has_host and self .url .host :
1102
- default_port = {"http" : b":80" , "https" : b":443" }.get (self .url .scheme , b"" )
1103
- host_header = self .url .netloc
1104
- if host_header .endswith (default_port ):
1105
- host_header = host_header [: - len (default_port )]
1106
- auto_headers .append ((b"Host" , host_header ))
1131
+ auto_headers .append ((b"Host" , self .url .netloc ))
1107
1132
if not has_content_length and self .method in ("POST" , "PUT" , "PATCH" ):
1108
1133
auto_headers .append ((b"Content-Length" , b"0" ))
1109
1134
0 commit comments