diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 32fd80e7..ec7ea763 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,6 +4,10 @@ updates:
directory: "/"
schedule:
interval: "monthly"
+ groups:
+ python-packages:
+ patterns:
+ - "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml
index 0bb570ce..ad7309d7 100644
--- a/.github/workflows/test-suite.yml
+++ b/.github/workflows/test-suite.yml
@@ -5,7 +5,7 @@ on:
push:
branches: ["master"]
pull_request:
- branches: ["master"]
+ branches: ["master", 'version*']
jobs:
tests:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47ac88c8..f3aba3cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
-## Unreleased
+## 0.27.2 (27th August, 2024)
+
+### Fixed
+
+* Reintroduced supposedly-private `URLTypes` shortcut. (#2673)
+
+## 0.27.1 (27th August, 2024)
+
+### Added
+
+* Support for `zstd` content decoding using the python `zstandard` package is added. Installable using `httpx[zstd]`. (#3139)
+
+### Fixed
+
+* Improved error messaging for `InvalidURL` exceptions. (#3250)
+* Fix `app` type signature in `ASGITransport`. (#3109)
+
+## 0.27.0 (21st February, 2024)
+
+### Deprecated
+
+* The `app=...` shortcut has been deprecated. Use the explicit style of `transport=httpx.WSGITransport()` or `transport=httpx.ASGITransport()` instead.
### Fixed
@@ -88,7 +109,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* The logging behaviour has been changed to be more in-line with other standard Python logging usages. We no longer have a custom `TRACE` log level, and we no longer use the `HTTPX_LOG_LEVEL` environment variable to auto-configure logging. We now have a significant amount of `DEBUG` logging available at the network level. Full documentation is available at https://www.python-httpx.org/logging/ (#2547, encode/httpcore#648)
* The `Response.iter_lines()` method now matches the stdlib behaviour and does not include the newline characters. It also resolves a performance issue. (#2423)
* Query parameter encoding switches from using + for spaces and %2F for forward slash, to instead using %20 for spaces and treating forward slash as a safe, unescaped character. This differs from `requests`, but is in line with browser behavior in Chrome, Safari, and Firefox. Both options are RFC valid. (#2543)
-* NetRC authentication is no longer automatically handled, but is instead supported by an explicit `httpx.NetRCAuth()` authentication class. See the documentation at https://www.python-httpx.org/advanced/#netrc-support (#2525)
+* NetRC authentication is no longer automatically handled, but is instead supported by an explicit `httpx.NetRCAuth()` authentication class. See the documentation at https://www.python-httpx.org/advanced/authentication/#netrc-authentication (#2525)
### Removed
@@ -141,7 +162,7 @@ See the "Removed" section of these release notes for details.
### Changed
* Drop support for Python 3.6. (#2097)
-* Use `utf-8` as the default character set, instead of falling back to `charset-normalizer` for auto-detection. To enable automatic character set detection, see [the documentation](https://www.python-httpx.org/advanced/#character-set-encodings-and-auto-detection). (#2165)
+* Use `utf-8` as the default character set, instead of falling back to `charset-normalizer` for auto-detection. To enable automatic character set detection, see [the documentation](https://www.python-httpx.org/advanced/text-encodings/#using-auto-detection). (#2165)
### Fixed
@@ -160,7 +181,7 @@ See the "Removed" section of these release notes for details.
### Added
-* Support for [the SOCKS5 proxy protocol](https://www.python-httpx.org/advanced/#socks) via [the `socksio` package](https://github.com/sethmlarson/socksio). (#2034)
+* Support for [the SOCKS5 proxy protocol](https://www.python-httpx.org/advanced/proxies/#socks) via [the `socksio` package](https://github.com/sethmlarson/socksio). (#2034)
* Support for custom headers in multipart/form-data requests (#1936)
### Fixed
@@ -315,7 +336,7 @@ finally:
The 0.18.x release series formalises our low-level Transport API, introducing the base classes `httpx.BaseTransport` and `httpx.AsyncBaseTransport`.
-See the "[Writing custom transports](https://www.python-httpx.org/advanced/#writing-custom-transports)" documentation and the [`httpx.BaseTransport.handle_request()`](https://github.com/encode/httpx/blob/397aad98fdc8b7580a5fc3e88f1578b4302c6382/httpx/_transports/base.py#L77-L147) docstring for more complete details on implementing custom transports.
+See the "[Custom transports](https://www.python-httpx.org/advanced/transports/#custom-transports)" documentation and the [`httpx.BaseTransport.handle_request()`](https://github.com/encode/httpx/blob/397aad98fdc8b7580a5fc3e88f1578b4302c6382/httpx/_transports/base.py#L77-L147) docstring for more complete details on implementing custom transports.
Pull request #1522 includes a checklist of differences from the previous `httpcore` transport API, for developers implementing custom transports.
@@ -632,7 +653,7 @@ This release switches to `httpcore` for all the internal networking, which means
It also means we've had to remove our UDS support, since maintaining that would have meant having to push back our work towards a 1.0 release, which isn't a trade-off we wanted to make.
-We also now have [a public "Transport API"](https://www.python-httpx.org/advanced/#custom-transports), which you can use to implement custom transport implementations against. This formalises and replaces our previously private "Dispatch API".
+We also now have [a public "Transport API"](https://www.python-httpx.org/advanced/transports/#custom-transports), which you can use to implement custom transport implementations against. This formalises and replaces our previously private "Dispatch API".
### Changed
diff --git a/README.md b/README.md
index f3238242..15d52d45 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ HTTPX builds on the well-established usability of `requests`, and gives you:
* An integrated command-line client.
* HTTP/1.1 [and HTTP/2 support](https://www.python-httpx.org/http2/).
* Standard synchronous interface, but with [async support if you need it](https://www.python-httpx.org/async/).
-* Ability to make requests directly to [WSGI applications](https://www.python-httpx.org/advanced/#calling-into-python-web-apps) or [ASGI applications](https://www.python-httpx.org/async/#calling-into-python-web-apps).
+* Ability to make requests directly to [WSGI applications](https://www.python-httpx.org/advanced/transports/#wsgi-transport) or [ASGI applications](https://www.python-httpx.org/advanced/transports/#asgi-transport).
* Strict timeouts everywhere.
* Fully type annotated.
* 100% test coverage.
@@ -141,6 +141,7 @@ As well as these optional installs:
* `rich` - Rich terminal support. *(Optional, with `httpx['cli']`)*
* `click` - Command line client support. *(Optional, with `httpx['cli']`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx['brotli']`)*
+* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design
diff --git a/README_chinese.md b/README_chinese.md
deleted file mode 100644
index ad20c5a1..00000000
--- a/README_chinese.md
+++ /dev/null
@@ -1,144 +0,0 @@
-
-
-
-
-HTTPX - 适用于 Python 的下一代 HTTP 客户端
-
-
-
-
-
-
-
-
-
-
-HTTPX 是适用于 Python3 的功能齐全的 HTTP 客户端。 它集成了 **一个命令行客户端**,同时支持 **HTTP/1.1 和 HTTP/2**,并提供了 **同步和异步 API**。
-
----
-
-通过 pip 安装 HTTPX:
-
-```shell
-$ pip install httpx
-```
-
-使用 httpx:
-
-```pycon
->>> import httpx
->>> r = httpx.get('https://www.example.org/')
->>> r
-
->>> r.status_code
-200
->>> r.headers['content-type']
-'text/html; charset=UTF-8'
->>> r.text
-'\n\n\nExample Domain ...'
-```
-
-或者使用命令行客户端。
-
-```shell
-$ pip install 'httpx[cli]' # 命令行功能是可选的。
-```
-
-它允许我们直接通过命令行来使用 HTTPX...
-
-
-
-
-
-发送一个请求...
-
-
-
-
-
-## 特性
-
-HTTPX 建立在成熟的 requests 可用性基础上,为您提供以下功能:
-
-* 广泛的 [requests 兼容 API](https://www.python-httpx.org/compatibility/)。
-* 内置的命令行客户端功能。
-* HTTP/1.1 [和 HTTP/2 支持](https://www.python-httpx.org/http2/)。
-* 标准同步接口,也支持 [异步](https://www.python-httpx.org/async/)。
-* 能够直接向 [WSGI 应用发送请求](https://www.python-httpx.org/advanced/#calling-into-python-web-apps) 或向 [ASGI 应用发送请求](https://www.python-httpx.org/async/#calling-into-python-web-apps)。
-* 每一处严格的超时控制。
-* 完整的类型注解。
-* 100% 测试。
-
-加上这些应该具备的标准功能...
-
-* 国际化域名与 URL
-* Keep-Alive & 连接池
-* Cookie 持久性会话
-* 浏览器风格的 SSL 验证
-* 基础或摘要身份验证
-* 优雅的键值 Cookies
-* 自动解压缩
-* 内容自动解码
-* Unicode 响应正文
-* 分段文件上传
-* HTTP(S)代理支持
-* 可配置的连接超时
-* 流式下载
-* .netrc 支持
-* 分块请求
-
-## 安装
-
-使用 pip 安装:
-
-```shell
-$ pip install httpx
-```
-
-或者,安装可选的 HTTP/2 支持:
-
-```shell
-$ pip install httpx[http2]
-```
-
-HTTPX 要求 Python 3.8+ 版本。
-
-## 文档
-
-项目文档现已就绪,请访问 [https://www.python-httpx.org/](https://www.python-httpx.org/) 来阅读。
-
-要浏览所有基础知识,请访问 [快速开始](https://www.python-httpx.org/quickstart/)。
-
-更高级的主题,可参阅 [高级用法](https://www.python-httpx.org/advanced/) 章节, [异步支持](https://www.python-httpx.org/async/) 或者 [HTTP/2](https://www.python-httpx.org/http2/) 章节。
-
-[Developer Interface](https://www.python-httpx.org/api/) 提供了全面的 API 参考。
-
-要了解与 HTTPX 集成的工具, 请访问 [第三方包](https://www.python-httpx.org/third_party_packages/)。
-
-## 贡献
-
-如果您想对本项目做出贡献,请访问 [贡献者指南](https://www.python-httpx.org/contributing/) 来了解如何开始。
-
-## 依赖
-
-HTTPX 项目依赖于这些优秀的库:
-
-* `httpcore` - `httpx` 基础传输接口实现。
- * `h11` - HTTP/1.1 支持。
-* `certifi` - SSL 证书。
-* `idna` - 国际化域名支持。
-* `sniffio` - 异步库自动检测。
-
-以及这些可选的安装:
-
-* `h2` - HTTP/2 支持。 *(可选的,通过 `httpx[http2]`)*
-* `socksio` - SOCKS 代理支持。 *(可选的, 通过 `httpx[socks]`)*
-* `rich` - 丰富的终端支持。 *(可选的,通过 `httpx[cli]`)*
-* `click` - 命令行客户端支持。 *(可选的,通过 `httpx[cli]`)*
-* `brotli` 或者 `brotlicffi` - 对 “brotli” 压缩响应的解码。*(可选的,通过 `httpx[brotli]`)*
-
-这项工作的大量功劳都归功于参考了 `requests` 所遵循的 API 结构,以及 `urllib3` 中众多围绕底层网络细节的设计灵感。
-
----
-
-HTTPX 使用 BSD 开源协议 code。 精心设计和制作。 — 🦋 —
diff --git a/docs/advanced.md b/docs/advanced.md
deleted file mode 100644
index bb003a1a..00000000
--- a/docs/advanced.md
+++ /dev/null
@@ -1,1296 +0,0 @@
-# Advanced Usage
-
-## Client Instances
-
-!!! hint
- If you are coming from Requests, `httpx.Client()` is what you can use instead of `requests.Session()`.
-
-### Why use a Client?
-
-!!! note "TL;DR"
- If you do anything more than experimentation, one-off scripts, or prototypes, then you should use a `Client` instance.
-
-#### More efficient usage of network resources
-
-When you make requests using the top-level API as documented in the [Quickstart](quickstart.md) guide, HTTPX has to establish a new connection _for every single request_ (connections are not reused). As the number of requests to a host increases, this quickly becomes inefficient.
-
-On the other hand, a `Client` instance uses [HTTP connection pooling](https://en.wikipedia.org/wiki/HTTP_persistent_connection). This means that when you make several requests to the same host, the `Client` will reuse the underlying TCP connection, instead of recreating one for every single request.
-
-This can bring **significant performance improvements** compared to using the top-level API, including:
-
-- Reduced latency across requests (no handshaking).
-- Reduced CPU usage and round-trips.
-- Reduced network congestion.
-
-#### Extra features
-
-`Client` instances also support features that aren't available at the top-level API, such as:
-
-- Cookie persistence across requests.
-- Applying configuration across all outgoing requests.
-- Sending requests through HTTP proxies.
-- Using [HTTP/2](http2.md).
-
-The other sections on this page go into further detail about what you can do with a `Client` instance.
-
-### Usage
-
-The recommended way to use a `Client` is as a context manager. This will ensure that connections are properly cleaned up when leaving the `with` block:
-
-```python
-with httpx.Client() as client:
- ...
-```
-
-Alternatively, you can explicitly close the connection pool without block-usage using `.close()`:
-
-```python
-client = httpx.Client()
-try:
- ...
-finally:
- client.close()
-```
-
-### Making requests
-
-Once you have a `Client`, you can send requests using `.get()`, `.post()`, etc. For example:
-
-```pycon
->>> with httpx.Client() as client:
-... r = client.get('https://example.com')
-...
->>> r
-
-```
-
-These methods accept the same arguments as `httpx.get()`, `httpx.post()`, etc. This means that all features documented in the [Quickstart](quickstart.md) guide are also available at the client level.
-
-For example, to send a request with custom headers:
-
-```pycon
->>> with httpx.Client() as client:
-... headers = {'X-Custom': 'value'}
-... r = client.get('https://example.com', headers=headers)
-...
->>> r.request.headers['X-Custom']
-'value'
-```
-
-### Sharing configuration across requests
-
-Clients allow you to apply configuration to all outgoing requests by passing parameters to the `Client` constructor.
-
-For example, to apply a set of custom headers _on every request_:
-
-```pycon
->>> url = 'http://httpbin.org/headers'
->>> headers = {'user-agent': 'my-app/0.0.1'}
->>> with httpx.Client(headers=headers) as client:
-... r = client.get(url)
-...
->>> r.json()['headers']['User-Agent']
-'my-app/0.0.1'
-```
-
-### Merging of configuration
-
-When a configuration option is provided at both the client-level and request-level, one of two things can happen:
-
-- For headers, query parameters and cookies, the values are combined together. For example:
-
-```pycon
->>> headers = {'X-Auth': 'from-client'}
->>> params = {'client_id': 'client1'}
->>> with httpx.Client(headers=headers, params=params) as client:
-... headers = {'X-Custom': 'from-request'}
-... params = {'request_id': 'request1'}
-... r = client.get('https://example.com', headers=headers, params=params)
-...
->>> r.request.url
-URL('https://example.com?client_id=client1&request_id=request1')
->>> r.request.headers['X-Auth']
-'from-client'
->>> r.request.headers['X-Custom']
-'from-request'
-```
-
-- For all other parameters, the request-level value takes priority. For example:
-
-```pycon
->>> with httpx.Client(auth=('tom', 'mot123')) as client:
-... r = client.get('https://example.com', auth=('alice', 'ecila123'))
-...
->>> _, _, auth = r.request.headers['Authorization'].partition(' ')
->>> import base64
->>> base64.b64decode(auth)
-b'alice:ecila123'
-```
-
-If you need finer-grained control on the merging of client-level and request-level parameters, see [Request instances](#request-instances).
-
-### Other Client-only configuration options
-
-Additionally, `Client` accepts some configuration options that aren't available at the request level.
-
-For example, `base_url` allows you to prepend an URL to all outgoing requests:
-
-```pycon
->>> with httpx.Client(base_url='http://httpbin.org') as client:
-... r = client.get('/headers')
-...
->>> r.request.url
-URL('http://httpbin.org/headers')
-```
-
-For a list of all available client parameters, see the [`Client`](api.md#client) API reference.
-
----
-
-## Character set encodings and auto-detection
-
-When accessing `response.text`, we need to decode the response bytes into a unicode text representation.
-
-By default `httpx` will use `"charset"` information included in the response `Content-Type` header to determine how the response bytes should be decoded into text.
-
-In cases where no charset information is included on the response, the default behaviour is to assume "utf-8" encoding, which is by far the most widely used text encoding on the internet.
-
-### Using the default encoding
-
-To understand this better let's start by looking at the default behaviour for text decoding...
-
-```python
-import httpx
-# Instantiate a client with the default configuration.
-client = httpx.Client()
-# Using the client...
-response = client.get(...)
-print(response.encoding) # This will either print the charset given in
- # the Content-Type charset, or else "utf-8".
-print(response.text) # The text will either be decoded with the Content-Type
- # charset, or using "utf-8".
-```
-
-This is normally absolutely fine. Most servers will respond with a properly formatted Content-Type header, including a charset encoding. And in most cases where no charset encoding is included, UTF-8 is very likely to be used, since it is so widely adopted.
-
-### Using an explicit encoding
-
-In some cases we might be making requests to a site where no character set information is being set explicitly by the server, but we know what the encoding is. In this case it's best to set the default encoding explicitly on the client.
-
-```python
-import httpx
-# Instantiate a client with a Japanese character set as the default encoding.
-client = httpx.Client(default_encoding="shift-jis")
-# Using the client...
-response = client.get(...)
-print(response.encoding) # This will either print the charset given in
- # the Content-Type charset, or else "shift-jis".
-print(response.text) # The text will either be decoded with the Content-Type
- # charset, or using "shift-jis".
-```
-
-### Using character set auto-detection
-
-In cases where the server is not reliably including character set information, and where we don't know what encoding is being used, we can enable auto-detection to make a best-guess attempt when decoding from bytes to text.
-
-To use auto-detection you need to set the `default_encoding` argument to a callable instead of a string. This callable should be a function which takes the input bytes as an argument and returns the character set to use for decoding those bytes to text.
-
-There are two widely used Python packages which both handle this functionality:
-
-* [`chardet`](https://chardet.readthedocs.io/) - This is a well established package, and is a port of [the auto-detection code in Mozilla](https://www-archive.mozilla.org/projects/intl/chardet.html).
-* [`charset-normalizer`](https://charset-normalizer.readthedocs.io/) - A newer package, motivated by `chardet`, with a different approach.
-
-Let's take a look at installing autodetection using one of these packages...
-
- ```shell
-$ pip install httpx
-$ pip install chardet
- ```
-
-Once `chardet` is installed, we can configure a client to use character-set autodetection.
-
-```python
-import httpx
-import chardet
-
-def autodetect(content):
- return chardet.detect(content).get("encoding")
-
-# Using a client with character-set autodetection enabled.
-client = httpx.Client(default_encoding=autodetect)
-response = client.get(...)
-print(response.encoding) # This will either print the charset given in
- # the Content-Type charset, or else the auto-detected
- # character set.
-print(response.text)
-```
-
----
-
-## Calling into Python Web Apps
-
-You can configure an `httpx` client to call directly into a Python web application using the WSGI protocol.
-
-This is particularly useful for two main use-cases:
-
-* Using `httpx` as a client inside test cases.
-* Mocking out external services during tests or in dev/staging environments.
-
-Here's an example of integrating against a Flask application:
-
-```python
-from flask import Flask
-import httpx
-
-
-app = Flask(__name__)
-
-@app.route("/")
-def hello():
- return "Hello World!"
-
-with httpx.Client(app=app, base_url="http://testserver") as client:
- r = client.get("/")
- assert r.status_code == 200
- assert r.text == "Hello World!"
-```
-
-For some more complex cases you might need to customize the WSGI transport. This allows you to:
-
-* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
-* Mount the WSGI application at a subpath by setting `script_name` (WSGI).
-* Use a given client address for requests by setting `remote_addr` (WSGI).
-
-For example:
-
-```python
-# Instantiate a client that makes WSGI requests with a client IP of "1.2.3.4".
-transport = httpx.WSGITransport(app=app, remote_addr="1.2.3.4")
-with httpx.Client(transport=transport, base_url="http://testserver") as client:
- ...
-```
-
-## Request instances
-
-For maximum control on what gets sent over the wire, HTTPX supports building explicit [`Request`](api.md#request) instances:
-
-```python
-request = httpx.Request("GET", "https://example.com")
-```
-
-To dispatch a `Request` instance across to the network, create a [`Client` instance](#client-instances) and use `.send()`:
-
-```python
-with httpx.Client() as client:
- response = client.send(request)
- ...
-```
-
-If you need to mix client-level and request-level options in a way that is not supported by the default [Merging of parameters](#merging-of-parameters), you can use `.build_request()` and then make arbitrary modifications to the `Request` instance. For example:
-
-```python
-headers = {"X-Api-Key": "...", "X-Client-ID": "ABC123"}
-
-with httpx.Client(headers=headers) as client:
- request = client.build_request("GET", "https://api.example.com")
-
- print(request.headers["X-Client-ID"]) # "ABC123"
-
- # Don't send the API key for this particular request.
- del request.headers["X-Api-Key"]
-
- response = client.send(request)
- ...
-```
-
-## Event Hooks
-
-HTTPX allows you to register "event hooks" with the client, that are called
-every time a particular type of event takes place.
-
-There are currently two event hooks:
-
-* `request` - Called after a request is fully prepared, but before it is sent to the network. Passed the `request` instance.
-* `response` - Called after the response has been fetched from the network, but before it is returned to the caller. Passed the `response` instance.
-
-These allow you to install client-wide functionality such as logging, monitoring or tracing.
-
-```python
-def log_request(request):
- print(f"Request event hook: {request.method} {request.url} - Waiting for response")
-
-def log_response(response):
- request = response.request
- print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
-
-client = httpx.Client(event_hooks={'request': [log_request], 'response': [log_response]})
-```
-
-You can also use these hooks to install response processing code, such as this
-example, which creates a client instance that always raises `httpx.HTTPStatusError`
-on 4xx and 5xx responses.
-
-```python
-def raise_on_4xx_5xx(response):
- response.raise_for_status()
-
-client = httpx.Client(event_hooks={'response': [raise_on_4xx_5xx]})
-```
-
-!!! note
- Response event hooks are called before determining if the response body
- should be read or not.
-
- If you need access to the response body inside an event hook, you'll
- need to call `response.read()`, or for AsyncClients, `response.aread()`.
-
-The hooks are also allowed to modify `request` and `response` objects.
-
-```python
-def add_timestamp(request):
- request.headers['x-request-timestamp'] = datetime.now(tz=datetime.utc).isoformat()
-
-client = httpx.Client(event_hooks={'request': [add_timestamp]})
-```
-
-Event hooks must always be set as a **list of callables**, and you may register
-multiple event hooks for each type of event.
-
-As well as being able to set event hooks on instantiating the client, there
-is also an `.event_hooks` property, that allows you to inspect and modify
-the installed hooks.
-
-```python
-client = httpx.Client()
-client.event_hooks['request'] = [log_request]
-client.event_hooks['response'] = [log_response, raise_on_4xx_5xx]
-```
-
-!!! note
- If you are using HTTPX's async support, then you need to be aware that
- hooks registered with `httpx.AsyncClient` MUST be async functions,
- rather than plain functions.
-
-## Monitoring download progress
-
-If you need to monitor download progress of large responses, you can use response streaming and inspect the `response.num_bytes_downloaded` property.
-
-This interface is required for properly determining download progress, because the total number of bytes returned by `response.content` or `response.iter_content()` will not always correspond with the raw content length of the response if HTTP response compression is being used.
-
-For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library while a response is being downloaded could be done like this…
-
-```python
-import tempfile
-
-import httpx
-from tqdm import tqdm
-
-with tempfile.NamedTemporaryFile() as download_file:
- url = "https://speed.hetzner.de/100MB.bin"
- with httpx.stream("GET", url) as response:
- total = int(response.headers["Content-Length"])
-
- with tqdm(total=total, unit_scale=True, unit_divisor=1024, unit="B") as progress:
- num_bytes_downloaded = response.num_bytes_downloaded
- for chunk in response.iter_bytes():
- download_file.write(chunk)
- progress.update(response.num_bytes_downloaded - num_bytes_downloaded)
- num_bytes_downloaded = response.num_bytes_downloaded
-```
-
-
-
-Or an alternate example, this time using the [`rich`](https://github.com/willmcgugan/rich) library…
-
-```python
-import tempfile
-import httpx
-import rich.progress
-
-with tempfile.NamedTemporaryFile() as download_file:
- url = "https://speed.hetzner.de/100MB.bin"
- with httpx.stream("GET", url) as response:
- total = int(response.headers["Content-Length"])
-
- with rich.progress.Progress(
- "[progress.percentage]{task.percentage:>3.0f}%",
- rich.progress.BarColumn(bar_width=None),
- rich.progress.DownloadColumn(),
- rich.progress.TransferSpeedColumn(),
- ) as progress:
- download_task = progress.add_task("Download", total=total)
- for chunk in response.iter_bytes():
- download_file.write(chunk)
- progress.update(download_task, completed=response.num_bytes_downloaded)
-```
-
-
-
-## Monitoring upload progress
-
-If you need to monitor upload progress of large responses, you can use request content generator streaming.
-
-For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library.
-
-```python
-import io
-import random
-
-import httpx
-from tqdm import tqdm
-
-
-def gen():
- """
- this is a complete example with generated random bytes.
- you can replace `io.BytesIO` with real file object.
- """
- total = 32 * 1024 * 1024 # 32m
- with tqdm(ascii=True, unit_scale=True, unit='B', unit_divisor=1024, total=total) as bar:
- with io.BytesIO(random.randbytes(total)) as f:
- while data := f.read(1024):
- yield data
- bar.update(len(data))
-
-
-httpx.post("https://httpbin.org/post", content=gen())
-```
-
-
-
-## .netrc Support
-
-HTTPX can be configured to use [a `.netrc` config file](https://everything.curl.dev/usingcurl/netrc) for authentication.
-
-The `.netrc` config file allows authentication credentials to be associated with specified hosts. When a request is made to a host that is found in the netrc file, the username and password will be included using HTTP basic auth.
-
-Example `.netrc` file:
-
-```
-machine example.org
-login example-username
-password example-password
-
-machine python-httpx.org
-login other-username
-password other-password
-```
-
-Some examples of configuring `.netrc` authentication with `httpx`.
-
-Use the default `.netrc` file in the users home directory:
-
-```pycon
->>> auth = httpx.NetRCAuth()
->>> client = httpx.Client(auth=auth)
-```
-
-Use an explicit path to a `.netrc` file:
-
-```pycon
->>> auth = httpx.NetRCAuth(file="/path/to/.netrc")
->>> client = httpx.Client(auth=auth)
-```
-
-Use the `NETRC` environment variable to configure a path to the `.netrc` file,
-or fallback to the default.
-
-```pycon
->>> auth = httpx.NetRCAuth(file=os.environ.get("NETRC"))
->>> client = httpx.Client(auth=auth)
-```
-
-The `NetRCAuth()` class uses [the `netrc.netrc()` function from the Python standard library](https://docs.python.org/3/library/netrc.html). See the documentation there for more details on exceptions that may be raised if the netrc file is not found, or cannot be parsed.
-
-## HTTP Proxying
-
-HTTPX supports setting up [HTTP proxies](https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers) via the `proxy` parameter to be passed on client initialization or top-level API functions like `httpx.get(..., proxy=...)`.
-
-
-
-
Diagram of how a proxy works (source: Wikipedia). The left hand side "Internet" blob may be your HTTPX client requesting example.com through a proxy.
-
-
-### Example
-
-To route all traffic (HTTP and HTTPS) to a proxy located at `http://localhost:8030`, pass the proxy URL to the client...
-
-```python
-with httpx.Client(proxy="http://localhost:8030") as client:
- ...
-```
-
-For more advanced use cases, pass a mounts `dict`. For example, to route HTTP and HTTPS requests to 2 different proxies, respectively located at `http://localhost:8030`, and `http://localhost:8031`, pass a `dict` of proxy URLs:
-
-```python
-proxy_mounts = {
- "http://": httpx.HTTPTransport(proxy="http://localhost:8030"),
- "https://": httpx.HTTPTransport(proxy="http://localhost:8031"),
-}
-
-with httpx.Client(mounts=proxy_mounts) as client:
- ...
-```
-
-For detailed information about proxy routing, see the [Routing](#routing) section.
-
-!!! tip "Gotcha"
- In most cases, the proxy URL for the `https://` key _should_ use the `http://` scheme (that's not a typo!).
-
- This is because HTTP proxying requires initiating a connection with the proxy server. While it's possible that your proxy supports doing it via HTTPS, most proxies only support doing it via HTTP.
-
- For more information, see [FORWARD vs TUNNEL](#forward-vs-tunnel).
-
-### Authentication
-
-Proxy credentials can be passed as the `userinfo` section of the proxy URL. For example:
-
-```python
-with httpx.Client(proxy="http://username:password@localhost:8030") as client:
- ...
-```
-
-### Proxy mechanisms
-
-!!! note
- This section describes **advanced** proxy concepts and functionality.
-
-#### FORWARD vs TUNNEL
-
-In general, the flow for making an HTTP request through a proxy is as follows:
-
-1. The client connects to the proxy (initial connection request).
-2. The proxy transfers data to the server on your behalf.
-
-How exactly step 2/ is performed depends on which of two proxying mechanisms is used:
-
-* **Forwarding**: the proxy makes the request for you, and sends back the response it obtained from the server.
-* **Tunnelling**: the proxy establishes a TCP connection to the server on your behalf, and the client reuses this connection to send the request and receive the response. This is known as an [HTTP Tunnel](https://en.wikipedia.org/wiki/HTTP_tunnel). This mechanism is how you can access websites that use HTTPS from an HTTP proxy (the client "upgrades" the connection to HTTPS by performing the TLS handshake with the server over the TCP connection provided by the proxy).
-
-### Troubleshooting proxies
-
-If you encounter issues when setting up proxies, please refer to our [Troubleshooting guide](troubleshooting.md#proxies).
-
-## SOCKS
-
-In addition to HTTP proxies, `httpcore` also supports proxies using the SOCKS protocol.
-This is an optional feature that requires an additional third-party library be installed before use.
-
-You can install SOCKS support using `pip`:
-
-```shell
-$ pip install httpx[socks]
-```
-
-You can now configure a client to make requests via a proxy using the SOCKS protocol:
-
-```python
-httpx.Client(proxy='socks5://user:pass@host:port')
-```
-
-## Timeout Configuration
-
-HTTPX is careful to enforce timeouts everywhere by default.
-
-The default behavior is to raise a `TimeoutException` after 5 seconds of
-network inactivity.
-
-### Setting and disabling timeouts
-
-You can set timeouts for an individual request:
-
-```python
-# Using the top-level API:
-httpx.get('http://example.com/api/v1/example', timeout=10.0)
-
-# Using a client instance:
-with httpx.Client() as client:
- client.get("http://example.com/api/v1/example", timeout=10.0)
-```
-
-Or disable timeouts for an individual request:
-
-```python
-# Using the top-level API:
-httpx.get('http://example.com/api/v1/example', timeout=None)
-
-# Using a client instance:
-with httpx.Client() as client:
- client.get("http://example.com/api/v1/example", timeout=None)
-```
-
-### Setting a default timeout on a client
-
-You can set a timeout on a client instance, which results in the given
-`timeout` being used as the default for requests made with this client:
-
-```python
-client = httpx.Client() # Use a default 5s timeout everywhere.
-client = httpx.Client(timeout=10.0) # Use a default 10s timeout everywhere.
-client = httpx.Client(timeout=None) # Disable all timeouts by default.
-```
-
-### Fine tuning the configuration
-
-HTTPX also allows you to specify the timeout behavior in more fine grained detail.
-
-There are four different types of timeouts that may occur. These are **connect**,
-**read**, **write**, and **pool** timeouts.
-
-* The **connect** timeout specifies the maximum amount of time to wait until
-a socket connection to the requested host is established. If HTTPX is unable to connect
-within this time frame, a `ConnectTimeout` exception is raised.
-* The **read** timeout specifies the maximum duration to wait for a chunk of
-data to be received (for example, a chunk of the response body). If HTTPX is
-unable to receive data within this time frame, a `ReadTimeout` exception is raised.
-* The **write** timeout specifies the maximum duration to wait for a chunk of
-data to be sent (for example, a chunk of the request body). If HTTPX is unable
-to send data within this time frame, a `WriteTimeout` exception is raised.
-* The **pool** timeout specifies the maximum duration to wait for acquiring
-a connection from the connection pool. If HTTPX is unable to acquire a connection
-within this time frame, a `PoolTimeout` exception is raised. A related
-configuration here is the maximum number of allowable connections in the
-connection pool, which is configured by the `limits` argument.
-
-You can configure the timeout behavior for any of these values...
-
-```python
-# A client with a 60s timeout for connecting, and a 10s timeout elsewhere.
-timeout = httpx.Timeout(10.0, connect=60.0)
-client = httpx.Client(timeout=timeout)
-
-response = client.get('http://example.com/')
-```
-
-## Pool limit configuration
-
-You can control the connection pool size using the `limits` keyword
-argument on the client. It takes instances of `httpx.Limits` which define:
-
-- `max_keepalive_connections`, number of allowable keep-alive connections, or `None` to always
-allow. (Defaults 20)
-- `max_connections`, maximum number of allowable connections, or `None` for no limits.
-(Default 100)
-- `keepalive_expiry`, time limit on idle keep-alive connections in seconds, or `None` for no limits. (Default 5)
-
-```python
-limits = httpx.Limits(max_keepalive_connections=5, max_connections=10)
-client = httpx.Client(limits=limits)
-```
-
-## Multipart file encoding
-
-As mentioned in the [quickstart](quickstart.md#sending-multipart-file-uploads)
-multipart file encoding is available by passing a dictionary with the
-name of the payloads as keys and either tuple of elements or a file-like object or a string as values.
-
-```pycon
->>> files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
->>> r = httpx.post("https://httpbin.org/post", files=files)
->>> print(r.text)
-{
- ...
- "files": {
- "upload-file": "<... binary content ...>"
- },
- ...
-}
-```
-
-More specifically, if a tuple is used as a value, it must have between 2 and 3 elements:
-
-- The first element is an optional file name which can be set to `None`.
-- The second element may be a file-like object or a string which will be automatically
-encoded in UTF-8.
-- An optional third element can be used to specify the
-[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_Types)
-of the file being uploaded. If not specified HTTPX will attempt to guess the MIME type based
-on the file name, with unknown file extensions defaulting to "application/octet-stream".
-If the file name is explicitly set to `None` then HTTPX will not include a content-type
-MIME header field.
-
-```pycon
->>> files = {'upload-file': (None, 'text content', 'text/plain')}
->>> r = httpx.post("https://httpbin.org/post", files=files)
->>> print(r.text)
-{
- ...
- "files": {},
- "form": {
- "upload-file": "text-content"
- },
- ...
-}
-```
-
-!!! tip
- It is safe to upload large files this way. File uploads are streaming by default, meaning that only one chunk will be loaded into memory at a time.
-
- Non-file data fields can be included in the multipart form using by passing them to `data=...`.
-
-You can also send multiple files in one go with a multiple file field form.
-To do that, pass a list of `(field, )` items instead of a dictionary, allowing you to pass multiple items with the same `field`.
-For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field:
-
-```pycon
->>> files = [('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
- ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))]
->>> r = httpx.post("https://httpbin.org/post", files=files)
-```
-
-## Customizing authentication
-
-When issuing requests or instantiating a client, the `auth` argument can be used to pass an authentication scheme to use. The `auth` argument may be one of the following...
-
-* A two-tuple of `username`/`password`, to be used with basic authentication.
-* An instance of `httpx.BasicAuth()`, `httpx.DigestAuth()`, or `httpx.NetRCAuth()`.
-* A callable, accepting a request and returning an authenticated request instance.
-* An instance of subclasses of `httpx.Auth`.
-
-The most involved of these is the last, which allows you to create authentication flows involving one or more requests. A subclass of `httpx.Auth` should implement `def auth_flow(request)`, and yield any requests that need to be made...
-
-```python
-class MyCustomAuth(httpx.Auth):
- def __init__(self, token):
- self.token = token
-
- def auth_flow(self, request):
- # Send the request, with a custom `X-Authentication` header.
- request.headers['X-Authentication'] = self.token
- yield request
-```
-
-If the auth flow requires more than one request, you can issue multiple yields, and obtain the response in each case...
-
-```python
-class MyCustomAuth(httpx.Auth):
- def __init__(self, token):
- self.token = token
-
- def auth_flow(self, request):
- response = yield request
- if response.status_code == 401:
- # If the server issues a 401 response then resend the request,
- # with a custom `X-Authentication` header.
- request.headers['X-Authentication'] = self.token
- yield request
-```
-
-Custom authentication classes are designed to not perform any I/O, so that they may be used with both sync and async client instances. If you are implementing an authentication scheme that requires the request body, then you need to indicate this on the class using a `requires_request_body` property.
-
-You will then be able to access `request.content` inside the `.auth_flow()` method.
-
-```python
-class MyCustomAuth(httpx.Auth):
- requires_request_body = True
-
- def __init__(self, token):
- self.token = token
-
- def auth_flow(self, request):
- response = yield request
- if response.status_code == 401:
- # If the server issues a 401 response then resend the request,
- # with a custom `X-Authentication` header.
- request.headers['X-Authentication'] = self.sign_request(...)
- yield request
-
- def sign_request(self, request):
- # Create a request signature, based on `request.method`, `request.url`,
- # `request.headers`, and `request.content`.
- ...
-```
-
-Similarly, if you are implementing a scheme that requires access to the response body, then use the `requires_response_body` property. You will then be able to access response body properties and methods such as `response.content`, `response.text`, `response.json()`, etc.
-
-```python
-class MyCustomAuth(httpx.Auth):
- requires_response_body = True
-
- def __init__(self, access_token, refresh_token, refresh_url):
- self.access_token = access_token
- self.refresh_token = refresh_token
- self.refresh_url = refresh_url
-
- def auth_flow(self, request):
- request.headers["X-Authentication"] = self.access_token
- response = yield request
-
- if response.status_code == 401:
- # If the server issues a 401 response, then issue a request to
- # refresh tokens, and resend the request.
- refresh_response = yield self.build_refresh_request()
- self.update_tokens(refresh_response)
-
- request.headers["X-Authentication"] = self.access_token
- yield request
-
- def build_refresh_request(self):
- # Return an `httpx.Request` for refreshing tokens.
- ...
-
- def update_tokens(self, response):
- # Update the `.access_token` and `.refresh_token` tokens
- # based on a refresh response.
- data = response.json()
- ...
-```
-
-If you _do_ need to perform I/O other than HTTP requests, such as accessing a disk-based cache, or you need to use concurrency primitives, such as locks, then you should override `.sync_auth_flow()` and `.async_auth_flow()` (instead of `.auth_flow()`). The former will be used by `httpx.Client`, while the latter will be used by `httpx.AsyncClient`.
-
-```python
-import asyncio
-import threading
-import httpx
-
-
-class MyCustomAuth(httpx.Auth):
- def __init__(self):
- self._sync_lock = threading.RLock()
- self._async_lock = asyncio.Lock()
-
- def sync_get_token(self):
- with self._sync_lock:
- ...
-
- def sync_auth_flow(self, request):
- token = self.sync_get_token()
- request.headers["Authorization"] = f"Token {token}"
- yield request
-
- async def async_get_token(self):
- async with self._async_lock:
- ...
-
- async def async_auth_flow(self, request):
- token = await self.async_get_token()
- request.headers["Authorization"] = f"Token {token}"
- yield request
-```
-
-If you only want to support one of the two methods, then you should still override it, but raise an explicit `RuntimeError`.
-
-```python
-import httpx
-import sync_only_library
-
-
-class MyCustomAuth(httpx.Auth):
- def sync_auth_flow(self, request):
- token = sync_only_library.get_token(...)
- request.headers["Authorization"] = f"Token {token}"
- yield request
-
- async def async_auth_flow(self, request):
- raise RuntimeError("Cannot use a sync authentication class with httpx.AsyncClient")
-```
-
-## SSL certificates
-
-When making a request over HTTPS, HTTPX needs to verify the identity of the requested host. To do this, it uses a bundle of SSL certificates (a.k.a. CA bundle) delivered by a trusted certificate authority (CA).
-
-### Changing the verification defaults
-
-By default, HTTPX uses the CA bundle provided by [Certifi](https://pypi.org/project/certifi/). This is what you want in most cases, even though some advanced situations may require you to use a different set of certificates.
-
-If you'd like to use a custom CA bundle, you can use the `verify` parameter.
-
-```python
-import httpx
-
-r = httpx.get("https://example.org", verify="path/to/client.pem")
-```
-
-Alternatively, you can pass a standard library `ssl.SSLContext`.
-
-```pycon
->>> import ssl
->>> import httpx
->>> context = ssl.create_default_context()
->>> context.load_verify_locations(cafile="/tmp/client.pem")
->>> httpx.get('https://example.org', verify=context)
-
-```
-
-We also include a helper function for creating properly configured `SSLContext` instances.
-
-```pycon
->>> context = httpx.create_ssl_context()
-```
-
-The `create_ssl_context` function accepts the same set of SSL configuration arguments
-(`trust_env`, `verify`, `cert` and `http2` arguments)
-as `httpx.Client` or `httpx.AsyncClient`
-
-```pycon
->>> import httpx
->>> context = httpx.create_ssl_context(verify="/tmp/client.pem")
->>> httpx.get('https://example.org', verify=context)
-
-```
-
-Or you can also disable the SSL verification entirely, which is _not_ recommended.
-
-```python
-import httpx
-
-r = httpx.get("https://example.org", verify=False)
-```
-
-### SSL configuration on client instances
-
-If you're using a `Client()` instance, then you should pass any SSL settings when instantiating the client.
-
-```python
-client = httpx.Client(verify=False)
-```
-
-The `client.get(...)` method and other request methods *do not* support changing the SSL settings on a per-request basis. If you need different SSL settings in different cases you should use more that one client instance, with different settings on each. Each client will then be using an isolated connection pool with a specific fixed SSL configuration on all connections within that pool.
-
-### Client Side Certificates
-
-You can also specify a local cert to use as a client-side certificate, either a path to an SSL certificate file, or two-tuple of (certificate file, key file), or a three-tuple of (certificate file, key file, password)
-
-```python
-import httpx
-
-r = httpx.get("https://example.org", cert="path/to/client.pem")
-```
-
-Alternatively,
-
-```pycon
->>> cert = ("path/to/client.pem", "path/to/client.key")
->>> httpx.get("https://example.org", cert=cert)
-
-```
-
-or
-
-```pycon
->>> cert = ("path/to/client.pem", "path/to/client.key", "password")
->>> httpx.get("https://example.org", cert=cert)
-
-```
-
-### Making HTTPS requests to a local server
-
-When making requests to local servers, such as a development server running on `localhost`, you will typically be using unencrypted HTTP connections.
-
-If you do need to make HTTPS connections to a local server, for example to test an HTTPS-only service, you will need to create and use your own certificates. Here's one way to do it:
-
-1. Use [trustme](https://github.com/python-trio/trustme) to generate a pair of server key/cert files, and a client cert file.
-1. Pass the server key/cert files when starting your local server. (This depends on the particular web server you're using. For example, [Uvicorn](https://www.uvicorn.org) provides the `--ssl-keyfile` and `--ssl-certfile` options.)
-1. Tell HTTPX to use the certificates stored in `client.pem`:
-
-```pycon
->>> import httpx
->>> r = httpx.get("https://localhost:8000", verify="/tmp/client.pem")
->>> r
-Response <200 OK>
-```
-
-## Custom Transports
-
-HTTPX's `Client` also accepts a `transport` argument. This argument allows you
-to provide a custom Transport object that will be used to perform the actual
-sending of the requests.
-
-### Usage
-
-For some advanced configuration you might need to instantiate a transport
-class directly, and pass it to the client instance. One example is the
-`local_address` configuration which is only available via this low-level API.
-
-```pycon
->>> import httpx
->>> transport = httpx.HTTPTransport(local_address="0.0.0.0")
->>> client = httpx.Client(transport=transport)
-```
-
-Connection retries are also available via this interface. Requests will be retried the given number of times in case an `httpx.ConnectError` or an `httpx.ConnectTimeout` occurs, allowing smoother operation under flaky networks. If you need other forms of retry behaviors, such as handling read/write errors or reacting to `503 Service Unavailable`, consider general-purpose tools such as [tenacity](https://github.com/jd/tenacity).
-
-```pycon
->>> import httpx
->>> transport = httpx.HTTPTransport(retries=1)
->>> client = httpx.Client(transport=transport)
-```
-
-Similarly, instantiating a transport directly provides a `uds` option for
-connecting via a Unix Domain Socket that is only available via this low-level API:
-
-```pycon
->>> import httpx
->>> # Connect to the Docker API via a Unix Socket.
->>> transport = httpx.HTTPTransport(uds="/var/run/docker.sock")
->>> client = httpx.Client(transport=transport)
->>> response = client.get("http://docker/info")
->>> response.json()
-{"ID": "...", "Containers": 4, "Images": 74, ...}
-```
-
-### urllib3 transport
-
-This [public gist](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165da388e546e) provides a transport that uses the excellent [`urllib3` library](https://urllib3.readthedocs.io/en/latest/), and can be used with the sync `Client`...
-
-```pycon
->>> import httpx
->>> from urllib3_transport import URLLib3Transport
->>> client = httpx.Client(transport=URLLib3Transport())
->>> client.get("https://example.org")
-
-```
-
-### Writing custom transports
-
-A transport instance must implement the low-level Transport API, which deals
-with sending a single request, and returning a response. You should either
-subclass `httpx.BaseTransport` to implement a transport to use with `Client`,
-or subclass `httpx.AsyncBaseTransport` to implement a transport to
-use with `AsyncClient`.
-
-At the layer of the transport API we're using the familiar `Request` and
-`Response` models.
-
-See the `handle_request` and `handle_async_request` docstrings for more details
-on the specifics of the Transport API.
-
-A complete example of a custom transport implementation would be:
-
-```python
-import json
-import httpx
-
-
-class HelloWorldTransport(httpx.BaseTransport):
- """
- A mock transport that always returns a JSON "Hello, world!" response.
- """
-
- def handle_request(self, request):
- message = {"text": "Hello, world!"}
- content = json.dumps(message).encode("utf-8")
- stream = httpx.ByteStream(content)
- headers = [(b"content-type", b"application/json")]
- return httpx.Response(200, headers=headers, stream=stream)
-```
-
-Which we can use in the same way:
-
-```pycon
->>> import httpx
->>> client = httpx.Client(transport=HelloWorldTransport())
->>> response = client.get("https://example.org/")
->>> response.json()
-{"text": "Hello, world!"}
-```
-
-### Mock transports
-
-During testing it can often be useful to be able to mock out a transport,
-and return pre-determined responses, rather than making actual network requests.
-
-The `httpx.MockTransport` class accepts a handler function, which can be used
-to map requests onto pre-determined responses:
-
-```python
-def handler(request):
- return httpx.Response(200, json={"text": "Hello, world!"})
-
-
-# Switch to a mock transport, if the TESTING environment variable is set.
-if os.environ.get('TESTING', '').upper() == "TRUE":
- transport = httpx.MockTransport(handler)
-else:
- transport = httpx.HTTPTransport()
-
-client = httpx.Client(transport=transport)
-```
-
-For more advanced use-cases you might want to take a look at either [the third-party
-mocking library, RESPX](https://lundberg.github.io/respx/), or the [pytest-httpx library](https://github.com/Colin-b/pytest_httpx).
-
-### Mounting transports
-
-You can also mount transports against given schemes or domains, to control
-which transport an outgoing request should be routed via, with [the same style
-used for specifying proxy routing](#routing).
-
-```python
-import httpx
-
-class HTTPSRedirectTransport(httpx.BaseTransport):
- """
- A transport that always redirects to HTTPS.
- """
-
- def handle_request(self, method, url, headers, stream, extensions):
- scheme, host, port, path = url
- if port is None:
- location = b"https://%s%s" % (host, path)
- else:
- location = b"https://%s:%d%s" % (host, port, path)
- stream = httpx.ByteStream(b"")
- headers = [(b"location", location)]
- extensions = {}
- return 303, headers, stream, extensions
-
-
-# A client where any `http` requests are always redirected to `https`
-mounts = {'http://': HTTPSRedirectTransport()}
-client = httpx.Client(mounts=mounts)
-```
-
-A couple of other sketches of how you might take advantage of mounted transports...
-
-Disabling HTTP/2 on a single given domain...
-
-```python
-mounts = {
- "all://": httpx.HTTPTransport(http2=True),
- "all://*example.org": httpx.HTTPTransport()
-}
-client = httpx.Client(mounts=mounts)
-```
-
-Mocking requests to a given domain:
-
-```python
-# All requests to "example.org" should be mocked out.
-# Other requests occur as usual.
-def handler(request):
- return httpx.Response(200, json={"text": "Hello, World!"})
-
-mounts = {"all://example.org": httpx.MockTransport(handler)}
-client = httpx.Client(mounts=mounts)
-```
-
-Adding support for custom schemes:
-
-```python
-# Support URLs like "file:///Users/sylvia_green/websites/new_client/index.html"
-mounts = {"file://": FileSystemTransport()}
-client = httpx.Client(mounts=mounts)
-```
-
-### Routing
-
-HTTPX provides a powerful mechanism for routing requests, allowing you to write complex rules that specify which transport should be used for each request.
-
-The `mounts` dictionary maps URL patterns to HTTP transports. HTTPX matches requested URLs against URL patterns to decide which transport should be used, if any. Matching is done from most specific URL patterns (e.g. `https://:`) to least specific ones (e.g. `https://`).
-
-HTTPX supports routing requests based on **scheme**, **domain**, **port**, or a combination of these.
-
-#### Wildcard routing
-
-Route everything through a transport...
-
-```python
-mounts = {
- "all://": httpx.HTTPTransport(proxy="http://localhost:8030"),
-}
-```
-
-#### Scheme routing
-
-Route HTTP requests through one transport, and HTTPS requests through another...
-
-```python
-mounts = {
- "http://": httpx.HTTPTransport(proxy="http://localhost:8030"),
- "https://": httpx.HTTPTransport(proxy="http://localhost:8031"),
-}
-```
-
-#### Domain routing
-
-Proxy all requests on domain "example.com", let other requests pass through...
-
-```python
-mounts = {
- "all://example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
-}
-```
-
-Proxy HTTP requests on domain "example.com", let HTTPS and other requests pass through...
-
-```python
-mounts = {
- "http://example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
-}
-```
-
-Proxy all requests to "example.com" and its subdomains, let other requests pass through...
-
-```python
-mounts = {
- "all://*example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
-}
-```
-
-Proxy all requests to strict subdomains of "example.com", let "example.com" and other requests pass through...
-
-```python
-mounts = {
- "all://*.example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
-}
-```
-
-#### Port routing
-
-Proxy HTTPS requests on port 1234 to "example.com"...
-
-```python
-mounts = {
- "https://example.com:1234": httpx.HTTPTransport(proxy="http://localhost:8030"),
-}
-```
-
-Proxy all requests on port 1234...
-
-```python
-mounts = {
- "all://*:1234": httpx.HTTPTransport(proxy="http://localhost:8030"),
-}
-```
-
-#### No-proxy support
-
-It is also possible to define requests that _shouldn't_ be routed through the transport.
-
-To do so, pass `None` as the proxy URL. For example...
-
-```python
-mounts = {
- # Route requests through a proxy by default...
- "all://": httpx.HTTPTransport(proxy="http://localhost:8031"),
- # Except those for "example.com".
- "all://example.com": None,
-}
-```
-
-#### Complex configuration example
-
-You can combine the routing features outlined above to build complex proxy routing configurations. For example...
-
-```python
-mounts = {
- # Route all traffic through a proxy by default...
- "all://": httpx.HTTPTransport(proxy="http://localhost:8030"),
- # But don't use proxies for HTTPS requests to "domain.io"...
- "https://domain.io": None,
- # And use another proxy for requests to "example.com" and its subdomains...
- "all://*example.com": httpx.HTTPTransport(proxy="http://localhost:8031"),
- # And yet another proxy if HTTP is used,
- # and the "internal" subdomain on port 5550 is requested...
- "http://internal.example.com:5550": httpx.HTTPTransport(proxy="http://localhost:8032"),
-}
-```
-
-#### Environment variables
-
-There are also environment variables that can be used to control the dictionary of the client mounts.
-They can be used to configure HTTP proxying for clients.
-
-See documentation on [`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`](environment_variables.md#http_proxy-https_proxy-all_proxy) for more information.
-
diff --git a/docs/advanced/authentication.md b/docs/advanced/authentication.md
new file mode 100644
index 00000000..63d26e5f
--- /dev/null
+++ b/docs/advanced/authentication.md
@@ -0,0 +1,232 @@
+Authentication can either be included on a per-request basis...
+
+```pycon
+>>> auth = httpx.BasicAuth(username="username", password="secret")
+>>> client = httpx.Client()
+>>> response = client.get("https://www.example.com/", auth=auth)
+```
+
+Or configured on the client instance, ensuring that all outgoing requests will include authentication credentials...
+
+```pycon
+>>> auth = httpx.BasicAuth(username="username", password="secret")
+>>> client = httpx.Client(auth=auth)
+>>> response = client.get("https://www.example.com/")
+```
+
+## Basic authentication
+
+HTTP basic authentication is an unencrypted authentication scheme that uses a simple encoding of the username and password in the request `Authorization` header. Since it is unencrypted it should typically only be used over `https`, although this is not strictly enforced.
+
+```pycon
+>>> auth = httpx.BasicAuth(username="finley", password="secret")
+>>> client = httpx.Client(auth=auth)
+>>> response = client.get("https://httpbin.org/basic-auth/finley/secret")
+>>> response
+
+```
+
+## Digest authentication
+
+HTTP digest authentication is a challenge-response authentication scheme. Unlike basic authentication it provides encryption, and can be used over unencrypted `http` connections. It requires an additional round-trip in order to negotiate the authentication.
+
+```pycon
+>>> auth = httpx.DigestAuth(username="olivia", password="secret")
+>>> client = httpx.Client(auth=auth)
+>>> response = client.get("https://httpbin.org/digest-auth/auth/olivia/secret")
+>>> response
+
+>>> response.history
+[]
+```
+
+## NetRC authentication
+
+HTTPX can be configured to use [a `.netrc` config file](https://everything.curl.dev/usingcurl/netrc) for authentication.
+
+The `.netrc` config file allows authentication credentials to be associated with specified hosts. When a request is made to a host that is found in the netrc file, the username and password will be included using HTTP basic authentication.
+
+Example `.netrc` file:
+
+```
+machine example.org
+login example-username
+password example-password
+
+machine python-httpx.org
+login other-username
+password other-password
+```
+
+Some examples of configuring `.netrc` authentication with `httpx`.
+
+Use the default `.netrc` file in the users home directory:
+
+```pycon
+>>> auth = httpx.NetRCAuth()
+>>> client = httpx.Client(auth=auth)
+```
+
+Use an explicit path to a `.netrc` file:
+
+```pycon
+>>> auth = httpx.NetRCAuth(file="/path/to/.netrc")
+>>> client = httpx.Client(auth=auth)
+```
+
+Use the `NETRC` environment variable to configure a path to the `.netrc` file,
+or fallback to the default.
+
+```pycon
+>>> auth = httpx.NetRCAuth(file=os.environ.get("NETRC"))
+>>> client = httpx.Client(auth=auth)
+```
+
+The `NetRCAuth()` class uses [the `netrc.netrc()` function from the Python standard library](https://docs.python.org/3/library/netrc.html). See the documentation there for more details on exceptions that may be raised if the `.netrc` file is not found, or cannot be parsed.
+
+## Custom authentication schemes
+
+When issuing requests or instantiating a client, the `auth` argument can be used to pass an authentication scheme to use. The `auth` argument may be one of the following...
+
+* A two-tuple of `username`/`password`, to be used with basic authentication.
+* An instance of `httpx.BasicAuth()`, `httpx.DigestAuth()`, or `httpx.NetRCAuth()`.
+* A callable, accepting a request and returning an authenticated request instance.
+* An instance of subclasses of `httpx.Auth`.
+
+The most involved of these is the last, which allows you to create authentication flows involving one or more requests. A subclass of `httpx.Auth` should implement `def auth_flow(request)`, and yield any requests that need to be made...
+
+```python
+class MyCustomAuth(httpx.Auth):
+ def __init__(self, token):
+ self.token = token
+
+ def auth_flow(self, request):
+ # Send the request, with a custom `X-Authentication` header.
+ request.headers['X-Authentication'] = self.token
+ yield request
+```
+
+If the auth flow requires more than one request, you can issue multiple yields, and obtain the response in each case...
+
+```python
+class MyCustomAuth(httpx.Auth):
+ def __init__(self, token):
+ self.token = token
+
+ def auth_flow(self, request):
+ response = yield request
+ if response.status_code == 401:
+ # If the server issues a 401 response then resend the request,
+ # with a custom `X-Authentication` header.
+ request.headers['X-Authentication'] = self.token
+ yield request
+```
+
+Custom authentication classes are designed to not perform any I/O, so that they may be used with both sync and async client instances. If you are implementing an authentication scheme that requires the request body, then you need to indicate this on the class using a `requires_request_body` property.
+
+You will then be able to access `request.content` inside the `.auth_flow()` method.
+
+```python
+class MyCustomAuth(httpx.Auth):
+ requires_request_body = True
+
+ def __init__(self, token):
+ self.token = token
+
+ def auth_flow(self, request):
+ response = yield request
+ if response.status_code == 401:
+ # If the server issues a 401 response then resend the request,
+ # with a custom `X-Authentication` header.
+ request.headers['X-Authentication'] = self.sign_request(...)
+ yield request
+
+ def sign_request(self, request):
+ # Create a request signature, based on `request.method`, `request.url`,
+ # `request.headers`, and `request.content`.
+ ...
+```
+
+Similarly, if you are implementing a scheme that requires access to the response body, then use the `requires_response_body` property. You will then be able to access response body properties and methods such as `response.content`, `response.text`, `response.json()`, etc.
+
+```python
+class MyCustomAuth(httpx.Auth):
+ requires_response_body = True
+
+ def __init__(self, access_token, refresh_token, refresh_url):
+ self.access_token = access_token
+ self.refresh_token = refresh_token
+ self.refresh_url = refresh_url
+
+ def auth_flow(self, request):
+ request.headers["X-Authentication"] = self.access_token
+ response = yield request
+
+ if response.status_code == 401:
+ # If the server issues a 401 response, then issue a request to
+ # refresh tokens, and resend the request.
+ refresh_response = yield self.build_refresh_request()
+ self.update_tokens(refresh_response)
+
+ request.headers["X-Authentication"] = self.access_token
+ yield request
+
+ def build_refresh_request(self):
+ # Return an `httpx.Request` for refreshing tokens.
+ ...
+
+ def update_tokens(self, response):
+ # Update the `.access_token` and `.refresh_token` tokens
+ # based on a refresh response.
+ data = response.json()
+ ...
+```
+
+If you _do_ need to perform I/O other than HTTP requests, such as accessing a disk-based cache, or you need to use concurrency primitives, such as locks, then you should override `.sync_auth_flow()` and `.async_auth_flow()` (instead of `.auth_flow()`). The former will be used by `httpx.Client`, while the latter will be used by `httpx.AsyncClient`.
+
+```python
+import asyncio
+import threading
+import httpx
+
+
+class MyCustomAuth(httpx.Auth):
+ def __init__(self):
+ self._sync_lock = threading.RLock()
+ self._async_lock = asyncio.Lock()
+
+ def sync_get_token(self):
+ with self._sync_lock:
+ ...
+
+ def sync_auth_flow(self, request):
+ token = self.sync_get_token()
+ request.headers["Authorization"] = f"Token {token}"
+ yield request
+
+ async def async_get_token(self):
+ async with self._async_lock:
+ ...
+
+ async def async_auth_flow(self, request):
+ token = await self.async_get_token()
+ request.headers["Authorization"] = f"Token {token}"
+ yield request
+```
+
+If you only want to support one of the two methods, then you should still override it, but raise an explicit `RuntimeError`.
+
+```python
+import httpx
+import sync_only_library
+
+
+class MyCustomAuth(httpx.Auth):
+ def sync_auth_flow(self, request):
+ token = sync_only_library.get_token(...)
+ request.headers["Authorization"] = f"Token {token}"
+ yield request
+
+ async def async_auth_flow(self, request):
+ raise RuntimeError("Cannot use a sync authentication class with httpx.AsyncClient")
+```
\ No newline at end of file
diff --git a/docs/advanced/clients.md b/docs/advanced/clients.md
new file mode 100644
index 00000000..a55fc596
--- /dev/null
+++ b/docs/advanced/clients.md
@@ -0,0 +1,324 @@
+!!! hint
+ If you are coming from Requests, `httpx.Client()` is what you can use instead of `requests.Session()`.
+
+## Why use a Client?
+
+!!! note "TL;DR"
+ If you do anything more than experimentation, one-off scripts, or prototypes, then you should use a `Client` instance.
+
+**More efficient usage of network resources**
+
+When you make requests using the top-level API as documented in the [Quickstart](../quickstart.md) guide, HTTPX has to establish a new connection _for every single request_ (connections are not reused). As the number of requests to a host increases, this quickly becomes inefficient.
+
+On the other hand, a `Client` instance uses [HTTP connection pooling](https://en.wikipedia.org/wiki/HTTP_persistent_connection). This means that when you make several requests to the same host, the `Client` will reuse the underlying TCP connection, instead of recreating one for every single request.
+
+This can bring **significant performance improvements** compared to using the top-level API, including:
+
+- Reduced latency across requests (no handshaking).
+- Reduced CPU usage and round-trips.
+- Reduced network congestion.
+
+**Extra features**
+
+`Client` instances also support features that aren't available at the top-level API, such as:
+
+- Cookie persistence across requests.
+- Applying configuration across all outgoing requests.
+- Sending requests through HTTP proxies.
+- Using [HTTP/2](../http2.md).
+
+The other sections on this page go into further detail about what you can do with a `Client` instance.
+
+## Usage
+
+The recommended way to use a `Client` is as a context manager. This will ensure that connections are properly cleaned up when leaving the `with` block:
+
+```python
+with httpx.Client() as client:
+ ...
+```
+
+Alternatively, you can explicitly close the connection pool without block-usage using `.close()`:
+
+```python
+client = httpx.Client()
+try:
+ ...
+finally:
+ client.close()
+```
+
+## Making requests
+
+Once you have a `Client`, you can send requests using `.get()`, `.post()`, etc. For example:
+
+```pycon
+>>> with httpx.Client() as client:
+... r = client.get('https://example.com')
+...
+>>> r
+
+```
+
+These methods accept the same arguments as `httpx.get()`, `httpx.post()`, etc. This means that all features documented in the [Quickstart](../quickstart.md) guide are also available at the client level.
+
+For example, to send a request with custom headers:
+
+```pycon
+>>> with httpx.Client() as client:
+... headers = {'X-Custom': 'value'}
+... r = client.get('https://example.com', headers=headers)
+...
+>>> r.request.headers['X-Custom']
+'value'
+```
+
+## Sharing configuration across requests
+
+Clients allow you to apply configuration to all outgoing requests by passing parameters to the `Client` constructor.
+
+For example, to apply a set of custom headers _on every request_:
+
+```pycon
+>>> url = 'http://httpbin.org/headers'
+>>> headers = {'user-agent': 'my-app/0.0.1'}
+>>> with httpx.Client(headers=headers) as client:
+... r = client.get(url)
+...
+>>> r.json()['headers']['User-Agent']
+'my-app/0.0.1'
+```
+
+## Merging of configuration
+
+When a configuration option is provided at both the client-level and request-level, one of two things can happen:
+
+- For headers, query parameters and cookies, the values are combined together. For example:
+
+```pycon
+>>> headers = {'X-Auth': 'from-client'}
+>>> params = {'client_id': 'client1'}
+>>> with httpx.Client(headers=headers, params=params) as client:
+... headers = {'X-Custom': 'from-request'}
+... params = {'request_id': 'request1'}
+... r = client.get('https://example.com', headers=headers, params=params)
+...
+>>> r.request.url
+URL('https://example.com?client_id=client1&request_id=request1')
+>>> r.request.headers['X-Auth']
+'from-client'
+>>> r.request.headers['X-Custom']
+'from-request'
+```
+
+- For all other parameters, the request-level value takes priority. For example:
+
+```pycon
+>>> with httpx.Client(auth=('tom', 'mot123')) as client:
+... r = client.get('https://example.com', auth=('alice', 'ecila123'))
+...
+>>> _, _, auth = r.request.headers['Authorization'].partition(' ')
+>>> import base64
+>>> base64.b64decode(auth)
+b'alice:ecila123'
+```
+
+If you need finer-grained control on the merging of client-level and request-level parameters, see [Request instances](#request-instances).
+
+## Other Client-only configuration options
+
+Additionally, `Client` accepts some configuration options that aren't available at the request level.
+
+For example, `base_url` allows you to prepend an URL to all outgoing requests:
+
+```pycon
+>>> with httpx.Client(base_url='http://httpbin.org') as client:
+... r = client.get('/headers')
+...
+>>> r.request.url
+URL('http://httpbin.org/headers')
+```
+
+For a list of all available client parameters, see the [`Client`](../api.md#client) API reference.
+
+---
+
+## Request instances
+
+For maximum control on what gets sent over the wire, HTTPX supports building explicit [`Request`](../api.md#request) instances:
+
+```python
+request = httpx.Request("GET", "https://example.com")
+```
+
+To dispatch a `Request` instance across to the network, create a [`Client` instance](#client-instances) and use `.send()`:
+
+```python
+with httpx.Client() as client:
+ response = client.send(request)
+ ...
+```
+
+If you need to mix client-level and request-level options in a way that is not supported by the default [Merging of parameters](#merging-of-parameters), you can use `.build_request()` and then make arbitrary modifications to the `Request` instance. For example:
+
+```python
+headers = {"X-Api-Key": "...", "X-Client-ID": "ABC123"}
+
+with httpx.Client(headers=headers) as client:
+ request = client.build_request("GET", "https://api.example.com")
+
+ print(request.headers["X-Client-ID"]) # "ABC123"
+
+ # Don't send the API key for this particular request.
+ del request.headers["X-Api-Key"]
+
+ response = client.send(request)
+ ...
+```
+
+## Monitoring download progress
+
+If you need to monitor download progress of large responses, you can use response streaming and inspect the `response.num_bytes_downloaded` property.
+
+This interface is required for properly determining download progress, because the total number of bytes returned by `response.content` or `response.iter_content()` will not always correspond with the raw content length of the response if HTTP response compression is being used.
+
+For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library while a response is being downloaded could be done like this…
+
+```python
+import tempfile
+
+import httpx
+from tqdm import tqdm
+
+with tempfile.NamedTemporaryFile() as download_file:
+ url = "https://speed.hetzner.de/100MB.bin"
+ with httpx.stream("GET", url) as response:
+ total = int(response.headers["Content-Length"])
+
+ with tqdm(total=total, unit_scale=True, unit_divisor=1024, unit="B") as progress:
+ num_bytes_downloaded = response.num_bytes_downloaded
+ for chunk in response.iter_bytes():
+ download_file.write(chunk)
+ progress.update(response.num_bytes_downloaded - num_bytes_downloaded)
+ num_bytes_downloaded = response.num_bytes_downloaded
+```
+
+
+
+Or an alternate example, this time using the [`rich`](https://github.com/willmcgugan/rich) library…
+
+```python
+import tempfile
+import httpx
+import rich.progress
+
+with tempfile.NamedTemporaryFile() as download_file:
+ url = "https://speed.hetzner.de/100MB.bin"
+ with httpx.stream("GET", url) as response:
+ total = int(response.headers["Content-Length"])
+
+ with rich.progress.Progress(
+ "[progress.percentage]{task.percentage:>3.0f}%",
+ rich.progress.BarColumn(bar_width=None),
+ rich.progress.DownloadColumn(),
+ rich.progress.TransferSpeedColumn(),
+ ) as progress:
+ download_task = progress.add_task("Download", total=total)
+ for chunk in response.iter_bytes():
+ download_file.write(chunk)
+ progress.update(download_task, completed=response.num_bytes_downloaded)
+```
+
+
+
+## Monitoring upload progress
+
+If you need to monitor upload progress of large responses, you can use request content generator streaming.
+
+For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library.
+
+```python
+import io
+import random
+
+import httpx
+from tqdm import tqdm
+
+
+def gen():
+ """
+ this is a complete example with generated random bytes.
+ you can replace `io.BytesIO` with real file object.
+ """
+ total = 32 * 1024 * 1024 # 32m
+ with tqdm(ascii=True, unit_scale=True, unit='B', unit_divisor=1024, total=total) as bar:
+ with io.BytesIO(random.randbytes(total)) as f:
+ while data := f.read(1024):
+ yield data
+ bar.update(len(data))
+
+
+httpx.post("https://httpbin.org/post", content=gen())
+```
+
+
+
+## Multipart file encoding
+
+As mentioned in the [quickstart](../quickstart.md#sending-multipart-file-uploads)
+multipart file encoding is available by passing a dictionary with the
+name of the payloads as keys and either tuple of elements or a file-like object or a string as values.
+
+```pycon
+>>> files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
+>>> r = httpx.post("https://httpbin.org/post", files=files)
+>>> print(r.text)
+{
+ ...
+ "files": {
+ "upload-file": "<... binary content ...>"
+ },
+ ...
+}
+```
+
+More specifically, if a tuple is used as a value, it must have between 2 and 3 elements:
+
+- The first element is an optional file name which can be set to `None`.
+- The second element may be a file-like object or a string which will be automatically
+encoded in UTF-8.
+- An optional third element can be used to specify the
+[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_Types)
+of the file being uploaded. If not specified HTTPX will attempt to guess the MIME type based
+on the file name, with unknown file extensions defaulting to "application/octet-stream".
+If the file name is explicitly set to `None` then HTTPX will not include a content-type
+MIME header field.
+
+```pycon
+>>> files = {'upload-file': (None, 'text content', 'text/plain')}
+>>> r = httpx.post("https://httpbin.org/post", files=files)
+>>> print(r.text)
+{
+ ...
+ "files": {},
+ "form": {
+ "upload-file": "text-content"
+ },
+ ...
+}
+```
+
+!!! tip
+ It is safe to upload large files this way. File uploads are streaming by default, meaning that only one chunk will be loaded into memory at a time.
+
+ Non-file data fields can be included in the multipart form using by passing them to `data=...`.
+
+You can also send multiple files in one go with a multiple file field form.
+To do that, pass a list of `(field, )` items instead of a dictionary, allowing you to pass multiple items with the same `field`.
+For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field:
+
+```pycon
+>>> files = [('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
+ ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))]
+>>> r = httpx.post("https://httpbin.org/post", files=files)
+```
diff --git a/docs/advanced/event-hooks.md b/docs/advanced/event-hooks.md
new file mode 100644
index 00000000..28cf353d
--- /dev/null
+++ b/docs/advanced/event-hooks.md
@@ -0,0 +1,65 @@
+HTTPX allows you to register "event hooks" with the client, that are called
+every time a particular type of event takes place.
+
+There are currently two event hooks:
+
+* `request` - Called after a request is fully prepared, but before it is sent to the network. Passed the `request` instance.
+* `response` - Called after the response has been fetched from the network, but before it is returned to the caller. Passed the `response` instance.
+
+These allow you to install client-wide functionality such as logging, monitoring or tracing.
+
+```python
+def log_request(request):
+ print(f"Request event hook: {request.method} {request.url} - Waiting for response")
+
+def log_response(response):
+ request = response.request
+ print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
+
+client = httpx.Client(event_hooks={'request': [log_request], 'response': [log_response]})
+```
+
+You can also use these hooks to install response processing code, such as this
+example, which creates a client instance that always raises `httpx.HTTPStatusError`
+on 4xx and 5xx responses.
+
+```python
+def raise_on_4xx_5xx(response):
+ response.raise_for_status()
+
+client = httpx.Client(event_hooks={'response': [raise_on_4xx_5xx]})
+```
+
+!!! note
+ Response event hooks are called before determining if the response body
+ should be read or not.
+
+ If you need access to the response body inside an event hook, you'll
+ need to call `response.read()`, or for AsyncClients, `response.aread()`.
+
+The hooks are also allowed to modify `request` and `response` objects.
+
+```python
+def add_timestamp(request):
+ request.headers['x-request-timestamp'] = datetime.now(tz=datetime.utc).isoformat()
+
+client = httpx.Client(event_hooks={'request': [add_timestamp]})
+```
+
+Event hooks must always be set as a **list of callables**, and you may register
+multiple event hooks for each type of event.
+
+As well as being able to set event hooks on instantiating the client, there
+is also an `.event_hooks` property, that allows you to inspect and modify
+the installed hooks.
+
+```python
+client = httpx.Client()
+client.event_hooks['request'] = [log_request]
+client.event_hooks['response'] = [log_response, raise_on_4xx_5xx]
+```
+
+!!! note
+ If you are using HTTPX's async support, then you need to be aware that
+ hooks registered with `httpx.AsyncClient` MUST be async functions,
+ rather than plain functions.
diff --git a/docs/advanced/extensions.md b/docs/advanced/extensions.md
new file mode 100644
index 00000000..d9208ccd
--- /dev/null
+++ b/docs/advanced/extensions.md
@@ -0,0 +1,242 @@
+# Extensions
+
+Request and response extensions provide a untyped space where additional information may be added.
+
+Extensions should be used for features that may not be available on all transports, and that do not fit neatly into [the simplified request/response model](https://www.encode.io/httpcore/extensions/) that the underlying `httpcore` package uses as its API.
+
+Several extensions are supported on the request:
+
+```python
+# Request timeouts actually implemented as an extension on
+# the request, ensuring that they are passed throughout the
+# entire call stack.
+client = httpx.Client()
+response = client.get(
+ "https://www.example.com",
+ extensions={"timeout": {"connect": 5.0}}
+)
+response.request.extensions["timeout"]
+{"connect": 5.0}
+```
+
+And on the response:
+
+```python
+client = httpx.Client()
+response = client.get("https://www.example.com")
+print(response.extensions["http_version"]) # b"HTTP/1.1"
+# Other server responses could have been
+# b"HTTP/0.9", b"HTTP/1.0", or b"HTTP/1.1"
+```
+
+## Request Extensions
+
+### `"trace"`
+
+The trace extension allows a callback handler to be installed to monitor the internal
+flow of events within the underlying `httpcore` transport.
+
+The simplest way to explain this is with an example:
+
+```python
+import httpx
+
+def log(event_name, info):
+ print(event_name, info)
+
+client = httpx.Client()
+response = client.get("https://www.example.com/", extensions={"trace": log})
+# connection.connect_tcp.started {'host': 'www.example.com', 'port': 443, 'local_address': None, 'timeout': None}
+# connection.connect_tcp.complete {'return_value': }
+# connection.start_tls.started {'ssl_context': , 'server_hostname': b'www.example.com', 'timeout': None}
+# connection.start_tls.complete {'return_value': }
+# http11.send_request_headers.started {'request': }
+# http11.send_request_headers.complete {'return_value': None}
+# http11.send_request_body.started {'request': }
+# http11.send_request_body.complete {'return_value': None}
+# http11.receive_response_headers.started {'request': }
+# http11.receive_response_headers.complete {'return_value': (b'HTTP/1.1', 200, b'OK', [(b'Age', b'553715'), (b'Cache-Control', b'max-age=604800'), (b'Content-Type', b'text/html; charset=UTF-8'), (b'Date', b'Thu, 21 Oct 2021 17:08:42 GMT'), (b'Etag', b'"3147526947+ident"'), (b'Expires', b'Thu, 28 Oct 2021 17:08:42 GMT'), (b'Last-Modified', b'Thu, 17 Oct 2019 07:18:26 GMT'), (b'Server', b'ECS (nyb/1DCD)'), (b'Vary', b'Accept-Encoding'), (b'X-Cache', b'HIT'), (b'Content-Length', b'1256')])}
+# http11.receive_response_body.started {'request': }
+# http11.receive_response_body.complete {'return_value': None}
+# http11.response_closed.started {}
+# http11.response_closed.complete {'return_value': None}
+```
+
+The `event_name` and `info` arguments here will be one of the following:
+
+* `{event_type}.{event_name}.started`, ``
+* `{event_type}.{event_name}.complete`, `{"return_value": <...>}`
+* `{event_type}.{event_name}.failed`, `{"exception": <...>}`
+
+Note that when using async code the handler function passed to `"trace"` must be an `async def ...` function.
+
+The following event types are currently exposed...
+
+**Establishing the connection**
+
+* `"connection.connect_tcp"`
+* `"connection.connect_unix_socket"`
+* `"connection.start_tls"`
+
+**HTTP/1.1 events**
+
+* `"http11.send_request_headers"`
+* `"http11.send_request_body"`
+* `"http11.receive_response"`
+* `"http11.receive_response_body"`
+* `"http11.response_closed"`
+
+**HTTP/2 events**
+
+* `"http2.send_connection_init"`
+* `"http2.send_request_headers"`
+* `"http2.send_request_body"`
+* `"http2.receive_response_headers"`
+* `"http2.receive_response_body"`
+* `"http2.response_closed"`
+
+The exact set of trace events may be subject to change across different versions of `httpcore`. If you need to rely on a particular set of events it is recommended that you pin installation of the package to a fixed version.
+
+### `"sni_hostname"`
+
+The server's hostname, which is used to confirm the hostname supplied by the SSL certificate.
+
+If you want to connect to an explicit IP address rather than using the standard DNS hostname lookup, then you'll need to use this request extension.
+
+For example:
+
+``` python
+# Connect to '185.199.108.153' but use 'www.encode.io' in the Host header,
+# and use 'www.encode.io' when SSL verifying the server hostname.
+client = httpx.Client()
+headers = {"Host": "www.encode.io"}
+extensions = {"sni_hostname": "www.encode.io"}
+response = client.get(
+ "https://185.199.108.153/path",
+ headers=headers,
+ extensions=extensions
+)
+```
+
+### `"timeout"`
+
+A dictionary of `str: Optional[float]` timeout values.
+
+May include values for `'connect'`, `'read'`, `'write'`, or `'pool'`.
+
+For example:
+
+```python
+# Timeout if a connection takes more than 5 seconds to established, or if
+# we are blocked waiting on the connection pool for more than 10 seconds.
+client = httpx.Client()
+response = client.get(
+ "https://www.example.com",
+ extensions={"timeout": {"connect": 5.0, "pool": 10.0}}
+)
+```
+
+This extension is how the `httpx` timeouts are implemented, ensuring that the timeout values are associated with the request instance and passed throughout the stack. You shouldn't typically be working with this extension directly, but use the higher level `timeout` API instead.
+
+### `"target"`
+
+The target that is used as [the HTTP target instead of the URL path](https://datatracker.ietf.org/doc/html/rfc2616#section-5.1.2).
+
+This enables support constructing requests that would otherwise be unsupported.
+
+* URL paths with non-standard escaping applied.
+* Forward proxy requests using an absolute URI.
+* Tunneling proxy requests using `CONNECT` with hostname as the target.
+* Server-wide `OPTIONS *` requests.
+
+Some examples:
+
+Using the 'target' extension to send requests without the standard path escaping rules...
+
+```python
+# Typically a request to "https://www.example.com/test^path" would
+# connect to "www.example.com" and send an HTTP/1.1 request like...
+#
+# GET /test%5Epath HTTP/1.1
+#
+# Using the target extension we can include the literal '^'...
+#
+# GET /test^path HTTP/1.1
+#
+# Note that requests must still be valid HTTP requests.
+# For example including whitespace in the target will raise a `LocalProtocolError`.
+extensions = {"target": b"/test^path"}
+response = httpx.get("https://www.example.com", extensions=extensions)
+```
+
+The `target` extension also allows server-wide `OPTIONS *` requests to be constructed...
+
+```python
+# This will send the following request...
+#
+# CONNECT * HTTP/1.1
+extensions = {"target": b"*"}
+response = httpx.request("CONNECT", "https://www.example.com", extensions=extensions)
+```
+
+## Response Extensions
+
+### `"http_version"`
+
+The HTTP version, as bytes. Eg. `b"HTTP/1.1"`.
+
+When using HTTP/1.1 the response line includes an explicit version, and the value of this key could feasibly be one of `b"HTTP/0.9"`, `b"HTTP/1.0"`, or `b"HTTP/1.1"`.
+
+When using HTTP/2 there is no further response versioning included in the protocol, and the value of this key will always be `b"HTTP/2"`.
+
+### `"reason_phrase"`
+
+The reason-phrase of the HTTP response, as bytes. For example `b"OK"`. Some servers may include a custom reason phrase, although this is not recommended.
+
+HTTP/2 onwards does not include a reason phrase on the wire.
+
+When no key is included, a default based on the status code may be used.
+
+### `"stream_id"`
+
+When HTTP/2 is being used the `"stream_id"` response extension can be accessed to determine the ID of the data stream that the response was sent on.
+
+### `"network_stream"`
+
+The `"network_stream"` extension allows developers to handle HTTP `CONNECT` and `Upgrade` requests, by providing an API that steps outside the standard request/response model, and can directly read or write to the network.
+
+The interface provided by the network stream:
+
+* `read(max_bytes, timeout = None) -> bytes`
+* `write(buffer, timeout = None)`
+* `close()`
+* `start_tls(ssl_context, server_hostname = None, timeout = None) -> NetworkStream`
+* `get_extra_info(info) -> Any`
+
+This API can be used as the foundation for working with HTTP proxies, WebSocket upgrades, and other advanced use-cases.
+
+See the [network backends documentation](https://www.encode.io/httpcore/network-backends/) for more information on working directly with network streams.
+
+**Extra network information**
+
+The network stream abstraction also allows access to various low-level information that may be exposed by the underlying socket:
+
+```python
+response = httpx.get("https://www.example.com")
+network_stream = response.extensions["network_stream"]
+
+client_addr = network_stream.get_extra_info("client_addr")
+server_addr = network_stream.get_extra_info("server_addr")
+print("Client address", client_addr)
+print("Server address", server_addr)
+```
+
+The socket SSL information is also available through this interface, although you need to ensure that the underlying connection is still open, in order to access it...
+
+```python
+with httpx.stream("GET", "https://www.example.com") as response:
+ network_stream = response.extensions["network_stream"]
+
+ ssl_object = network_stream.get_extra_info("ssl_object")
+ print("TLS version", ssl_object.version())
+```
diff --git a/docs/advanced/proxies.md b/docs/advanced/proxies.md
new file mode 100644
index 00000000..2a6b7d5f
--- /dev/null
+++ b/docs/advanced/proxies.md
@@ -0,0 +1,83 @@
+HTTPX supports setting up [HTTP proxies](https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers) via the `proxy` parameter to be passed on client initialization or top-level API functions like `httpx.get(..., proxy=...)`.
+
+
+
+
Diagram of how a proxy works (source: Wikipedia). The left hand side "Internet" blob may be your HTTPX client requesting example.com through a proxy.
+
+
+## HTTP Proxies
+
+To route all traffic (HTTP and HTTPS) to a proxy located at `http://localhost:8030`, pass the proxy URL to the client...
+
+```python
+with httpx.Client(proxy="http://localhost:8030") as client:
+ ...
+```
+
+For more advanced use cases, pass a mounts `dict`. For example, to route HTTP and HTTPS requests to 2 different proxies, respectively located at `http://localhost:8030`, and `http://localhost:8031`, pass a `dict` of proxy URLs:
+
+```python
+proxy_mounts = {
+ "http://": httpx.HTTPTransport(proxy="http://localhost:8030"),
+ "https://": httpx.HTTPTransport(proxy="http://localhost:8031"),
+}
+
+with httpx.Client(mounts=proxy_mounts) as client:
+ ...
+```
+
+For detailed information about proxy routing, see the [Routing](#routing) section.
+
+!!! tip "Gotcha"
+ In most cases, the proxy URL for the `https://` key _should_ use the `http://` scheme (that's not a typo!).
+
+ This is because HTTP proxying requires initiating a connection with the proxy server. While it's possible that your proxy supports doing it via HTTPS, most proxies only support doing it via HTTP.
+
+ For more information, see [FORWARD vs TUNNEL](#forward-vs-tunnel).
+
+## Authentication
+
+Proxy credentials can be passed as the `userinfo` section of the proxy URL. For example:
+
+```python
+with httpx.Client(proxy="http://username:password@localhost:8030") as client:
+ ...
+```
+
+## Proxy mechanisms
+
+!!! note
+ This section describes **advanced** proxy concepts and functionality.
+
+### FORWARD vs TUNNEL
+
+In general, the flow for making an HTTP request through a proxy is as follows:
+
+1. The client connects to the proxy (initial connection request).
+2. The proxy transfers data to the server on your behalf.
+
+How exactly step 2/ is performed depends on which of two proxying mechanisms is used:
+
+* **Forwarding**: the proxy makes the request for you, and sends back the response it obtained from the server.
+* **Tunnelling**: the proxy establishes a TCP connection to the server on your behalf, and the client reuses this connection to send the request and receive the response. This is known as an [HTTP Tunnel](https://en.wikipedia.org/wiki/HTTP_tunnel). This mechanism is how you can access websites that use HTTPS from an HTTP proxy (the client "upgrades" the connection to HTTPS by performing the TLS handshake with the server over the TCP connection provided by the proxy).
+
+### Troubleshooting proxies
+
+If you encounter issues when setting up proxies, please refer to our [Troubleshooting guide](../troubleshooting.md#proxies).
+
+## SOCKS
+
+In addition to HTTP proxies, `httpcore` also supports proxies using the SOCKS protocol.
+This is an optional feature that requires an additional third-party library be installed before use.
+
+You can install SOCKS support using `pip`:
+
+```shell
+$ pip install httpx[socks]
+```
+
+You can now configure a client to make requests via a proxy using the SOCKS protocol:
+
+```python
+httpx.Client(proxy='socks5://user:pass@host:port')
+```
diff --git a/docs/advanced/resource-limits.md b/docs/advanced/resource-limits.md
new file mode 100644
index 00000000..20024283
--- /dev/null
+++ b/docs/advanced/resource-limits.md
@@ -0,0 +1,13 @@
+You can control the connection pool size using the `limits` keyword
+argument on the client. It takes instances of `httpx.Limits` which define:
+
+- `max_keepalive_connections`, number of allowable keep-alive connections, or `None` to always
+allow. (Defaults 20)
+- `max_connections`, maximum number of allowable connections, or `None` for no limits.
+(Default 100)
+- `keepalive_expiry`, time limit on idle keep-alive connections in seconds, or `None` for no limits. (Default 5)
+
+```python
+limits = httpx.Limits(max_keepalive_connections=5, max_connections=10)
+client = httpx.Client(limits=limits)
+```
\ No newline at end of file
diff --git a/docs/advanced/ssl.md b/docs/advanced/ssl.md
new file mode 100644
index 00000000..d96bbe19
--- /dev/null
+++ b/docs/advanced/ssl.md
@@ -0,0 +1,100 @@
+When making a request over HTTPS, HTTPX needs to verify the identity of the requested host. To do this, it uses a bundle of SSL certificates (a.k.a. CA bundle) delivered by a trusted certificate authority (CA).
+
+## Changing the verification defaults
+
+By default, HTTPX uses the CA bundle provided by [Certifi](https://pypi.org/project/certifi/). This is what you want in most cases, even though some advanced situations may require you to use a different set of certificates.
+
+If you'd like to use a custom CA bundle, you can use the `verify` parameter.
+
+```python
+import httpx
+
+r = httpx.get("https://example.org", verify="path/to/client.pem")
+```
+
+Alternatively, you can pass a standard library `ssl.SSLContext`.
+
+```pycon
+>>> import ssl
+>>> import httpx
+>>> context = ssl.create_default_context()
+>>> context.load_verify_locations(cafile="/tmp/client.pem")
+>>> httpx.get('https://example.org', verify=context)
+
+```
+
+We also include a helper function for creating properly configured `SSLContext` instances.
+
+```pycon
+>>> context = httpx.create_ssl_context()
+```
+
+The `create_ssl_context` function accepts the same set of SSL configuration arguments
+(`trust_env`, `verify`, `cert` and `http2` arguments)
+as `httpx.Client` or `httpx.AsyncClient`
+
+```pycon
+>>> import httpx
+>>> context = httpx.create_ssl_context(verify="/tmp/client.pem")
+>>> httpx.get('https://example.org', verify=context)
+
+```
+
+Or you can also disable the SSL verification entirely, which is _not_ recommended.
+
+```python
+import httpx
+
+r = httpx.get("https://example.org", verify=False)
+```
+
+## SSL configuration on client instances
+
+If you're using a `Client()` instance, then you should pass any SSL settings when instantiating the client.
+
+```python
+client = httpx.Client(verify=False)
+```
+
+The `client.get(...)` method and other request methods *do not* support changing the SSL settings on a per-request basis. If you need different SSL settings in different cases you should use more that one client instance, with different settings on each. Each client will then be using an isolated connection pool with a specific fixed SSL configuration on all connections within that pool.
+
+## Client Side Certificates
+
+You can also specify a local cert to use as a client-side certificate, either a path to an SSL certificate file, or two-tuple of (certificate file, key file), or a three-tuple of (certificate file, key file, password)
+
+```python
+cert = "path/to/client.pem"
+client = httpx.Client(cert=cert)
+response = client.get("https://example.org")
+```
+
+Alternatively...
+
+```python
+cert = ("path/to/client.pem", "path/to/client.key")
+client = httpx.Client(cert=cert)
+response = client.get("https://example.org")
+```
+
+Or...
+
+```python
+cert = ("path/to/client.pem", "path/to/client.key", "password")
+client = httpx.Client(cert=cert)
+response = client.get("https://example.org")
+```
+
+## Making HTTPS requests to a local server
+
+When making requests to local servers, such as a development server running on `localhost`, you will typically be using unencrypted HTTP connections.
+
+If you do need to make HTTPS connections to a local server, for example to test an HTTPS-only service, you will need to create and use your own certificates. Here's one way to do it:
+
+1. Use [trustme](https://github.com/python-trio/trustme) to generate a pair of server key/cert files, and a client cert file.
+1. Pass the server key/cert files when starting your local server. (This depends on the particular web server you're using. For example, [Uvicorn](https://www.uvicorn.org) provides the `--ssl-keyfile` and `--ssl-certfile` options.)
+1. Tell HTTPX to use the certificates stored in `client.pem`:
+
+```python
+client = httpx.Client(verify="/tmp/client.pem")
+response = client.get("https://localhost:8000")
+```
diff --git a/docs/advanced/text-encodings.md b/docs/advanced/text-encodings.md
new file mode 100644
index 00000000..5565f026
--- /dev/null
+++ b/docs/advanced/text-encodings.md
@@ -0,0 +1,75 @@
+When accessing `response.text`, we need to decode the response bytes into a unicode text representation.
+
+By default `httpx` will use `"charset"` information included in the response `Content-Type` header to determine how the response bytes should be decoded into text.
+
+In cases where no charset information is included on the response, the default behaviour is to assume "utf-8" encoding, which is by far the most widely used text encoding on the internet.
+
+## Using the default encoding
+
+To understand this better let's start by looking at the default behaviour for text decoding...
+
+```python
+import httpx
+# Instantiate a client with the default configuration.
+client = httpx.Client()
+# Using the client...
+response = client.get(...)
+print(response.encoding) # This will either print the charset given in
+ # the Content-Type charset, or else "utf-8".
+print(response.text) # The text will either be decoded with the Content-Type
+ # charset, or using "utf-8".
+```
+
+This is normally absolutely fine. Most servers will respond with a properly formatted Content-Type header, including a charset encoding. And in most cases where no charset encoding is included, UTF-8 is very likely to be used, since it is so widely adopted.
+
+## Using an explicit encoding
+
+In some cases we might be making requests to a site where no character set information is being set explicitly by the server, but we know what the encoding is. In this case it's best to set the default encoding explicitly on the client.
+
+```python
+import httpx
+# Instantiate a client with a Japanese character set as the default encoding.
+client = httpx.Client(default_encoding="shift-jis")
+# Using the client...
+response = client.get(...)
+print(response.encoding) # This will either print the charset given in
+ # the Content-Type charset, or else "shift-jis".
+print(response.text) # The text will either be decoded with the Content-Type
+ # charset, or using "shift-jis".
+```
+
+## Using auto-detection
+
+In cases where the server is not reliably including character set information, and where we don't know what encoding is being used, we can enable auto-detection to make a best-guess attempt when decoding from bytes to text.
+
+To use auto-detection you need to set the `default_encoding` argument to a callable instead of a string. This callable should be a function which takes the input bytes as an argument and returns the character set to use for decoding those bytes to text.
+
+There are two widely used Python packages which both handle this functionality:
+
+* [`chardet`](https://chardet.readthedocs.io/) - This is a well established package, and is a port of [the auto-detection code in Mozilla](https://www-archive.mozilla.org/projects/intl/chardet.html).
+* [`charset-normalizer`](https://charset-normalizer.readthedocs.io/) - A newer package, motivated by `chardet`, with a different approach.
+
+Let's take a look at installing autodetection using one of these packages...
+
+```shell
+$ pip install httpx
+$ pip install chardet
+```
+
+Once `chardet` is installed, we can configure a client to use character-set autodetection.
+
+```python
+import httpx
+import chardet
+
+def autodetect(content):
+ return chardet.detect(content).get("encoding")
+
+# Using a client with character-set autodetection enabled.
+client = httpx.Client(default_encoding=autodetect)
+response = client.get(...)
+print(response.encoding) # This will either print the charset given in
+ # the Content-Type charset, or else the auto-detected
+ # character set.
+print(response.text)
+```
diff --git a/docs/advanced/timeouts.md b/docs/advanced/timeouts.md
new file mode 100644
index 00000000..aedcfb62
--- /dev/null
+++ b/docs/advanced/timeouts.md
@@ -0,0 +1,71 @@
+HTTPX is careful to enforce timeouts everywhere by default.
+
+The default behavior is to raise a `TimeoutException` after 5 seconds of
+network inactivity.
+
+## Setting and disabling timeouts
+
+You can set timeouts for an individual request:
+
+```python
+# Using the top-level API:
+httpx.get('http://example.com/api/v1/example', timeout=10.0)
+
+# Using a client instance:
+with httpx.Client() as client:
+ client.get("http://example.com/api/v1/example", timeout=10.0)
+```
+
+Or disable timeouts for an individual request:
+
+```python
+# Using the top-level API:
+httpx.get('http://example.com/api/v1/example', timeout=None)
+
+# Using a client instance:
+with httpx.Client() as client:
+ client.get("http://example.com/api/v1/example", timeout=None)
+```
+
+## Setting a default timeout on a client
+
+You can set a timeout on a client instance, which results in the given
+`timeout` being used as the default for requests made with this client:
+
+```python
+client = httpx.Client() # Use a default 5s timeout everywhere.
+client = httpx.Client(timeout=10.0) # Use a default 10s timeout everywhere.
+client = httpx.Client(timeout=None) # Disable all timeouts by default.
+```
+
+## Fine tuning the configuration
+
+HTTPX also allows you to specify the timeout behavior in more fine grained detail.
+
+There are four different types of timeouts that may occur. These are **connect**,
+**read**, **write**, and **pool** timeouts.
+
+* The **connect** timeout specifies the maximum amount of time to wait until
+a socket connection to the requested host is established. If HTTPX is unable to connect
+within this time frame, a `ConnectTimeout` exception is raised.
+* The **read** timeout specifies the maximum duration to wait for a chunk of
+data to be received (for example, a chunk of the response body). If HTTPX is
+unable to receive data within this time frame, a `ReadTimeout` exception is raised.
+* The **write** timeout specifies the maximum duration to wait for a chunk of
+data to be sent (for example, a chunk of the request body). If HTTPX is unable
+to send data within this time frame, a `WriteTimeout` exception is raised.
+* The **pool** timeout specifies the maximum duration to wait for acquiring
+a connection from the connection pool. If HTTPX is unable to acquire a connection
+within this time frame, a `PoolTimeout` exception is raised. A related
+configuration here is the maximum number of allowable connections in the
+connection pool, which is configured by the `limits` argument.
+
+You can configure the timeout behavior for any of these values...
+
+```python
+# A client with a 60s timeout for connecting, and a 10s timeout elsewhere.
+timeout = httpx.Timeout(10.0, connect=60.0)
+client = httpx.Client(timeout=timeout)
+
+response = client.get('http://example.com/')
+```
\ No newline at end of file
diff --git a/docs/advanced/transports.md b/docs/advanced/transports.md
new file mode 100644
index 00000000..d4e7615d
--- /dev/null
+++ b/docs/advanced/transports.md
@@ -0,0 +1,454 @@
+HTTPX's `Client` also accepts a `transport` argument. This argument allows you
+to provide a custom Transport object that will be used to perform the actual
+sending of the requests.
+
+## HTTP Transport
+
+For some advanced configuration you might need to instantiate a transport
+class directly, and pass it to the client instance. One example is the
+`local_address` configuration which is only available via this low-level API.
+
+```pycon
+>>> import httpx
+>>> transport = httpx.HTTPTransport(local_address="0.0.0.0")
+>>> client = httpx.Client(transport=transport)
+```
+
+Connection retries are also available via this interface. Requests will be retried the given number of times in case an `httpx.ConnectError` or an `httpx.ConnectTimeout` occurs, allowing smoother operation under flaky networks. If you need other forms of retry behaviors, such as handling read/write errors or reacting to `503 Service Unavailable`, consider general-purpose tools such as [tenacity](https://github.com/jd/tenacity).
+
+```pycon
+>>> import httpx
+>>> transport = httpx.HTTPTransport(retries=1)
+>>> client = httpx.Client(transport=transport)
+```
+
+Similarly, instantiating a transport directly provides a `uds` option for
+connecting via a Unix Domain Socket that is only available via this low-level API:
+
+```pycon
+>>> import httpx
+>>> # Connect to the Docker API via a Unix Socket.
+>>> transport = httpx.HTTPTransport(uds="/var/run/docker.sock")
+>>> client = httpx.Client(transport=transport)
+>>> response = client.get("http://docker/info")
+>>> response.json()
+{"ID": "...", "Containers": 4, "Images": 74, ...}
+```
+
+## WSGI Transport
+
+You can configure an `httpx` client to call directly into a Python web application using the WSGI protocol.
+
+This is particularly useful for two main use-cases:
+
+* Using `httpx` as a client inside test cases.
+* Mocking out external services during tests or in dev or staging environments.
+
+### Example
+
+Here's an example of integrating against a Flask application:
+
+```python
+from flask import Flask
+import httpx
+
+
+app = Flask(__name__)
+
+@app.route("/")
+def hello():
+ return "Hello World!"
+
+transport = httpx.WSGITransport(app=app)
+with httpx.Client(transport=transport, base_url="http://testserver") as client:
+ r = client.get("/")
+ assert r.status_code == 200
+ assert r.text == "Hello World!"
+```
+
+### Configuration
+
+For some more complex cases you might need to customize the WSGI transport. This allows you to:
+
+* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
+* Mount the WSGI application at a subpath by setting `script_name` (WSGI).
+* Use a given client address for requests by setting `remote_addr` (WSGI).
+
+For example:
+
+```python
+# Instantiate a client that makes WSGI requests with a client IP of "1.2.3.4".
+transport = httpx.WSGITransport(app=app, remote_addr="1.2.3.4")
+with httpx.Client(transport=transport, base_url="http://testserver") as client:
+ ...
+```
+
+## ASGI Transport
+
+You can configure an `httpx` client to call directly into an async Python web application using the ASGI protocol.
+
+This is particularly useful for two main use-cases:
+
+* Using `httpx` as a client inside test cases.
+* Mocking out external services during tests or in dev or staging environments.
+
+### Example
+
+Let's take this Starlette application as an example:
+
+```python
+from starlette.applications import Starlette
+from starlette.responses import HTMLResponse
+from starlette.routing import Route
+
+
+async def hello(request):
+ return HTMLResponse("Hello World!")
+
+
+app = Starlette(routes=[Route("/", hello)])
+```
+
+We can make requests directly against the application, like so:
+
+```python
+transport = httpx.ASGITransport(app=app)
+
+async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
+ r = await client.get("/")
+ assert r.status_code == 200
+ assert r.text == "Hello World!"
+```
+
+### Configuration
+
+For some more complex cases you might need to customise the ASGI transport. This allows you to:
+
+* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
+* Mount the ASGI application at a subpath by setting `root_path`.
+* Use a given client address for requests by setting `client`.
+
+For example:
+
+```python
+# Instantiate a client that makes ASGI requests with a client IP of "1.2.3.4",
+# on port 123.
+transport = httpx.ASGITransport(app=app, client=("1.2.3.4", 123))
+async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
+ ...
+```
+
+See [the ASGI documentation](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope) for more details on the `client` and `root_path` keys.
+
+### ASGI startup and shutdown
+
+It is not in the scope of HTTPX to trigger ASGI lifespan events of your app.
+
+However it is suggested to use `LifespanManager` from [asgi-lifespan](https://github.com/florimondmanca/asgi-lifespan#usage) in pair with `AsyncClient`.
+
+## Custom transports
+
+A transport instance must implement the low-level Transport API which deals
+with sending a single request, and returning a response. You should either
+subclass `httpx.BaseTransport` to implement a transport to use with `Client`,
+or subclass `httpx.AsyncBaseTransport` to implement a transport to
+use with `AsyncClient`.
+
+At the layer of the transport API we're using the familiar `Request` and
+`Response` models.
+
+See the `handle_request` and `handle_async_request` docstrings for more details
+on the specifics of the Transport API.
+
+A complete example of a custom transport implementation would be:
+
+```python
+import json
+import httpx
+
+class HelloWorldTransport(httpx.BaseTransport):
+ """
+ A mock transport that always returns a JSON "Hello, world!" response.
+ """
+
+ def handle_request(self, request):
+ return httpx.Response(200, json={"text": "Hello, world!"})
+```
+
+Or this example, which uses a custom transport and `httpx.Mounts` to always redirect `http://` requests.
+
+```python
+class HTTPSRedirect(httpx.BaseTransport):
+ """
+ A transport that always redirects to HTTPS.
+ """
+ def handle_request(self, request):
+ url = request.url.copy_with(scheme="https")
+ return httpx.Response(303, headers={"Location": str(url)})
+
+# A client where any `http` requests are always redirected to `https`
+transport = httpx.Mounts({
+ 'http://': HTTPSRedirect()
+ 'https://': httpx.HTTPTransport()
+})
+client = httpx.Client(transport=transport)
+```
+
+A useful pattern here is custom transport classes that wrap the default HTTP implementation. For example...
+
+```python
+class DebuggingTransport(httpx.BaseTransport):
+ def __init__(self, **kwargs):
+ self._wrapper = httpx.HTTPTransport(**kwargs)
+
+ def handle_request(self, request):
+ print(f">>> {request}")
+ response = self._wrapper.handle_request(request)
+ print(f"<<< {response}")
+ return response
+
+ def close(self):
+ self._wrapper.close()
+
+transport = DebuggingTransport()
+client = httpx.Client(transport=transport)
+```
+
+Here's another case, where we're using a round-robin across a number of different proxies...
+
+```python
+class ProxyRoundRobin(httpx.BaseTransport):
+ def __init__(self, proxies, **kwargs):
+ self._transports = [
+ httpx.HTTPTransport(proxy=proxy, **kwargs)
+ for proxy in proxies
+ ]
+ self._idx = 0
+
+ def handle_request(self, request):
+ transport = self._transports[self._idx]
+ self._idx = (self._idx + 1) % len(self._transports)
+ return transport.handle_request(request)
+
+ def close(self):
+ for transport in self._transports:
+ transport.close()
+
+proxies = [
+ httpx.Proxy("http://127.0.0.1:8081"),
+ httpx.Proxy("http://127.0.0.1:8082"),
+ httpx.Proxy("http://127.0.0.1:8083"),
+]
+transport = ProxyRoundRobin(proxies=proxies)
+client = httpx.Client(transport=transport)
+```
+
+## Mock transports
+
+During testing it can often be useful to be able to mock out a transport,
+and return pre-determined responses, rather than making actual network requests.
+
+The `httpx.MockTransport` class accepts a handler function, which can be used
+to map requests onto pre-determined responses:
+
+```python
+def handler(request):
+ return httpx.Response(200, json={"text": "Hello, world!"})
+
+
+# Switch to a mock transport, if the TESTING environment variable is set.
+if os.environ.get('TESTING', '').upper() == "TRUE":
+ transport = httpx.MockTransport(handler)
+else:
+ transport = httpx.HTTPTransport()
+
+client = httpx.Client(transport=transport)
+```
+
+For more advanced use-cases you might want to take a look at either [the third-party
+mocking library, RESPX](https://lundberg.github.io/respx/), or the [pytest-httpx library](https://github.com/Colin-b/pytest_httpx).
+
+## Mounting transports
+
+You can also mount transports against given schemes or domains, to control
+which transport an outgoing request should be routed via, with [the same style
+used for specifying proxy routing](#routing).
+
+```python
+import httpx
+
+class HTTPSRedirectTransport(httpx.BaseTransport):
+ """
+ A transport that always redirects to HTTPS.
+ """
+
+ def handle_request(self, method, url, headers, stream, extensions):
+ scheme, host, port, path = url
+ if port is None:
+ location = b"https://%s%s" % (host, path)
+ else:
+ location = b"https://%s:%d%s" % (host, port, path)
+ stream = httpx.ByteStream(b"")
+ headers = [(b"location", location)]
+ extensions = {}
+ return 303, headers, stream, extensions
+
+
+# A client where any `http` requests are always redirected to `https`
+mounts = {'http://': HTTPSRedirectTransport()}
+client = httpx.Client(mounts=mounts)
+```
+
+A couple of other sketches of how you might take advantage of mounted transports...
+
+Disabling HTTP/2 on a single given domain...
+
+```python
+mounts = {
+ "all://": httpx.HTTPTransport(http2=True),
+ "all://*example.org": httpx.HTTPTransport()
+}
+client = httpx.Client(mounts=mounts)
+```
+
+Mocking requests to a given domain:
+
+```python
+# All requests to "example.org" should be mocked out.
+# Other requests occur as usual.
+def handler(request):
+ return httpx.Response(200, json={"text": "Hello, World!"})
+
+mounts = {"all://example.org": httpx.MockTransport(handler)}
+client = httpx.Client(mounts=mounts)
+```
+
+Adding support for custom schemes:
+
+```python
+# Support URLs like "file:///Users/sylvia_green/websites/new_client/index.html"
+mounts = {"file://": FileSystemTransport()}
+client = httpx.Client(mounts=mounts)
+```
+
+### Routing
+
+HTTPX provides a powerful mechanism for routing requests, allowing you to write complex rules that specify which transport should be used for each request.
+
+The `mounts` dictionary maps URL patterns to HTTP transports. HTTPX matches requested URLs against URL patterns to decide which transport should be used, if any. Matching is done from most specific URL patterns (e.g. `https://:`) to least specific ones (e.g. `https://`).
+
+HTTPX supports routing requests based on **scheme**, **domain**, **port**, or a combination of these.
+
+### Wildcard routing
+
+Route everything through a transport...
+
+```python
+mounts = {
+ "all://": httpx.HTTPTransport(proxy="http://localhost:8030"),
+}
+```
+
+### Scheme routing
+
+Route HTTP requests through one transport, and HTTPS requests through another...
+
+```python
+mounts = {
+ "http://": httpx.HTTPTransport(proxy="http://localhost:8030"),
+ "https://": httpx.HTTPTransport(proxy="http://localhost:8031"),
+}
+```
+
+### Domain routing
+
+Proxy all requests on domain "example.com", let other requests pass through...
+
+```python
+mounts = {
+ "all://example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
+}
+```
+
+Proxy HTTP requests on domain "example.com", let HTTPS and other requests pass through...
+
+```python
+mounts = {
+ "http://example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
+}
+```
+
+Proxy all requests to "example.com" and its subdomains, let other requests pass through...
+
+```python
+mounts = {
+ "all://*example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
+}
+```
+
+Proxy all requests to strict subdomains of "example.com", let "example.com" and other requests pass through...
+
+```python
+mounts = {
+ "all://*.example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
+}
+```
+
+### Port routing
+
+Proxy HTTPS requests on port 1234 to "example.com"...
+
+```python
+mounts = {
+ "https://example.com:1234": httpx.HTTPTransport(proxy="http://localhost:8030"),
+}
+```
+
+Proxy all requests on port 1234...
+
+```python
+mounts = {
+ "all://*:1234": httpx.HTTPTransport(proxy="http://localhost:8030"),
+}
+```
+
+### No-proxy support
+
+It is also possible to define requests that _shouldn't_ be routed through the transport.
+
+To do so, pass `None` as the proxy URL. For example...
+
+```python
+mounts = {
+ # Route requests through a proxy by default...
+ "all://": httpx.HTTPTransport(proxy="http://localhost:8031"),
+ # Except those for "example.com".
+ "all://example.com": None,
+}
+```
+
+### Complex configuration example
+
+You can combine the routing features outlined above to build complex proxy routing configurations. For example...
+
+```python
+mounts = {
+ # Route all traffic through a proxy by default...
+ "all://": httpx.HTTPTransport(proxy="http://localhost:8030"),
+ # But don't use proxies for HTTPS requests to "domain.io"...
+ "https://domain.io": None,
+ # And use another proxy for requests to "example.com" and its subdomains...
+ "all://*example.com": httpx.HTTPTransport(proxy="http://localhost:8031"),
+ # And yet another proxy if HTTP is used,
+ # and the "internal" subdomain on port 5550 is requested...
+ "http://internal.example.com:5550": httpx.HTTPTransport(proxy="http://localhost:8032"),
+}
+```
+
+### Environment variables
+
+There are also environment variables that can be used to control the dictionary of the client mounts.
+They can be used to configure HTTP proxying for clients.
+
+See documentation on [`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`](../environment_variables.md#http_proxy-https_proxy-all_proxy)
+and [`NO_PROXY`](../environment_variables.md#no_proxy) for more information.
diff --git a/docs/api.md b/docs/api.md
index 3f9878c7..d01cc649 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -114,7 +114,7 @@ what gets sent over the wire.*
'example.org'
```
-* `def __init__(url, allow_relative=False, params=None)`
+* `def __init__(url, **kwargs)`
* `.scheme` - **str**
* `.authority` - **str**
* `.host` - **str**
diff --git a/docs/async.md b/docs/async.md
index f0d7e46f..4b62d85e 100644
--- a/docs/async.md
+++ b/docs/async.md
@@ -72,7 +72,7 @@ async with httpx.AsyncClient() as client:
```
!!! warning
-In order to get the most benefit from connection pooling, make sure you're not instantiating multiple client instances - for example by using `async with` inside a "hot loop". This can be achieved either by having a single scoped client that's passed throughout wherever it's needed, or by having a single global client instance.
+ In order to get the most benefit from connection pooling, make sure you're not instantiating multiple client instances - for example by using `async with` inside a "hot loop". This can be achieved either by having a single scoped client that's passed throughout wherever it's needed, or by having a single global client instance.
Alternatively, use `await client.aclose()` if you want to close a client explicitly:
@@ -102,7 +102,7 @@ The async response streaming methods are:
* `Response.aiter_raw()` - For streaming the raw response bytes, without applying content decoding.
* `Response.aclose()` - For closing the response. You don't usually need this, since `.stream` block closes the response automatically on exit.
-For situations when context block usage is not practical, it is possible to enter "manual mode" by sending a [`Request` instance](./advanced.md#request-instances) using `client.send(..., stream=True)`.
+For situations when context block usage is not practical, it is possible to enter "manual mode" by sending a [`Request` instance](advanced/clients.md#request-instances) using `client.send(..., stream=True)`.
Example in the context of forwarding the response to a streaming web endpoint with [Starlette](https://www.starlette.io):
@@ -209,54 +209,4 @@ anyio.run(main, backend='trio')
## Calling into Python Web Apps
-Just as `httpx.Client` allows you to call directly into WSGI web applications,
-the `httpx.AsyncClient` class allows you to call directly into ASGI web applications.
-
-Let's take this Starlette application as an example:
-
-```python
-from starlette.applications import Starlette
-from starlette.responses import HTMLResponse
-from starlette.routing import Route
-
-
-async def hello(request):
- return HTMLResponse("Hello World!")
-
-
-app = Starlette(routes=[Route("/", hello)])
-```
-
-We can make requests directly against the application, like so:
-
-```pycon
->>> import httpx
->>> async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
-... r = await client.get("/")
-... assert r.status_code == 200
-... assert r.text == "Hello World!"
-```
-
-For some more complex cases you might need to customise the ASGI transport. This allows you to:
-
-* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
-* Mount the ASGI application at a subpath by setting `root_path`.
-* Use a given client address for requests by setting `client`.
-
-For example:
-
-```python
-# Instantiate a client that makes ASGI requests with a client IP of "1.2.3.4",
-# on port 123.
-transport = httpx.ASGITransport(app=app, client=("1.2.3.4", 123))
-async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
- ...
-```
-
-See [the ASGI documentation](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope) for more details on the `client` and `root_path` keys.
-
-## Startup/shutdown of ASGI apps
-
-It is not in the scope of HTTPX to trigger lifespan events of your app.
-
-However it is suggested to use `LifespanManager` from [asgi-lifespan](https://github.com/florimondmanca/asgi-lifespan#usage) in pair with `AsyncClient`.
+For details on calling directly into ASGI applications, see [the `ASGITransport` docs](../advanced/transports#asgitransport).
\ No newline at end of file
diff --git a/docs/compatibility.md b/docs/compatibility.md
index 7190b658..e820a67b 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -159,7 +159,7 @@ httpx.get('https://www.example.com', timeout=None)
HTTPX uses the mounts argument for HTTP proxying and transport routing.
It can do much more than proxies and allows you to configure more than just the proxy route.
-For more detailed documentation, see [Mounting Transports](advanced.md#mounting-transports).
+For more detailed documentation, see [Mounting Transports](advanced/transports.md#mounting-transports).
When using `httpx.Client(mounts={...})` to map to a selection of different transports, we use full URL schemes, such as `mounts={"http://": ..., "https://": ...}`.
@@ -197,9 +197,9 @@ We don't support `response.is_ok` since the naming is ambiguous there, and might
## Request instantiation
-There is no notion of [prepared requests](https://requests.readthedocs.io/en/stable/user/advanced/#prepared-requests) in HTTPX. If you need to customize request instantiation, see [Request instances](advanced.md#request-instances).
+There is no notion of [prepared requests](https://requests.readthedocs.io/en/stable/user/advanced/#prepared-requests) in HTTPX. If you need to customize request instantiation, see [Request instances](advanced/clients.md#request-instances).
-Besides, `httpx.Request()` does not support the `auth`, `timeout`, `follow_redirects`, `mounts`, `verify` and `cert` parameters. However these are available in `httpx.request`, `httpx.get`, `httpx.post` etc., as well as on [`Client` instances](advanced.md#client-instances).
+Besides, `httpx.Request()` does not support the `auth`, `timeout`, `follow_redirects`, `mounts`, `verify` and `cert` parameters. However these are available in `httpx.request`, `httpx.get`, `httpx.post` etc., as well as on [`Client` instances](advanced/clients.md#client-instances).
## Mocking
@@ -227,4 +227,4 @@ For both query params (`params=`) and form data (`data=`), `requests` supports s
In HTTPX, event hooks may access properties of requests and responses, but event hook callbacks cannot mutate the original request/response.
-If you are looking for more control, consider checking out [Custom Transports](advanced.md#custom-transports).
+If you are looking for more control, consider checking out [Custom Transports](advanced/transports.md#custom-transports).
diff --git a/docs/contributing.md b/docs/contributing.md
index 47dd9dc5..0d3ad5f1 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -206,8 +206,8 @@ UI options.
At this point the server is ready to start serving requests, you'll need to
configure HTTPX as described in the
-[proxy section](https://www.python-httpx.org/advanced/#http-proxying) and
-the [SSL certificates section](https://www.python-httpx.org/advanced/#ssl-certificates),
+[proxy section](https://www.python-httpx.org/advanced/proxies/#http-proxies) and
+the [SSL certificates section](https://www.python-httpx.org/advanced/ssl/),
this is where our previously generated `client.pem` comes in:
```
diff --git a/docs/environment_variables.md b/docs/environment_variables.md
index 71329fc1..28fdc5e8 100644
--- a/docs/environment_variables.md
+++ b/docs/environment_variables.md
@@ -75,7 +75,7 @@ The environment variables documented below are used as a convention by various H
* [cURL](https://github.com/curl/curl/blob/master/docs/MANUAL.md#environment-variables)
* [requests](https://github.com/psf/requests/blob/master/docs/user/advanced.rst#proxies)
-For more information on using proxies in HTTPX, see [HTTP Proxying](advanced.md#http-proxying).
+For more information on using proxies in HTTPX, see [HTTP Proxying](advanced/proxies.md#http-proxying).
### `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`
diff --git a/docs/img/speakeasy.png b/docs/img/speakeasy.png
new file mode 100644
index 00000000..f8a22cca
Binary files /dev/null and b/docs/img/speakeasy.png differ
diff --git a/docs/index.md b/docs/index.md
index ec974669..c2210bc7 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -68,7 +68,7 @@ HTTPX builds on the well-established usability of `requests`, and gives you:
* A broadly [requests-compatible API](compatibility.md).
* Standard synchronous interface, but with [async support if you need it](async.md).
* HTTP/1.1 [and HTTP/2 support](http2.md).
-* Ability to make requests directly to [WSGI applications](advanced.md#calling-into-python-web-apps) or [ASGI applications](async.md#calling-into-python-web-apps).
+* Ability to make requests directly to [WSGI applications](advanced/transports.md#wsgi-transport) or [ASGI applications](advanced/transports.md#asgi-transport).
* Strict timeouts everywhere.
* Fully type annotated.
* 100% test coverage.
@@ -95,7 +95,7 @@ Plus all the standard features of `requests`...
For a run-through of all the basics, head over to the [QuickStart](quickstart.md).
-For more advanced topics, see the [Advanced Usage](advanced.md) section,
+For more advanced topics, see the **Advanced** section,
the [async support](async.md) section, or the [HTTP/2](http2.md) section.
The [Developer Interface](api.md) provides a comprehensive API reference.
@@ -119,6 +119,7 @@ As well as these optional installs:
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
+* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design
@@ -138,10 +139,10 @@ Or, to include the optional HTTP/2 support, use:
$ pip install httpx[http2]
```
-To include the optional brotli decoder support, use:
+To include the optional brotli and zstandard decoders support, use:
```shell
-$ pip install httpx[brotli]
+$ pip install httpx[brotli,zstd]
```
HTTPX requires Python 3.8+
diff --git a/docs/overrides/partials/nav.html b/docs/overrides/partials/nav.html
new file mode 100644
index 00000000..d5a413f0
--- /dev/null
+++ b/docs/overrides/partials/nav.html
@@ -0,0 +1,54 @@
+{% import "partials/nav-item.html" as item with context %}
+
+
+ {% set class = "md-nav md-nav--primary" %}
+ {% if "navigation.tabs" in features %}
+ {% set class = class ~ " md-nav--lifted" %}
+ {% endif %}
+ {% if "toc.integrate" in features %}
+ {% set class = class ~ " md-nav--integrated" %}
+ {% endif %}
+
+
+
+
+
+
+
+ {% include "partials/logo.html" %}
+
+ {{ config.site_name }}
+
+
+
+ {% if config.repo_url %}
+
+ {% include "partials/source.html" %}
+
+ {% endif %}
+
+
+
+ {% for nav_item in nav %}
+ {% set path = "__nav_" ~ loop.index %}
+ {{ item.render(nav_item, path, 1) }}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 068547ff..aa203a83 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -100,7 +100,8 @@ b'\n\n\nExample Domain ...'
Any `gzip` and `deflate` HTTP response encodings will automatically
be decoded for you. If `brotlipy` is installed, then the `brotli` response
-encoding will also be supported.
+encoding will be supported. If `zstandard` is installed, then `zstd`
+response encodings will also be supported.
For example, to create an image from binary data returned by a request, you can use the following code:
@@ -362,7 +363,8 @@ Or stream the text, on a line-by-line basis...
HTTPX will use universal line endings, normalising all cases to `\n`.
-In some cases you might want to access the raw bytes on the response without applying any HTTP content decoding. In this case any content encoding that the web server has applied such as `gzip`, `deflate`, or `brotli` will not be automatically decoded.
+In some cases you might want to access the raw bytes on the response without applying any HTTP content decoding. In this case any content encoding that the web server has applied such as `gzip`, `deflate`, `brotli`, or `zstd` will
+not be automatically decoded.
```pycon
>>> with httpx.stream("GET", "https://www.example.com") as r:
@@ -462,7 +464,7 @@ You can also disable the timeout behavior completely...
>>> httpx.get('https://github.com/', timeout=None)
```
-For advanced timeout management, see [Timeout fine-tuning](advanced.md#fine-tuning-the-configuration).
+For advanced timeout management, see [Timeout fine-tuning](advanced/timeouts.md#fine-tuning-the-configuration).
## Authentication
diff --git a/docs/third_party_packages.md b/docs/third_party_packages.md
index 3d5f4778..f6ce96d7 100644
--- a/docs/third_party_packages.md
+++ b/docs/third_party_packages.md
@@ -28,7 +28,7 @@ An asynchronous GitHub API library. Includes [HTTPX support](https://gidgethub.r
[GitHub](https://github.com/Colin-b/httpx_auth) - [Documentation](https://colin-b.github.io/httpx_auth/)
-Provides authentication classes to be used with HTTPX [authentication parameter](advanced.md#customizing-authentication).
+Provides authentication classes to be used with HTTPX [authentication parameter](advanced/authentication.md#customizing-authentication).
### pytest-HTTPX
@@ -80,4 +80,4 @@ A library for scraping the web built on top of HTTPX.
[GitHub](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165da388e546e)
-This public gist provides an example implementation for a [custom transport](advanced.md#custom-transports) implementation on top of the battle-tested [`urllib3`](https://urllib3.readthedocs.io) library.
+This public gist provides an example implementation for a [custom transport](advanced/transports.md#custom-transports) implementation on top of the battle-tested [`urllib3`](https://urllib3.readthedocs.io) library.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index a0cb210c..a2ca15f5 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -27,7 +27,7 @@ mounts = {
Using this setup, you're telling HTTPX to connect to the proxy using HTTP for HTTP requests, and using HTTPS for HTTPS requests.
-But if you get the error above, it is likely that your proxy doesn't support connecting via HTTPS. Don't worry: that's a [common gotcha](advanced.md#example).
+But if you get the error above, it is likely that your proxy doesn't support connecting via HTTPS. Don't worry: that's a [common gotcha](advanced/proxies.md#http-proxies).
Change the scheme of your HTTPS proxy to `http://...` instead of `https://...`:
@@ -46,7 +46,7 @@ with httpx.Client(proxy=proxy) as client:
...
```
-For more information, see [Proxies: FORWARD vs TUNNEL](advanced.md#forward-vs-tunnel).
+For more information, see [Proxies: FORWARD vs TUNNEL](advanced/proxies.md#forward-vs-tunnel).
---
diff --git a/httpx/__init__.py b/httpx/__init__.py
index f61112f8..e9addde0 100644
--- a/httpx/__init__.py
+++ b/httpx/__init__.py
@@ -1,48 +1,15 @@
from .__version__ import __description__, __title__, __version__
-from ._api import delete, get, head, options, patch, post, put, request, stream
-from ._auth import Auth, BasicAuth, DigestAuth, NetRCAuth
-from ._client import USE_CLIENT_DEFAULT, AsyncClient, Client
-from ._config import Limits, Proxy, Timeout, create_ssl_context
-from ._content import ByteStream
-from ._exceptions import (
- CloseError,
- ConnectError,
- ConnectTimeout,
- CookieConflict,
- DecodingError,
- HTTPError,
- HTTPStatusError,
- InvalidURL,
- LocalProtocolError,
- NetworkError,
- PoolTimeout,
- ProtocolError,
- ProxyError,
- ReadError,
- ReadTimeout,
- RemoteProtocolError,
- RequestError,
- RequestNotRead,
- ResponseNotRead,
- StreamClosed,
- StreamConsumed,
- StreamError,
- TimeoutException,
- TooManyRedirects,
- TransportError,
- UnsupportedProtocol,
- WriteError,
- WriteTimeout,
-)
-from ._models import Cookies, Headers, Request, Response
-from ._status_codes import codes
-from ._transports.asgi import ASGITransport
-from ._transports.base import AsyncBaseTransport, BaseTransport
-from ._transports.default import AsyncHTTPTransport, HTTPTransport
-from ._transports.mock import MockTransport
-from ._transports.wsgi import WSGITransport
-from ._types import AsyncByteStream, SyncByteStream
-from ._urls import URL, QueryParams
+from ._api import *
+from ._auth import *
+from ._client import *
+from ._config import *
+from ._content import *
+from ._exceptions import *
+from ._models import *
+from ._status_codes import *
+from ._transports import *
+from ._types import *
+from ._urls import *
try:
from ._main import main
diff --git a/httpx/__version__.py b/httpx/__version__.py
index 3edc842c..5eaaddba 100644
--- a/httpx/__version__.py
+++ b/httpx/__version__.py
@@ -1,3 +1,3 @@
__title__ = "httpx"
__description__ = "A next generation HTTP client, for Python 3."
-__version__ = "0.26.0"
+__version__ = "0.27.2"
diff --git a/httpx/_api.py b/httpx/_api.py
index c7af9472..4e98b606 100644
--- a/httpx/_api.py
+++ b/httpx/_api.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import typing
from contextlib import contextmanager
@@ -16,29 +18,41 @@ from ._types import (
RequestData,
RequestFiles,
TimeoutTypes,
- URLTypes,
VerifyTypes,
)
+from ._urls import URL
+
+__all__ = [
+ "delete",
+ "get",
+ "head",
+ "options",
+ "patch",
+ "post",
+ "put",
+ "request",
+ "stream",
+]
def request(
method: str,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ params: QueryParamTypes | None = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
trust_env: bool = True,
) -> Response:
"""
@@ -118,22 +132,22 @@ def request(
@contextmanager
def stream(
method: str,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ params: QueryParamTypes | None = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
trust_env: bool = True,
) -> typing.Iterator[Response]:
"""
@@ -171,16 +185,16 @@ def stream(
def get(
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
follow_redirects: bool = False,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
@@ -211,16 +225,16 @@ def get(
def options(
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
follow_redirects: bool = False,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
@@ -251,16 +265,16 @@ def options(
def head(
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
follow_redirects: bool = False,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
@@ -291,20 +305,20 @@ def head(
def post(
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
follow_redirects: bool = False,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
@@ -336,20 +350,20 @@ def post(
def put(
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
follow_redirects: bool = False,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
@@ -381,20 +395,20 @@ def put(
def patch(
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
follow_redirects: bool = False,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
@@ -426,16 +440,16 @@ def patch(
def delete(
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Optional[AuthTypes] = None,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | None = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
follow_redirects: bool = False,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
diff --git a/httpx/_auth.py b/httpx/_auth.py
index e8bc0cd9..b03971ab 100644
--- a/httpx/_auth.py
+++ b/httpx/_auth.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import hashlib
import os
import re
@@ -14,6 +16,9 @@ if typing.TYPE_CHECKING: # pragma: no cover
from hashlib import _Hash
+__all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"]
+
+
class Auth:
"""
Base class for all authentication schemes.
@@ -124,18 +129,14 @@ class BasicAuth(Auth):
and uses HTTP Basic authentication.
"""
- def __init__(
- self, username: typing.Union[str, bytes], password: typing.Union[str, bytes]
- ) -> None:
+ def __init__(self, username: str | bytes, password: str | bytes) -> None:
self._auth_header = self._build_auth_header(username, password)
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
request.headers["Authorization"] = self._auth_header
yield request
- def _build_auth_header(
- self, username: typing.Union[str, bytes], password: typing.Union[str, bytes]
- ) -> str:
+ def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
userpass = b":".join((to_bytes(username), to_bytes(password)))
token = b64encode(userpass).decode()
return f"Basic {token}"
@@ -146,7 +147,7 @@ class NetRCAuth(Auth):
Use a 'netrc' file to lookup basic auth credentials based on the url host.
"""
- def __init__(self, file: typing.Optional[str] = None) -> None:
+ def __init__(self, file: str | None = None) -> None:
# Lazily import 'netrc'.
# There's no need for us to load this module unless 'NetRCAuth' is being used.
import netrc
@@ -165,16 +166,14 @@ class NetRCAuth(Auth):
)
yield request
- def _build_auth_header(
- self, username: typing.Union[str, bytes], password: typing.Union[str, bytes]
- ) -> str:
+ def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
userpass = b":".join((to_bytes(username), to_bytes(password)))
token = b64encode(userpass).decode()
return f"Basic {token}"
class DigestAuth(Auth):
- _ALGORITHM_TO_HASH_FUNCTION: typing.Dict[str, typing.Callable[[bytes], "_Hash"]] = {
+ _ALGORITHM_TO_HASH_FUNCTION: dict[str, typing.Callable[[bytes], _Hash]] = {
"MD5": hashlib.md5,
"MD5-SESS": hashlib.md5,
"SHA": hashlib.sha1,
@@ -185,12 +184,10 @@ class DigestAuth(Auth):
"SHA-512-SESS": hashlib.sha512,
}
- def __init__(
- self, username: typing.Union[str, bytes], password: typing.Union[str, bytes]
- ) -> None:
+ def __init__(self, username: str | bytes, password: str | bytes) -> None:
self._username = to_bytes(username)
self._password = to_bytes(password)
- self._last_challenge: typing.Optional[_DigestAuthChallenge] = None
+ self._last_challenge: _DigestAuthChallenge | None = None
self._nonce_count = 1
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
@@ -226,7 +223,7 @@ class DigestAuth(Auth):
def _parse_challenge(
self, request: Request, response: Response, auth_header: str
- ) -> "_DigestAuthChallenge":
+ ) -> _DigestAuthChallenge:
"""
Returns a challenge from a Digest WWW-Authenticate header.
These take the form of:
@@ -237,7 +234,7 @@ class DigestAuth(Auth):
# This method should only ever have been called with a Digest auth header.
assert scheme.lower() == "digest"
- header_dict: typing.Dict[str, str] = {}
+ header_dict: dict[str, str] = {}
for field in parse_http_list(fields):
key, value = field.strip().split("=", 1)
header_dict[key] = unquote(value)
@@ -256,7 +253,7 @@ class DigestAuth(Auth):
raise ProtocolError(message, request=request) from exc
def _build_auth_header(
- self, request: Request, challenge: "_DigestAuthChallenge"
+ self, request: Request, challenge: _DigestAuthChallenge
) -> str:
hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()]
@@ -311,7 +308,7 @@ class DigestAuth(Auth):
return hashlib.sha1(s).hexdigest()[:16].encode()
- def _get_header_value(self, header_fields: typing.Dict[str, bytes]) -> str:
+ def _get_header_value(self, header_fields: dict[str, bytes]) -> str:
NON_QUOTED_FIELDS = ("algorithm", "qop", "nc")
QUOTED_TEMPLATE = '{}="{}"'
NON_QUOTED_TEMPLATE = "{}={}"
@@ -329,9 +326,7 @@ class DigestAuth(Auth):
return header_value
- def _resolve_qop(
- self, qop: typing.Optional[bytes], request: Request
- ) -> typing.Optional[bytes]:
+ def _resolve_qop(self, qop: bytes | None, request: Request) -> bytes | None:
if qop is None:
return None
qops = re.split(b", ?", qop)
@@ -349,5 +344,5 @@ class _DigestAuthChallenge(typing.NamedTuple):
realm: bytes
nonce: bytes
algorithm: str
- opaque: typing.Optional[bytes]
- qop: typing.Optional[bytes]
+ opaque: bytes | None
+ qop: bytes | None
diff --git a/httpx/_client.py b/httpx/_client.py
index a0b4209c..26610f6e 100644
--- a/httpx/_client.py
+++ b/httpx/_client.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import datetime
import enum
import logging
@@ -44,7 +46,6 @@ from ._types import (
RequestFiles,
SyncByteStream,
TimeoutTypes,
- URLTypes,
VerifyTypes,
)
from ._urls import URL, QueryParams
@@ -56,6 +57,8 @@ from ._utils import (
same_origin,
)
+__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]
+
# The type annotation for @classmethod and context managers here follows PEP 484
# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods
T = typing.TypeVar("T", bound="Client")
@@ -160,19 +163,17 @@ class BaseClient:
def __init__(
self,
*,
- auth: typing.Optional[AuthTypes] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
+ auth: AuthTypes | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
- event_hooks: typing.Optional[
- typing.Mapping[str, typing.List[EventHook]]
- ] = None,
- base_url: URLTypes = "",
+ event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
+ base_url: URL | str = "",
trust_env: bool = True,
- default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8",
+ default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
) -> None:
event_hooks = {} if event_hooks is None else event_hooks
@@ -210,8 +211,8 @@ class BaseClient:
return url.copy_with(raw_path=url.raw_path + b"/")
def _get_proxy_map(
- self, proxies: typing.Optional[ProxiesTypes], allow_env_proxies: bool
- ) -> typing.Dict[str, typing.Optional[Proxy]]:
+ self, proxies: ProxiesTypes | None, allow_env_proxies: bool
+ ) -> dict[str, Proxy | None]:
if proxies is None:
if allow_env_proxies:
return {
@@ -238,20 +239,18 @@ class BaseClient:
self._timeout = Timeout(timeout)
@property
- def event_hooks(self) -> typing.Dict[str, typing.List[EventHook]]:
+ def event_hooks(self) -> dict[str, list[EventHook]]:
return self._event_hooks
@event_hooks.setter
- def event_hooks(
- self, event_hooks: typing.Dict[str, typing.List[EventHook]]
- ) -> None:
+ def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None:
self._event_hooks = {
"request": list(event_hooks.get("request", [])),
"response": list(event_hooks.get("response", [])),
}
@property
- def auth(self) -> typing.Optional[Auth]:
+ def auth(self) -> Auth | None:
"""
Authentication class used when none is passed at the request-level.
@@ -273,7 +272,7 @@ class BaseClient:
return self._base_url
@base_url.setter
- def base_url(self, url: URLTypes) -> None:
+ def base_url(self, url: URL | str) -> None:
self._base_url = self._enforce_trailing_slash(URL(url))
@property
@@ -321,17 +320,17 @@ class BaseClient:
def build_request(
self,
method: str,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Request:
"""
Build and return a request instance.
@@ -342,7 +341,7 @@ class BaseClient:
See also: [Request instances][0]
- [0]: /advanced/#request-instances
+ [0]: /advanced/clients/#request-instances
"""
url = self._merge_url(url)
headers = self._merge_headers(headers)
@@ -369,7 +368,7 @@ class BaseClient:
extensions=extensions,
)
- def _merge_url(self, url: URLTypes) -> URL:
+ def _merge_url(self, url: URL | str) -> URL:
"""
Merge a URL argument together with any 'base_url' on the client,
to create the URL used for the outgoing request.
@@ -391,9 +390,7 @@ class BaseClient:
return self.base_url.copy_with(raw_path=merge_raw_path)
return merge_url
- def _merge_cookies(
- self, cookies: typing.Optional[CookieTypes] = None
- ) -> typing.Optional[CookieTypes]:
+ def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | None:
"""
Merge a cookies argument together with any cookies on the client,
to create the cookies used for the outgoing request.
@@ -404,9 +401,7 @@ class BaseClient:
return merged_cookies
return cookies
- def _merge_headers(
- self, headers: typing.Optional[HeaderTypes] = None
- ) -> typing.Optional[HeaderTypes]:
+ def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None:
"""
Merge a headers argument together with any headers on the client,
to create the headers used for the outgoing request.
@@ -416,8 +411,8 @@ class BaseClient:
return merged_headers
def _merge_queryparams(
- self, params: typing.Optional[QueryParamTypes] = None
- ) -> typing.Optional[QueryParamTypes]:
+ self, params: QueryParamTypes | None = None
+ ) -> QueryParamTypes | None:
"""
Merge a queryparams argument together with any queryparams on the client,
to create the queryparams used for the outgoing request.
@@ -427,7 +422,7 @@ class BaseClient:
return merged_queryparams.merge(params)
return params
- def _build_auth(self, auth: typing.Optional[AuthTypes]) -> typing.Optional[Auth]:
+ def _build_auth(self, auth: AuthTypes | None) -> Auth | None:
if auth is None:
return None
elif isinstance(auth, tuple):
@@ -442,7 +437,7 @@ class BaseClient:
def _build_request_auth(
self,
request: Request,
- auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
) -> Auth:
auth = (
self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth)
@@ -557,7 +552,7 @@ class BaseClient:
def _redirect_stream(
self, request: Request, method: str
- ) -> typing.Optional[typing.Union[SyncByteStream, AsyncByteStream]]:
+ ) -> SyncByteStream | AsyncByteStream | None:
"""
Return the body that should be used for the redirect request.
"""
@@ -566,6 +561,15 @@ class BaseClient:
return request.stream
+ def _set_timeout(self, request: Request) -> None:
+ if "timeout" not in request.extensions:
+ timeout = (
+ self.timeout
+ if isinstance(self.timeout, UseClientDefault)
+ else Timeout(self.timeout)
+ )
+ request.extensions = dict(**request.extensions, timeout=timeout.as_dict())
+
class Client(BaseClient):
"""
@@ -624,31 +628,27 @@ class Client(BaseClient):
def __init__(
self,
*,
- auth: typing.Optional[AuthTypes] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
+ auth: AuthTypes | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
- mounts: typing.Optional[
- typing.Mapping[str, typing.Optional[BaseTransport]]
- ] = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
+ mounts: None | (typing.Mapping[str, BaseTransport | None]) = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
limits: Limits = DEFAULT_LIMITS,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
- event_hooks: typing.Optional[
- typing.Mapping[str, typing.List[EventHook]]
- ] = None,
- base_url: URLTypes = "",
- transport: typing.Optional[BaseTransport] = None,
- app: typing.Optional[typing.Callable[..., typing.Any]] = None,
+ event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
+ base_url: URL | str = "",
+ transport: BaseTransport | None = None,
+ app: typing.Callable[..., typing.Any] | None = None,
trust_env: bool = True,
- default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8",
+ default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
) -> None:
super().__init__(
auth=auth,
@@ -682,6 +682,13 @@ class Client(BaseClient):
if proxy:
raise RuntimeError("Use either `proxy` or 'proxies', not both.")
+ if app:
+ message = (
+ "The 'app' shortcut is now deprecated."
+ " Use the explicit style 'transport=WSGITransport(app=...)' instead."
+ )
+ warnings.warn(message, DeprecationWarning)
+
allow_env_proxies = trust_env and app is None and transport is None
proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies)
@@ -695,7 +702,7 @@ class Client(BaseClient):
app=app,
trust_env=trust_env,
)
- self._mounts: typing.Dict[URLPattern, typing.Optional[BaseTransport]] = {
+ self._mounts: dict[URLPattern, BaseTransport | None] = {
URLPattern(key): None
if proxy is None
else self._init_proxy_transport(
@@ -719,12 +726,12 @@ class Client(BaseClient):
def _init_transport(
self,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
- transport: typing.Optional[BaseTransport] = None,
- app: typing.Optional[typing.Callable[..., typing.Any]] = None,
+ transport: BaseTransport | None = None,
+ app: typing.Callable[..., typing.Any] | None = None,
trust_env: bool = True,
) -> BaseTransport:
if transport is not None:
@@ -746,7 +753,7 @@ class Client(BaseClient):
self,
proxy: Proxy,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
@@ -776,19 +783,19 @@ class Client(BaseClient):
def request(
self,
method: str,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Build and send a request.
@@ -804,7 +811,7 @@ class Client(BaseClient):
[Merging of configuration][0] for how the various parameters
are merged with client-level configuration.
- [0]: /advanced/#merging-of-configuration
+ [0]: /advanced/clients/#merging-of-configuration
"""
if cookies is not None:
message = (
@@ -833,19 +840,19 @@ class Client(BaseClient):
def stream(
self,
method: str,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> typing.Iterator[Response]:
"""
Alternative to `httpx.request()` that streams the response body
@@ -886,8 +893,8 @@ class Client(BaseClient):
request: Request,
*,
stream: bool = False,
- auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
) -> Response:
"""
Send a request.
@@ -900,7 +907,7 @@ class Client(BaseClient):
See also: [Request instances][0]
- [0]: /advanced/#request-instances
+ [0]: /advanced/clients/#request-instances
"""
if self._state == ClientState.CLOSED:
raise RuntimeError("Cannot send a request, as the client has been closed.")
@@ -912,6 +919,8 @@ class Client(BaseClient):
else follow_redirects
)
+ self._set_timeout(request)
+
auth = self._build_request_auth(request, auth)
response = self._send_handling_auth(
@@ -935,7 +944,7 @@ class Client(BaseClient):
request: Request,
auth: Auth,
follow_redirects: bool,
- history: typing.List[Response],
+ history: list[Response],
) -> Response:
auth_flow = auth.sync_auth_flow(request)
try:
@@ -968,7 +977,7 @@ class Client(BaseClient):
self,
request: Request,
follow_redirects: bool,
- history: typing.List[Response],
+ history: list[Response],
) -> Response:
while True:
if len(history) > self.max_redirects:
@@ -1039,15 +1048,15 @@ class Client(BaseClient):
def get(
self,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `GET` request.
@@ -1068,15 +1077,15 @@ class Client(BaseClient):
def options(
self,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send an `OPTIONS` request.
@@ -1097,15 +1106,15 @@ class Client(BaseClient):
def head(
self,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `HEAD` request.
@@ -1126,19 +1135,19 @@ class Client(BaseClient):
def post(
self,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `POST` request.
@@ -1163,19 +1172,19 @@ class Client(BaseClient):
def put(
self,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `PUT` request.
@@ -1200,19 +1209,19 @@ class Client(BaseClient):
def patch(
self,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `PATCH` request.
@@ -1237,15 +1246,15 @@ class Client(BaseClient):
def delete(
self,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `DELETE` request.
@@ -1296,9 +1305,9 @@ class Client(BaseClient):
def __exit__(
self,
- exc_type: typing.Optional[typing.Type[BaseException]] = None,
- exc_value: typing.Optional[BaseException] = None,
- traceback: typing.Optional[TracebackType] = None,
+ exc_type: type[BaseException] | None = None,
+ exc_value: BaseException | None = None,
+ traceback: TracebackType | None = None,
) -> None:
self._state = ClientState.CLOSED
@@ -1366,31 +1375,27 @@ class AsyncClient(BaseClient):
def __init__(
self,
*,
- auth: typing.Optional[AuthTypes] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
+ auth: AuthTypes | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
- proxy: typing.Optional[ProxyTypes] = None,
- proxies: typing.Optional[ProxiesTypes] = None,
- mounts: typing.Optional[
- typing.Mapping[str, typing.Optional[AsyncBaseTransport]]
- ] = None,
+ proxy: ProxyTypes | None = None,
+ proxies: ProxiesTypes | None = None,
+ mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
limits: Limits = DEFAULT_LIMITS,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
- event_hooks: typing.Optional[
- typing.Mapping[str, typing.List[typing.Callable[..., typing.Any]]]
- ] = None,
- base_url: URLTypes = "",
- transport: typing.Optional[AsyncBaseTransport] = None,
- app: typing.Optional[typing.Callable[..., typing.Any]] = None,
+ event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
+ base_url: URL | str = "",
+ transport: AsyncBaseTransport | None = None,
+ app: typing.Callable[..., typing.Any] | None = None,
trust_env: bool = True,
- default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8",
+ default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
) -> None:
super().__init__(
auth=auth,
@@ -1424,6 +1429,13 @@ class AsyncClient(BaseClient):
if proxy:
raise RuntimeError("Use either `proxy` or 'proxies', not both.")
+ if app:
+ message = (
+ "The 'app' shortcut is now deprecated."
+ " Use the explicit style 'transport=ASGITransport(app=...)' instead."
+ )
+ warnings.warn(message, DeprecationWarning)
+
allow_env_proxies = trust_env and app is None and transport is None
proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies)
@@ -1438,7 +1450,7 @@ class AsyncClient(BaseClient):
trust_env=trust_env,
)
- self._mounts: typing.Dict[URLPattern, typing.Optional[AsyncBaseTransport]] = {
+ self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
URLPattern(key): None
if proxy is None
else self._init_proxy_transport(
@@ -1461,12 +1473,12 @@ class AsyncClient(BaseClient):
def _init_transport(
self,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
- transport: typing.Optional[AsyncBaseTransport] = None,
- app: typing.Optional[typing.Callable[..., typing.Any]] = None,
+ transport: AsyncBaseTransport | None = None,
+ app: typing.Callable[..., typing.Any] | None = None,
trust_env: bool = True,
) -> AsyncBaseTransport:
if transport is not None:
@@ -1488,7 +1500,7 @@ class AsyncClient(BaseClient):
self,
proxy: Proxy,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
@@ -1518,19 +1530,19 @@ class AsyncClient(BaseClient):
async def request(
self,
method: str,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Build and send a request.
@@ -1546,7 +1558,7 @@ class AsyncClient(BaseClient):
and [Merging of configuration][0] for how the various parameters
are merged with client-level configuration.
- [0]: /advanced/#merging-of-configuration
+ [0]: /advanced/clients/#merging-of-configuration
"""
if cookies is not None: # pragma: no cover
@@ -1576,19 +1588,19 @@ class AsyncClient(BaseClient):
async def stream(
self,
method: str,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> typing.AsyncIterator[Response]:
"""
Alternative to `httpx.request()` that streams the response body
@@ -1629,8 +1641,8 @@ class AsyncClient(BaseClient):
request: Request,
*,
stream: bool = False,
- auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
) -> Response:
"""
Send a request.
@@ -1643,7 +1655,7 @@ class AsyncClient(BaseClient):
See also: [Request instances][0]
- [0]: /advanced/#request-instances
+ [0]: /advanced/clients/#request-instances
"""
if self._state == ClientState.CLOSED:
raise RuntimeError("Cannot send a request, as the client has been closed.")
@@ -1655,6 +1667,8 @@ class AsyncClient(BaseClient):
else follow_redirects
)
+ self._set_timeout(request)
+
auth = self._build_request_auth(request, auth)
response = await self._send_handling_auth(
@@ -1678,7 +1692,7 @@ class AsyncClient(BaseClient):
request: Request,
auth: Auth,
follow_redirects: bool,
- history: typing.List[Response],
+ history: list[Response],
) -> Response:
auth_flow = auth.async_auth_flow(request)
try:
@@ -1711,7 +1725,7 @@ class AsyncClient(BaseClient):
self,
request: Request,
follow_redirects: bool,
- history: typing.List[Response],
+ history: list[Response],
) -> Response:
while True:
if len(history) > self.max_redirects:
@@ -1782,15 +1796,15 @@ class AsyncClient(BaseClient):
async def get(
self,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault, None] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `GET` request.
@@ -1811,15 +1825,15 @@ class AsyncClient(BaseClient):
async def options(
self,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send an `OPTIONS` request.
@@ -1840,15 +1854,15 @@ class AsyncClient(BaseClient):
async def head(
self,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `HEAD` request.
@@ -1869,19 +1883,19 @@ class AsyncClient(BaseClient):
async def post(
self,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `POST` request.
@@ -1906,19 +1920,19 @@ class AsyncClient(BaseClient):
async def put(
self,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `PUT` request.
@@ -1943,19 +1957,19 @@ class AsyncClient(BaseClient):
async def patch(
self,
- url: URLTypes,
+ url: URL | str,
*,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `PATCH` request.
@@ -1980,15 +1994,15 @@ class AsyncClient(BaseClient):
async def delete(
self,
- url: URLTypes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- auth: typing.Union[AuthTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- follow_redirects: typing.Union[bool, UseClientDefault] = USE_CLIENT_DEFAULT,
- timeout: typing.Union[TimeoutTypes, UseClientDefault] = USE_CLIENT_DEFAULT,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
+ timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
+ extensions: RequestExtensions | None = None,
) -> Response:
"""
Send a `DELETE` request.
@@ -2039,9 +2053,9 @@ class AsyncClient(BaseClient):
async def __aexit__(
self,
- exc_type: typing.Optional[typing.Type[BaseException]] = None,
- exc_value: typing.Optional[BaseException] = None,
- traceback: typing.Optional[TracebackType] = None,
+ exc_type: type[BaseException] | None = None,
+ exc_value: BaseException | None = None,
+ traceback: TracebackType | None = None,
) -> None:
self._state = ClientState.CLOSED
diff --git a/httpx/_compat.py b/httpx/_compat.py
index 493e6210..7d86dced 100644
--- a/httpx/_compat.py
+++ b/httpx/_compat.py
@@ -2,8 +2,12 @@
The _compat module is used for code which requires branching between different
Python environments. It is excluded from the code coverage checks.
"""
+
+import re
import ssl
import sys
+from types import ModuleType
+from typing import Optional
# Brotli support is optional
# The C bindings in `brotli` are recommended for CPython.
@@ -16,6 +20,24 @@ except ImportError: # pragma: no cover
except ImportError:
brotli = None
+# Zstandard support is optional
+zstd: Optional[ModuleType] = None
+try:
+ import zstandard as zstd
+except (AttributeError, ImportError, ValueError): # Defensive:
+ zstd = None
+else:
+ # The package 'zstandard' added the 'eof' property starting
+ # in v0.18.0 which we require to ensure a complete and
+ # valid zstd stream was fed into the ZstdDecoder.
+ # See: https://github.com/urllib3/urllib3/pull/2624
+ _zstd_version = tuple(
+ map(int, re.search(r"^([0-9]+)\.([0-9]+)", zstd.__version__).groups()) # type: ignore[union-attr]
+ )
+ if _zstd_version < (0, 18): # Defensive:
+ zstd = None
+
+
if sys.version_info >= (3, 10) or ssl.OPENSSL_VERSION_INFO >= (1, 1, 0, 7):
def set_minimum_tls_version_1_2(context: ssl.SSLContext) -> None:
diff --git a/httpx/_config.py b/httpx/_config.py
index 0cfd552e..1b12911f 100644
--- a/httpx/_config.py
+++ b/httpx/_config.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import logging
import os
import ssl
@@ -8,10 +10,12 @@ import certifi
from ._compat import set_minimum_tls_version_1_2
from ._models import Headers
-from ._types import CertTypes, HeaderTypes, TimeoutTypes, URLTypes, VerifyTypes
+from ._types import CertTypes, HeaderTypes, TimeoutTypes, VerifyTypes
from ._urls import URL
from ._utils import get_ca_bundle_from_env
+__all__ = ["Limits", "Proxy", "Timeout", "create_ssl_context"]
+
DEFAULT_CIPHERS = ":".join(
[
"ECDHE+AESGCM",
@@ -43,7 +47,7 @@ UNSET = UnsetType()
def create_ssl_context(
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
trust_env: bool = True,
http2: bool = False,
@@ -63,7 +67,7 @@ class SSLConfig:
def __init__(
self,
*,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
verify: VerifyTypes = True,
trust_env: bool = True,
http2: bool = False,
@@ -205,12 +209,12 @@ class Timeout:
def __init__(
self,
- timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
+ timeout: TimeoutTypes | UnsetType = UNSET,
*,
- connect: typing.Union[None, float, UnsetType] = UNSET,
- read: typing.Union[None, float, UnsetType] = UNSET,
- write: typing.Union[None, float, UnsetType] = UNSET,
- pool: typing.Union[None, float, UnsetType] = UNSET,
+ connect: None | float | UnsetType = UNSET,
+ read: None | float | UnsetType = UNSET,
+ write: None | float | UnsetType = UNSET,
+ pool: None | float | UnsetType = UNSET,
) -> None:
if isinstance(timeout, Timeout):
# Passed as a single explicit Timeout.
@@ -249,7 +253,7 @@ class Timeout:
self.write = timeout if isinstance(write, UnsetType) else write
self.pool = timeout if isinstance(pool, UnsetType) else pool
- def as_dict(self) -> typing.Dict[str, typing.Optional[float]]:
+ def as_dict(self) -> dict[str, float | None]:
return {
"connect": self.connect,
"read": self.read,
@@ -293,9 +297,9 @@ class Limits:
def __init__(
self,
*,
- max_connections: typing.Optional[int] = None,
- max_keepalive_connections: typing.Optional[int] = None,
- keepalive_expiry: typing.Optional[float] = 5.0,
+ max_connections: int | None = None,
+ max_keepalive_connections: int | None = None,
+ keepalive_expiry: float | None = 5.0,
) -> None:
self.max_connections = max_connections
self.max_keepalive_connections = max_keepalive_connections
@@ -321,11 +325,11 @@ class Limits:
class Proxy:
def __init__(
self,
- url: URLTypes,
+ url: URL | str,
*,
- ssl_context: typing.Optional[ssl.SSLContext] = None,
- auth: typing.Optional[typing.Tuple[str, str]] = None,
- headers: typing.Optional[HeaderTypes] = None,
+ ssl_context: ssl.SSLContext | None = None,
+ auth: tuple[str, str] | None = None,
+ headers: HeaderTypes | None = None,
) -> None:
url = URL(url)
headers = Headers(headers)
@@ -344,7 +348,7 @@ class Proxy:
self.ssl_context = ssl_context
@property
- def raw_auth(self) -> typing.Optional[typing.Tuple[bytes, bytes]]:
+ def raw_auth(self) -> tuple[bytes, bytes] | None:
# The proxy authentication as raw bytes.
return (
None
diff --git a/httpx/_content.py b/httpx/_content.py
index cd0d17f1..786699f3 100644
--- a/httpx/_content.py
+++ b/httpx/_content.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import inspect
import warnings
from json import dumps as json_dumps
@@ -5,13 +7,9 @@ from typing import (
Any,
AsyncIterable,
AsyncIterator,
- Dict,
Iterable,
Iterator,
Mapping,
- Optional,
- Tuple,
- Union,
)
from urllib.parse import urlencode
@@ -27,6 +25,8 @@ from ._types import (
)
from ._utils import peek_filelike_length, primitive_value_to_str
+__all__ = ["ByteStream"]
+
class ByteStream(AsyncByteStream, SyncByteStream):
def __init__(self, stream: bytes) -> None:
@@ -105,8 +105,8 @@ class UnattachedStream(AsyncByteStream, SyncByteStream):
def encode_content(
- content: Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]],
-) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]:
+ content: str | bytes | Iterable[bytes] | AsyncIterable[bytes],
+) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
if isinstance(content, (bytes, str)):
body = content.encode("utf-8") if isinstance(content, str) else content
content_length = len(body)
@@ -135,7 +135,7 @@ def encode_content(
def encode_urlencoded_data(
data: RequestData,
-) -> Tuple[Dict[str, str], ByteStream]:
+) -> tuple[dict[str, str], ByteStream]:
plain_data = []
for key, value in data.items():
if isinstance(value, (list, tuple)):
@@ -150,14 +150,14 @@ def encode_urlencoded_data(
def encode_multipart_data(
- data: RequestData, files: RequestFiles, boundary: Optional[bytes]
-) -> Tuple[Dict[str, str], MultipartStream]:
+ data: RequestData, files: RequestFiles, boundary: bytes | None
+) -> tuple[dict[str, str], MultipartStream]:
multipart = MultipartStream(data=data, files=files, boundary=boundary)
headers = multipart.get_headers()
return headers, multipart
-def encode_text(text: str) -> Tuple[Dict[str, str], ByteStream]:
+def encode_text(text: str) -> tuple[dict[str, str], ByteStream]:
body = text.encode("utf-8")
content_length = str(len(body))
content_type = "text/plain; charset=utf-8"
@@ -165,7 +165,7 @@ def encode_text(text: str) -> Tuple[Dict[str, str], ByteStream]:
return headers, ByteStream(body)
-def encode_html(html: str) -> Tuple[Dict[str, str], ByteStream]:
+def encode_html(html: str) -> tuple[dict[str, str], ByteStream]:
body = html.encode("utf-8")
content_length = str(len(body))
content_type = "text/html; charset=utf-8"
@@ -173,7 +173,7 @@ def encode_html(html: str) -> Tuple[Dict[str, str], ByteStream]:
return headers, ByteStream(body)
-def encode_json(json: Any) -> Tuple[Dict[str, str], ByteStream]:
+def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]:
body = json_dumps(json).encode("utf-8")
content_length = str(len(body))
content_type = "application/json"
@@ -182,12 +182,12 @@ def encode_json(json: Any) -> Tuple[Dict[str, str], ByteStream]:
def encode_request(
- content: Optional[RequestContent] = None,
- data: Optional[RequestData] = None,
- files: Optional[RequestFiles] = None,
- json: Optional[Any] = None,
- boundary: Optional[bytes] = None,
-) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]:
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: Any | None = None,
+ boundary: bytes | None = None,
+) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
"""
Handles encoding the given `content`, `data`, `files`, and `json`,
returning a two-tuple of (, ).
@@ -217,11 +217,11 @@ def encode_request(
def encode_response(
- content: Optional[ResponseContent] = None,
- text: Optional[str] = None,
- html: Optional[str] = None,
- json: Optional[Any] = None,
-) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]:
+ content: ResponseContent | None = None,
+ text: str | None = None,
+ html: str | None = None,
+ json: Any | None = None,
+) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
"""
Handles encoding the given `content`, returning a two-tuple of
(, ).
diff --git a/httpx/_decoders.py b/httpx/_decoders.py
index 3f507c8e..62f2c0b9 100644
--- a/httpx/_decoders.py
+++ b/httpx/_decoders.py
@@ -3,12 +3,15 @@ Handlers for Content-Encoding.
See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
"""
+
+from __future__ import annotations
+
import codecs
import io
import typing
import zlib
-from ._compat import brotli
+from ._compat import brotli, zstd
from ._exceptions import DecodingError
@@ -137,6 +140,44 @@ class BrotliDecoder(ContentDecoder):
raise DecodingError(str(exc)) from exc
+class ZStandardDecoder(ContentDecoder):
+ """
+ Handle 'zstd' RFC 8878 decoding.
+
+ Requires `pip install zstandard`.
+ Can be installed as a dependency of httpx using `pip install httpx[zstd]`.
+ """
+
+ # inspired by the ZstdDecoder implementation in urllib3
+ def __init__(self) -> None:
+ if zstd is None: # pragma: no cover
+ raise ImportError(
+ "Using 'ZStandardDecoder', ..."
+ "Make sure to install httpx using `pip install httpx[zstd]`."
+ ) from None
+
+ self.decompressor = zstd.ZstdDecompressor().decompressobj()
+
+ def decode(self, data: bytes) -> bytes:
+ assert zstd is not None
+ output = io.BytesIO()
+ try:
+ output.write(self.decompressor.decompress(data))
+ while self.decompressor.eof and self.decompressor.unused_data:
+ unused_data = self.decompressor.unused_data
+ self.decompressor = zstd.ZstdDecompressor().decompressobj()
+ output.write(self.decompressor.decompress(unused_data))
+ except zstd.ZstdError as exc:
+ raise DecodingError(str(exc)) from exc
+ return output.getvalue()
+
+ def flush(self) -> bytes:
+ ret = self.decompressor.flush() # note: this is a no-op
+ if not self.decompressor.eof:
+ raise DecodingError("Zstandard data is incomplete") # pragma: no cover
+ return bytes(ret)
+
+
class MultiDecoder(ContentDecoder):
"""
Handle the case where multiple encodings have been applied.
@@ -167,11 +208,11 @@ class ByteChunker:
Handles returning byte content in fixed-size chunks.
"""
- def __init__(self, chunk_size: typing.Optional[int] = None) -> None:
+ def __init__(self, chunk_size: int | None = None) -> None:
self._buffer = io.BytesIO()
self._chunk_size = chunk_size
- def decode(self, content: bytes) -> typing.List[bytes]:
+ def decode(self, content: bytes) -> list[bytes]:
if self._chunk_size is None:
return [content] if content else []
@@ -194,7 +235,7 @@ class ByteChunker:
else:
return []
- def flush(self) -> typing.List[bytes]:
+ def flush(self) -> list[bytes]:
value = self._buffer.getvalue()
self._buffer.seek(0)
self._buffer.truncate()
@@ -206,11 +247,11 @@ class TextChunker:
Handles returning text content in fixed-size chunks.
"""
- def __init__(self, chunk_size: typing.Optional[int] = None) -> None:
+ def __init__(self, chunk_size: int | None = None) -> None:
self._buffer = io.StringIO()
self._chunk_size = chunk_size
- def decode(self, content: str) -> typing.List[str]:
+ def decode(self, content: str) -> list[str]:
if self._chunk_size is None:
return [content] if content else []
@@ -233,7 +274,7 @@ class TextChunker:
else:
return []
- def flush(self) -> typing.List[str]:
+ def flush(self) -> list[str]:
value = self._buffer.getvalue()
self._buffer.seek(0)
self._buffer.truncate()
@@ -264,10 +305,10 @@ class LineDecoder:
"""
def __init__(self) -> None:
- self.buffer: typing.List[str] = []
+ self.buffer: list[str] = []
self.trailing_cr: bool = False
- def decode(self, text: str) -> typing.List[str]:
+ def decode(self, text: str) -> list[str]:
# See https://docs.python.org/3/library/stdtypes.html#str.splitlines
NEWLINE_CHARS = "\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029"
@@ -305,7 +346,7 @@ class LineDecoder:
return lines
- def flush(self) -> typing.List[str]:
+ def flush(self) -> list[str]:
if not self.buffer and not self.trailing_cr:
return []
@@ -320,8 +361,11 @@ SUPPORTED_DECODERS = {
"gzip": GZipDecoder,
"deflate": DeflateDecoder,
"br": BrotliDecoder,
+ "zstd": ZStandardDecoder,
}
if brotli is None:
SUPPORTED_DECODERS.pop("br") # pragma: no cover
+if zstd is None:
+ SUPPORTED_DECODERS.pop("zstd") # pragma: no cover
diff --git a/httpx/_exceptions.py b/httpx/_exceptions.py
index 12369295..77f45a6d 100644
--- a/httpx/_exceptions.py
+++ b/httpx/_exceptions.py
@@ -30,12 +30,46 @@ Our exception hierarchy:
x ResponseNotRead
x RequestNotRead
"""
+
+from __future__ import annotations
+
import contextlib
import typing
if typing.TYPE_CHECKING:
from ._models import Request, Response # pragma: no cover
+__all__ = [
+ "CloseError",
+ "ConnectError",
+ "ConnectTimeout",
+ "CookieConflict",
+ "DecodingError",
+ "HTTPError",
+ "HTTPStatusError",
+ "InvalidURL",
+ "LocalProtocolError",
+ "NetworkError",
+ "PoolTimeout",
+ "ProtocolError",
+ "ProxyError",
+ "ReadError",
+ "ReadTimeout",
+ "RemoteProtocolError",
+ "RequestError",
+ "RequestNotRead",
+ "ResponseNotRead",
+ "StreamClosed",
+ "StreamConsumed",
+ "StreamError",
+ "TimeoutException",
+ "TooManyRedirects",
+ "TransportError",
+ "UnsupportedProtocol",
+ "WriteError",
+ "WriteTimeout",
+]
+
class HTTPError(Exception):
"""
@@ -57,16 +91,16 @@ class HTTPError(Exception):
def __init__(self, message: str) -> None:
super().__init__(message)
- self._request: typing.Optional["Request"] = None
+ self._request: Request | None = None
@property
- def request(self) -> "Request":
+ def request(self) -> Request:
if self._request is None:
raise RuntimeError("The .request property has not been set.")
return self._request
@request.setter
- def request(self, request: "Request") -> None:
+ def request(self, request: Request) -> None:
self._request = request
@@ -75,9 +109,7 @@ class RequestError(HTTPError):
Base class for all exceptions that may occur when issuing a `.request()`.
"""
- def __init__(
- self, message: str, *, request: typing.Optional["Request"] = None
- ) -> None:
+ def __init__(self, message: str, *, request: Request | None = None) -> None:
super().__init__(message)
# At the point an exception is raised we won't typically have a request
# instance to associate it with.
@@ -230,9 +262,7 @@ class HTTPStatusError(HTTPError):
May be raised when calling `response.raise_for_status()`
"""
- def __init__(
- self, message: str, *, request: "Request", response: "Response"
- ) -> None:
+ def __init__(self, message: str, *, request: Request, response: Response) -> None:
super().__init__(message)
self.request = request
self.response = response
@@ -335,7 +365,7 @@ class RequestNotRead(StreamError):
@contextlib.contextmanager
def request_context(
- request: typing.Optional["Request"] = None,
+ request: Request | None = None,
) -> typing.Iterator[None]:
"""
A context manager that can be used to attach the given request context
diff --git a/httpx/_main.py b/httpx/_main.py
index adb57d5f..72657f8c 100644
--- a/httpx/_main.py
+++ b/httpx/_main.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import functools
import json
import sys
@@ -125,8 +127,8 @@ def format_request_headers(request: httpcore.Request, http2: bool = False) -> st
def format_response_headers(
http_version: bytes,
status: int,
- reason_phrase: typing.Optional[bytes],
- headers: typing.List[typing.Tuple[bytes, bytes]],
+ reason_phrase: bytes | None,
+ headers: list[tuple[bytes, bytes]],
) -> str:
version = http_version.decode("ascii")
reason = (
@@ -152,8 +154,8 @@ def print_request_headers(request: httpcore.Request, http2: bool = False) -> Non
def print_response_headers(
http_version: bytes,
status: int,
- reason_phrase: typing.Optional[bytes],
- headers: typing.List[typing.Tuple[bytes, bytes]],
+ reason_phrase: bytes | None,
+ headers: list[tuple[bytes, bytes]],
) -> None:
console = rich.console.Console()
http_text = format_response_headers(http_version, status, reason_phrase, headers)
@@ -268,7 +270,7 @@ def download_response(response: Response, download: typing.BinaryIO) -> None:
def validate_json(
ctx: click.Context,
- param: typing.Union[click.Option, click.Parameter],
+ param: click.Option | click.Parameter,
value: typing.Any,
) -> typing.Any:
if value is None:
@@ -282,7 +284,7 @@ def validate_json(
def validate_auth(
ctx: click.Context,
- param: typing.Union[click.Option, click.Parameter],
+ param: click.Option | click.Parameter,
value: typing.Any,
) -> typing.Any:
if value == (None, None):
@@ -296,7 +298,7 @@ def validate_auth(
def handle_help(
ctx: click.Context,
- param: typing.Union[click.Option, click.Parameter],
+ param: click.Option | click.Parameter,
value: typing.Any,
) -> None:
if not value or ctx.resilient_parsing:
@@ -448,20 +450,20 @@ def handle_help(
def main(
url: str,
method: str,
- params: typing.List[typing.Tuple[str, str]],
+ params: list[tuple[str, str]],
content: str,
- data: typing.List[typing.Tuple[str, str]],
- files: typing.List[typing.Tuple[str, click.File]],
+ data: list[tuple[str, str]],
+ files: list[tuple[str, click.File]],
json: str,
- headers: typing.List[typing.Tuple[str, str]],
- cookies: typing.List[typing.Tuple[str, str]],
- auth: typing.Optional[typing.Tuple[str, str]],
+ headers: list[tuple[str, str]],
+ cookies: list[tuple[str, str]],
+ auth: tuple[str, str] | None,
proxy: str,
timeout: float,
follow_redirects: bool,
verify: bool,
http2: bool,
- download: typing.Optional[typing.BinaryIO],
+ download: typing.BinaryIO | None,
verbose: bool,
) -> None:
"""
diff --git a/httpx/_models.py b/httpx/_models.py
index b8617cda..01d9583b 100644
--- a/httpx/_models.py
+++ b/httpx/_models.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import datetime
import email.message
import json as jsonlib
@@ -51,6 +53,8 @@ from ._utils import (
parse_header_links,
)
+__all__ = ["Cookies", "Headers", "Request", "Response"]
+
class Headers(typing.MutableMapping[str, str]):
"""
@@ -59,8 +63,8 @@ class Headers(typing.MutableMapping[str, str]):
def __init__(
self,
- headers: typing.Optional[HeaderTypes] = None,
- encoding: typing.Optional[str] = None,
+ headers: HeaderTypes | None = None,
+ encoding: str | None = None,
) -> None:
if headers is None:
self._list = [] # type: typing.List[typing.Tuple[bytes, bytes, bytes]]
@@ -117,7 +121,7 @@ class Headers(typing.MutableMapping[str, str]):
self._encoding = value
@property
- def raw(self) -> typing.List[typing.Tuple[bytes, bytes]]:
+ def raw(self) -> list[tuple[bytes, bytes]]:
"""
Returns a list of the raw header items, as byte pairs.
"""
@@ -127,7 +131,7 @@ class Headers(typing.MutableMapping[str, str]):
return {key.decode(self.encoding): None for _, key, value in self._list}.keys()
def values(self) -> typing.ValuesView[str]:
- values_dict: typing.Dict[str, str] = {}
+ values_dict: dict[str, str] = {}
for _, key, value in self._list:
str_key = key.decode(self.encoding)
str_value = value.decode(self.encoding)
@@ -142,7 +146,7 @@ class Headers(typing.MutableMapping[str, str]):
Return `(key, value)` items of headers. Concatenate headers
into a single comma separated value when a key occurs multiple times.
"""
- values_dict: typing.Dict[str, str] = {}
+ values_dict: dict[str, str] = {}
for _, key, value in self._list:
str_key = key.decode(self.encoding)
str_value = value.decode(self.encoding)
@@ -152,7 +156,7 @@ class Headers(typing.MutableMapping[str, str]):
values_dict[str_key] = str_value
return values_dict.items()
- def multi_items(self) -> typing.List[typing.Tuple[str, str]]:
+ def multi_items(self) -> list[tuple[str, str]]:
"""
Return a list of `(key, value)` pairs of headers. Allow multiple
occurrences of the same key without concatenating into a single
@@ -173,7 +177,7 @@ class Headers(typing.MutableMapping[str, str]):
except KeyError:
return default
- def get_list(self, key: str, split_commas: bool = False) -> typing.List[str]:
+ def get_list(self, key: str, split_commas: bool = False) -> list[str]:
"""
Return a list of all header values for a given key.
If `split_commas=True` is passed, then any comma separated header
@@ -195,14 +199,14 @@ class Headers(typing.MutableMapping[str, str]):
split_values.extend([item.strip() for item in value.split(",")])
return split_values
- def update(self, headers: typing.Optional[HeaderTypes] = None) -> None: # type: ignore
+ def update(self, headers: HeaderTypes | None = None) -> None: # type: ignore
headers = Headers(headers)
for key in headers.keys():
if key in self:
self.pop(key)
self._list.extend(headers._list)
- def copy(self) -> "Headers":
+ def copy(self) -> Headers:
return Headers(self, encoding=self.encoding)
def __getitem__(self, key: str) -> str:
@@ -306,18 +310,18 @@ class Headers(typing.MutableMapping[str, str]):
class Request:
def __init__(
self,
- method: typing.Union[str, bytes],
- url: typing.Union["URL", str],
+ method: str | bytes,
+ url: URL | str,
*,
- params: typing.Optional[QueryParamTypes] = None,
- headers: typing.Optional[HeaderTypes] = None,
- cookies: typing.Optional[CookieTypes] = None,
- content: typing.Optional[RequestContent] = None,
- data: typing.Optional[RequestData] = None,
- files: typing.Optional[RequestFiles] = None,
- json: typing.Optional[typing.Any] = None,
- stream: typing.Union[SyncByteStream, AsyncByteStream, None] = None,
- extensions: typing.Optional[RequestExtensions] = None,
+ params: QueryParamTypes | None = None,
+ headers: HeaderTypes | None = None,
+ cookies: CookieTypes | None = None,
+ content: RequestContent | None = None,
+ data: RequestData | None = None,
+ files: RequestFiles | None = None,
+ json: typing.Any | None = None,
+ stream: SyncByteStream | AsyncByteStream | None = None,
+ extensions: RequestExtensions | None = None,
) -> None:
self.method = (
method.decode("ascii").upper()
@@ -334,7 +338,7 @@ class Request:
Cookies(cookies).set_cookie_header(self)
if stream is None:
- content_type: typing.Optional[str] = self.headers.get("content-type")
+ content_type: str | None = self.headers.get("content-type")
headers, stream = encode_request(
content=content,
data=data,
@@ -368,14 +372,14 @@ class Request:
# * Creating request instances on the *server-side* of the transport API.
self.stream = stream
- def _prepare(self, default_headers: typing.Dict[str, str]) -> None:
+ def _prepare(self, default_headers: dict[str, str]) -> None:
for key, value in default_headers.items():
# Ignore Transfer-Encoding if the Content-Length has been set explicitly.
if key.lower() == "transfer-encoding" and "Content-Length" in self.headers:
continue
self.headers.setdefault(key, value)
- auto_headers: typing.List[typing.Tuple[bytes, bytes]] = []
+ auto_headers: list[tuple[bytes, bytes]] = []
has_host = "Host" in self.headers
has_content_length = (
@@ -428,14 +432,14 @@ class Request:
url = str(self.url)
return f"<{class_name}({self.method!r}, {url!r})>"
- def __getstate__(self) -> typing.Dict[str, typing.Any]:
+ def __getstate__(self) -> dict[str, typing.Any]:
return {
name: value
for name, value in self.__dict__.items()
if name not in ["extensions", "stream"]
}
- def __setstate__(self, state: typing.Dict[str, typing.Any]) -> None:
+ def __setstate__(self, state: dict[str, typing.Any]) -> None:
for name, value in state.items():
setattr(self, name, value)
self.extensions = {}
@@ -447,25 +451,25 @@ class Response:
self,
status_code: int,
*,
- headers: typing.Optional[HeaderTypes] = None,
- content: typing.Optional[ResponseContent] = None,
- text: typing.Optional[str] = None,
- html: typing.Optional[str] = None,
+ headers: HeaderTypes | None = None,
+ content: ResponseContent | None = None,
+ text: str | None = None,
+ html: str | None = None,
json: typing.Any = None,
- stream: typing.Union[SyncByteStream, AsyncByteStream, None] = None,
- request: typing.Optional[Request] = None,
- extensions: typing.Optional[ResponseExtensions] = None,
- history: typing.Optional[typing.List["Response"]] = None,
- default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8",
+ stream: SyncByteStream | AsyncByteStream | None = None,
+ request: Request | None = None,
+ extensions: ResponseExtensions | None = None,
+ history: list[Response] | None = None,
+ default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
) -> None:
self.status_code = status_code
self.headers = Headers(headers)
- self._request: typing.Optional[Request] = request
+ self._request: Request | None = request
# When follow_redirects=False and a redirect is received,
# the client will set `response.next_request`.
- self.next_request: typing.Optional[Request] = None
+ self.next_request: Request | None = None
self.extensions: ResponseExtensions = {} if extensions is None else extensions
self.history = [] if history is None else list(history)
@@ -498,7 +502,7 @@ class Response:
self._num_bytes_downloaded = 0
- def _prepare(self, default_headers: typing.Dict[str, str]) -> None:
+ def _prepare(self, default_headers: dict[str, str]) -> None:
for key, value in default_headers.items():
# Ignore Transfer-Encoding if the Content-Length has been set explicitly.
if key.lower() == "transfer-encoding" and "content-length" in self.headers:
@@ -580,7 +584,7 @@ class Response:
return self._text
@property
- def encoding(self) -> typing.Optional[str]:
+ def encoding(self) -> str | None:
"""
Return an encoding to use for decoding the byte content into text.
The priority for determining this is given by...
@@ -616,7 +620,7 @@ class Response:
self._encoding = value
@property
- def charset_encoding(self) -> typing.Optional[str]:
+ def charset_encoding(self) -> str | None:
"""
Return the encoding, as specified by the Content-Type header.
"""
@@ -632,7 +636,7 @@ class Response:
content, depending on the Content-Encoding used in the response.
"""
if not hasattr(self, "_decoder"):
- decoders: typing.List[ContentDecoder] = []
+ decoders: list[ContentDecoder] = []
values = self.headers.get_list("content-encoding", split_commas=True)
for value in values:
value = value.strip().lower()
@@ -721,7 +725,7 @@ class Response:
and "Location" in self.headers
)
- def raise_for_status(self) -> "Response":
+ def raise_for_status(self) -> Response:
"""
Raise the `HTTPStatusError` if one occurred.
"""
@@ -762,25 +766,25 @@ class Response:
return jsonlib.loads(self.content, **kwargs)
@property
- def cookies(self) -> "Cookies":
+ def cookies(self) -> Cookies:
if not hasattr(self, "_cookies"):
self._cookies = Cookies()
self._cookies.extract_cookies(self)
return self._cookies
@property
- def links(self) -> typing.Dict[typing.Optional[str], typing.Dict[str, str]]:
+ def links(self) -> dict[str | None, dict[str, str]]:
"""
Returns the parsed header links of the response, if any
"""
header = self.headers.get("link")
- ldict = {}
- if header:
- links = parse_header_links(header)
- for link in links:
- key = link.get("rel") or link.get("url")
- ldict[key] = link
- return ldict
+ if header is None:
+ return {}
+
+ return {
+ (link.get("rel") or link.get("url")): link
+ for link in parse_header_links(header)
+ }
@property
def num_bytes_downloaded(self) -> int:
@@ -789,14 +793,14 @@ class Response:
def __repr__(self) -> str:
return f""
- def __getstate__(self) -> typing.Dict[str, typing.Any]:
+ def __getstate__(self) -> dict[str, typing.Any]:
return {
name: value
for name, value in self.__dict__.items()
if name not in ["extensions", "stream", "is_closed", "_decoder"]
}
- def __setstate__(self, state: typing.Dict[str, typing.Any]) -> None:
+ def __setstate__(self, state: dict[str, typing.Any]) -> None:
for name, value in state.items():
setattr(self, name, value)
self.is_closed = True
@@ -811,12 +815,10 @@ class Response:
self._content = b"".join(self.iter_bytes())
return self._content
- def iter_bytes(
- self, chunk_size: typing.Optional[int] = None
- ) -> typing.Iterator[bytes]:
+ def iter_bytes(self, chunk_size: int | None = None) -> typing.Iterator[bytes]:
"""
A byte-iterator over the decoded response content.
- This allows us to handle gzip, deflate, and brotli encoded responses.
+ This allows us to handle gzip, deflate, brotli, and zstd encoded responses.
"""
if hasattr(self, "_content"):
chunk_size = len(self._content) if chunk_size is None else chunk_size
@@ -836,9 +838,7 @@ class Response:
for chunk in chunker.flush():
yield chunk
- def iter_text(
- self, chunk_size: typing.Optional[int] = None
- ) -> typing.Iterator[str]:
+ def iter_text(self, chunk_size: int | None = None) -> typing.Iterator[str]:
"""
A str-iterator over the decoded response content
that handles both gzip, deflate, etc but also detects the content's
@@ -866,9 +866,7 @@ class Response:
for line in decoder.flush():
yield line
- def iter_raw(
- self, chunk_size: typing.Optional[int] = None
- ) -> typing.Iterator[bytes]:
+ def iter_raw(self, chunk_size: int | None = None) -> typing.Iterator[bytes]:
"""
A byte-iterator over the raw response content.
"""
@@ -916,11 +914,11 @@ class Response:
return self._content
async def aiter_bytes(
- self, chunk_size: typing.Optional[int] = None
+ self, chunk_size: int | None = None
) -> typing.AsyncIterator[bytes]:
"""
A byte-iterator over the decoded response content.
- This allows us to handle gzip, deflate, and brotli encoded responses.
+ This allows us to handle gzip, deflate, brotli, and zstd encoded responses.
"""
if hasattr(self, "_content"):
chunk_size = len(self._content) if chunk_size is None else chunk_size
@@ -941,7 +939,7 @@ class Response:
yield chunk
async def aiter_text(
- self, chunk_size: typing.Optional[int] = None
+ self, chunk_size: int | None = None
) -> typing.AsyncIterator[str]:
"""
A str-iterator over the decoded response content
@@ -971,7 +969,7 @@ class Response:
yield line
async def aiter_raw(
- self, chunk_size: typing.Optional[int] = None
+ self, chunk_size: int | None = None
) -> typing.AsyncIterator[bytes]:
"""
A byte-iterator over the raw response content.
@@ -1017,7 +1015,7 @@ class Cookies(typing.MutableMapping[str, str]):
HTTP Cookies, as a mutable mapping.
"""
- def __init__(self, cookies: typing.Optional[CookieTypes] = None) -> None:
+ def __init__(self, cookies: CookieTypes | None = None) -> None:
if cookies is None or isinstance(cookies, dict):
self.jar = CookieJar()
if isinstance(cookies, dict):
@@ -1079,10 +1077,10 @@ class Cookies(typing.MutableMapping[str, str]):
def get( # type: ignore
self,
name: str,
- default: typing.Optional[str] = None,
- domain: typing.Optional[str] = None,
- path: typing.Optional[str] = None,
- ) -> typing.Optional[str]:
+ default: str | None = None,
+ domain: str | None = None,
+ path: str | None = None,
+ ) -> str | None:
"""
Get a cookie by name. May optionally include domain and path
in order to specify exactly which cookie to retrieve.
@@ -1104,8 +1102,8 @@ class Cookies(typing.MutableMapping[str, str]):
def delete(
self,
name: str,
- domain: typing.Optional[str] = None,
- path: typing.Optional[str] = None,
+ domain: str | None = None,
+ path: str | None = None,
) -> None:
"""
Delete a cookie by name. May optionally include domain and path
@@ -1125,9 +1123,7 @@ class Cookies(typing.MutableMapping[str, str]):
for cookie in remove:
self.jar.clear(cookie.domain, cookie.path, cookie.name)
- def clear(
- self, domain: typing.Optional[str] = None, path: typing.Optional[str] = None
- ) -> None:
+ def clear(self, domain: str | None = None, path: str | None = None) -> None:
"""
Delete all cookies. Optionally include a domain and path in
order to only delete a subset of all the cookies.
@@ -1140,7 +1136,7 @@ class Cookies(typing.MutableMapping[str, str]):
args.append(path)
self.jar.clear(*args)
- def update(self, cookies: typing.Optional[CookieTypes] = None) -> None: # type: ignore
+ def update(self, cookies: CookieTypes | None = None) -> None: # type: ignore
cookies = Cookies(cookies)
for cookie in cookies.jar:
self.jar.set_cookie(cookie)
diff --git a/httpx/_multipart.py b/httpx/_multipart.py
index 1d451c38..8edb6227 100644
--- a/httpx/_multipart.py
+++ b/httpx/_multipart.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import io
import os
import typing
@@ -21,8 +23,8 @@ from ._utils import (
def get_multipart_boundary_from_content_type(
- content_type: typing.Optional[bytes],
-) -> typing.Optional[bytes]:
+ content_type: bytes | None,
+) -> bytes | None:
if not content_type or not content_type.startswith(b"multipart/form-data"):
return None
# parse boundary according to
@@ -39,9 +41,7 @@ class DataField:
A single form field item, within a multipart form field.
"""
- def __init__(
- self, name: str, value: typing.Union[str, bytes, int, float, None]
- ) -> None:
+ def __init__(self, name: str, value: str | bytes | int | float | None) -> None:
if not isinstance(name, str):
raise TypeError(
f"Invalid type for name. Expected str, got {type(name)}: {name!r}"
@@ -52,7 +52,7 @@ class DataField:
f" got {type(value)}: {value!r}"
)
self.name = name
- self.value: typing.Union[str, bytes] = (
+ self.value: str | bytes = (
value if isinstance(value, bytes) else primitive_value_to_str(value)
)
@@ -93,8 +93,8 @@ class FileField:
fileobj: FileContent
- headers: typing.Dict[str, str] = {}
- content_type: typing.Optional[str] = None
+ headers: dict[str, str] = {}
+ content_type: str | None = None
# This large tuple based API largely mirror's requests' API
# It would be good to think of better APIs for this that we could
@@ -137,7 +137,7 @@ class FileField:
self.file = fileobj
self.headers = headers
- def get_length(self) -> typing.Optional[int]:
+ def get_length(self) -> int | None:
headers = self.render_headers()
if isinstance(self.file, (str, bytes)):
@@ -199,7 +199,7 @@ class MultipartStream(SyncByteStream, AsyncByteStream):
self,
data: RequestData,
files: RequestFiles,
- boundary: typing.Optional[bytes] = None,
+ boundary: bytes | None = None,
) -> None:
if boundary is None:
boundary = os.urandom(16).hex().encode("ascii")
@@ -212,7 +212,7 @@ class MultipartStream(SyncByteStream, AsyncByteStream):
def _iter_fields(
self, data: RequestData, files: RequestFiles
- ) -> typing.Iterator[typing.Union[FileField, DataField]]:
+ ) -> typing.Iterator[FileField | DataField]:
for name, value in data.items():
if isinstance(value, (tuple, list)):
for item in value:
@@ -231,7 +231,7 @@ class MultipartStream(SyncByteStream, AsyncByteStream):
yield b"\r\n"
yield b"--%s--\r\n" % self.boundary
- def get_content_length(self) -> typing.Optional[int]:
+ def get_content_length(self) -> int | None:
"""
Return the length of the multipart encoded content, or `None` if
any of the files have a length that cannot be determined upfront.
@@ -253,7 +253,7 @@ class MultipartStream(SyncByteStream, AsyncByteStream):
# Content stream interface.
- def get_headers(self) -> typing.Dict[str, str]:
+ def get_headers(self) -> dict[str, str]:
content_length = self.get_content_length()
content_type = self.content_type
if content_length is None:
diff --git a/httpx/_status_codes.py b/httpx/_status_codes.py
index 671c30e1..133a6231 100644
--- a/httpx/_status_codes.py
+++ b/httpx/_status_codes.py
@@ -1,5 +1,9 @@
+from __future__ import annotations
+
from enum import IntEnum
+__all__ = ["codes"]
+
class codes(IntEnum):
"""HTTP status codes and reason phrases
@@ -21,7 +25,7 @@ class codes(IntEnum):
* RFC 8470: Using Early Data in HTTP
"""
- def __new__(cls, value: int, phrase: str = "") -> "codes":
+ def __new__(cls, value: int, phrase: str = "") -> codes:
obj = int.__new__(cls, value)
obj._value_ = value
diff --git a/httpx/_transports/__init__.py b/httpx/_transports/__init__.py
index e69de29b..7a321053 100644
--- a/httpx/_transports/__init__.py
+++ b/httpx/_transports/__init__.py
@@ -0,0 +1,15 @@
+from .asgi import *
+from .base import *
+from .default import *
+from .mock import *
+from .wsgi import *
+
+__all__ = [
+ "ASGITransport",
+ "AsyncBaseTransport",
+ "BaseTransport",
+ "AsyncHTTPTransport",
+ "HTTPTransport",
+ "MockTransport",
+ "WSGITransport",
+]
diff --git a/httpx/_transports/asgi.py b/httpx/_transports/asgi.py
index fd9ffdce..17ad2b0a 100644
--- a/httpx/_transports/asgi.py
+++ b/httpx/_transports/asgi.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import typing
from .._models import Request, Response
@@ -12,15 +14,17 @@ if typing.TYPE_CHECKING: # pragma: no cover
Event = typing.Union[asyncio.Event, trio.Event]
-_Message = typing.Dict[str, typing.Any]
+_Message = typing.MutableMapping[str, typing.Any]
_Receive = typing.Callable[[], typing.Awaitable[_Message]]
_Send = typing.Callable[
- [typing.Dict[str, typing.Any]], typing.Coroutine[None, None, None]
+ [typing.MutableMapping[str, typing.Any]], typing.Awaitable[None]
]
_ASGIApp = typing.Callable[
- [typing.Dict[str, typing.Any], _Receive, _Send], typing.Coroutine[None, None, None]
+ [typing.MutableMapping[str, typing.Any], _Receive, _Send], typing.Awaitable[None]
]
+__all__ = ["ASGITransport"]
+
def create_event() -> "Event":
import sniffio
@@ -36,7 +40,7 @@ def create_event() -> "Event":
class ASGIResponseStream(AsyncByteStream):
- def __init__(self, body: typing.List[bytes]) -> None:
+ def __init__(self, body: list[bytes]) -> None:
self._body = body
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
@@ -46,17 +50,8 @@ class ASGIResponseStream(AsyncByteStream):
class ASGITransport(AsyncBaseTransport):
"""
A custom AsyncTransport that handles sending requests directly to an ASGI app.
- The simplest way to use this functionality is to use the `app` argument.
- ```
- client = httpx.AsyncClient(app=app)
- ```
-
- Alternatively, you can setup the transport instance explicitly.
- This allows you to include any additional configuration arguments specific
- to the ASGITransport class:
-
- ```
+ ```python
transport = httpx.ASGITransport(
app=app,
root_path="/submount",
@@ -81,7 +76,7 @@ class ASGITransport(AsyncBaseTransport):
app: _ASGIApp,
raise_app_exceptions: bool = True,
root_path: str = "",
- client: typing.Tuple[str, int] = ("127.0.0.1", 123),
+ client: tuple[str, int] = ("127.0.0.1", 123),
) -> None:
self.app = app
self.raise_app_exceptions = raise_app_exceptions
@@ -123,7 +118,7 @@ class ASGITransport(AsyncBaseTransport):
# ASGI callables.
- async def receive() -> typing.Dict[str, typing.Any]:
+ async def receive() -> dict[str, typing.Any]:
nonlocal request_complete
if request_complete:
@@ -137,7 +132,7 @@ class ASGITransport(AsyncBaseTransport):
return {"type": "http.request", "body": b"", "more_body": False}
return {"type": "http.request", "body": body, "more_body": True}
- async def send(message: typing.Dict[str, typing.Any]) -> None:
+ async def send(message: typing.MutableMapping[str, typing.Any]) -> None:
nonlocal status_code, response_headers, response_started
if message["type"] == "http.response.start":
diff --git a/httpx/_transports/base.py b/httpx/_transports/base.py
index f6fdfe69..66fd99d7 100644
--- a/httpx/_transports/base.py
+++ b/httpx/_transports/base.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import typing
from types import TracebackType
@@ -6,6 +8,8 @@ from .._models import Request, Response
T = typing.TypeVar("T", bound="BaseTransport")
A = typing.TypeVar("A", bound="AsyncBaseTransport")
+__all__ = ["AsyncBaseTransport", "BaseTransport"]
+
class BaseTransport:
def __enter__(self: T) -> T:
@@ -13,9 +17,9 @@ class BaseTransport:
def __exit__(
self,
- exc_type: typing.Optional[typing.Type[BaseException]] = None,
- exc_value: typing.Optional[BaseException] = None,
- traceback: typing.Optional[TracebackType] = None,
+ exc_type: type[BaseException] | None = None,
+ exc_value: BaseException | None = None,
+ traceback: TracebackType | None = None,
) -> None:
self.close()
@@ -64,9 +68,9 @@ class AsyncBaseTransport:
async def __aexit__(
self,
- exc_type: typing.Optional[typing.Type[BaseException]] = None,
- exc_value: typing.Optional[BaseException] = None,
- traceback: typing.Optional[TracebackType] = None,
+ exc_type: type[BaseException] | None = None,
+ exc_value: BaseException | None = None,
+ traceback: TracebackType | None = None,
) -> None:
await self.aclose()
diff --git a/httpx/_transports/default.py b/httpx/_transports/default.py
index 9e9ce90e..3ebf2102 100644
--- a/httpx/_transports/default.py
+++ b/httpx/_transports/default.py
@@ -23,6 +23,9 @@ client = httpx.Client(transport=transport)
transport = httpx.HTTPTransport(uds="socket.uds")
client = httpx.Client(transport=transport)
"""
+
+from __future__ import annotations
+
import contextlib
import typing
from types import TracebackType
@@ -60,6 +63,8 @@ SOCKET_OPTION = typing.Union[
typing.Tuple[int, int, None, int],
]
+__all__ = ["AsyncHTTPTransport", "HTTPTransport"]
+
@contextlib.contextmanager
def map_httpcore_exceptions() -> typing.Iterator[None]:
@@ -120,16 +125,16 @@ class HTTPTransport(BaseTransport):
def __init__(
self,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
trust_env: bool = True,
- proxy: typing.Optional[ProxyTypes] = None,
- uds: typing.Optional[str] = None,
- local_address: typing.Optional[str] = None,
+ proxy: ProxyTypes | None = None,
+ uds: str | None = None,
+ local_address: str | None = None,
retries: int = 0,
- socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None,
+ socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
) -> None:
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
@@ -202,9 +207,9 @@ class HTTPTransport(BaseTransport):
def __exit__(
self,
- exc_type: typing.Optional[typing.Type[BaseException]] = None,
- exc_value: typing.Optional[BaseException] = None,
- traceback: typing.Optional[TracebackType] = None,
+ exc_type: type[BaseException] | None = None,
+ exc_value: BaseException | None = None,
+ traceback: TracebackType | None = None,
) -> None:
with map_httpcore_exceptions():
self._pool.__exit__(exc_type, exc_value, traceback)
@@ -261,16 +266,16 @@ class AsyncHTTPTransport(AsyncBaseTransport):
def __init__(
self,
verify: VerifyTypes = True,
- cert: typing.Optional[CertTypes] = None,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
trust_env: bool = True,
- proxy: typing.Optional[ProxyTypes] = None,
- uds: typing.Optional[str] = None,
- local_address: typing.Optional[str] = None,
+ proxy: ProxyTypes | None = None,
+ uds: str | None = None,
+ local_address: str | None = None,
retries: int = 0,
- socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None,
+ socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
) -> None:
try:
import sniffio # noqa: F401
@@ -306,6 +311,7 @@ class AsyncHTTPTransport(AsyncBaseTransport):
),
proxy_auth=proxy.raw_auth,
proxy_headers=proxy.headers.raw,
+ proxy_ssl_context=proxy.ssl_context,
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
@@ -350,9 +356,9 @@ class AsyncHTTPTransport(AsyncBaseTransport):
async def __aexit__(
self,
- exc_type: typing.Optional[typing.Type[BaseException]] = None,
- exc_value: typing.Optional[BaseException] = None,
- traceback: typing.Optional[TracebackType] = None,
+ exc_type: type[BaseException] | None = None,
+ exc_value: BaseException | None = None,
+ traceback: TracebackType | None = None,
) -> None:
with map_httpcore_exceptions():
await self._pool.__aexit__(exc_type, exc_value, traceback)
diff --git a/httpx/_transports/mock.py b/httpx/_transports/mock.py
index 82043da2..8c418f59 100644
--- a/httpx/_transports/mock.py
+++ b/httpx/_transports/mock.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import typing
from .._models import Request, Response
@@ -7,8 +9,11 @@ SyncHandler = typing.Callable[[Request], Response]
AsyncHandler = typing.Callable[[Request], typing.Coroutine[None, None, Response]]
+__all__ = ["MockTransport"]
+
+
class MockTransport(AsyncBaseTransport, BaseTransport):
- def __init__(self, handler: typing.Union[SyncHandler, AsyncHandler]) -> None:
+ def __init__(self, handler: SyncHandler | AsyncHandler) -> None:
self.handler = handler
def handle_request(
diff --git a/httpx/_transports/wsgi.py b/httpx/_transports/wsgi.py
index a23d42c4..8592ffe0 100644
--- a/httpx/_transports/wsgi.py
+++ b/httpx/_transports/wsgi.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import io
import itertools
import sys
@@ -14,6 +16,9 @@ if typing.TYPE_CHECKING:
_T = typing.TypeVar("_T")
+__all__ = ["WSGITransport"]
+
+
def _skip_leading_empty_chunks(body: typing.Iterable[_T]) -> typing.Iterable[_T]:
body = iter(body)
for chunk in body:
@@ -71,11 +76,11 @@ class WSGITransport(BaseTransport):
def __init__(
self,
- app: "WSGIApplication",
+ app: WSGIApplication,
raise_app_exceptions: bool = True,
script_name: str = "",
remote_addr: str = "127.0.0.1",
- wsgi_errors: typing.Optional[typing.TextIO] = None,
+ wsgi_errors: typing.TextIO | None = None,
) -> None:
self.app = app
self.raise_app_exceptions = raise_app_exceptions
@@ -117,8 +122,8 @@ class WSGITransport(BaseTransport):
def start_response(
status: str,
- response_headers: typing.List[typing.Tuple[str, str]],
- exc_info: typing.Optional["OptExcInfo"] = None,
+ response_headers: list[tuple[str, str]],
+ exc_info: OptExcInfo | None = None,
) -> typing.Callable[[bytes], typing.Any]:
nonlocal seen_status, seen_response_headers, seen_exc_info
seen_status = status
diff --git a/httpx/_types.py b/httpx/_types.py
index 649d101d..661af262 100644
--- a/httpx/_types.py
+++ b/httpx/_types.py
@@ -78,8 +78,8 @@ TimeoutTypes = Union[
Tuple[Optional[float], Optional[float], Optional[float], Optional[float]],
"Timeout",
]
-ProxyTypes = Union[URLTypes, "Proxy"]
-ProxiesTypes = Union[ProxyTypes, Dict[URLTypes, Union[None, ProxyTypes]]]
+ProxyTypes = Union["URL", str, "Proxy"]
+ProxiesTypes = Union[ProxyTypes, Dict[Union["URL", str], Union[None, ProxyTypes]]]
AuthTypes = Union[
Tuple[Union[str, bytes], Union[str, bytes]],
@@ -108,6 +108,8 @@ RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
RequestExtensions = MutableMapping[str, Any]
+__all__ = ["AsyncByteStream", "SyncByteStream"]
+
class SyncByteStream:
def __iter__(self) -> Iterator[bytes]:
diff --git a/httpx/_urlparse.py b/httpx/_urlparse.py
index 07bbea90..479c2ef8 100644
--- a/httpx/_urlparse.py
+++ b/httpx/_urlparse.py
@@ -15,6 +15,9 @@ Previously we relied on the excellent `rfc3986` package to handle URL parsing an
validation, but this module provides a simpler alternative, with less indirection
required.
"""
+
+from __future__ import annotations
+
import ipaddress
import re
import typing
@@ -95,10 +98,10 @@ class ParseResult(typing.NamedTuple):
scheme: str
userinfo: str
host: str
- port: typing.Optional[int]
+ port: int | None
path: str
- query: typing.Optional[str]
- fragment: typing.Optional[str]
+ query: str | None
+ fragment: str | None
@property
def authority(self) -> str:
@@ -119,7 +122,7 @@ class ParseResult(typing.NamedTuple):
]
)
- def copy_with(self, **kwargs: typing.Optional[str]) -> "ParseResult":
+ def copy_with(self, **kwargs: str | None) -> ParseResult:
if not kwargs:
return self
@@ -146,7 +149,7 @@ class ParseResult(typing.NamedTuple):
)
-def urlparse(url: str = "", **kwargs: typing.Optional[str]) -> ParseResult:
+def urlparse(url: str = "", **kwargs: str | None) -> ParseResult:
# Initial basic checks on allowable URLs.
# ---------------------------------------
@@ -157,7 +160,12 @@ def urlparse(url: str = "", **kwargs: typing.Optional[str]) -> ParseResult:
# If a URL includes any ASCII control characters including \t, \r, \n,
# then treat it as invalid.
if any(char.isascii() and not char.isprintable() for char in url):
- raise InvalidURL("Invalid non-printable ASCII character in URL")
+ char = next(char for char in url if char.isascii() and not char.isprintable())
+ idx = url.find(char)
+ error = (
+ f"Invalid non-printable ASCII character in URL, {char!r} at position {idx}."
+ )
+ raise InvalidURL(error)
# Some keyword arguments require special handling.
# ------------------------------------------------
@@ -202,9 +210,15 @@ def urlparse(url: str = "", **kwargs: typing.Optional[str]) -> ParseResult:
# If a component includes any ASCII control characters including \t, \r, \n,
# then treat it as invalid.
if any(char.isascii() and not char.isprintable() for char in value):
- raise InvalidURL(
- f"Invalid non-printable ASCII character in URL component '{key}'"
+ char = next(
+ char for char in value if char.isascii() and not char.isprintable()
)
+ idx = value.find(char)
+ error = (
+ f"Invalid non-printable ASCII character in URL {key} component, "
+ f"{char!r} at position {idx}."
+ )
+ raise InvalidURL(error)
# Ensure that keyword arguments match as a valid regex.
if not COMPONENT_REGEX[key].fullmatch(value):
@@ -243,29 +257,34 @@ def urlparse(url: str = "", **kwargs: typing.Optional[str]) -> ParseResult:
parsed_scheme: str = scheme.lower()
parsed_userinfo: str = quote(userinfo, safe=SUB_DELIMS + ":")
parsed_host: str = encode_host(host)
- parsed_port: typing.Optional[int] = normalize_port(port, scheme)
+ parsed_port: int | None = normalize_port(port, scheme)
has_scheme = parsed_scheme != ""
has_authority = (
parsed_userinfo != "" or parsed_host != "" or parsed_port is not None
)
validate_path(path, has_scheme=has_scheme, has_authority=has_authority)
- if has_authority:
+ if has_scheme or has_authority:
path = normalize_path(path)
# The GEN_DELIMS set is... : / ? # [ ] @
# These do not need to be percent-quoted unless they serve as delimiters for the
# specific component.
+ WHATWG_SAFE = '`{}%|^\\"'
# For 'path' we need to drop ? and # from the GEN_DELIMS set.
- parsed_path: str = quote(path, safe=SUB_DELIMS + ":/[]@")
+ parsed_path: str = quote(path, safe=SUB_DELIMS + WHATWG_SAFE + ":/[]@")
# For 'query' we need to drop '#' from the GEN_DELIMS set.
- parsed_query: typing.Optional[str] = (
- None if query is None else quote(query, safe=SUB_DELIMS + ":/?[]@")
+ parsed_query: str | None = (
+ None
+ if query is None
+ else quote(query, safe=SUB_DELIMS + WHATWG_SAFE + ":/?[]@")
)
# For 'fragment' we can include all of the GEN_DELIMS set.
- parsed_fragment: typing.Optional[str] = (
- None if fragment is None else quote(fragment, safe=SUB_DELIMS + ":/?#[]@")
+ parsed_fragment: str | None = (
+ None
+ if fragment is None
+ else quote(fragment, safe=SUB_DELIMS + WHATWG_SAFE + ":/?#[]@")
)
# The parsed ASCII bytestrings are our canonical form.
@@ -318,7 +337,8 @@ def encode_host(host: str) -> str:
# From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2
#
# reg-name = *( unreserved / pct-encoded / sub-delims )
- return quote(host.lower(), safe=SUB_DELIMS)
+ WHATWG_SAFE = '"`{}%|\\'
+ return quote(host.lower(), safe=SUB_DELIMS + WHATWG_SAFE)
# IDNA hostnames
try:
@@ -327,9 +347,7 @@ def encode_host(host: str) -> str:
raise InvalidURL(f"Invalid IDNA hostname: {host!r}")
-def normalize_port(
- port: typing.Optional[typing.Union[str, int]], scheme: str
-) -> typing.Optional[int]:
+def normalize_port(port: str | int | None, scheme: str) -> int | None:
# From https://tools.ietf.org/html/rfc3986#section-3.2.3
#
# "A scheme may define a default port. For example, the "http" scheme
@@ -368,19 +386,17 @@ def validate_path(path: str, has_scheme: bool, has_authority: bool) -> None:
# must either be empty or begin with a slash ("/") character."
if path and not path.startswith("/"):
raise InvalidURL("For absolute URLs, path must be empty or begin with '/'")
- else:
+
+ if not has_scheme and not has_authority:
# If a URI does not contain an authority component, then the path cannot begin
# with two slash characters ("//").
if path.startswith("//"):
- raise InvalidURL(
- "URLs with no authority component cannot have a path starting with '//'"
- )
+ raise InvalidURL("Relative URLs cannot have a path starting with '//'")
+
# In addition, a URI reference (Section 4.1) may be a relative-path reference,
# in which case the first path segment cannot contain a colon (":") character.
- if path.startswith(":") and not has_scheme:
- raise InvalidURL(
- "URLs with no scheme component cannot have a path starting with ':'"
- )
+ if path.startswith(":"):
+ raise InvalidURL("Relative URLs cannot have a path starting with ':'")
def normalize_path(path: str) -> str:
@@ -391,9 +407,18 @@ def normalize_path(path: str) -> str:
normalize_path("/path/./to/somewhere/..") == "/path/to"
"""
- # https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
+ # Fast return when no '.' characters in the path.
+ if "." not in path:
+ return path
+
components = path.split("/")
- output: typing.List[str] = []
+
+ # Fast return when no '.' or '..' components in the path.
+ if "." not in components and ".." not in components:
+ return path
+
+ # https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
+ output: list[str] = []
for component in components:
if component == ".":
pass
@@ -405,44 +430,22 @@ def normalize_path(path: str) -> str:
return "/".join(output)
-def percent_encode(char: str) -> str:
- """
- Replace a single character with the percent-encoded representation.
-
- Characters outside the ASCII range are represented with their a percent-encoded
- representation of their UTF-8 byte sequence.
-
- For example:
-
- percent_encode(" ") == "%20"
- """
- return "".join([f"%{byte:02x}" for byte in char.encode("utf-8")]).upper()
-
-
-def is_safe(string: str, safe: str = "/") -> bool:
- """
- Determine if a given string is already quote-safe.
- """
- NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe + "%"
-
- # All characters must already be non-escaping or '%'
- for char in string:
- if char not in NON_ESCAPED_CHARS:
- return False
-
- return True
+def PERCENT(string: str) -> str:
+ return "".join([f"%{byte:02X}" for byte in string.encode("utf-8")])
def percent_encoded(string: str, safe: str = "/") -> str:
"""
Use percent-encoding to quote a string.
"""
- if is_safe(string, safe=safe):
+ NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe
+
+ # Fast path for strings that don't need escaping.
+ if not string.rstrip(NON_ESCAPED_CHARS):
return string
- NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe
return "".join(
- [char if char in NON_ESCAPED_CHARS else percent_encode(char) for char in string]
+ [char if char in NON_ESCAPED_CHARS else PERCENT(char) for char in string]
)
@@ -479,7 +482,7 @@ def quote(string: str, safe: str = "/") -> str:
return "".join(parts)
-def urlencode(items: typing.List[typing.Tuple[str, str]]) -> str:
+def urlencode(items: list[tuple[str, str]]) -> str:
"""
We can use a much simpler version of the stdlib urlencode here because
we don't need to handle a bunch of different typing cases, such as bytes vs str.
diff --git a/httpx/_urls.py b/httpx/_urls.py
index 26202e95..ec4ea6b3 100644
--- a/httpx/_urls.py
+++ b/httpx/_urls.py
@@ -1,12 +1,16 @@
+from __future__ import annotations
+
import typing
from urllib.parse import parse_qs, unquote
import idna
-from ._types import QueryParamTypes, RawURL, URLTypes
+from ._types import QueryParamTypes, RawURL
from ._urlparse import urlencode, urlparse
from ._utils import primitive_value_to_str
+__all__ = ["URL", "QueryParams"]
+
class URL:
"""
@@ -70,9 +74,7 @@ class URL:
themselves.
"""
- def __init__(
- self, url: typing.Union["URL", str] = "", **kwargs: typing.Any
- ) -> None:
+ def __init__(self, url: URL | str = "", **kwargs: typing.Any) -> None:
if kwargs:
allowed = {
"scheme": str,
@@ -213,7 +215,7 @@ class URL:
return self._uri_reference.host.encode("ascii")
@property
- def port(self) -> typing.Optional[int]:
+ def port(self) -> int | None:
"""
The URL port as an integer.
@@ -270,7 +272,7 @@ class URL:
return query.encode("ascii")
@property
- def params(self) -> "QueryParams":
+ def params(self) -> QueryParams:
"""
The URL query parameters, neatly parsed and packaged into an immutable
multidict representation.
@@ -338,7 +340,7 @@ class URL:
"""
return not self.is_absolute_url
- def copy_with(self, **kwargs: typing.Any) -> "URL":
+ def copy_with(self, **kwargs: typing.Any) -> URL:
"""
Copy this URL, returning a new URL with some components altered.
Accepts the same set of parameters as the components that are made
@@ -353,19 +355,19 @@ class URL:
"""
return URL(self, **kwargs)
- def copy_set_param(self, key: str, value: typing.Any = None) -> "URL":
+ def copy_set_param(self, key: str, value: typing.Any = None) -> URL:
return self.copy_with(params=self.params.set(key, value))
- def copy_add_param(self, key: str, value: typing.Any = None) -> "URL":
+ def copy_add_param(self, key: str, value: typing.Any = None) -> URL:
return self.copy_with(params=self.params.add(key, value))
- def copy_remove_param(self, key: str) -> "URL":
+ def copy_remove_param(self, key: str) -> URL:
return self.copy_with(params=self.params.remove(key))
- def copy_merge_params(self, params: QueryParamTypes) -> "URL":
+ def copy_merge_params(self, params: QueryParamTypes) -> URL:
return self.copy_with(params=self.params.merge(params))
- def join(self, url: URLTypes) -> "URL":
+ def join(self, url: URL | str) -> URL:
"""
Return an absolute URL, using this URL as the base.
@@ -420,9 +422,7 @@ class QueryParams(typing.Mapping[str, str]):
URL query parameters, as a multi-dict.
"""
- def __init__(
- self, *args: typing.Optional[QueryParamTypes], **kwargs: typing.Any
- ) -> None:
+ def __init__(self, *args: QueryParamTypes | None, **kwargs: typing.Any) -> None:
assert len(args) < 2, "Too many arguments."
assert not (args and kwargs), "Cannot mix named and unnamed arguments."
@@ -434,7 +434,7 @@ class QueryParams(typing.Mapping[str, str]):
elif isinstance(value, QueryParams):
self._dict = {k: list(v) for k, v in value._dict.items()}
else:
- dict_value: typing.Dict[typing.Any, typing.List[typing.Any]] = {}
+ dict_value: dict[typing.Any, list[typing.Any]] = {}
if isinstance(value, (list, tuple)):
# Convert list inputs like:
# [("a", "123"), ("a", "456"), ("b", "789")]
@@ -495,7 +495,7 @@ class QueryParams(typing.Mapping[str, str]):
"""
return {k: v[0] for k, v in self._dict.items()}.items()
- def multi_items(self) -> typing.List[typing.Tuple[str, str]]:
+ def multi_items(self) -> list[tuple[str, str]]:
"""
Return all items in the query params. Allow duplicate keys to occur.
@@ -504,7 +504,7 @@ class QueryParams(typing.Mapping[str, str]):
q = httpx.QueryParams("a=123&a=456&b=789")
assert list(q.multi_items()) == [("a", "123"), ("a", "456"), ("b", "789")]
"""
- multi_items: typing.List[typing.Tuple[str, str]] = []
+ multi_items: list[tuple[str, str]] = []
for k, v in self._dict.items():
multi_items.extend([(k, i) for i in v])
return multi_items
@@ -523,7 +523,7 @@ class QueryParams(typing.Mapping[str, str]):
return self._dict[str(key)][0]
return default
- def get_list(self, key: str) -> typing.List[str]:
+ def get_list(self, key: str) -> list[str]:
"""
Get all values from the query param for a given key.
@@ -534,7 +534,7 @@ class QueryParams(typing.Mapping[str, str]):
"""
return list(self._dict.get(str(key), []))
- def set(self, key: str, value: typing.Any = None) -> "QueryParams":
+ def set(self, key: str, value: typing.Any = None) -> QueryParams:
"""
Return a new QueryParams instance, setting the value of a key.
@@ -549,7 +549,7 @@ class QueryParams(typing.Mapping[str, str]):
q._dict[str(key)] = [primitive_value_to_str(value)]
return q
- def add(self, key: str, value: typing.Any = None) -> "QueryParams":
+ def add(self, key: str, value: typing.Any = None) -> QueryParams:
"""
Return a new QueryParams instance, setting or appending the value of a key.
@@ -564,7 +564,7 @@ class QueryParams(typing.Mapping[str, str]):
q._dict[str(key)] = q.get_list(key) + [primitive_value_to_str(value)]
return q
- def remove(self, key: str) -> "QueryParams":
+ def remove(self, key: str) -> QueryParams:
"""
Return a new QueryParams instance, removing the value of a key.
@@ -579,7 +579,7 @@ class QueryParams(typing.Mapping[str, str]):
q._dict.pop(str(key), None)
return q
- def merge(self, params: typing.Optional[QueryParamTypes] = None) -> "QueryParams":
+ def merge(self, params: QueryParamTypes | None = None) -> QueryParams:
"""
Return a new QueryParams instance, updated with.
@@ -635,7 +635,7 @@ class QueryParams(typing.Mapping[str, str]):
query_string = str(self)
return f"{class_name}({query_string!r})"
- def update(self, params: typing.Optional[QueryParamTypes] = None) -> None:
+ def update(self, params: QueryParamTypes | None = None) -> None:
raise RuntimeError(
"QueryParams are immutable since 0.18.0. "
"Use `q = q.merge(...)` to create an updated copy."
diff --git a/httpx/_utils.py b/httpx/_utils.py
index 81a4c0ef..0160d61d 100644
--- a/httpx/_utils.py
+++ b/httpx/_utils.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import codecs
import email.message
import ipaddress
@@ -25,9 +27,9 @@ _HTML5_FORM_ENCODING_RE = re.compile(
def normalize_header_key(
- value: typing.Union[str, bytes],
+ value: str | bytes,
lower: bool,
- encoding: typing.Optional[str] = None,
+ encoding: str | None = None,
) -> bytes:
"""
Coerce str/bytes into a strictly byte-wise HTTP header key.
@@ -40,18 +42,18 @@ def normalize_header_key(
return bytes_value.lower() if lower else bytes_value
-def normalize_header_value(
- value: typing.Union[str, bytes], encoding: typing.Optional[str] = None
-) -> bytes:
+def normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes:
"""
Coerce str/bytes into a strictly byte-wise HTTP header value.
"""
if isinstance(value, bytes):
return value
+ if not isinstance(value, str):
+ raise TypeError(f"Header value must be str or bytes, not {type(value)}")
return value.encode(encoding or "ascii")
-def primitive_value_to_str(value: "PrimitiveData") -> str:
+def primitive_value_to_str(value: PrimitiveData) -> str:
"""
Coerce a primitive data type into a string value.
@@ -89,7 +91,7 @@ def format_form_param(name: str, value: str) -> bytes:
return f'{name}="{value}"'.encode()
-def get_ca_bundle_from_env() -> typing.Optional[str]:
+def get_ca_bundle_from_env() -> str | None:
if "SSL_CERT_FILE" in os.environ:
ssl_file = Path(os.environ["SSL_CERT_FILE"])
if ssl_file.is_file():
@@ -101,7 +103,7 @@ def get_ca_bundle_from_env() -> typing.Optional[str]:
return None
-def parse_header_links(value: str) -> typing.List[typing.Dict[str, str]]:
+def parse_header_links(value: str) -> list[dict[str, str]]:
"""
Returns a list of parsed link headers, for more info see:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
@@ -117,7 +119,7 @@ def parse_header_links(value: str) -> typing.List[typing.Dict[str, str]]:
:param value: HTTP Link entity-header field
:return: list of parsed link headers
"""
- links: typing.List[typing.Dict[str, str]] = []
+ links: list[dict[str, str]] = []
replace_chars = " '\""
value = value.strip(replace_chars)
if not value:
@@ -138,7 +140,7 @@ def parse_header_links(value: str) -> typing.List[typing.Dict[str, str]]:
return links
-def parse_content_type_charset(content_type: str) -> typing.Optional[str]:
+def parse_content_type_charset(content_type: str) -> str | None:
# We used to use `cgi.parse_header()` here, but `cgi` became a dead battery.
# See: https://peps.python.org/pep-0594/#cgi
msg = email.message.Message()
@@ -150,21 +152,21 @@ SENSITIVE_HEADERS = {"authorization", "proxy-authorization"}
def obfuscate_sensitive_headers(
- items: typing.Iterable[typing.Tuple[typing.AnyStr, typing.AnyStr]],
-) -> typing.Iterator[typing.Tuple[typing.AnyStr, typing.AnyStr]]:
+ items: typing.Iterable[tuple[typing.AnyStr, typing.AnyStr]],
+) -> typing.Iterator[tuple[typing.AnyStr, typing.AnyStr]]:
for k, v in items:
if to_str(k.lower()) in SENSITIVE_HEADERS:
v = to_bytes_or_str("[secure]", match_type_of=v)
yield k, v
-def port_or_default(url: "URL") -> typing.Optional[int]:
+def port_or_default(url: URL) -> int | None:
if url.port is not None:
return url.port
return {"http": 80, "https": 443}.get(url.scheme)
-def same_origin(url: "URL", other: "URL") -> bool:
+def same_origin(url: URL, other: URL) -> bool:
"""
Return 'True' if the given URLs share the same origin.
"""
@@ -175,7 +177,7 @@ def same_origin(url: "URL", other: "URL") -> bool:
)
-def is_https_redirect(url: "URL", location: "URL") -> bool:
+def is_https_redirect(url: URL, location: URL) -> bool:
"""
Return 'True' if 'location' is a HTTPS upgrade of 'url'
"""
@@ -190,7 +192,7 @@ def is_https_redirect(url: "URL", location: "URL") -> bool:
)
-def get_environment_proxies() -> typing.Dict[str, typing.Optional[str]]:
+def get_environment_proxies() -> dict[str, str | None]:
"""Gets proxy information from the environment"""
# urllib.request.getproxies() falls back on System
@@ -198,7 +200,7 @@ def get_environment_proxies() -> typing.Dict[str, typing.Optional[str]]:
# We don't want to propagate non-HTTP proxies into
# our configuration such as 'TRAVIS_APT_PROXY'.
proxy_info = getproxies()
- mounts: typing.Dict[str, typing.Optional[str]] = {}
+ mounts: dict[str, str | None] = {}
for scheme in ("http", "https", "all"):
if proxy_info.get(scheme):
@@ -239,11 +241,11 @@ def get_environment_proxies() -> typing.Dict[str, typing.Optional[str]]:
return mounts
-def to_bytes(value: typing.Union[str, bytes], encoding: str = "utf-8") -> bytes:
+def to_bytes(value: str | bytes, encoding: str = "utf-8") -> bytes:
return value.encode(encoding) if isinstance(value, str) else value
-def to_str(value: typing.Union[str, bytes], encoding: str = "utf-8") -> str:
+def to_str(value: str | bytes, encoding: str = "utf-8") -> str:
return value if isinstance(value, str) else value.decode(encoding)
@@ -255,13 +257,13 @@ def unquote(value: str) -> str:
return value[1:-1] if value[0] == value[-1] == '"' else value
-def guess_content_type(filename: typing.Optional[str]) -> typing.Optional[str]:
+def guess_content_type(filename: str | None) -> str | None:
if filename:
return mimetypes.guess_type(filename)[0] or "application/octet-stream"
return None
-def peek_filelike_length(stream: typing.Any) -> typing.Optional[int]:
+def peek_filelike_length(stream: typing.Any) -> int | None:
"""
Given a file-like stream object, return its length in number of bytes
without reading it into memory.
@@ -360,7 +362,7 @@ class URLPattern:
self.host = "" if url.host == "*" else url.host
self.port = url.port
if not url.host or url.host == "*":
- self.host_regex: typing.Optional[typing.Pattern[str]] = None
+ self.host_regex: typing.Pattern[str] | None = None
elif url.host.startswith("*."):
# *.example.com should match "www.example.com", but not "example.com"
domain = re.escape(url.host[2:])
@@ -374,7 +376,7 @@ class URLPattern:
domain = re.escape(url.host)
self.host_regex = re.compile(f"^{domain}$")
- def matches(self, other: "URL") -> bool:
+ def matches(self, other: URL) -> bool:
if self.scheme and self.scheme != other.scheme:
return False
if (
@@ -388,7 +390,7 @@ class URLPattern:
return True
@property
- def priority(self) -> typing.Tuple[int, int, int]:
+ def priority(self) -> tuple[int, int, int]:
"""
The priority allows URLPattern instances to be sortable, so that
we can match from most specific to least specific.
@@ -404,7 +406,7 @@ class URLPattern:
def __hash__(self) -> int:
return hash(self.pattern)
- def __lt__(self, other: "URLPattern") -> bool:
+ def __lt__(self, other: URLPattern) -> bool:
return self.priority < other.priority
def __eq__(self, other: typing.Any) -> bool:
diff --git a/mkdocs.yml b/mkdocs.yml
index c0ccd805..86ca1e53 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -4,6 +4,7 @@ site_url: https://www.python-httpx.org/
theme:
name: 'material'
+ custom_dir: 'docs/overrides'
palette:
- scheme: 'default'
media: '(prefers-color-scheme: light)'
@@ -16,8 +17,6 @@ theme:
toggle:
icon: 'material/lightbulb-outline'
name: 'Switch to light mode'
- features:
- - navigation.sections
repo_name: encode/httpx
repo_url: https://github.com/encode/httpx/
@@ -25,9 +24,18 @@ edit_uri: ""
nav:
- Introduction: 'index.md'
- - Usage:
- - QuickStart: 'quickstart.md'
- - Advanced Usage: 'advanced.md'
+ - QuickStart: 'quickstart.md'
+ - Advanced:
+ - Clients: 'advanced/clients.md'
+ - Authentication: 'advanced/authentication.md'
+ - SSL: 'advanced/ssl.md'
+ - Proxies: 'advanced/proxies.md'
+ - Timeouts: 'advanced/timeouts.md'
+ - Resource Limits: 'advanced/resource-limits.md'
+ - Event Hooks: 'advanced/event-hooks.md'
+ - Transports: 'advanced/transports.md'
+ - Text Encodings: 'advanced/text-encodings.md'
+ - Extensions: 'advanced/extensions.md'
- Guides:
- Async Support: 'async.md'
- HTTP/2 Support: 'http2.md'
diff --git a/pyproject.toml b/pyproject.toml
index 271694d2..484989a1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,6 +56,9 @@ asyncio = [
trio = [
"httpcore[trio]"
]
+zstd = [
+ "zstandard>=0.18.0",
+]
[project.scripts]
httpx = "httpx:main"
@@ -97,14 +100,16 @@ text = "\n---\n\n[Full changelog](https://github.com/encode/httpx/blob/master/CH
pattern = 'src="(docs/img/.*?)"'
replacement = 'src="https://raw.githubusercontent.com/encode/httpx/master/\1"'
-# https://beta.ruff.rs/docs/configuration/#using-rufftoml
-[tool.ruff]
+[tool.ruff.lint]
select = ["E", "F", "I", "B", "PIE"]
ignore = ["B904", "B028"]
-[tool.ruff.isort]
+[tool.ruff.lint.isort]
combine-as-imports = true
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["F403", "F405"]
+
[tool.mypy]
ignore_missing_imports = true
strict = true
diff --git a/requirements.txt b/requirements.txt
index 31f05044..bb2ddd20 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,27 +2,27 @@
# On the other hand, we're not pinning package dependencies, because our tests
# needs to pass with the latest version of the packages.
# Reference: https://github.com/encode/httpx/pull/1721#discussion_r661241588
--e .[asyncio,trio,brotli,cli,http2,socks]
+-e .[asyncio,trio,brotli,cli,http2,socks,zstd]
# Optional charset auto-detection
# Used in our test cases
chardet==5.2.0
# Documentation
-mkdocs==1.5.3
+mkdocs==1.6.1
mkautodoc==0.2.0
-mkdocs-material==9.5.3
+mkdocs-material==9.5.34
# Packaging
-build==1.0.3
-twine==4.0.2
+build==1.2.1
+twine==5.1.1
# Tests & Linting
-coverage[toml]==7.4.0
-cryptography==41.0.7
-mypy==1.8.0
-pytest==7.4.4
-ruff==0.1.9
-trio==0.22.2
+coverage[toml]==7.6.1
+cryptography==43.0.1
+mypy==1.11.2
+pytest==8.3.2
+ruff==0.6.3
+trio==0.26.2
trustme==1.1.0
-uvicorn==0.24.0.post1
+uvicorn==0.30.6
diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py
index 49664df5..8d7eaa3c 100644
--- a/tests/client/test_async_client.py
+++ b/tests/client/test_async_client.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import typing
from datetime import timedelta
@@ -181,7 +183,7 @@ async def test_100_continue(server):
async def test_context_managed_transport():
class Transport(httpx.AsyncBaseTransport):
def __init__(self) -> None:
- self.events: typing.List[str] = []
+ self.events: list[str] = []
async def aclose(self):
# The base implementation of httpx.AsyncBaseTransport just
@@ -214,7 +216,7 @@ async def test_context_managed_transport_and_mount():
class Transport(httpx.AsyncBaseTransport):
def __init__(self, name: str) -> None:
self.name: str = name
- self.events: typing.List[str] = []
+ self.events: list[str] = []
async def aclose(self):
# The base implementation of httpx.AsyncBaseTransport just
diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py
index e6bac23d..5776fc33 100644
--- a/tests/client/test_auth.py
+++ b/tests/client/test_auth.py
@@ -3,6 +3,7 @@ Integration tests for authentication.
Unit tests for auth classes also exist in tests/test_auth.py
"""
+
import hashlib
import netrc
import os
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
index fcc6ec6a..65783901 100644
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import typing
from datetime import timedelta
@@ -230,7 +232,7 @@ def test_merge_relative_url_with_encoded_slashes():
def test_context_managed_transport():
class Transport(httpx.BaseTransport):
def __init__(self) -> None:
- self.events: typing.List[str] = []
+ self.events: list[str] = []
def close(self):
# The base implementation of httpx.BaseTransport just
@@ -262,7 +264,7 @@ def test_context_managed_transport_and_mount():
class Transport(httpx.BaseTransport):
def __init__(self, name: str) -> None:
self.name: str = name
- self.events: typing.List[str] = []
+ self.events: list[str] = []
def close(self):
# The base implementation of httpx.BaseTransport just
@@ -355,7 +357,7 @@ def test_raw_client_header():
assert response.json() == [
["Host", "example.org"],
["Accept", "*/*"],
- ["Accept-Encoding", "gzip, deflate, br"],
+ ["Accept-Encoding", "gzip, deflate, br, zstd"],
["Connection", "keep-alive"],
["User-Agent", f"python-httpx/{httpx.__version__}"],
["Example-Header", "example-value"],
diff --git a/tests/client/test_event_hooks.py b/tests/client/test_event_hooks.py
index 6604dd31..78fb0484 100644
--- a/tests/client/test_event_hooks.py
+++ b/tests/client/test_event_hooks.py
@@ -36,7 +36,7 @@ def test_event_hooks():
"host": "127.0.0.1:8000",
"user-agent": f"python-httpx/{httpx.__version__}",
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
},
@@ -87,7 +87,7 @@ async def test_async_event_hooks():
"host": "127.0.0.1:8000",
"user-agent": f"python-httpx/{httpx.__version__}",
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
},
@@ -144,7 +144,7 @@ def test_event_hooks_with_redirect():
"host": "127.0.0.1:8000",
"user-agent": f"python-httpx/{httpx.__version__}",
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
},
@@ -159,7 +159,7 @@ def test_event_hooks_with_redirect():
"host": "127.0.0.1:8000",
"user-agent": f"python-httpx/{httpx.__version__}",
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
},
@@ -201,7 +201,7 @@ async def test_async_event_hooks_with_redirect():
"host": "127.0.0.1:8000",
"user-agent": f"python-httpx/{httpx.__version__}",
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
},
@@ -216,7 +216,7 @@ async def test_async_event_hooks_with_redirect():
"host": "127.0.0.1:8000",
"user-agent": f"python-httpx/{httpx.__version__}",
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
},
diff --git a/tests/client/test_headers.py b/tests/client/test_headers.py
index 264ca0bd..b8d29767 100755
--- a/tests/client/test_headers.py
+++ b/tests/client/test_headers.py
@@ -34,7 +34,7 @@ def test_client_header():
assert response.json() == {
"headers": {
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"example-header": "example-value",
"host": "example.org",
@@ -56,7 +56,7 @@ def test_header_merge():
assert response.json() == {
"headers": {
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"host": "example.org",
"user-agent": "python-myclient/0.2.1",
@@ -78,7 +78,7 @@ def test_header_merge_conflicting_headers():
assert response.json() == {
"headers": {
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"host": "example.org",
"user-agent": f"python-httpx/{httpx.__version__}",
@@ -100,7 +100,7 @@ def test_header_update():
assert first_response.json() == {
"headers": {
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"host": "example.org",
"user-agent": f"python-httpx/{httpx.__version__}",
@@ -111,7 +111,7 @@ def test_header_update():
assert second_response.json() == {
"headers": {
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"another-header": "AThing",
"connection": "keep-alive",
"host": "example.org",
@@ -164,7 +164,7 @@ def test_remove_default_header():
assert response.json() == {
"headers": {
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"host": "example.org",
}
@@ -177,6 +177,14 @@ def test_header_does_not_exist():
del headers["baz"]
+def test_header_with_incorrect_value():
+ with pytest.raises(
+ TypeError,
+ match=f"Header value must be str or bytes, not {type(None)}",
+ ):
+ httpx.Headers({"foo": None}) # type: ignore
+
+
def test_host_with_auth_and_port_in_url():
"""
The Host header should only include the hostname, or hostname:port
@@ -192,7 +200,7 @@ def test_host_with_auth_and_port_in_url():
assert response.json() == {
"headers": {
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"host": "example.org",
"user-agent": f"python-httpx/{httpx.__version__}",
@@ -215,7 +223,7 @@ def test_host_with_non_default_port_in_url():
assert response.json() == {
"headers": {
"accept": "*/*",
- "accept-encoding": "gzip, deflate, br",
+ "accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"host": "example.org:123",
"user-agent": f"python-httpx/{httpx.__version__}",
diff --git a/tests/conftest.py b/tests/conftest.py
index 1bcb6a42..5c4a6ae5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -236,7 +236,7 @@ class TestServer(Server):
def install_signal_handlers(self) -> None:
# Disable the default installation of handlers for signals such as SIGTERM,
# because it can only be done in the main thread.
- pass
+ pass # pragma: nocover
async def serve(self, sockets=None):
self.restart_requested = asyncio.Event()
diff --git a/tests/models/test_url.py b/tests/models/test_url.py
index 79e1605a..523a89bf 100644
--- a/tests/models/test_url.py
+++ b/tests/models/test_url.py
@@ -229,6 +229,11 @@ def test_url_normalized_host():
assert url.host == "example.com"
+def test_url_percent_escape_host():
+ url = httpx.URL("https://exam le.com/")
+ assert url.host == "exam%20le.com"
+
+
def test_url_ipv4_like_host():
"""rare host names used to quality as IPv4"""
url = httpx.URL("https://023b76x43144/")
@@ -278,24 +283,64 @@ def test_url_leading_dot_prefix_on_relative_url():
assert url.path == "../abc"
-# Tests for optional percent encoding
+# Tests for query parameter percent encoding.
+#
+# Percent-encoding in `params={}` should match browser form behavior.
-def test_param_requires_encoding():
+def test_param_with_space():
+ # Params passed as form key-value pairs should be escaped.
url = httpx.URL("http://webservice", params={"u": "with spaces"})
assert str(url) == "http://webservice?u=with%20spaces"
def test_param_does_not_require_encoding():
+ # Params passed as form key-value pairs should be escaped.
+ url = httpx.URL("http://webservice", params={"u": "%"})
+ assert str(url) == "http://webservice?u=%25"
+
+
+def test_param_with_percent_encoded():
+ # Params passed as form key-value pairs should always be escaped,
+ # even if they include a valid escape sequence.
+ # We want to match browser form behaviour here.
url = httpx.URL("http://webservice", params={"u": "with%20spaces"})
- assert str(url) == "http://webservice?u=with%20spaces"
+ assert str(url) == "http://webservice?u=with%2520spaces"
def test_param_with_existing_escape_requires_encoding():
+ # Params passed as form key-value pairs should always be escaped,
+ # even if they include a valid escape sequence.
+ # We want to match browser form behaviour here.
url = httpx.URL("http://webservice", params={"u": "http://example.com?q=foo%2Fa"})
assert str(url) == "http://webservice?u=http%3A%2F%2Fexample.com%3Fq%3Dfoo%252Fa"
+# Tests for query parameter percent encoding.
+#
+# Percent-encoding in `url={}` should match browser URL bar behavior.
+
+
+def test_query_with_existing_percent_encoding():
+ # Valid percent encoded sequences should not be double encoded.
+ url = httpx.URL("http://webservice?u=phrase%20with%20spaces")
+ assert str(url) == "http://webservice?u=phrase%20with%20spaces"
+
+
+def test_query_requiring_percent_encoding():
+ # Characters that require percent encoding should be encoded.
+ url = httpx.URL("http://webservice?u=phrase with spaces")
+ assert str(url) == "http://webservice?u=phrase%20with%20spaces"
+
+
+def test_query_with_mixed_percent_encoding():
+ # When a mix of encoded and unencoded characters are present,
+ # characters that require percent encoding should be encoded,
+ # while existing sequences should not be double encoded.
+ url = httpx.URL("http://webservice?u=phrase%20with spaces")
+ assert str(url) == "http://webservice?u=phrase%20with%20spaces"
+
+
# Tests for invalid URLs
@@ -322,15 +367,17 @@ def test_url_excessively_long_component():
def test_url_non_printing_character_in_url():
with pytest.raises(httpx.InvalidURL) as exc:
httpx.URL("https://www.example.com/\n")
- assert str(exc.value) == "Invalid non-printable ASCII character in URL"
+ assert str(exc.value) == (
+ "Invalid non-printable ASCII character in URL, '\\n' at position 24."
+ )
def test_url_non_printing_character_in_component():
with pytest.raises(httpx.InvalidURL) as exc:
httpx.URL("https://www.example.com", path="/\n")
- assert (
- str(exc.value)
- == "Invalid non-printable ASCII character in URL component 'path'"
+ assert str(exc.value) == (
+ "Invalid non-printable ASCII character in URL path component, "
+ "'\\n' at position 1."
)
@@ -370,17 +417,11 @@ def test_urlparse_with_invalid_path():
with pytest.raises(httpx.InvalidURL) as exc:
httpx.URL(path="//abc")
- assert (
- str(exc.value)
- == "URLs with no authority component cannot have a path starting with '//'"
- )
+ assert str(exc.value) == "Relative URLs cannot have a path starting with '//'"
with pytest.raises(httpx.InvalidURL) as exc:
httpx.URL(path=":abc")
- assert (
- str(exc.value)
- == "URLs with no scheme component cannot have a path starting with ':'"
- )
+ assert str(exc.value) == "Relative URLs cannot have a path starting with ':'"
def test_url_with_relative_path():
diff --git a/tests/models/test_whatwg.py b/tests/models/test_whatwg.py
new file mode 100644
index 00000000..6e00a921
--- /dev/null
+++ b/tests/models/test_whatwg.py
@@ -0,0 +1,52 @@
+# The WHATWG have various tests that can be used to validate the URL parsing.
+#
+# https://url.spec.whatwg.org/
+
+import json
+
+import pytest
+
+from httpx._urlparse import urlparse
+
+# URL test cases from...
+# https://github.com/web-platform-tests/wpt/blob/master/url/resources/urltestdata.json
+with open("tests/models/whatwg.json", "r") as input:
+ test_cases = json.load(input)
+ test_cases = [
+ item
+ for item in test_cases
+ if not isinstance(item, str) and not item.get("failure")
+ ]
+
+
+@pytest.mark.parametrize("test_case", test_cases)
+def test_urlparse(test_case):
+ if test_case["href"] in ("a: foo.com", "lolscheme:x x#x%20x"):
+ # Skip these two test cases.
+ # WHATWG cases where are not using percent-encoding for the space character.
+ # Anyone know what's going on here?
+ return
+
+ p = urlparse(test_case["href"])
+
+ # Test cases include the protocol with the trailing ":"
+ protocol = p.scheme + ":"
+ # Include the square brackets for IPv6 addresses.
+ hostname = f"[{p.host}]" if ":" in p.host else p.host
+ # The test cases use a string representation of the port.
+ port = "" if p.port is None else str(p.port)
+ # I have nothing to say about this one.
+ path = p.path
+ # The 'search' and 'hash' components in the whatwg tests are semantic, not literal.
+ # Our parsing differentiates between no query/hash and empty-string query/hash.
+ search = "" if p.query in (None, "") else "?" + str(p.query)
+ hash = "" if p.fragment in (None, "") else "#" + str(p.fragment)
+
+ # URL hostnames are case-insensitive.
+ # We normalize these, unlike the WHATWG test cases.
+ assert protocol == test_case["protocol"]
+ assert hostname.lower() == test_case["hostname"].lower()
+ assert port == test_case["port"]
+ assert path == test_case["pathname"]
+ assert search == test_case["search"]
+ assert hash == test_case["hash"]
diff --git a/tests/models/whatwg.json b/tests/models/whatwg.json
new file mode 100644
index 00000000..85a5140f
--- /dev/null
+++ b/tests/models/whatwg.json
@@ -0,0 +1,9746 @@
+[
+ "See ../README.md for a description of the format.",
+ {
+ "input": "http://example\t.\norg",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://user:pass@foo:21/bar;par?b#c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://user:pass@foo:21/bar;par?b#c",
+ "origin": "http://foo:21",
+ "protocol": "http:",
+ "username": "user",
+ "password": "pass",
+ "host": "foo:21",
+ "hostname": "foo",
+ "port": "21",
+ "pathname": "/bar;par",
+ "search": "?b",
+ "hash": "#c"
+ },
+ {
+ "input": "https://test:@test",
+ "base": null,
+ "href": "https://test@test/",
+ "origin": "https://test",
+ "protocol": "https:",
+ "username": "test",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https://:@test",
+ "base": null,
+ "href": "https://test/",
+ "origin": "https://test",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-special://test:@test/x",
+ "base": null,
+ "href": "non-special://test@test/x",
+ "origin": "null",
+ "protocol": "non-special:",
+ "username": "test",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/x",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-special://:@test/x",
+ "base": null,
+ "href": "non-special://test/x",
+ "origin": "null",
+ "protocol": "non-special:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/x",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:foo.com",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/foo.com",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/foo.com",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "\t :foo.com \n",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:foo.com",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:foo.com",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": " foo.com ",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/foo.com",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/foo.com",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "a:\t foo.com",
+ "base": "http://example.org/foo/bar",
+ "href": "a: foo.com",
+ "origin": "null",
+ "protocol": "a:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": " foo.com",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://f:21/ b ? d # e ",
+ "base": "http://example.org/foo/bar",
+ "href": "http://f:21/%20b%20?%20d%20#%20e",
+ "origin": "http://f:21",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "f:21",
+ "hostname": "f",
+ "port": "21",
+ "pathname": "/%20b%20",
+ "search": "?%20d%20",
+ "hash": "#%20e"
+ },
+ {
+ "input": "lolscheme:x x#x x",
+ "base": null,
+ "href": "lolscheme:x x#x%20x",
+ "protocol": "lolscheme:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "x x",
+ "search": "",
+ "hash": "#x%20x"
+ },
+ {
+ "input": "http://f:/c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://f/c",
+ "origin": "http://f",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "f",
+ "hostname": "f",
+ "port": "",
+ "pathname": "/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://f:0/c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://f:0/c",
+ "origin": "http://f:0",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "f:0",
+ "hostname": "f",
+ "port": "0",
+ "pathname": "/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://f:00000000000000/c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://f:0/c",
+ "origin": "http://f:0",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "f:0",
+ "hostname": "f",
+ "port": "0",
+ "pathname": "/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://f:00000000000000000000080/c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://f/c",
+ "origin": "http://f",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "f",
+ "hostname": "f",
+ "port": "",
+ "pathname": "/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://f:b/c",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://f: /c",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://f:\n/c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://f/c",
+ "origin": "http://f",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "f",
+ "hostname": "f",
+ "port": "",
+ "pathname": "/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://f:fifty-two/c",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://f:999999/c",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "non-special://f:999999/c",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://f: 21 / b ? d # e ",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": " \t",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ":foo.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:foo.com/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:foo.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ":foo.com\\",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:foo.com/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:foo.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ":",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ":a",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:a",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:a",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ":/",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ":\\",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ":#",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:#",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "#",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar#",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "#/",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar#/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": "#/"
+ },
+ {
+ "input": "#\\",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar#\\",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": "#\\"
+ },
+ {
+ "input": "#;?",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar#;?",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": "#;?"
+ },
+ {
+ "input": "?",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar?",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ":23",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:23",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:23",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/:23",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/:23",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/:23",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "\\x",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/x",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/x",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "\\\\x\\hello",
+ "base": "http://example.org/foo/bar",
+ "href": "http://x/hello",
+ "origin": "http://x",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "x",
+ "hostname": "x",
+ "port": "",
+ "pathname": "/hello",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "::",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/::",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/::",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "::23",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/::23",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/::23",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "foo://",
+ "base": "http://example.org/foo/bar",
+ "href": "foo://",
+ "origin": "null",
+ "protocol": "foo:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://a:b@c:29/d",
+ "base": "http://example.org/foo/bar",
+ "href": "http://a:b@c:29/d",
+ "origin": "http://c:29",
+ "protocol": "http:",
+ "username": "a",
+ "password": "b",
+ "host": "c:29",
+ "hostname": "c",
+ "port": "29",
+ "pathname": "/d",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http::@c:29",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/:@c:29",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/:@c:29",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://&a:foo(b]c@d:2/",
+ "base": "http://example.org/foo/bar",
+ "href": "http://&a:foo(b%5Dc@d:2/",
+ "origin": "http://d:2",
+ "protocol": "http:",
+ "username": "&a",
+ "password": "foo(b%5Dc",
+ "host": "d:2",
+ "hostname": "d",
+ "port": "2",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://::@c@d:2",
+ "base": "http://example.org/foo/bar",
+ "href": "http://:%3A%40c@d:2/",
+ "origin": "http://d:2",
+ "protocol": "http:",
+ "username": "",
+ "password": "%3A%40c",
+ "host": "d:2",
+ "hostname": "d",
+ "port": "2",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://foo.com:b@d/",
+ "base": "http://example.org/foo/bar",
+ "href": "http://foo.com:b@d/",
+ "origin": "http://d",
+ "protocol": "http:",
+ "username": "foo.com",
+ "password": "b",
+ "host": "d",
+ "hostname": "d",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://foo.com/\\@",
+ "base": "http://example.org/foo/bar",
+ "href": "http://foo.com//@",
+ "origin": "http://foo.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo.com",
+ "hostname": "foo.com",
+ "port": "",
+ "pathname": "//@",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:\\\\foo.com\\",
+ "base": "http://example.org/foo/bar",
+ "href": "http://foo.com/",
+ "origin": "http://foo.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo.com",
+ "hostname": "foo.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:\\\\a\\b:c\\d@foo.com\\",
+ "base": "http://example.org/foo/bar",
+ "href": "http://a/b:c/d@foo.com/",
+ "origin": "http://a",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "a",
+ "hostname": "a",
+ "port": "",
+ "pathname": "/b:c/d@foo.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://a:b@c\\",
+ "base": null,
+ "href": "http://a:b@c/",
+ "origin": "http://c",
+ "protocol": "http:",
+ "username": "a",
+ "password": "b",
+ "host": "c",
+ "hostname": "c",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws://a@b\\c",
+ "base": null,
+ "href": "ws://a@b/c",
+ "origin": "ws://b",
+ "protocol": "ws:",
+ "username": "a",
+ "password": "",
+ "host": "b",
+ "hostname": "b",
+ "port": "",
+ "pathname": "/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "foo:/",
+ "base": "http://example.org/foo/bar",
+ "href": "foo:/",
+ "origin": "null",
+ "protocol": "foo:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "foo:/bar.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "foo:/bar.com/",
+ "origin": "null",
+ "protocol": "foo:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/bar.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "foo://///////",
+ "base": "http://example.org/foo/bar",
+ "href": "foo://///////",
+ "origin": "null",
+ "protocol": "foo:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "///////",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "foo://///////bar.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "foo://///////bar.com/",
+ "origin": "null",
+ "protocol": "foo:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "///////bar.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "foo:////://///",
+ "base": "http://example.org/foo/bar",
+ "href": "foo:////://///",
+ "origin": "null",
+ "protocol": "foo:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//://///",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "c:/foo",
+ "base": "http://example.org/foo/bar",
+ "href": "c:/foo",
+ "origin": "null",
+ "protocol": "c:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//foo/bar",
+ "base": "http://example.org/foo/bar",
+ "href": "http://foo/bar",
+ "origin": "http://foo",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://foo/path;a??e#f#g",
+ "base": "http://example.org/foo/bar",
+ "href": "http://foo/path;a??e#f#g",
+ "origin": "http://foo",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/path;a",
+ "search": "??e",
+ "hash": "#f#g"
+ },
+ {
+ "input": "http://foo/abcd?efgh?ijkl",
+ "base": "http://example.org/foo/bar",
+ "href": "http://foo/abcd?efgh?ijkl",
+ "origin": "http://foo",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/abcd",
+ "search": "?efgh?ijkl",
+ "hash": ""
+ },
+ {
+ "input": "http://foo/abcd#foo?bar",
+ "base": "http://example.org/foo/bar",
+ "href": "http://foo/abcd#foo?bar",
+ "origin": "http://foo",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/abcd",
+ "search": "",
+ "hash": "#foo?bar"
+ },
+ {
+ "input": "[61:24:74]:98",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/[61:24:74]:98",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/[61:24:74]:98",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:[61:27]/:foo",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/[61:27]/:foo",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/[61:27]/:foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://[1::2]:3:4",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://2001::1",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://2001::1]",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://2001::1]:80",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://[2001::1]",
+ "base": "http://example.org/foo/bar",
+ "href": "http://[2001::1]/",
+ "origin": "http://[2001::1]",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "[2001::1]",
+ "hostname": "[2001::1]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://[::127.0.0.1]",
+ "base": "http://example.org/foo/bar",
+ "href": "http://[::7f00:1]/",
+ "origin": "http://[::7f00:1]",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "[::7f00:1]",
+ "hostname": "[::7f00:1]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://[::127.0.0.1.]",
+ "base": "http://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "http://[0:0:0:0:0:0:13.1.68.3]",
+ "base": "http://example.org/foo/bar",
+ "href": "http://[::d01:4403]/",
+ "origin": "http://[::d01:4403]",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "[::d01:4403]",
+ "hostname": "[::d01:4403]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://[2001::1]:80",
+ "base": "http://example.org/foo/bar",
+ "href": "http://[2001::1]/",
+ "origin": "http://[2001::1]",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "[2001::1]",
+ "hostname": "[2001::1]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/example.com/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftp:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "ftp://example.com/",
+ "origin": "ftp://example.com",
+ "protocol": "ftp:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "https://example.com/",
+ "origin": "https://example.com",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "madeupscheme:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "madeupscheme:/example.com/",
+ "origin": "null",
+ "protocol": "madeupscheme:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "file:///example.com/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://example:1/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://example:test/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://example%/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://[example]/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "ftps:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "ftps:/example.com/",
+ "origin": "null",
+ "protocol": "ftps:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "gopher:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "gopher:/example.com/",
+ "origin": "null",
+ "protocol": "gopher:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "ws://example.com/",
+ "origin": "ws://example.com",
+ "protocol": "ws:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wss:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "wss://example.com/",
+ "origin": "wss://example.com",
+ "protocol": "wss:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "data:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "data:/example.com/",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "javascript:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "javascript:/example.com/",
+ "origin": "null",
+ "protocol": "javascript:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "mailto:/example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "mailto:/example.com/",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/example.com/",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftp:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "ftp://example.com/",
+ "origin": "ftp://example.com",
+ "protocol": "ftp:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "https://example.com/",
+ "origin": "https://example.com",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "madeupscheme:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "madeupscheme:example.com/",
+ "origin": "null",
+ "protocol": "madeupscheme:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftps:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "ftps:example.com/",
+ "origin": "null",
+ "protocol": "ftps:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "gopher:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "gopher:example.com/",
+ "origin": "null",
+ "protocol": "gopher:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "ws://example.com/",
+ "origin": "ws://example.com",
+ "protocol": "ws:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wss:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "wss://example.com/",
+ "origin": "wss://example.com",
+ "protocol": "wss:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "data:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "data:example.com/",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "javascript:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "javascript:example.com/",
+ "origin": "null",
+ "protocol": "javascript:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "mailto:example.com/",
+ "base": "http://example.org/foo/bar",
+ "href": "mailto:example.com/",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/a/b/c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/a/b/c",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/a/b/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/a/ /c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/a/%20/c",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/a/%20/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/a%2fc",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/a%2fc",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/a%2fc",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/a/%2f/c",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/a/%2f/c",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/a/%2f/c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "#β",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar#%CE%B2",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": "#%CE%B2"
+ },
+ {
+ "input": "data:text/html,test#test",
+ "base": "http://example.org/foo/bar",
+ "href": "data:text/html,test#test",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "text/html,test",
+ "search": "",
+ "hash": "#test"
+ },
+ {
+ "input": "tel:1234567890",
+ "base": "http://example.org/foo/bar",
+ "href": "tel:1234567890",
+ "origin": "null",
+ "protocol": "tel:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "1234567890",
+ "search": "",
+ "hash": ""
+ },
+ "# Based on https://felixfbecker.github.io/whatwg-url-custom-host-repro/",
+ {
+ "input": "ssh://example.com/foo/bar.git",
+ "base": "http://example.org/",
+ "href": "ssh://example.com/foo/bar.git",
+ "origin": "null",
+ "protocol": "ssh:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/bar.git",
+ "search": "",
+ "hash": ""
+ },
+ "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/file.html",
+ {
+ "input": "file:c:\\foo\\bar.html",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///c:/foo/bar.html",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/c:/foo/bar.html",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": " File:c|////foo\\bar.html",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///c:////foo/bar.html",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/c:////foo/bar.html",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C|/foo/bar",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///C:/foo/bar",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/C|\\foo\\bar",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///C:/foo/bar",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//C|/foo/bar",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///C:/foo/bar",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//server/file",
+ "base": "file:///tmp/mock/path",
+ "href": "file://server/file",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "server",
+ "hostname": "server",
+ "port": "",
+ "pathname": "/file",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "\\\\server\\file",
+ "base": "file:///tmp/mock/path",
+ "href": "file://server/file",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "server",
+ "hostname": "server",
+ "port": "",
+ "pathname": "/file",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/\\server/file",
+ "base": "file:///tmp/mock/path",
+ "href": "file://server/file",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "server",
+ "hostname": "server",
+ "port": "",
+ "pathname": "/file",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///foo/bar.txt",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///foo/bar.txt",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/foo/bar.txt",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///home/me",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///home/me",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/home/me",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///test",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///test",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://test",
+ "base": "file:///tmp/mock/path",
+ "href": "file://test/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://localhost",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://localhost/",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://localhost/test",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///test",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "test",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///tmp/mock/test",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/tmp/mock/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:test",
+ "base": "file:///tmp/mock/path",
+ "href": "file:///tmp/mock/test",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/tmp/mock/test",
+ "search": "",
+ "hash": ""
+ },
+ "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js",
+ {
+ "input": "http://example.com/././foo",
+ "base": null,
+ "href": "http://example.com/foo",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/./.foo",
+ "base": null,
+ "href": "http://example.com/.foo",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/.foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/.",
+ "base": null,
+ "href": "http://example.com/foo/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/./",
+ "base": null,
+ "href": "http://example.com/foo/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/bar/..",
+ "base": null,
+ "href": "http://example.com/foo/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/bar/../",
+ "base": null,
+ "href": "http://example.com/foo/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/..bar",
+ "base": null,
+ "href": "http://example.com/foo/..bar",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/..bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/bar/../ton",
+ "base": null,
+ "href": "http://example.com/foo/ton",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/ton",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/bar/../ton/../../a",
+ "base": null,
+ "href": "http://example.com/a",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/a",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/../../..",
+ "base": null,
+ "href": "http://example.com/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/../../../ton",
+ "base": null,
+ "href": "http://example.com/ton",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/ton",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/%2e",
+ "base": null,
+ "href": "http://example.com/foo/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/%2e%2",
+ "base": null,
+ "href": "http://example.com/foo/%2e%2",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/%2e%2",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar",
+ "base": null,
+ "href": "http://example.com/%2e.bar",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%2e.bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com////../..",
+ "base": null,
+ "href": "http://example.com//",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/bar//../..",
+ "base": null,
+ "href": "http://example.com/foo/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo/bar//..",
+ "base": null,
+ "href": "http://example.com/foo/bar/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo/bar/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo",
+ "base": null,
+ "href": "http://example.com/foo",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/%20foo",
+ "base": null,
+ "href": "http://example.com/%20foo",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%20foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo%",
+ "base": null,
+ "href": "http://example.com/foo%",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo%",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo%2",
+ "base": null,
+ "href": "http://example.com/foo%2",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo%2",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo%2zbar",
+ "base": null,
+ "href": "http://example.com/foo%2zbar",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo%2zbar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo%2©zbar",
+ "base": null,
+ "href": "http://example.com/foo%2%C3%82%C2%A9zbar",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo%2%C3%82%C2%A9zbar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo%41%7a",
+ "base": null,
+ "href": "http://example.com/foo%41%7a",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo%41%7a",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo\t\u0091%91",
+ "base": null,
+ "href": "http://example.com/foo%C2%91%91",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo%C2%91%91",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo%00%51",
+ "base": null,
+ "href": "http://example.com/foo%00%51",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foo%00%51",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/(%28:%3A%29)",
+ "base": null,
+ "href": "http://example.com/(%28:%3A%29)",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/(%28:%3A%29)",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/%3A%3a%3C%3c",
+ "base": null,
+ "href": "http://example.com/%3A%3a%3C%3c",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%3A%3a%3C%3c",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/foo\tbar",
+ "base": null,
+ "href": "http://example.com/foobar",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/foobar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com\\\\foo\\\\bar",
+ "base": null,
+ "href": "http://example.com//foo//bar",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "//foo//bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd",
+ "base": null,
+ "href": "http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%7Ffp3%3Eju%3Dduvgw%3Dd",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/@asdf%40",
+ "base": null,
+ "href": "http://example.com/@asdf%40",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/@asdf%40",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/你好你好",
+ "base": null,
+ "href": "http://example.com/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/‥/foo",
+ "base": null,
+ "href": "http://example.com/%E2%80%A5/foo",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%E2%80%A5/foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com//foo",
+ "base": null,
+ "href": "http://example.com/%EF%BB%BF/foo",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%EF%BB%BF/foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com//foo//bar",
+ "base": null,
+ "href": "http://example.com/%E2%80%AE/foo/%E2%80%AD/bar",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%E2%80%AE/foo/%E2%80%AD/bar",
+ "search": "",
+ "hash": ""
+ },
+ "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/relative.js",
+ {
+ "input": "http://www.google.com/foo?bar=baz#",
+ "base": null,
+ "href": "http://www.google.com/foo?bar=baz#",
+ "origin": "http://www.google.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.google.com",
+ "hostname": "www.google.com",
+ "port": "",
+ "pathname": "/foo",
+ "search": "?bar=baz",
+ "hash": ""
+ },
+ {
+ "input": "http://www.google.com/foo?bar=baz# »",
+ "base": null,
+ "href": "http://www.google.com/foo?bar=baz#%20%C2%BB",
+ "origin": "http://www.google.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.google.com",
+ "hostname": "www.google.com",
+ "port": "",
+ "pathname": "/foo",
+ "search": "?bar=baz",
+ "hash": "#%20%C2%BB"
+ },
+ {
+ "input": "data:test# »",
+ "base": null,
+ "href": "data:test#%20%C2%BB",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "test",
+ "search": "",
+ "hash": "#%20%C2%BB"
+ },
+ {
+ "input": "http://www.google.com",
+ "base": null,
+ "href": "http://www.google.com/",
+ "origin": "http://www.google.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.google.com",
+ "hostname": "www.google.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://192.0x00A80001",
+ "base": null,
+ "href": "http://192.168.0.1/",
+ "origin": "http://192.168.0.1",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "192.168.0.1",
+ "hostname": "192.168.0.1",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://www/foo%2Ehtml",
+ "base": null,
+ "href": "http://www/foo%2Ehtml",
+ "origin": "http://www",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www",
+ "hostname": "www",
+ "port": "",
+ "pathname": "/foo%2Ehtml",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://www/foo/%2E/html",
+ "base": null,
+ "href": "http://www/foo/html",
+ "origin": "http://www",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www",
+ "hostname": "www",
+ "port": "",
+ "pathname": "/foo/html",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://user:pass@/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://%25DOMAIN:foobar@foodomain.com/",
+ "base": null,
+ "href": "http://%25DOMAIN:foobar@foodomain.com/",
+ "origin": "http://foodomain.com",
+ "protocol": "http:",
+ "username": "%25DOMAIN",
+ "password": "foobar",
+ "host": "foodomain.com",
+ "hostname": "foodomain.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:\\\\www.google.com\\foo",
+ "base": null,
+ "href": "http://www.google.com/foo",
+ "origin": "http://www.google.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.google.com",
+ "hostname": "www.google.com",
+ "port": "",
+ "pathname": "/foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://foo:80/",
+ "base": null,
+ "href": "http://foo/",
+ "origin": "http://foo",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://foo:81/",
+ "base": null,
+ "href": "http://foo:81/",
+ "origin": "http://foo:81",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo:81",
+ "hostname": "foo",
+ "port": "81",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "httpa://foo:80/",
+ "base": null,
+ "href": "httpa://foo:80/",
+ "origin": "null",
+ "protocol": "httpa:",
+ "username": "",
+ "password": "",
+ "host": "foo:80",
+ "hostname": "foo",
+ "port": "80",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://foo:-80/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://foo:443/",
+ "base": null,
+ "href": "https://foo/",
+ "origin": "https://foo",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https://foo:80/",
+ "base": null,
+ "href": "https://foo:80/",
+ "origin": "https://foo:80",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "foo:80",
+ "hostname": "foo",
+ "port": "80",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftp://foo:21/",
+ "base": null,
+ "href": "ftp://foo/",
+ "origin": "ftp://foo",
+ "protocol": "ftp:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftp://foo:80/",
+ "base": null,
+ "href": "ftp://foo:80/",
+ "origin": "ftp://foo:80",
+ "protocol": "ftp:",
+ "username": "",
+ "password": "",
+ "host": "foo:80",
+ "hostname": "foo",
+ "port": "80",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "gopher://foo:70/",
+ "base": null,
+ "href": "gopher://foo:70/",
+ "origin": "null",
+ "protocol": "gopher:",
+ "username": "",
+ "password": "",
+ "host": "foo:70",
+ "hostname": "foo",
+ "port": "70",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "gopher://foo:443/",
+ "base": null,
+ "href": "gopher://foo:443/",
+ "origin": "null",
+ "protocol": "gopher:",
+ "username": "",
+ "password": "",
+ "host": "foo:443",
+ "hostname": "foo",
+ "port": "443",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws://foo:80/",
+ "base": null,
+ "href": "ws://foo/",
+ "origin": "ws://foo",
+ "protocol": "ws:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws://foo:81/",
+ "base": null,
+ "href": "ws://foo:81/",
+ "origin": "ws://foo:81",
+ "protocol": "ws:",
+ "username": "",
+ "password": "",
+ "host": "foo:81",
+ "hostname": "foo",
+ "port": "81",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws://foo:443/",
+ "base": null,
+ "href": "ws://foo:443/",
+ "origin": "ws://foo:443",
+ "protocol": "ws:",
+ "username": "",
+ "password": "",
+ "host": "foo:443",
+ "hostname": "foo",
+ "port": "443",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws://foo:815/",
+ "base": null,
+ "href": "ws://foo:815/",
+ "origin": "ws://foo:815",
+ "protocol": "ws:",
+ "username": "",
+ "password": "",
+ "host": "foo:815",
+ "hostname": "foo",
+ "port": "815",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wss://foo:80/",
+ "base": null,
+ "href": "wss://foo:80/",
+ "origin": "wss://foo:80",
+ "protocol": "wss:",
+ "username": "",
+ "password": "",
+ "host": "foo:80",
+ "hostname": "foo",
+ "port": "80",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wss://foo:81/",
+ "base": null,
+ "href": "wss://foo:81/",
+ "origin": "wss://foo:81",
+ "protocol": "wss:",
+ "username": "",
+ "password": "",
+ "host": "foo:81",
+ "hostname": "foo",
+ "port": "81",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wss://foo:443/",
+ "base": null,
+ "href": "wss://foo/",
+ "origin": "wss://foo",
+ "protocol": "wss:",
+ "username": "",
+ "password": "",
+ "host": "foo",
+ "hostname": "foo",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wss://foo:815/",
+ "base": null,
+ "href": "wss://foo:815/",
+ "origin": "wss://foo:815",
+ "protocol": "wss:",
+ "username": "",
+ "password": "",
+ "host": "foo:815",
+ "hostname": "foo",
+ "port": "815",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:/example.com/",
+ "base": null,
+ "href": "http://example.com/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftp:/example.com/",
+ "base": null,
+ "href": "ftp://example.com/",
+ "origin": "ftp://example.com",
+ "protocol": "ftp:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https:/example.com/",
+ "base": null,
+ "href": "https://example.com/",
+ "origin": "https://example.com",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "madeupscheme:/example.com/",
+ "base": null,
+ "href": "madeupscheme:/example.com/",
+ "origin": "null",
+ "protocol": "madeupscheme:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:/example.com/",
+ "base": null,
+ "href": "file:///example.com/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftps:/example.com/",
+ "base": null,
+ "href": "ftps:/example.com/",
+ "origin": "null",
+ "protocol": "ftps:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "gopher:/example.com/",
+ "base": null,
+ "href": "gopher:/example.com/",
+ "origin": "null",
+ "protocol": "gopher:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws:/example.com/",
+ "base": null,
+ "href": "ws://example.com/",
+ "origin": "ws://example.com",
+ "protocol": "ws:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wss:/example.com/",
+ "base": null,
+ "href": "wss://example.com/",
+ "origin": "wss://example.com",
+ "protocol": "wss:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "data:/example.com/",
+ "base": null,
+ "href": "data:/example.com/",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "javascript:/example.com/",
+ "base": null,
+ "href": "javascript:/example.com/",
+ "origin": "null",
+ "protocol": "javascript:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "mailto:/example.com/",
+ "base": null,
+ "href": "mailto:/example.com/",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:example.com/",
+ "base": null,
+ "href": "http://example.com/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftp:example.com/",
+ "base": null,
+ "href": "ftp://example.com/",
+ "origin": "ftp://example.com",
+ "protocol": "ftp:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https:example.com/",
+ "base": null,
+ "href": "https://example.com/",
+ "origin": "https://example.com",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "madeupscheme:example.com/",
+ "base": null,
+ "href": "madeupscheme:example.com/",
+ "origin": "null",
+ "protocol": "madeupscheme:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ftps:example.com/",
+ "base": null,
+ "href": "ftps:example.com/",
+ "origin": "null",
+ "protocol": "ftps:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "gopher:example.com/",
+ "base": null,
+ "href": "gopher:example.com/",
+ "origin": "null",
+ "protocol": "gopher:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ws:example.com/",
+ "base": null,
+ "href": "ws://example.com/",
+ "origin": "ws://example.com",
+ "protocol": "ws:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wss:example.com/",
+ "base": null,
+ "href": "wss://example.com/",
+ "origin": "wss://example.com",
+ "protocol": "wss:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "data:example.com/",
+ "base": null,
+ "href": "data:example.com/",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "javascript:example.com/",
+ "base": null,
+ "href": "javascript:example.com/",
+ "origin": "null",
+ "protocol": "javascript:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "mailto:example.com/",
+ "base": null,
+ "href": "mailto:example.com/",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "example.com/",
+ "search": "",
+ "hash": ""
+ },
+ "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/segments-userinfo-vs-host.html",
+ {
+ "input": "http:@www.example.com",
+ "base": null,
+ "href": "http://www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:/@www.example.com",
+ "base": null,
+ "href": "http://www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://@www.example.com",
+ "base": null,
+ "href": "http://www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:a:b@www.example.com",
+ "base": null,
+ "href": "http://a:b@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "a",
+ "password": "b",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:/a:b@www.example.com",
+ "base": null,
+ "href": "http://a:b@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "a",
+ "password": "b",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://a:b@www.example.com",
+ "base": null,
+ "href": "http://a:b@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "a",
+ "password": "b",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://@pple.com",
+ "base": null,
+ "href": "http://pple.com/",
+ "origin": "http://pple.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "pple.com",
+ "hostname": "pple.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http::b@www.example.com",
+ "base": null,
+ "href": "http://:b@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "b",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:/:b@www.example.com",
+ "base": null,
+ "href": "http://:b@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "b",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://:b@www.example.com",
+ "base": null,
+ "href": "http://:b@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "b",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:/:@/www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http://user@/www.example.com",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http:@/www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http:/@/www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http://@/www.example.com",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https:@/www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http:a:b@/www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http:/a:b@/www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http://a:b@/www.example.com",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http::@/www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http:a:@www.example.com",
+ "base": null,
+ "href": "http://a@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "a",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:/a:@www.example.com",
+ "base": null,
+ "href": "http://a@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "a",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://a:@www.example.com",
+ "base": null,
+ "href": "http://a@www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "a",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://www.@pple.com",
+ "base": null,
+ "href": "http://www.@pple.com/",
+ "origin": "http://pple.com",
+ "protocol": "http:",
+ "username": "www.",
+ "password": "",
+ "host": "pple.com",
+ "hostname": "pple.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http:@:www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http:/@:www.example.com",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "http://@:www.example.com",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://:@www.example.com",
+ "base": null,
+ "href": "http://www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "# Others",
+ {
+ "input": "/",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/test.txt",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/test.txt",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/test.txt",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": ".",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "..",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "test.txt",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/test.txt",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/test.txt",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "./test.txt",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/test.txt",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/test.txt",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "../test.txt",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/test.txt",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/test.txt",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "../aaa/test.txt",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/aaa/test.txt",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/aaa/test.txt",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "../../test.txt",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/test.txt",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/test.txt",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "中/test.txt",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example.com/%E4%B8%AD/test.txt",
+ "origin": "http://www.example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "port": "",
+ "pathname": "/%E4%B8%AD/test.txt",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://www.example2.com",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example2.com/",
+ "origin": "http://www.example2.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example2.com",
+ "hostname": "www.example2.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//www.example2.com",
+ "base": "http://www.example.com/test",
+ "href": "http://www.example2.com/",
+ "origin": "http://www.example2.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.example2.com",
+ "hostname": "www.example2.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:...",
+ "base": "http://www.example.com/test",
+ "href": "file:///...",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/...",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:..",
+ "base": "http://www.example.com/test",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:a",
+ "base": "http://www.example.com/test",
+ "href": "file:///a",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/a",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:.",
+ "base": null,
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:.",
+ "base": "http://www.example.com/test",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/host.html",
+ "Basic canonicalization, uppercase should be converted to lowercase",
+ {
+ "input": "http://ExAmPlE.CoM",
+ "base": "http://other.com/",
+ "href": "http://example.com/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example example.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://Goo%20 goo%7C|.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[:]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "U+3000 is mapped to U+0020 (space) which is disallowed",
+ {
+ "input": "http://GOO\u00a0\u3000goo.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "Other types of space (no-break, zero-width, zero-width-no-break) are name-prepped away to nothing. U+200B, U+2060, and U+FEFF, are ignored",
+ {
+ "input": "http://GOO\u200b\u2060\ufeffgoo.com",
+ "base": "http://other.com/",
+ "href": "http://googoo.com/",
+ "origin": "http://googoo.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "googoo.com",
+ "hostname": "googoo.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "Leading and trailing C0 control or space",
+ {
+ "input": "\u0000\u001b\u0004\u0012 http://example.com/\u001f \u000d ",
+ "base": null,
+ "href": "http://example.com/",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "Ideographic full stop (full-width period for Chinese, etc.) should be treated as a dot. U+3002 is mapped to U+002E (dot)",
+ {
+ "input": "http://www.foo。bar.com",
+ "base": "http://other.com/",
+ "href": "http://www.foo.bar.com/",
+ "origin": "http://www.foo.bar.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "www.foo.bar.com",
+ "hostname": "www.foo.bar.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "Invalid unicode characters should fail... U+FDD0 is disallowed; %ef%b7%90 is U+FDD0",
+ {
+ "input": "http://\ufdd0zyx.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "This is the same as previous but escaped",
+ {
+ "input": "http://%ef%b7%90zyx.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "U+FFFD",
+ {
+ "input": "https://\ufffd",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://%EF%BF%BD",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://x/\ufffd?\ufffd#\ufffd",
+ "base": null,
+ "href": "https://x/%EF%BF%BD?%EF%BF%BD#%EF%BF%BD",
+ "origin": "https://x",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "x",
+ "hostname": "x",
+ "port": "",
+ "pathname": "/%EF%BF%BD",
+ "search": "?%EF%BF%BD",
+ "hash": "#%EF%BF%BD"
+ },
+ "Domain is ASCII, but a label is invalid IDNA",
+ {
+ "input": "http://a.b.c.xn--pokxncvks",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://10.0.0.xn--pokxncvks",
+ "base": null,
+ "failure": true
+ },
+ "IDNA labels should be matched case-insensitively",
+ {
+ "input": "http://a.b.c.XN--pokxncvks",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a.b.c.Xn--pokxncvks",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://10.0.0.XN--pokxncvks",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://10.0.0.xN--pokxncvks",
+ "base": null,
+ "failure": true
+ },
+ "Test name prepping, fullwidth input should be converted to ASCII and NOT IDN-ized. This is 'Go' in fullwidth UTF-8/UTF-16.",
+ {
+ "input": "http://Go.com",
+ "base": "http://other.com/",
+ "href": "http://go.com/",
+ "origin": "http://go.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "go.com",
+ "hostname": "go.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "URL spec forbids the following. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24257",
+ {
+ "input": "http://%41.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://%ef%bc%85%ef%bc%94%ef%bc%91.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "...%00 in fullwidth should fail (also as escaped UTF-8 input)",
+ {
+ "input": "http://%00.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://%ef%bc%85%ef%bc%90%ef%bc%90.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "Basic IDN support, UTF-8 and UTF-16 input should be converted to IDN",
+ {
+ "input": "http://你好你好",
+ "base": "http://other.com/",
+ "href": "http://xn--6qqa088eba/",
+ "origin": "http://xn--6qqa088eba",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "xn--6qqa088eba",
+ "hostname": "xn--6qqa088eba",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https://faß.ExAmPlE/",
+ "base": null,
+ "href": "https://xn--fa-hia.example/",
+ "origin": "https://xn--fa-hia.example",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "xn--fa-hia.example",
+ "hostname": "xn--fa-hia.example",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "sc://faß.ExAmPlE/",
+ "base": null,
+ "href": "sc://fa%C3%9F.ExAmPlE/",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "fa%C3%9F.ExAmPlE",
+ "hostname": "fa%C3%9F.ExAmPlE",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "Invalid escaped characters should fail and the percents should be escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191",
+ {
+ "input": "http://%zz%66%a.com",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "If we get an invalid character that has been escaped.",
+ {
+ "input": "http://%25",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://hello%00",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "Escaped numbers should be treated like IP addresses if they are.",
+ {
+ "input": "http://%30%78%63%30%2e%30%32%35%30.01",
+ "base": "http://other.com/",
+ "href": "http://192.168.0.1/",
+ "origin": "http://192.168.0.1",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "192.168.0.1",
+ "hostname": "192.168.0.1",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://%30%78%63%30%2e%30%32%35%30.01%2e",
+ "base": "http://other.com/",
+ "href": "http://192.168.0.1/",
+ "origin": "http://192.168.0.1",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "192.168.0.1",
+ "hostname": "192.168.0.1",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://192.168.0.257",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "Invalid escaping in hosts causes failure",
+ {
+ "input": "http://%3g%78%63%30%2e%30%32%35%30%2E.01",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "A space in a host causes failure",
+ {
+ "input": "http://192.168.0.1 hello",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "https://x x:12",
+ "base": null,
+ "failure": true
+ },
+ "Fullwidth and escaped UTF-8 fullwidth should still be treated as IP",
+ {
+ "input": "http://0Xc0.0250.01",
+ "base": "http://other.com/",
+ "href": "http://192.168.0.1/",
+ "origin": "http://192.168.0.1",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "192.168.0.1",
+ "hostname": "192.168.0.1",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "Domains with empty labels",
+ {
+ "input": "http://./",
+ "base": null,
+ "href": "http://./",
+ "origin": "http://.",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": ".",
+ "hostname": ".",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://../",
+ "base": null,
+ "href": "http://../",
+ "origin": "http://..",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "..",
+ "hostname": "..",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "Non-special domains with empty labels",
+ {
+ "input": "h://.",
+ "base": null,
+ "href": "h://.",
+ "origin": "null",
+ "protocol": "h:",
+ "username": "",
+ "password": "",
+ "host": ".",
+ "hostname": ".",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "hash": ""
+ },
+ "Broken IPv6",
+ {
+ "input": "http://[www.google.com]/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://[google.com]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[::1.2.3.4x]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[::1.2.3.]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[::1.2.]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[::.1.2]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[::1.]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[::.1]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://[::%31]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://%5B::1]",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ "Misc Unicode",
+ {
+ "input": "http://foo:💩@example.com/bar",
+ "base": "http://other.com/",
+ "href": "http://foo:%F0%9F%92%A9@example.com/bar",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "foo",
+ "password": "%F0%9F%92%A9",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/bar",
+ "search": "",
+ "hash": ""
+ },
+ "# resolving a fragment against any scheme succeeds",
+ {
+ "input": "#",
+ "base": "test:test",
+ "href": "test:test#",
+ "origin": "null",
+ "protocol": "test:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "#x",
+ "base": "mailto:x@x.com",
+ "href": "mailto:x@x.com#x",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "x@x.com",
+ "search": "",
+ "hash": "#x"
+ },
+ {
+ "input": "#x",
+ "base": "data:,",
+ "href": "data:,#x",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": ",",
+ "search": "",
+ "hash": "#x"
+ },
+ {
+ "input": "#x",
+ "base": "about:blank",
+ "href": "about:blank#x",
+ "origin": "null",
+ "protocol": "about:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "blank",
+ "search": "",
+ "hash": "#x"
+ },
+ {
+ "input": "#x:y",
+ "base": "about:blank",
+ "href": "about:blank#x:y",
+ "origin": "null",
+ "protocol": "about:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "blank",
+ "search": "",
+ "hash": "#x:y"
+ },
+ {
+ "input": "#",
+ "base": "test:test?test",
+ "href": "test:test?test#",
+ "origin": "null",
+ "protocol": "test:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "test",
+ "search": "?test",
+ "hash": ""
+ },
+ "# multiple @ in authority state",
+ {
+ "input": "https://@test@test@example:800/",
+ "base": "http://doesnotmatter/",
+ "href": "https://%40test%40test@example:800/",
+ "origin": "https://example:800",
+ "protocol": "https:",
+ "username": "%40test%40test",
+ "password": "",
+ "host": "example:800",
+ "hostname": "example",
+ "port": "800",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https://@@@example",
+ "base": "http://doesnotmatter/",
+ "href": "https://%40%40@example/",
+ "origin": "https://example",
+ "protocol": "https:",
+ "username": "%40%40",
+ "password": "",
+ "host": "example",
+ "hostname": "example",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "non-az-09 characters",
+ {
+ "input": "http://`{}:`{}@h/`{}?`{}",
+ "base": "http://doesnotmatter/",
+ "href": "http://%60%7B%7D:%60%7B%7D@h/%60%7B%7D?`{}",
+ "origin": "http://h",
+ "protocol": "http:",
+ "username": "%60%7B%7D",
+ "password": "%60%7B%7D",
+ "host": "h",
+ "hostname": "h",
+ "port": "",
+ "pathname": "/%60%7B%7D",
+ "search": "?`{}",
+ "hash": ""
+ },
+ "byte is ' and url is special",
+ {
+ "input": "http://host/?'",
+ "base": null,
+ "href": "http://host/?%27",
+ "origin": "http://host",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/",
+ "search": "?%27",
+ "hash": ""
+ },
+ {
+ "input": "notspecial://host/?'",
+ "base": null,
+ "href": "notspecial://host/?'",
+ "origin": "null",
+ "protocol": "notspecial:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/",
+ "search": "?'",
+ "hash": ""
+ },
+ "# Credentials in base",
+ {
+ "input": "/some/path",
+ "base": "http://user@example.org/smth",
+ "href": "http://user@example.org/some/path",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "user",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/some/path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "",
+ "base": "http://user:pass@example.org:21/smth",
+ "href": "http://user:pass@example.org:21/smth",
+ "origin": "http://example.org:21",
+ "protocol": "http:",
+ "username": "user",
+ "password": "pass",
+ "host": "example.org:21",
+ "hostname": "example.org",
+ "port": "21",
+ "pathname": "/smth",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/some/path",
+ "base": "http://user:pass@example.org:21/smth",
+ "href": "http://user:pass@example.org:21/some/path",
+ "origin": "http://example.org:21",
+ "protocol": "http:",
+ "username": "user",
+ "password": "pass",
+ "host": "example.org:21",
+ "hostname": "example.org",
+ "port": "21",
+ "pathname": "/some/path",
+ "search": "",
+ "hash": ""
+ },
+ "# a set of tests designed by zcorpan for relative URLs with unknown schemes",
+ {
+ "input": "i",
+ "base": "sc:sd",
+ "failure": true
+ },
+ {
+ "input": "i",
+ "base": "sc:sd/sd",
+ "failure": true
+ },
+ {
+ "input": "i",
+ "base": "sc:/pa/pa",
+ "href": "sc:/pa/i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/pa/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "i",
+ "base": "sc://ho/pa",
+ "href": "sc://ho/i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "ho",
+ "hostname": "ho",
+ "port": "",
+ "pathname": "/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "i",
+ "base": "sc:///pa/pa",
+ "href": "sc:///pa/i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/pa/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "../i",
+ "base": "sc:sd",
+ "failure": true
+ },
+ {
+ "input": "../i",
+ "base": "sc:sd/sd",
+ "failure": true
+ },
+ {
+ "input": "../i",
+ "base": "sc:/pa/pa",
+ "href": "sc:/i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "../i",
+ "base": "sc://ho/pa",
+ "href": "sc://ho/i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "ho",
+ "hostname": "ho",
+ "port": "",
+ "pathname": "/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "../i",
+ "base": "sc:///pa/pa",
+ "href": "sc:///i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/i",
+ "base": "sc:sd",
+ "failure": true
+ },
+ {
+ "input": "/i",
+ "base": "sc:sd/sd",
+ "failure": true
+ },
+ {
+ "input": "/i",
+ "base": "sc:/pa/pa",
+ "href": "sc:/i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/i",
+ "base": "sc://ho/pa",
+ "href": "sc://ho/i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "ho",
+ "hostname": "ho",
+ "port": "",
+ "pathname": "/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/i",
+ "base": "sc:///pa/pa",
+ "href": "sc:///i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/i",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "?i",
+ "base": "sc:sd",
+ "failure": true
+ },
+ {
+ "input": "?i",
+ "base": "sc:sd/sd",
+ "failure": true
+ },
+ {
+ "input": "?i",
+ "base": "sc:/pa/pa",
+ "href": "sc:/pa/pa?i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/pa/pa",
+ "search": "?i",
+ "hash": ""
+ },
+ {
+ "input": "?i",
+ "base": "sc://ho/pa",
+ "href": "sc://ho/pa?i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "ho",
+ "hostname": "ho",
+ "port": "",
+ "pathname": "/pa",
+ "search": "?i",
+ "hash": ""
+ },
+ {
+ "input": "?i",
+ "base": "sc:///pa/pa",
+ "href": "sc:///pa/pa?i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/pa/pa",
+ "search": "?i",
+ "hash": ""
+ },
+ {
+ "input": "#i",
+ "base": "sc:sd",
+ "href": "sc:sd#i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "sd",
+ "search": "",
+ "hash": "#i"
+ },
+ {
+ "input": "#i",
+ "base": "sc:sd/sd",
+ "href": "sc:sd/sd#i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "sd/sd",
+ "search": "",
+ "hash": "#i"
+ },
+ {
+ "input": "#i",
+ "base": "sc:/pa/pa",
+ "href": "sc:/pa/pa#i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/pa/pa",
+ "search": "",
+ "hash": "#i"
+ },
+ {
+ "input": "#i",
+ "base": "sc://ho/pa",
+ "href": "sc://ho/pa#i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "ho",
+ "hostname": "ho",
+ "port": "",
+ "pathname": "/pa",
+ "search": "",
+ "hash": "#i"
+ },
+ {
+ "input": "#i",
+ "base": "sc:///pa/pa",
+ "href": "sc:///pa/pa#i",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/pa/pa",
+ "search": "",
+ "hash": "#i"
+ },
+ "# make sure that relative URL logic works on known typically non-relative schemes too",
+ {
+ "input": "about:/../",
+ "base": null,
+ "href": "about:/",
+ "origin": "null",
+ "protocol": "about:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "data:/../",
+ "base": null,
+ "href": "data:/",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "javascript:/../",
+ "base": null,
+ "href": "javascript:/",
+ "origin": "null",
+ "protocol": "javascript:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "mailto:/../",
+ "base": null,
+ "href": "mailto:/",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "# unknown schemes and their hosts",
+ {
+ "input": "sc://ñ.test/",
+ "base": null,
+ "href": "sc://%C3%B1.test/",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%C3%B1.test",
+ "hostname": "%C3%B1.test",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "sc://%/",
+ "base": null,
+ "href": "sc://%/",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%",
+ "hostname": "%",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "sc://@/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://te@s:t@/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://:/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://:12/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "x",
+ "base": "sc://ñ",
+ "href": "sc://%C3%B1/x",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%C3%B1",
+ "hostname": "%C3%B1",
+ "port": "",
+ "pathname": "/x",
+ "search": "",
+ "hash": ""
+ },
+ "# unknown schemes and backslashes",
+ {
+ "input": "sc:\\../",
+ "base": null,
+ "href": "sc:\\../",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "\\../",
+ "search": "",
+ "hash": ""
+ },
+ "# unknown scheme with path looking like a password",
+ {
+ "input": "sc::a@example.net",
+ "base": null,
+ "href": "sc::a@example.net",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": ":a@example.net",
+ "search": "",
+ "hash": ""
+ },
+ "# unknown scheme with bogus percent-encoding",
+ {
+ "input": "wow:%NBD",
+ "base": null,
+ "href": "wow:%NBD",
+ "origin": "null",
+ "protocol": "wow:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "%NBD",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "wow:%1G",
+ "base": null,
+ "href": "wow:%1G",
+ "origin": "null",
+ "protocol": "wow:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "%1G",
+ "search": "",
+ "hash": ""
+ },
+ "# unknown scheme with non-URL characters",
+ {
+ "input": "wow:\uFFFF",
+ "base": null,
+ "href": "wow:%EF%BF%BF",
+ "origin": "null",
+ "protocol": "wow:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "%EF%BF%BF",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.com/\uD800\uD801\uDFFE\uDFFF\uFDD0\uFDCF\uFDEF\uFDF0\uFFFE\uFFFF?\uD800\uD801\uDFFE\uDFFF\uFDD0\uFDCF\uFDEF\uFDF0\uFFFE\uFFFF",
+ "base": null,
+ "href": "http://example.com/%EF%BF%BD%F0%90%9F%BE%EF%BF%BD%EF%B7%90%EF%B7%8F%EF%B7%AF%EF%B7%B0%EF%BF%BE%EF%BF%BF?%EF%BF%BD%F0%90%9F%BE%EF%BF%BD%EF%B7%90%EF%B7%8F%EF%B7%AF%EF%B7%B0%EF%BF%BE%EF%BF%BF",
+ "origin": "http://example.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "port": "",
+ "pathname": "/%EF%BF%BD%F0%90%9F%BE%EF%BF%BD%EF%B7%90%EF%B7%8F%EF%B7%AF%EF%B7%B0%EF%BF%BE%EF%BF%BF",
+ "search": "?%EF%BF%BD%F0%90%9F%BE%EF%BF%BD%EF%B7%90%EF%B7%8F%EF%B7%AF%EF%B7%B0%EF%BF%BE%EF%BF%BF",
+ "hash": ""
+ },
+ "Forbidden host code points",
+ {
+ "input": "sc://a\u0000b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://a b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://ab",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://a[b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://a\\b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://a]b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://a^b",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "sc://a|b/",
+ "base": null,
+ "failure": true
+ },
+ "Forbidden host codepoints: tabs and newlines are removed during preprocessing",
+ {
+ "input": "foo://ho\u0009st/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"foo://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "foo://ho\u000Ast/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"foo://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "foo://ho\u000Dst/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"foo://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": ""
+ },
+ "Forbidden domain code-points",
+ {
+ "input": "http://a\u0000b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0001b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0002b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0003b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0004b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0005b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0006b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0007b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0008b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u000Bb/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u000Cb/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u000Eb/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u000Fb/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0010b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0011b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0012b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0013b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0014b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0015b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0016b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0017b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0018b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u0019b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Ab/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Bb/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Cb/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Db/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Eb/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Fb/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a%b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ab",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a[b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a]b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a^b",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a|b/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://a\u007Fb/",
+ "base": null,
+ "failure": true
+ },
+ "Forbidden domain codepoints: tabs and newlines are removed during preprocessing",
+ {
+ "input": "http://ho\u0009st/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"http://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "http:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "http://ho\u000Ast/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"http://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "http:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "http://ho\u000Dst/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"http://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "http:",
+ "search": "",
+ "username": ""
+ },
+ "Encoded forbidden domain codepoints in special URLs",
+ {
+ "input": "http://ho%00st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%01st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%02st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%03st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%04st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%05st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%06st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%07st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%08st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%09st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Ast/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Bst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Cst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Dst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Est/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Fst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%10st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%11st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%12st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%13st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%14st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%15st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%16st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%17st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%18st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%19st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%1Ast/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%1Bst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%1Cst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%1Dst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%1Est/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%1Fst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%20st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%23st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%25st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%2Fst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%3Ast/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%3Cst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%3Est/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%3Fst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%40st/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%5Bst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%5Cst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%5Dst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%7Cst/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://ho%7Fst/",
+ "base": null,
+ "failure": true
+ },
+ "Allowed host/domain code points",
+ {
+ "input": "http://!\"$&'()*+,-.;=_`{}~/",
+ "base": null,
+ "href": "http://!\"$&'()*+,-.;=_`{}~/",
+ "origin": "http://!\"$&'()*+,-.;=_`{}~",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "!\"$&'()*+,-.;=_`{}~",
+ "hostname": "!\"$&'()*+,-.;=_`{}~",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "sc://\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u000B\u000C\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u007F!\"$%&'()*+,-.;=_`{}~/",
+ "base": null,
+ "href": "sc://%01%02%03%04%05%06%07%08%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%7F!\"$%&'()*+,-.;=_`{}~/",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%01%02%03%04%05%06%07%08%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%7F!\"$%&'()*+,-.;=_`{}~",
+ "hostname": "%01%02%03%04%05%06%07%08%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%7F!\"$%&'()*+,-.;=_`{}~",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "# Hosts and percent-encoding",
+ {
+ "input": "ftp://example.com%80/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "ftp://example.com%A0/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://example.com%80/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://example.com%A0/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "ftp://%e2%98%83",
+ "base": null,
+ "href": "ftp://xn--n3h/",
+ "origin": "ftp://xn--n3h",
+ "protocol": "ftp:",
+ "username": "",
+ "password": "",
+ "host": "xn--n3h",
+ "hostname": "xn--n3h",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "https://%e2%98%83",
+ "base": null,
+ "href": "https://xn--n3h/",
+ "origin": "https://xn--n3h",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "xn--n3h",
+ "hostname": "xn--n3h",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "# tests from jsdom/whatwg-url designed for code coverage",
+ {
+ "input": "http://127.0.0.1:10100/relative_import.html",
+ "base": null,
+ "href": "http://127.0.0.1:10100/relative_import.html",
+ "origin": "http://127.0.0.1:10100",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "127.0.0.1:10100",
+ "hostname": "127.0.0.1",
+ "port": "10100",
+ "pathname": "/relative_import.html",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://facebook.com/?foo=%7B%22abc%22",
+ "base": null,
+ "href": "http://facebook.com/?foo=%7B%22abc%22",
+ "origin": "http://facebook.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "facebook.com",
+ "hostname": "facebook.com",
+ "port": "",
+ "pathname": "/",
+ "search": "?foo=%7B%22abc%22",
+ "hash": ""
+ },
+ {
+ "input": "https://localhost:3000/jqueryui@1.2.3",
+ "base": null,
+ "href": "https://localhost:3000/jqueryui@1.2.3",
+ "origin": "https://localhost:3000",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "localhost:3000",
+ "hostname": "localhost",
+ "port": "3000",
+ "pathname": "/jqueryui@1.2.3",
+ "search": "",
+ "hash": ""
+ },
+ "# tab/LF/CR",
+ {
+ "input": "h\tt\nt\rp://h\to\ns\rt:9\t0\n0\r0/p\ta\nt\rh?q\tu\ne\rry#f\tr\na\rg",
+ "base": null,
+ "href": "http://host:9000/path?query#frag",
+ "origin": "http://host:9000",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "host:9000",
+ "hostname": "host",
+ "port": "9000",
+ "pathname": "/path",
+ "search": "?query",
+ "hash": "#frag"
+ },
+ "# Stringification of URL.searchParams",
+ {
+ "input": "?a=b&c=d",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar?a=b&c=d",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "?a=b&c=d",
+ "searchParams": "a=b&c=d",
+ "hash": ""
+ },
+ {
+ "input": "??a=b&c=d",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar??a=b&c=d",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "??a=b&c=d",
+ "searchParams": "%3Fa=b&c=d",
+ "hash": ""
+ },
+ "# Scheme only",
+ {
+ "input": "http:",
+ "base": "http://example.org/foo/bar",
+ "href": "http://example.org/foo/bar",
+ "origin": "http://example.org",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "searchParams": "",
+ "hash": ""
+ },
+ {
+ "input": "http:",
+ "base": "https://example.org/foo/bar",
+ "failure": true
+ },
+ {
+ "input": "sc:",
+ "base": "https://example.org/foo/bar",
+ "href": "sc:",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "searchParams": "",
+ "hash": ""
+ },
+ "# Percent encoding of fragments",
+ {
+ "input": "http://foo.bar/baz?qux#foo\bbar",
+ "base": null,
+ "href": "http://foo.bar/baz?qux#foo%08bar",
+ "origin": "http://foo.bar",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo.bar",
+ "hostname": "foo.bar",
+ "port": "",
+ "pathname": "/baz",
+ "search": "?qux",
+ "searchParams": "qux=",
+ "hash": "#foo%08bar"
+ },
+ {
+ "input": "http://foo.bar/baz?qux#foo\"bar",
+ "base": null,
+ "href": "http://foo.bar/baz?qux#foo%22bar",
+ "origin": "http://foo.bar",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo.bar",
+ "hostname": "foo.bar",
+ "port": "",
+ "pathname": "/baz",
+ "search": "?qux",
+ "searchParams": "qux=",
+ "hash": "#foo%22bar"
+ },
+ {
+ "input": "http://foo.bar/baz?qux#foobar",
+ "base": null,
+ "href": "http://foo.bar/baz?qux#foo%3Ebar",
+ "origin": "http://foo.bar",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo.bar",
+ "hostname": "foo.bar",
+ "port": "",
+ "pathname": "/baz",
+ "search": "?qux",
+ "searchParams": "qux=",
+ "hash": "#foo%3Ebar"
+ },
+ {
+ "input": "http://foo.bar/baz?qux#foo`bar",
+ "base": null,
+ "href": "http://foo.bar/baz?qux#foo%60bar",
+ "origin": "http://foo.bar",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "foo.bar",
+ "hostname": "foo.bar",
+ "port": "",
+ "pathname": "/baz",
+ "search": "?qux",
+ "searchParams": "qux=",
+ "hash": "#foo%60bar"
+ },
+ "# IPv4 parsing (via https://github.com/nodejs/node/pull/10317)",
+ {
+ "input": "http://1.2.3.4/",
+ "base": "http://other.com/",
+ "href": "http://1.2.3.4/",
+ "origin": "http://1.2.3.4",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "1.2.3.4",
+ "hostname": "1.2.3.4",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://1.2.3.4./",
+ "base": "http://other.com/",
+ "href": "http://1.2.3.4/",
+ "origin": "http://1.2.3.4",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "1.2.3.4",
+ "hostname": "1.2.3.4",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://192.168.257",
+ "base": "http://other.com/",
+ "href": "http://192.168.1.1/",
+ "origin": "http://192.168.1.1",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "192.168.1.1",
+ "hostname": "192.168.1.1",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://192.168.257.",
+ "base": "http://other.com/",
+ "href": "http://192.168.1.1/",
+ "origin": "http://192.168.1.1",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "192.168.1.1",
+ "hostname": "192.168.1.1",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://192.168.257.com",
+ "base": "http://other.com/",
+ "href": "http://192.168.257.com/",
+ "origin": "http://192.168.257.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "192.168.257.com",
+ "hostname": "192.168.257.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://256",
+ "base": "http://other.com/",
+ "href": "http://0.0.1.0/",
+ "origin": "http://0.0.1.0",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "0.0.1.0",
+ "hostname": "0.0.1.0",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://256.com",
+ "base": "http://other.com/",
+ "href": "http://256.com/",
+ "origin": "http://256.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "256.com",
+ "hostname": "256.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://999999999",
+ "base": "http://other.com/",
+ "href": "http://59.154.201.255/",
+ "origin": "http://59.154.201.255",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "59.154.201.255",
+ "hostname": "59.154.201.255",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://999999999.",
+ "base": "http://other.com/",
+ "href": "http://59.154.201.255/",
+ "origin": "http://59.154.201.255",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "59.154.201.255",
+ "hostname": "59.154.201.255",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://999999999.com",
+ "base": "http://other.com/",
+ "href": "http://999999999.com/",
+ "origin": "http://999999999.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "999999999.com",
+ "hostname": "999999999.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://10000000000",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://10000000000.com",
+ "base": "http://other.com/",
+ "href": "http://10000000000.com/",
+ "origin": "http://10000000000.com",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "10000000000.com",
+ "hostname": "10000000000.com",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://4294967295",
+ "base": "http://other.com/",
+ "href": "http://255.255.255.255/",
+ "origin": "http://255.255.255.255",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "255.255.255.255",
+ "hostname": "255.255.255.255",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://4294967296",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://0xffffffff",
+ "base": "http://other.com/",
+ "href": "http://255.255.255.255/",
+ "origin": "http://255.255.255.255",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "255.255.255.255",
+ "hostname": "255.255.255.255",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://0xffffffff1",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://256.256.256.256",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "https://0x.0x.0",
+ "base": null,
+ "href": "https://0.0.0.0/",
+ "origin": "https://0.0.0.0",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "0.0.0.0",
+ "hostname": "0.0.0.0",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "More IPv4 parsing (via https://github.com/jsdom/whatwg-url/issues/92)",
+ {
+ "input": "https://0x100000000/test",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://256.0.0.1/test",
+ "base": null,
+ "failure": true
+ },
+ "# file URLs containing percent-encoded Windows drive letters (shouldn't work)",
+ {
+ "input": "file:///C%3A/",
+ "base": null,
+ "href": "file:///C%3A/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C%3A/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///C%7C/",
+ "base": null,
+ "href": "file:///C%7C/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C%7C/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://%43%3A",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://%43%7C",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://%43|",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://C%7C",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://%43%7C/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://%43%7C/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "asdf://%43|/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "asdf://%43%7C/",
+ "base": null,
+ "href": "asdf://%43%7C/",
+ "origin": "null",
+ "protocol": "asdf:",
+ "username": "",
+ "password": "",
+ "host": "%43%7C",
+ "hostname": "%43%7C",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "# file URLs relative to other file URLs (via https://github.com/jsdom/whatwg-url/pull/60)",
+ {
+ "input": "pix/submit.gif",
+ "base": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/anchor.html",
+ "href": "file:///C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/Users/Domenic/Dropbox/GitHub/tmpvar/jsdom/test/level2/html/files/pix/submit.gif",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "..",
+ "base": "file:///C:/",
+ "href": "file:///C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "..",
+ "base": "file:///",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "# More file URL tests by zcorpan and annevk",
+ {
+ "input": "/",
+ "base": "file:///C:/a/b",
+ "href": "file:///C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/",
+ "base": "file://h/C:/a/b",
+ "href": "file://h/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "h",
+ "hostname": "h",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/",
+ "base": "file://h/a/b",
+ "href": "file://h/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "h",
+ "hostname": "h",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//d:",
+ "base": "file:///C:/a/b",
+ "href": "file:///d:",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/d:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//d:/..",
+ "base": "file:///C:/a/b",
+ "href": "file:///d:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/d:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "..",
+ "base": "file:///ab:/",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "..",
+ "base": "file:///1:/",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "",
+ "base": "file:///test?test#test",
+ "href": "file:///test?test",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "?test",
+ "hash": ""
+ },
+ {
+ "input": "file:",
+ "base": "file:///test?test#test",
+ "href": "file:///test?test",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "?test",
+ "hash": ""
+ },
+ {
+ "input": "?x",
+ "base": "file:///test?test#test",
+ "href": "file:///test?x",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "?x",
+ "hash": ""
+ },
+ {
+ "input": "file:?x",
+ "base": "file:///test?test#test",
+ "href": "file:///test?x",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "?x",
+ "hash": ""
+ },
+ {
+ "input": "#x",
+ "base": "file:///test?test#test",
+ "href": "file:///test?test#x",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "?test",
+ "hash": "#x"
+ },
+ {
+ "input": "file:#x",
+ "base": "file:///test?test#test",
+ "href": "file:///test?test#x",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "?test",
+ "hash": "#x"
+ },
+ "# File URLs and many (back)slashes",
+ {
+ "input": "file:\\\\//",
+ "base": null,
+ "href": "file:////",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:\\\\\\\\",
+ "base": null,
+ "href": "file:////",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:\\\\\\\\?fox",
+ "base": null,
+ "href": "file:////?fox",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "?fox",
+ "hash": ""
+ },
+ {
+ "input": "file:\\\\\\\\#guppy",
+ "base": null,
+ "href": "file:////#guppy",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": "#guppy"
+ },
+ {
+ "input": "file://spider///",
+ "base": null,
+ "href": "file://spider///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "spider",
+ "hostname": "spider",
+ "port": "",
+ "pathname": "///",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:\\\\localhost//",
+ "base": null,
+ "href": "file:////",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///localhost//cat",
+ "base": null,
+ "href": "file:///localhost//cat",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/localhost//cat",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://\\/localhost//cat",
+ "base": null,
+ "href": "file:////localhost//cat",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//localhost//cat",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://localhost//a//../..//",
+ "base": null,
+ "href": "file://///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "///",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/////mouse",
+ "base": "file:///elephant",
+ "href": "file://///mouse",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "///mouse",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "\\//pig",
+ "base": "file://lion/",
+ "href": "file:///pig",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/pig",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "\\/localhost//pig",
+ "base": "file://lion/",
+ "href": "file:////pig",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//pig",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//localhost//pig",
+ "base": "file://lion/",
+ "href": "file:////pig",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//pig",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/..//localhost//pig",
+ "base": "file://lion/",
+ "href": "file://lion//localhost//pig",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "lion",
+ "hostname": "lion",
+ "port": "",
+ "pathname": "//localhost//pig",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://",
+ "base": "file://ape/",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "# File URLs with non-empty hosts",
+ {
+ "input": "/rooibos",
+ "base": "file://tea/",
+ "href": "file://tea/rooibos",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "tea",
+ "hostname": "tea",
+ "port": "",
+ "pathname": "/rooibos",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/?chai",
+ "base": "file://tea/",
+ "href": "file://tea/?chai",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "tea",
+ "hostname": "tea",
+ "port": "",
+ "pathname": "/",
+ "search": "?chai",
+ "hash": ""
+ },
+ "# Windows drive letter handling with the 'file:' base URL",
+ {
+ "input": "C|",
+ "base": "file://host/dir/file",
+ "href": "file://host/C:",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C|",
+ "base": "file://host/D:/dir1/dir2/file",
+ "href": "file://host/C:",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C|#",
+ "base": "file://host/dir/file",
+ "href": "file://host/C:#",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C|?",
+ "base": "file://host/dir/file",
+ "href": "file://host/C:?",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C|/",
+ "base": "file://host/dir/file",
+ "href": "file://host/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C|\n/",
+ "base": "file://host/dir/file",
+ "href": "file://host/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C|\\",
+ "base": "file://host/dir/file",
+ "href": "file://host/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C",
+ "base": "file://host/dir/file",
+ "href": "file://host/dir/C",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/dir/C",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "C|a",
+ "base": "file://host/dir/file",
+ "href": "file://host/dir/C|a",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/dir/C|a",
+ "search": "",
+ "hash": ""
+ },
+ "# Windows drive letter quirk in the file slash state",
+ {
+ "input": "/c:/foo/bar",
+ "base": "file:///c:/baz/qux",
+ "href": "file:///c:/foo/bar",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/c:/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/c|/foo/bar",
+ "base": "file:///c:/baz/qux",
+ "href": "file:///c:/foo/bar",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/c:/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:\\c:\\foo\\bar",
+ "base": "file:///c:/baz/qux",
+ "href": "file:///c:/foo/bar",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/c:/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/c:/foo/bar",
+ "base": "file://host/path",
+ "href": "file://host/c:/foo/bar",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/c:/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ "# Do not drop the host in the presence of a drive letter",
+ {
+ "input": "file://example.net/C:/",
+ "base": null,
+ "href": "file://example.net/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "example.net",
+ "hostname": "example.net",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://1.2.3.4/C:/",
+ "base": null,
+ "href": "file://1.2.3.4/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "1.2.3.4",
+ "hostname": "1.2.3.4",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://[1::8]/C:/",
+ "base": null,
+ "href": "file://[1::8]/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "[1::8]",
+ "hostname": "[1::8]",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ "# Copy the host from the base URL in the following cases",
+ {
+ "input": "C|/",
+ "base": "file://host/",
+ "href": "file://host/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/C:/",
+ "base": "file://host/",
+ "href": "file://host/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:C:/",
+ "base": "file://host/",
+ "href": "file://host/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:/C:/",
+ "base": "file://host/",
+ "href": "file://host/C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "host",
+ "hostname": "host",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ "# Copy the empty host from the input in the following cases",
+ {
+ "input": "//C:/",
+ "base": "file://host/",
+ "href": "file:///C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://C:/",
+ "base": "file://host/",
+ "href": "file:///C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///C:/",
+ "base": "file://host/",
+ "href": "file:///C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///C:/",
+ "base": "file://host/",
+ "href": "file:///C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ "# Windows drive letter quirk (no host)",
+ {
+ "input": "file:/C|/",
+ "base": null,
+ "href": "file:///C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://C|/",
+ "base": null,
+ "href": "file:///C:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/C:/",
+ "search": "",
+ "hash": ""
+ },
+ "# file URLs without base URL by Rimas Misevičius",
+ {
+ "input": "file:",
+ "base": null,
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:?q=v",
+ "base": null,
+ "href": "file:///?q=v",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "?q=v",
+ "hash": ""
+ },
+ {
+ "input": "file:#frag",
+ "base": null,
+ "href": "file:///#frag",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": "#frag"
+ },
+ "# file: drive letter cases from https://crbug.com/1078698",
+ {
+ "input": "file:///Y:",
+ "base": null,
+ "href": "file:///Y:",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/Y:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///Y:/",
+ "base": null,
+ "href": "file:///Y:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/Y:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///./Y",
+ "base": null,
+ "href": "file:///Y",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/Y",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///./Y:",
+ "base": null,
+ "href": "file:///Y:",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/Y:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "\\\\\\.\\Y:",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ "# file: drive letter cases from https://crbug.com/1078698 but lowercased",
+ {
+ "input": "file:///y:",
+ "base": null,
+ "href": "file:///y:",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/y:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///y:/",
+ "base": null,
+ "href": "file:///y:/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/y:/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///./y",
+ "base": null,
+ "href": "file:///y",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/y",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///./y:",
+ "base": null,
+ "href": "file:///y:",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/y:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "\\\\\\.\\y:",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ "# Additional file URL tests for (https://github.com/whatwg/url/issues/405)",
+ {
+ "input": "file://localhost//a//../..//foo",
+ "base": null,
+ "href": "file://///foo",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "///foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://localhost////foo",
+ "base": null,
+ "href": "file://////foo",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "////foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:////foo",
+ "base": null,
+ "href": "file:////foo",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//foo",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///one/two",
+ "base": "file:///",
+ "href": "file:///one/two",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/one/two",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:////one/two",
+ "base": "file:///",
+ "href": "file:////one/two",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//one/two",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//one/two",
+ "base": "file:///",
+ "href": "file://one/two",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "one",
+ "hostname": "one",
+ "port": "",
+ "pathname": "/two",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///one/two",
+ "base": "file:///",
+ "href": "file:///one/two",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/one/two",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "////one/two",
+ "base": "file:///",
+ "href": "file:////one/two",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//one/two",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:///.//",
+ "base": "file:////",
+ "href": "file:////",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ "File URL tests for https://github.com/whatwg/url/issues/549",
+ {
+ "input": "file:.//p",
+ "base": null,
+ "href": "file:////p",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//p",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file:/.//p",
+ "base": null,
+ "href": "file:////p",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//p",
+ "search": "",
+ "hash": ""
+ },
+ "# IPv6 tests",
+ {
+ "input": "http://[1:0::]",
+ "base": "http://example.net/",
+ "href": "http://[1::]/",
+ "origin": "http://[1::]",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "[1::]",
+ "hostname": "[1::]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://[0:1:2:3:4:5:6:7:8]",
+ "base": "http://example.net/",
+ "failure": true
+ },
+ {
+ "input": "https://[0::0::0]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://[0:.0]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://[0:0:]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://[0:1:2:3:4:5:6:7.0.0.0.1]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://[0:1.00.0.0.0]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://[0:1.290.0.0.0]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://[0:1.23.23]",
+ "base": null,
+ "failure": true
+ },
+ "# Empty host",
+ {
+ "input": "http://?",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://#",
+ "base": null,
+ "failure": true
+ },
+ "Port overflow (2^32 + 81)",
+ {
+ "input": "http://f:4294967377/c",
+ "base": "http://example.org/",
+ "failure": true
+ },
+ "Port overflow (2^64 + 81)",
+ {
+ "input": "http://f:18446744073709551697/c",
+ "base": "http://example.org/",
+ "failure": true
+ },
+ "Port overflow (2^128 + 81)",
+ {
+ "input": "http://f:340282366920938463463374607431768211537/c",
+ "base": "http://example.org/",
+ "failure": true
+ },
+ "# Non-special-URL path tests",
+ {
+ "input": "sc://ñ",
+ "base": null,
+ "href": "sc://%C3%B1",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%C3%B1",
+ "hostname": "%C3%B1",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "sc://ñ?x",
+ "base": null,
+ "href": "sc://%C3%B1?x",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%C3%B1",
+ "hostname": "%C3%B1",
+ "port": "",
+ "pathname": "",
+ "search": "?x",
+ "hash": ""
+ },
+ {
+ "input": "sc://ñ#x",
+ "base": null,
+ "href": "sc://%C3%B1#x",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%C3%B1",
+ "hostname": "%C3%B1",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "hash": "#x"
+ },
+ {
+ "input": "#x",
+ "base": "sc://ñ",
+ "href": "sc://%C3%B1#x",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%C3%B1",
+ "hostname": "%C3%B1",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "hash": "#x"
+ },
+ {
+ "input": "?x",
+ "base": "sc://ñ",
+ "href": "sc://%C3%B1?x",
+ "origin": "null",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "%C3%B1",
+ "hostname": "%C3%B1",
+ "port": "",
+ "pathname": "",
+ "search": "?x",
+ "hash": ""
+ },
+ {
+ "input": "sc://?",
+ "base": null,
+ "href": "sc://?",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "sc://#",
+ "base": null,
+ "href": "sc://#",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///",
+ "base": "sc://x/",
+ "href": "sc:///",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "////",
+ "base": "sc://x/",
+ "href": "sc:////",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "////x/",
+ "base": "sc://x/",
+ "href": "sc:////x/",
+ "protocol": "sc:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//x/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "tftp://foobar.com/someconfig;mode=netascii",
+ "base": null,
+ "href": "tftp://foobar.com/someconfig;mode=netascii",
+ "origin": "null",
+ "protocol": "tftp:",
+ "username": "",
+ "password": "",
+ "host": "foobar.com",
+ "hostname": "foobar.com",
+ "port": "",
+ "pathname": "/someconfig;mode=netascii",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "telnet://user:pass@foobar.com:23/",
+ "base": null,
+ "href": "telnet://user:pass@foobar.com:23/",
+ "origin": "null",
+ "protocol": "telnet:",
+ "username": "user",
+ "password": "pass",
+ "host": "foobar.com:23",
+ "hostname": "foobar.com",
+ "port": "23",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "ut2004://10.10.10.10:7777/Index.ut2",
+ "base": null,
+ "href": "ut2004://10.10.10.10:7777/Index.ut2",
+ "origin": "null",
+ "protocol": "ut2004:",
+ "username": "",
+ "password": "",
+ "host": "10.10.10.10:7777",
+ "hostname": "10.10.10.10",
+ "port": "7777",
+ "pathname": "/Index.ut2",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz",
+ "base": null,
+ "href": "redis://foo:bar@somehost:6379/0?baz=bam&qux=baz",
+ "origin": "null",
+ "protocol": "redis:",
+ "username": "foo",
+ "password": "bar",
+ "host": "somehost:6379",
+ "hostname": "somehost",
+ "port": "6379",
+ "pathname": "/0",
+ "search": "?baz=bam&qux=baz",
+ "hash": ""
+ },
+ {
+ "input": "rsync://foo@host:911/sup",
+ "base": null,
+ "href": "rsync://foo@host:911/sup",
+ "origin": "null",
+ "protocol": "rsync:",
+ "username": "foo",
+ "password": "",
+ "host": "host:911",
+ "hostname": "host",
+ "port": "911",
+ "pathname": "/sup",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "git://github.com/foo/bar.git",
+ "base": null,
+ "href": "git://github.com/foo/bar.git",
+ "origin": "null",
+ "protocol": "git:",
+ "username": "",
+ "password": "",
+ "host": "github.com",
+ "hostname": "github.com",
+ "port": "",
+ "pathname": "/foo/bar.git",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "irc://myserver.com:6999/channel?passwd",
+ "base": null,
+ "href": "irc://myserver.com:6999/channel?passwd",
+ "origin": "null",
+ "protocol": "irc:",
+ "username": "",
+ "password": "",
+ "host": "myserver.com:6999",
+ "hostname": "myserver.com",
+ "port": "6999",
+ "pathname": "/channel",
+ "search": "?passwd",
+ "hash": ""
+ },
+ {
+ "input": "dns://fw.example.org:9999/foo.bar.org?type=TXT",
+ "base": null,
+ "href": "dns://fw.example.org:9999/foo.bar.org?type=TXT",
+ "origin": "null",
+ "protocol": "dns:",
+ "username": "",
+ "password": "",
+ "host": "fw.example.org:9999",
+ "hostname": "fw.example.org",
+ "port": "9999",
+ "pathname": "/foo.bar.org",
+ "search": "?type=TXT",
+ "hash": ""
+ },
+ {
+ "input": "ldap://localhost:389/ou=People,o=JNDITutorial",
+ "base": null,
+ "href": "ldap://localhost:389/ou=People,o=JNDITutorial",
+ "origin": "null",
+ "protocol": "ldap:",
+ "username": "",
+ "password": "",
+ "host": "localhost:389",
+ "hostname": "localhost",
+ "port": "389",
+ "pathname": "/ou=People,o=JNDITutorial",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "git+https://github.com/foo/bar",
+ "base": null,
+ "href": "git+https://github.com/foo/bar",
+ "origin": "null",
+ "protocol": "git+https:",
+ "username": "",
+ "password": "",
+ "host": "github.com",
+ "hostname": "github.com",
+ "port": "",
+ "pathname": "/foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "urn:ietf:rfc:2648",
+ "base": null,
+ "href": "urn:ietf:rfc:2648",
+ "origin": "null",
+ "protocol": "urn:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "ietf:rfc:2648",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "tag:joe@example.org,2001:foo/bar",
+ "base": null,
+ "href": "tag:joe@example.org,2001:foo/bar",
+ "origin": "null",
+ "protocol": "tag:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "joe@example.org,2001:foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ "Serialize /. in path",
+ {
+ "input": "non-spec:/.//",
+ "base": null,
+ "href": "non-spec:/.//",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-spec:/..//",
+ "base": null,
+ "href": "non-spec:/.//",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-spec:/a/..//",
+ "base": null,
+ "href": "non-spec:/.//",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-spec:/.//path",
+ "base": null,
+ "href": "non-spec:/.//path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-spec:/..//path",
+ "base": null,
+ "href": "non-spec:/.//path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-spec:/a/..//path",
+ "base": null,
+ "href": "non-spec:/.//path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/.//path",
+ "base": "non-spec:/p",
+ "href": "non-spec:/.//path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/..//path",
+ "base": "non-spec:/p",
+ "href": "non-spec:/.//path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "..//path",
+ "base": "non-spec:/p",
+ "href": "non-spec:/.//path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "a/..//path",
+ "base": "non-spec:/p",
+ "href": "non-spec:/.//path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "",
+ "base": "non-spec:/..//p",
+ "href": "non-spec:/.//p",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//p",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "path",
+ "base": "non-spec:/..//p",
+ "href": "non-spec:/.//path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "//path",
+ "search": "",
+ "hash": ""
+ },
+ "Do not serialize /. in path",
+ {
+ "input": "../path",
+ "base": "non-spec:/.//p",
+ "href": "non-spec:/path",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/path",
+ "search": "",
+ "hash": ""
+ },
+ "# percent encoded hosts in non-special-URLs",
+ {
+ "input": "non-special://%E2%80%A0/",
+ "base": null,
+ "href": "non-special://%E2%80%A0/",
+ "protocol": "non-special:",
+ "username": "",
+ "password": "",
+ "host": "%E2%80%A0",
+ "hostname": "%E2%80%A0",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-special://H%4fSt/path",
+ "base": null,
+ "href": "non-special://H%4fSt/path",
+ "protocol": "non-special:",
+ "username": "",
+ "password": "",
+ "host": "H%4fSt",
+ "hostname": "H%4fSt",
+ "port": "",
+ "pathname": "/path",
+ "search": "",
+ "hash": ""
+ },
+ "# IPv6 in non-special-URLs",
+ {
+ "input": "non-special://[1:2:0:0:5:0:0:0]/",
+ "base": null,
+ "href": "non-special://[1:2:0:0:5::]/",
+ "protocol": "non-special:",
+ "username": "",
+ "password": "",
+ "host": "[1:2:0:0:5::]",
+ "hostname": "[1:2:0:0:5::]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-special://[1:2:0:0:0:0:0:3]/",
+ "base": null,
+ "href": "non-special://[1:2::3]/",
+ "protocol": "non-special:",
+ "username": "",
+ "password": "",
+ "host": "[1:2::3]",
+ "hostname": "[1:2::3]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-special://[1:2::3]:80/",
+ "base": null,
+ "href": "non-special://[1:2::3]:80/",
+ "protocol": "non-special:",
+ "username": "",
+ "password": "",
+ "host": "[1:2::3]:80",
+ "hostname": "[1:2::3]",
+ "port": "80",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "non-special://[:80/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "blob:https://example.com:443/",
+ "base": null,
+ "href": "blob:https://example.com:443/",
+ "origin": "https://example.com",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "https://example.com:443/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "blob:http://example.org:88/",
+ "base": null,
+ "href": "blob:http://example.org:88/",
+ "origin": "http://example.org:88",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "http://example.org:88/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf",
+ "base": null,
+ "href": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "d3958f5c-0777-0845-9dcf-2cb28783acaf",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "blob:",
+ "base": null,
+ "href": "blob:",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "",
+ "search": "",
+ "hash": ""
+ },
+ "blob: in blob:",
+ {
+ "input": "blob:blob:",
+ "base": null,
+ "href": "blob:blob:",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "blob:",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "blob:blob:https://example.org/",
+ "base": null,
+ "href": "blob:blob:https://example.org/",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "blob:https://example.org/",
+ "search": "",
+ "hash": ""
+ },
+ "Non-http(s): in blob:",
+ {
+ "input": "blob:about:blank",
+ "base": null,
+ "href": "blob:about:blank",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "about:blank",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "blob:file://host/path",
+ "base": null,
+ "href": "blob:file://host/path",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "file://host/path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "blob:ftp://host/path",
+ "base": null,
+ "href": "blob:ftp://host/path",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "ftp://host/path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "blob:ws://example.org/",
+ "base": null,
+ "href": "blob:ws://example.org/",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "ws://example.org/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "blob:wss://example.org/",
+ "base": null,
+ "href": "blob:wss://example.org/",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "wss://example.org/",
+ "search": "",
+ "hash": ""
+ },
+ "Percent-encoded http: in blob:",
+ {
+ "input": "blob:http%3a//example.org/",
+ "base": null,
+ "href": "blob:http%3a//example.org/",
+ "origin": "null",
+ "protocol": "blob:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "http%3a//example.org/",
+ "search": "",
+ "hash": ""
+ },
+ "Invalid IPv4 radix digits",
+ {
+ "input": "http://0x7f.0.0.0x7g",
+ "base": null,
+ "href": "http://0x7f.0.0.0x7g/",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "0x7f.0.0.0x7g",
+ "hostname": "0x7f.0.0.0x7g",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://0X7F.0.0.0X7G",
+ "base": null,
+ "href": "http://0x7f.0.0.0x7g/",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "0x7f.0.0.0x7g",
+ "hostname": "0x7f.0.0.0x7g",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "Invalid IPv4 portion of IPv6 address",
+ {
+ "input": "http://[::127.0.0.0.1]",
+ "base": null,
+ "failure": true
+ },
+ "Uncompressed IPv6 addresses with 0",
+ {
+ "input": "http://[0:1:0:1:0:1:0:1]",
+ "base": null,
+ "href": "http://[0:1:0:1:0:1:0:1]/",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "[0:1:0:1:0:1:0:1]",
+ "hostname": "[0:1:0:1:0:1:0:1]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://[1:0:1:0:1:0:1:0]",
+ "base": null,
+ "href": "http://[1:0:1:0:1:0:1:0]/",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "[1:0:1:0:1:0:1:0]",
+ "hostname": "[1:0:1:0:1:0:1:0]",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ "Percent-encoded query and fragment",
+ {
+ "input": "http://example.org/test?\u0022",
+ "base": null,
+ "href": "http://example.org/test?%22",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?%22",
+ "hash": ""
+ },
+ {
+ "input": "http://example.org/test?\u0023",
+ "base": null,
+ "href": "http://example.org/test?#",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://example.org/test?\u003C",
+ "base": null,
+ "href": "http://example.org/test?%3C",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?%3C",
+ "hash": ""
+ },
+ {
+ "input": "http://example.org/test?\u003E",
+ "base": null,
+ "href": "http://example.org/test?%3E",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?%3E",
+ "hash": ""
+ },
+ {
+ "input": "http://example.org/test?\u2323",
+ "base": null,
+ "href": "http://example.org/test?%E2%8C%A3",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?%E2%8C%A3",
+ "hash": ""
+ },
+ {
+ "input": "http://example.org/test?%23%23",
+ "base": null,
+ "href": "http://example.org/test?%23%23",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?%23%23",
+ "hash": ""
+ },
+ {
+ "input": "http://example.org/test?%GH",
+ "base": null,
+ "href": "http://example.org/test?%GH",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?%GH",
+ "hash": ""
+ },
+ {
+ "input": "http://example.org/test?a#%EF",
+ "base": null,
+ "href": "http://example.org/test?a#%EF",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?a",
+ "hash": "#%EF"
+ },
+ {
+ "input": "http://example.org/test?a#%GH",
+ "base": null,
+ "href": "http://example.org/test?a#%GH",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?a",
+ "hash": "#%GH"
+ },
+ "URLs that require a non-about:blank base. (Also serve as invalid base tests.)",
+ {
+ "input": "a",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "a/",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "a//",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ "Bases that don't fail to parse but fail to be bases",
+ {
+ "input": "test-a-colon.html",
+ "base": "a:",
+ "failure": true
+ },
+ {
+ "input": "test-a-colon-b.html",
+ "base": "a:b",
+ "failure": true
+ },
+ "Other base URL tests, that must succeed",
+ {
+ "input": "test-a-colon-slash.html",
+ "base": "a:/",
+ "href": "a:/test-a-colon-slash.html",
+ "protocol": "a:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test-a-colon-slash.html",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "test-a-colon-slash-slash.html",
+ "base": "a://",
+ "href": "a:///test-a-colon-slash-slash.html",
+ "protocol": "a:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test-a-colon-slash-slash.html",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "test-a-colon-slash-b.html",
+ "base": "a:/b",
+ "href": "a:/test-a-colon-slash-b.html",
+ "protocol": "a:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test-a-colon-slash-b.html",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "test-a-colon-slash-slash-b.html",
+ "base": "a://b",
+ "href": "a://b/test-a-colon-slash-slash-b.html",
+ "protocol": "a:",
+ "username": "",
+ "password": "",
+ "host": "b",
+ "hostname": "b",
+ "port": "",
+ "pathname": "/test-a-colon-slash-slash-b.html",
+ "search": "",
+ "hash": ""
+ },
+ "Null code point in fragment",
+ {
+ "input": "http://example.org/test?a#b\u0000c",
+ "base": null,
+ "href": "http://example.org/test?a#b%00c",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?a",
+ "hash": "#b%00c"
+ },
+ {
+ "input": "non-spec://example.org/test?a#b\u0000c",
+ "base": null,
+ "href": "non-spec://example.org/test?a#b%00c",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/test",
+ "search": "?a",
+ "hash": "#b%00c"
+ },
+ {
+ "input": "non-spec:/test?a#b\u0000c",
+ "base": null,
+ "href": "non-spec:/test?a#b%00c",
+ "protocol": "non-spec:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "?a",
+ "hash": "#b%00c"
+ },
+ "First scheme char - not allowed: https://github.com/whatwg/url/issues/464",
+ {
+ "input": "10.0.0.7:8080/foo.html",
+ "base": "file:///some/dir/bar.html",
+ "href": "file:///some/dir/10.0.0.7:8080/foo.html",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/some/dir/10.0.0.7:8080/foo.html",
+ "search": "",
+ "hash": ""
+ },
+ "Subsequent scheme chars - not allowed",
+ {
+ "input": "a!@$*=/foo.html",
+ "base": "file:///some/dir/bar.html",
+ "href": "file:///some/dir/a!@$*=/foo.html",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/some/dir/a!@$*=/foo.html",
+ "search": "",
+ "hash": ""
+ },
+ "First and subsequent scheme chars - allowed",
+ {
+ "input": "a1234567890-+.:foo/bar",
+ "base": "http://example.com/dir/file",
+ "href": "a1234567890-+.:foo/bar",
+ "protocol": "a1234567890-+.:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "foo/bar",
+ "search": "",
+ "hash": ""
+ },
+ "IDNA ignored code points in file URLs hosts",
+ {
+ "input": "file://a\u00ADb/p",
+ "base": null,
+ "href": "file://ab/p",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "ab",
+ "hostname": "ab",
+ "port": "",
+ "pathname": "/p",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "file://a%C2%ADb/p",
+ "base": null,
+ "href": "file://ab/p",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "ab",
+ "hostname": "ab",
+ "port": "",
+ "pathname": "/p",
+ "search": "",
+ "hash": ""
+ },
+ "IDNA hostnames which get mapped to 'localhost'",
+ {
+ "input": "file://loC𝐀𝐋𝐇𝐨𝐬𝐭/usr/bin",
+ "base": null,
+ "href": "file:///usr/bin",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/usr/bin",
+ "search": "",
+ "hash": ""
+ },
+ "Empty host after the domain to ASCII",
+ {
+ "input": "file://\u00ad/p",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://%C2%AD/p",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "file://xn--/p",
+ "base": null,
+ "failure": true
+ },
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1647058",
+ {
+ "input": "#link",
+ "base": "https://example.org/##link",
+ "href": "https://example.org/#link",
+ "protocol": "https:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": "#link"
+ },
+ "UTF-8 percent-encode of C0 control percent-encode set and supersets",
+ {
+ "input": "non-special:cannot-be-a-base-url-\u0000\u0001\u001F\u001E\u007E\u007F\u0080",
+ "base": null,
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:cannot-be-a-base-url-%00%01%1F%1E~%7F%C2%80",
+ "origin": "null",
+ "password": "",
+ "pathname": "cannot-be-a-base-url-%00%01%1F%1E~%7F%C2%80",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "https://www.example.com/path{\u007Fpath.html?query'\u007F=query#fragment<\u007Ffragment",
+ "base": null,
+ "hash": "#fragment%3C%7Ffragment",
+ "host": "www.example.com",
+ "hostname": "www.example.com",
+ "href": "https://www.example.com/path%7B%7Fpath.html?query%27%7F=query#fragment%3C%7Ffragment",
+ "origin": "https://www.example.com",
+ "password": "",
+ "pathname": "/path%7B%7Fpath.html",
+ "port": "",
+ "protocol": "https:",
+ "search": "?query%27%7F=query",
+ "username": ""
+ },
+ {
+ "input": "https://user:pass[\u007F@foo/bar",
+ "base": "http://example.org",
+ "hash": "",
+ "host": "foo",
+ "hostname": "foo",
+ "href": "https://user:pass%5B%7F@foo/bar",
+ "origin": "https://foo",
+ "password": "pass%5B%7F",
+ "pathname": "/bar",
+ "port": "",
+ "protocol": "https:",
+ "search": "",
+ "username": "user"
+ },
+ "Tests for the distinct percent-encode sets",
+ {
+ "input": "foo:// !\"$%&'()*+,-.;<=>@[\\]^_`{|}~@host/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href": "foo://%20!%22$%&'()*+,-.%3B%3C%3D%3E%40%5B%5C%5D%5E_%60%7B%7C%7D~@host/",
+ "origin": "null",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": "%20!%22$%&'()*+,-.%3B%3C%3D%3E%40%5B%5C%5D%5E_%60%7B%7C%7D~"
+ },
+ {
+ "input": "wss:// !\"$%&'()*+,-.;<=>@[]^_`{|}~@host/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href": "wss://%20!%22$%&'()*+,-.%3B%3C%3D%3E%40%5B%5D%5E_%60%7B%7C%7D~@host/",
+ "origin": "wss://host",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "wss:",
+ "search": "",
+ "username": "%20!%22$%&'()*+,-.%3B%3C%3D%3E%40%5B%5D%5E_%60%7B%7C%7D~"
+ },
+ {
+ "input": "foo://joe: !\"$%&'()*+,-.:;<=>@[\\]^_`{|}~@host/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href": "foo://joe:%20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5C%5D%5E_%60%7B%7C%7D~@host/",
+ "origin": "null",
+ "password": "%20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5C%5D%5E_%60%7B%7C%7D~",
+ "pathname": "/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": "joe"
+ },
+ {
+ "input": "wss://joe: !\"$%&'()*+,-.:;<=>@[]^_`{|}~@host/",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href": "wss://joe:%20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5D%5E_%60%7B%7C%7D~@host/",
+ "origin": "wss://host",
+ "password": "%20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5D%5E_%60%7B%7C%7D~",
+ "pathname": "/",
+ "port":"",
+ "protocol": "wss:",
+ "search": "",
+ "username": "joe"
+ },
+ {
+ "input": "foo://!\"$%&'()*+,-.;=_`{}~/",
+ "base": null,
+ "hash": "",
+ "host": "!\"$%&'()*+,-.;=_`{}~",
+ "hostname": "!\"$%&'()*+,-.;=_`{}~",
+ "href":"foo://!\"$%&'()*+,-.;=_`{}~/",
+ "origin": "null",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "wss://!\"$&'()*+,-.;=_`{}~/",
+ "base": null,
+ "hash": "",
+ "host": "!\"$&'()*+,-.;=_`{}~",
+ "hostname": "!\"$&'()*+,-.;=_`{}~",
+ "href":"wss://!\"$&'()*+,-.;=_`{}~/",
+ "origin": "wss://!\"$&'()*+,-.;=_`{}~",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "wss:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "foo://host/ !\"$%&'()*+,-./:;<=>@[\\]^_`{|}~",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href": "foo://host/%20!%22$%&'()*+,-./:;%3C=%3E@[\\]^_%60%7B|%7D~",
+ "origin": "null",
+ "password": "",
+ "pathname": "/%20!%22$%&'()*+,-./:;%3C=%3E@[\\]^_%60%7B|%7D~",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "wss://host/ !\"$%&'()*+,-./:;<=>@[\\]^_`{|}~",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href": "wss://host/%20!%22$%&'()*+,-./:;%3C=%3E@[/]^_%60%7B|%7D~",
+ "origin": "wss://host",
+ "password": "",
+ "pathname": "/%20!%22$%&'()*+,-./:;%3C=%3E@[/]^_%60%7B|%7D~",
+ "port":"",
+ "protocol": "wss:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "foo://host/dir/? !\"$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href": "foo://host/dir/?%20!%22$%&'()*+,-./:;%3C=%3E?@[\\]^_`{|}~",
+ "origin": "null",
+ "password": "",
+ "pathname": "/dir/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "?%20!%22$%&'()*+,-./:;%3C=%3E?@[\\]^_`{|}~",
+ "username": ""
+ },
+ {
+ "input": "wss://host/dir/? !\"$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
+ "base": null,
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href": "wss://host/dir/?%20!%22$%&%27()*+,-./:;%3C=%3E?@[\\]^_`{|}~",
+ "origin": "wss://host",
+ "password": "",
+ "pathname": "/dir/",
+ "port":"",
+ "protocol": "wss:",
+ "search": "?%20!%22$%&%27()*+,-./:;%3C=%3E?@[\\]^_`{|}~",
+ "username": ""
+ },
+ {
+ "input": "foo://host/dir/# !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
+ "base": null,
+ "hash": "#%20!%22#$%&'()*+,-./:;%3C=%3E?@[\\]^_%60{|}~",
+ "host": "host",
+ "hostname": "host",
+ "href": "foo://host/dir/#%20!%22#$%&'()*+,-./:;%3C=%3E?@[\\]^_%60{|}~",
+ "origin": "null",
+ "password": "",
+ "pathname": "/dir/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "wss://host/dir/# !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
+ "base": null,
+ "hash": "#%20!%22#$%&'()*+,-./:;%3C=%3E?@[\\]^_%60{|}~",
+ "host": "host",
+ "hostname": "host",
+ "href": "wss://host/dir/#%20!%22#$%&'()*+,-./:;%3C=%3E?@[\\]^_%60{|}~",
+ "origin": "wss://host",
+ "password": "",
+ "pathname": "/dir/",
+ "port":"",
+ "protocol": "wss:",
+ "search": "",
+ "username": ""
+ },
+ "Ensure that input schemes are not ignored when resolving non-special URLs",
+ {
+ "input": "abc:rootless",
+ "base": "abc://host/path",
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href":"abc:rootless",
+ "password": "",
+ "pathname": "rootless",
+ "port":"",
+ "protocol": "abc:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "abc:rootless",
+ "base": "abc:/path",
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href":"abc:rootless",
+ "password": "",
+ "pathname": "rootless",
+ "port":"",
+ "protocol": "abc:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "abc:rootless",
+ "base": "abc:path",
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href":"abc:rootless",
+ "password": "",
+ "pathname": "rootless",
+ "port":"",
+ "protocol": "abc:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "abc:/rooted",
+ "base": "abc://host/path",
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href":"abc:/rooted",
+ "password": "",
+ "pathname": "/rooted",
+ "port":"",
+ "protocol": "abc:",
+ "search": "",
+ "username": ""
+ },
+ "Empty query and fragment with blank should throw an error",
+ {
+ "input": "#",
+ "base": null,
+ "failure": true,
+ "relativeTo": "any-base"
+ },
+ {
+ "input": "?",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ "Last component looks like a number, but not valid IPv4",
+ {
+ "input": "http://1.2.3.4.5",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://1.2.3.4.5.",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://0..0x300/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://0..0x300./",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://256.256.256.256.256",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://256.256.256.256.256.",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://1.2.3.08",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://1.2.3.08.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://1.2.3.09",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://09.2.3.4",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://09.2.3.4.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://01.2.3.4.5",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://01.2.3.4.5.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://0x100.2.3.4",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://0x100.2.3.4.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://0x1.2.3.4.5",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://0x1.2.3.4.5.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.1.2.3.4",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.1.2.3.4.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.2.3.4",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.2.3.4.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.09",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.09.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.0x4",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.0x4.",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.09..",
+ "base": null,
+ "hash": "",
+ "host": "foo.09..",
+ "hostname": "foo.09..",
+ "href":"http://foo.09../",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "http:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "http://0999999999999999999/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.0x",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://foo.0XFfFfFfFfFfFfFfFfFfAcE123",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "http://💩.123/",
+ "base": null,
+ "failure": true
+ },
+ "U+0000 and U+FFFF in various places",
+ {
+ "input": "https://\u0000y",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://x/\u0000y",
+ "base": null,
+ "hash": "",
+ "host": "x",
+ "hostname": "x",
+ "href": "https://x/%00y",
+ "password": "",
+ "pathname": "/%00y",
+ "port": "",
+ "protocol": "https:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "https://x/?\u0000y",
+ "base": null,
+ "hash": "",
+ "host": "x",
+ "hostname": "x",
+ "href": "https://x/?%00y",
+ "password": "",
+ "pathname": "/",
+ "port": "",
+ "protocol": "https:",
+ "search": "?%00y",
+ "username": ""
+ },
+ {
+ "input": "https://x/?#\u0000y",
+ "base": null,
+ "hash": "#%00y",
+ "host": "x",
+ "hostname": "x",
+ "href": "https://x/?#%00y",
+ "password": "",
+ "pathname": "/",
+ "port": "",
+ "protocol": "https:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "https://\uFFFFy",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://x/\uFFFFy",
+ "base": null,
+ "hash": "",
+ "host": "x",
+ "hostname": "x",
+ "href": "https://x/%EF%BF%BFy",
+ "password": "",
+ "pathname": "/%EF%BF%BFy",
+ "port": "",
+ "protocol": "https:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "https://x/?\uFFFFy",
+ "base": null,
+ "hash": "",
+ "host": "x",
+ "hostname": "x",
+ "href": "https://x/?%EF%BF%BFy",
+ "password": "",
+ "pathname": "/",
+ "port": "",
+ "protocol": "https:",
+ "search": "?%EF%BF%BFy",
+ "username": ""
+ },
+ {
+ "input": "https://x/?#\uFFFFy",
+ "base": null,
+ "hash": "#%EF%BF%BFy",
+ "host": "x",
+ "hostname": "x",
+ "href": "https://x/?#%EF%BF%BFy",
+ "password": "",
+ "pathname": "/",
+ "port": "",
+ "protocol": "https:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "non-special:\u0000y",
+ "base": null,
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:%00y",
+ "password": "",
+ "pathname": "%00y",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "non-special:x/\u0000y",
+ "base": null,
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:x/%00y",
+ "password": "",
+ "pathname": "x/%00y",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "non-special:x/?\u0000y",
+ "base": null,
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:x/?%00y",
+ "password": "",
+ "pathname": "x/",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "?%00y",
+ "username": ""
+ },
+ {
+ "input": "non-special:x/?#\u0000y",
+ "base": null,
+ "hash": "#%00y",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:x/?#%00y",
+ "password": "",
+ "pathname": "x/",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "non-special:\uFFFFy",
+ "base": null,
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:%EF%BF%BFy",
+ "password": "",
+ "pathname": "%EF%BF%BFy",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "non-special:x/\uFFFFy",
+ "base": null,
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:x/%EF%BF%BFy",
+ "password": "",
+ "pathname": "x/%EF%BF%BFy",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "non-special:x/?\uFFFFy",
+ "base": null,
+ "hash": "",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:x/?%EF%BF%BFy",
+ "password": "",
+ "pathname": "x/",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "?%EF%BF%BFy",
+ "username": ""
+ },
+ {
+ "input": "non-special:x/?#\uFFFFy",
+ "base": null,
+ "hash": "#%EF%BF%BFy",
+ "host": "",
+ "hostname": "",
+ "href": "non-special:x/?#%EF%BF%BFy",
+ "password": "",
+ "pathname": "x/",
+ "port": "",
+ "protocol": "non-special:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "",
+ "base": null,
+ "failure": true,
+ "relativeTo": "non-opaque-path-base"
+ },
+ {
+ "input": "https://example.com/\"quoted\"",
+ "base": null,
+ "hash": "",
+ "host": "example.com",
+ "hostname": "example.com",
+ "href": "https://example.com/%22quoted%22",
+ "origin": "https://example.com",
+ "password": "",
+ "pathname": "/%22quoted%22",
+ "port": "",
+ "protocol": "https:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "https://a%C2%ADb/",
+ "base": null,
+ "hash": "",
+ "host": "ab",
+ "hostname": "ab",
+ "href": "https://ab/",
+ "origin": "https://ab",
+ "password": "",
+ "pathname": "/",
+ "port": "",
+ "protocol": "https:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "comment": "Empty host after domain to ASCII",
+ "input": "https://\u00AD/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://%C2%AD/",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "https://xn--/",
+ "base": null,
+ "failure": true
+ },
+ "Non-special schemes that some implementations might incorrectly treat as special",
+ {
+ "input": "data://example.com:8080/pathname?search#hash",
+ "base": null,
+ "href": "data://example.com:8080/pathname?search#hash",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "example.com:8080",
+ "hostname": "example.com",
+ "port": "8080",
+ "pathname": "/pathname",
+ "search": "?search",
+ "hash": "#hash"
+ },
+ {
+ "input": "data:///test",
+ "base": null,
+ "href": "data:///test",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "data://test/a/../b",
+ "base": null,
+ "href": "data://test/b",
+ "origin": "null",
+ "protocol": "data:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/b",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "data://:443",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "data://test:test",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "data://[:1]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "javascript://example.com:8080/pathname?search#hash",
+ "base": null,
+ "href": "javascript://example.com:8080/pathname?search#hash",
+ "origin": "null",
+ "protocol": "javascript:",
+ "username": "",
+ "password": "",
+ "host": "example.com:8080",
+ "hostname": "example.com",
+ "port": "8080",
+ "pathname": "/pathname",
+ "search": "?search",
+ "hash": "#hash"
+ },
+ {
+ "input": "javascript:///test",
+ "base": null,
+ "href": "javascript:///test",
+ "origin": "null",
+ "protocol": "javascript:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "javascript://test/a/../b",
+ "base": null,
+ "href": "javascript://test/b",
+ "origin": "null",
+ "protocol": "javascript:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/b",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "javascript://:443",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "javascript://test:test",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "javascript://[:1]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "mailto://example.com:8080/pathname?search#hash",
+ "base": null,
+ "href": "mailto://example.com:8080/pathname?search#hash",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "example.com:8080",
+ "hostname": "example.com",
+ "port": "8080",
+ "pathname": "/pathname",
+ "search": "?search",
+ "hash": "#hash"
+ },
+ {
+ "input": "mailto:///test",
+ "base": null,
+ "href": "mailto:///test",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "mailto://test/a/../b",
+ "base": null,
+ "href": "mailto://test/b",
+ "origin": "null",
+ "protocol": "mailto:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/b",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "mailto://:443",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "mailto://test:test",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "mailto://[:1]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "intent://example.com:8080/pathname?search#hash",
+ "base": null,
+ "href": "intent://example.com:8080/pathname?search#hash",
+ "origin": "null",
+ "protocol": "intent:",
+ "username": "",
+ "password": "",
+ "host": "example.com:8080",
+ "hostname": "example.com",
+ "port": "8080",
+ "pathname": "/pathname",
+ "search": "?search",
+ "hash": "#hash"
+ },
+ {
+ "input": "intent:///test",
+ "base": null,
+ "href": "intent:///test",
+ "origin": "null",
+ "protocol": "intent:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "intent://test/a/../b",
+ "base": null,
+ "href": "intent://test/b",
+ "origin": "null",
+ "protocol": "intent:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/b",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "intent://:443",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "intent://test:test",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "intent://[:1]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "urn://example.com:8080/pathname?search#hash",
+ "base": null,
+ "href": "urn://example.com:8080/pathname?search#hash",
+ "origin": "null",
+ "protocol": "urn:",
+ "username": "",
+ "password": "",
+ "host": "example.com:8080",
+ "hostname": "example.com",
+ "port": "8080",
+ "pathname": "/pathname",
+ "search": "?search",
+ "hash": "#hash"
+ },
+ {
+ "input": "urn:///test",
+ "base": null,
+ "href": "urn:///test",
+ "origin": "null",
+ "protocol": "urn:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "urn://test/a/../b",
+ "base": null,
+ "href": "urn://test/b",
+ "origin": "null",
+ "protocol": "urn:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/b",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "urn://:443",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "urn://test:test",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "urn://[:1]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "turn://example.com:8080/pathname?search#hash",
+ "base": null,
+ "href": "turn://example.com:8080/pathname?search#hash",
+ "origin": "null",
+ "protocol": "turn:",
+ "username": "",
+ "password": "",
+ "host": "example.com:8080",
+ "hostname": "example.com",
+ "port": "8080",
+ "pathname": "/pathname",
+ "search": "?search",
+ "hash": "#hash"
+ },
+ {
+ "input": "turn:///test",
+ "base": null,
+ "href": "turn:///test",
+ "origin": "null",
+ "protocol": "turn:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "turn://test/a/../b",
+ "base": null,
+ "href": "turn://test/b",
+ "origin": "null",
+ "protocol": "turn:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/b",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "turn://:443",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "turn://test:test",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "turn://[:1]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "stun://example.com:8080/pathname?search#hash",
+ "base": null,
+ "href": "stun://example.com:8080/pathname?search#hash",
+ "origin": "null",
+ "protocol": "stun:",
+ "username": "",
+ "password": "",
+ "host": "example.com:8080",
+ "hostname": "example.com",
+ "port": "8080",
+ "pathname": "/pathname",
+ "search": "?search",
+ "hash": "#hash"
+ },
+ {
+ "input": "stun:///test",
+ "base": null,
+ "href": "stun:///test",
+ "origin": "null",
+ "protocol": "stun:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/test",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "stun://test/a/../b",
+ "base": null,
+ "href": "stun://test/b",
+ "origin": "null",
+ "protocol": "stun:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/b",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "stun://:443",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "stun://test:test",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "stun://[:1]",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "w://x:0",
+ "base": null,
+ "href": "w://x:0",
+ "origin": "null",
+ "protocol": "w:",
+ "username": "",
+ "password": "",
+ "host": "x:0",
+ "hostname": "x",
+ "port": "0",
+ "pathname": "",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "west://x:0",
+ "base": null,
+ "href": "west://x:0",
+ "origin": "null",
+ "protocol": "west:",
+ "username": "",
+ "password": "",
+ "host": "x:0",
+ "hostname": "x",
+ "port": "0",
+ "pathname": "",
+ "search": "",
+ "hash": ""
+ },
+ "Scheme relative path starting with multiple slashes",
+ {
+ "input": "///test",
+ "base": "http://example.org/",
+ "href": "http://test/",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///\\//\\//test",
+ "base": "http://example.org/",
+ "href": "http://test/",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "test",
+ "hostname": "test",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///example.org/path",
+ "base": "http://example.org/",
+ "href": "http://example.org/path",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///example.org/../path",
+ "base": "http://example.org/",
+ "href": "http://example.org/path",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///example.org/../../",
+ "base": "http://example.org/",
+ "href": "http://example.org/",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///example.org/../path/../../",
+ "base": "http://example.org/",
+ "href": "http://example.org/",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///example.org/../path/../../path",
+ "base": "http://example.org/",
+ "href": "http://example.org/path",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/\\/\\//example.org/../path",
+ "base": "http://example.org/",
+ "href": "http://example.org/path",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "example.org",
+ "hostname": "example.org",
+ "port": "",
+ "pathname": "/path",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "///abcdef/../",
+ "base": "file:///",
+ "href": "file:///",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "/\\//\\/a/../",
+ "base": "file:///",
+ "href": "file://////",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "",
+ "hostname": "",
+ "port": "",
+ "pathname": "////",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "//a/../",
+ "base": "file:///",
+ "href": "file://a/",
+ "protocol": "file:",
+ "username": "",
+ "password": "",
+ "host": "a",
+ "hostname": "a",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ }
+]
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
index 29715060..8b817891 100644
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -92,7 +92,8 @@ async def test_asgi_transport_no_body():
@pytest.mark.anyio
async def test_asgi():
- async with httpx.AsyncClient(app=hello_world) as client:
+ transport = httpx.ASGITransport(app=hello_world)
+ async with httpx.AsyncClient(transport=transport) as client:
response = await client.get("http://www.example.org/")
assert response.status_code == 200
@@ -101,7 +102,8 @@ async def test_asgi():
@pytest.mark.anyio
async def test_asgi_urlencoded_path():
- async with httpx.AsyncClient(app=echo_path) as client:
+ transport = httpx.ASGITransport(app=echo_path)
+ async with httpx.AsyncClient(transport=transport) as client:
url = httpx.URL("http://www.example.org/").copy_with(path="/user@example.org")
response = await client.get(url)
@@ -111,7 +113,8 @@ async def test_asgi_urlencoded_path():
@pytest.mark.anyio
async def test_asgi_raw_path():
- async with httpx.AsyncClient(app=echo_raw_path) as client:
+ transport = httpx.ASGITransport(app=echo_raw_path)
+ async with httpx.AsyncClient(transport=transport) as client:
url = httpx.URL("http://www.example.org/").copy_with(path="/user@example.org")
response = await client.get(url)
@@ -124,7 +127,8 @@ async def test_asgi_raw_path_should_not_include_querystring_portion():
"""
See https://github.com/encode/httpx/issues/2810
"""
- async with httpx.AsyncClient(app=echo_raw_path) as client:
+ transport = httpx.ASGITransport(app=echo_raw_path)
+ async with httpx.AsyncClient(transport=transport) as client:
url = httpx.URL("http://www.example.org/path?query")
response = await client.get(url)
@@ -134,7 +138,8 @@ async def test_asgi_raw_path_should_not_include_querystring_portion():
@pytest.mark.anyio
async def test_asgi_upload():
- async with httpx.AsyncClient(app=echo_body) as client:
+ transport = httpx.ASGITransport(app=echo_body)
+ async with httpx.AsyncClient(transport=transport) as client:
response = await client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
@@ -143,7 +148,8 @@ async def test_asgi_upload():
@pytest.mark.anyio
async def test_asgi_headers():
- async with httpx.AsyncClient(app=echo_headers) as client:
+ transport = httpx.ASGITransport(app=echo_headers)
+ async with httpx.AsyncClient(transport=transport) as client:
response = await client.get("http://www.example.org/")
assert response.status_code == 200
@@ -151,7 +157,7 @@ async def test_asgi_headers():
"headers": [
["host", "www.example.org"],
["accept", "*/*"],
- ["accept-encoding", "gzip, deflate, br"],
+ ["accept-encoding", "gzip, deflate, br, zstd"],
["connection", "keep-alive"],
["user-agent", f"python-httpx/{httpx.__version__}"],
]
@@ -160,14 +166,16 @@ async def test_asgi_headers():
@pytest.mark.anyio
async def test_asgi_exc():
- async with httpx.AsyncClient(app=raise_exc) as client:
+ transport = httpx.ASGITransport(app=raise_exc)
+ async with httpx.AsyncClient(transport=transport) as client:
with pytest.raises(RuntimeError):
await client.get("http://www.example.org/")
@pytest.mark.anyio
async def test_asgi_exc_after_response():
- async with httpx.AsyncClient(app=raise_exc_after_response) as client:
+ transport = httpx.ASGITransport(app=raise_exc_after_response)
+ async with httpx.AsyncClient(transport=transport) as client:
with pytest.raises(RuntimeError):
await client.get("http://www.example.org/")
@@ -199,7 +207,8 @@ async def test_asgi_disconnect_after_response_complete():
message = await receive()
disconnect = message.get("type") == "http.disconnect"
- async with httpx.AsyncClient(app=read_body) as client:
+ transport = httpx.ASGITransport(app=read_body)
+ async with httpx.AsyncClient(transport=transport) as client:
response = await client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
@@ -213,3 +222,13 @@ async def test_asgi_exc_no_raise():
response = await client.get("http://www.example.org/")
assert response.status_code == 500
+
+
+@pytest.mark.anyio
+async def test_deprecated_shortcut():
+ """
+ The `app=...` shortcut is now deprecated.
+ Use the explicit transport style instead.
+ """
+ with pytest.warns(DeprecationWarning):
+ httpx.AsyncClient(app=hello_world)
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 7bb45de5..6b6df922 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -3,6 +3,7 @@ Unit tests for auth classes.
Integration tests also exist in tests/client/test_auth.py
"""
+
from urllib.request import parse_keqv_list
import pytest
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
index 170a9345..bcbb18bb 100644
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -1,8 +1,12 @@
+from __future__ import annotations
+
+import io
import typing
import zlib
import chardet
import pytest
+import zstandard as zstd
import httpx
@@ -71,6 +75,53 @@ def test_brotli():
assert response.content == body
+def test_zstd():
+ body = b"test 123"
+ compressed_body = zstd.compress(body)
+
+ headers = [(b"Content-Encoding", b"zstd")]
+ response = httpx.Response(
+ 200,
+ headers=headers,
+ content=compressed_body,
+ )
+ assert response.content == body
+
+
+def test_zstd_decoding_error():
+ compressed_body = "this_is_not_zstd_compressed_data"
+
+ headers = [(b"Content-Encoding", b"zstd")]
+ with pytest.raises(httpx.DecodingError):
+ httpx.Response(
+ 200,
+ headers=headers,
+ content=compressed_body,
+ )
+
+
+def test_zstd_multiframe():
+ # test inspired by urllib3 test suite
+ data = (
+ # Zstandard frame
+ zstd.compress(b"foo")
+ # skippable frame (must be ignored)
+ + bytes.fromhex(
+ "50 2A 4D 18" # Magic_Number (little-endian)
+ "07 00 00 00" # Frame_Size (little-endian)
+ "00 00 00 00 00 00 00" # User_Data
+ )
+ # Zstandard frame
+ + zstd.compress(b"bar")
+ )
+ compressed_body = io.BytesIO(data)
+
+ headers = [(b"Content-Encoding", b"zstd")]
+ response = httpx.Response(200, headers=headers, content=compressed_body)
+ response.read()
+ assert response.content == b"foobar"
+
+
def test_multi():
body = b"test 123"
@@ -224,7 +275,7 @@ def test_text_decoder_empty_cases():
[((b"Hello,", b" world!"), ["Hello,", " world!"])],
)
def test_streaming_text_decoder(
- data: typing.Iterable[bytes], expected: typing.List[str]
+ data: typing.Iterable[bytes], expected: list[str]
) -> None:
response = httpx.Response(200, content=iter(data))
assert list(response.iter_text()) == expected
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 6547ab37..60c8721c 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import typing
import httpcore
@@ -34,7 +36,7 @@ def test_httpcore_all_exceptions_mapped() -> None:
pytest.fail(f"Unmapped httpcore exceptions: {unmapped_exceptions}")
-def test_httpcore_exception_mapping(server: "TestServer") -> None:
+def test_httpcore_exception_mapping(server: TestServer) -> None:
"""
HTTPCore exception mapping works as expected.
"""
diff --git a/tests/test_main.py b/tests/test_main.py
index 67eeb0d2..feb796e1 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -129,7 +129,7 @@ def test_verbose(server):
"GET / HTTP/1.1",
f"Host: {server.url.netloc.decode('ascii')}",
"Accept: */*",
- "Accept-Encoding: gzip, deflate, br",
+ "Accept-Encoding: gzip, deflate, br, zstd",
"Connection: keep-alive",
f"User-Agent: python-httpx/{httpx.__version__}",
"",
@@ -154,7 +154,7 @@ def test_auth(server):
"GET / HTTP/1.1",
f"Host: {server.url.netloc.decode('ascii')}",
"Accept: */*",
- "Accept-Encoding: gzip, deflate, br",
+ "Accept-Encoding: gzip, deflate, br, zstd",
"Connection: keep-alive",
f"User-Agent: python-httpx/{httpx.__version__}",
"Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
diff --git a/tests/test_multipart.py b/tests/test_multipart.py
index fc283c9c..764f85a2 100644
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import io
import tempfile
import typing
@@ -148,7 +150,7 @@ def test_multipart_file_tuple():
@pytest.mark.parametrize("file_content_type", [None, "text/plain"])
-def test_multipart_file_tuple_headers(file_content_type: typing.Optional[str]) -> None:
+def test_multipart_file_tuple_headers(file_content_type: str | None) -> None:
file_name = "test.txt"
file_content = io.BytesIO(b"")
file_headers = {"Expires": "0"}
@@ -460,8 +462,8 @@ class TestHeaderParamHTML5Formatting:
assert expected in request.read()
def test_unicode_with_control_character(self):
- filename = "hello\x1A\x1B\x1C"
- expected = b'filename="hello%1A\x1B%1C"'
+ filename = "hello\x1a\x1b\x1c"
+ expected = b'filename="hello%1A\x1b%1C"'
files = {"upload": (filename, b"")}
request = httpx.Request("GET", "https://www.example.com", files=files)
assert expected in request.read()
diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py
index 09b25160..666cc8e3 100644
--- a/tests/test_timeouts.py
+++ b/tests/test_timeouts.py
@@ -42,3 +42,14 @@ async def test_pool_timeout(server):
with pytest.raises(httpx.PoolTimeout):
async with client.stream("GET", server.url):
await client.get(server.url)
+
+
+@pytest.mark.anyio
+async def test_async_client_new_request_send_timeout(server):
+ timeout = httpx.Timeout(1e-6)
+
+ async with httpx.AsyncClient(timeout=timeout) as client:
+ with pytest.raises(httpx.TimeoutException):
+ await client.send(
+ httpx.Request("GET", server.url.copy_with(path="/slow_response"))
+ )
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 5391f9c2..f98a18f2 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -11,10 +11,6 @@ from httpx._utils import (
URLPattern,
get_ca_bundle_from_env,
get_environment_proxies,
- is_https_redirect,
- obfuscate_sensitive_headers,
- parse_header_links,
- same_origin,
)
from .common import TESTS_DIR
@@ -81,7 +77,13 @@ def test_guess_by_bom(encoding, expected):
),
)
def test_parse_header_links(value, expected):
- assert parse_header_links(value) == expected
+ all_links = httpx.Response(200, headers={"link": value}).links.values()
+ assert all(link in all_links for link in expected)
+
+
+def test_parse_header_links_no_link():
+ all_links = httpx.Response(200).links
+ assert all_links == {}
def test_logging_request(server, caplog):
@@ -215,40 +217,65 @@ def test_get_environment_proxies(environment, proxies):
],
)
def test_obfuscate_sensitive_headers(headers, output):
- bytes_headers = [(k.encode(), v.encode()) for k, v in headers]
- bytes_output = [(k.encode(), v.encode()) for k, v in output]
- assert list(obfuscate_sensitive_headers(headers)) == output
- assert list(obfuscate_sensitive_headers(bytes_headers)) == bytes_output
+ as_dict = {k: v for k, v in output}
+ headers_class = httpx.Headers({k: v for k, v in headers})
+ assert repr(headers_class) == f"Headers({as_dict!r})"
def test_same_origin():
- origin1 = httpx.URL("https://example.com")
- origin2 = httpx.URL("HTTPS://EXAMPLE.COM:443")
- assert same_origin(origin1, origin2)
+ origin = httpx.URL("https://example.com")
+ request = httpx.Request("GET", "HTTPS://EXAMPLE.COM:443")
+
+ client = httpx.Client()
+ headers = client._redirect_headers(request, origin, "GET")
+
+ assert headers["Host"] == request.url.netloc.decode("ascii")
def test_not_same_origin():
- origin1 = httpx.URL("https://example.com")
- origin2 = httpx.URL("HTTP://EXAMPLE.COM")
- assert not same_origin(origin1, origin2)
+ origin = httpx.URL("https://example.com")
+ request = httpx.Request("GET", "HTTP://EXAMPLE.COM:80")
+
+ client = httpx.Client()
+ headers = client._redirect_headers(request, origin, "GET")
+
+ assert headers["Host"] == origin.netloc.decode("ascii")
def test_is_https_redirect():
- url = httpx.URL("http://example.com")
- location = httpx.URL("https://example.com")
- assert is_https_redirect(url, location)
+ url = httpx.URL("https://example.com")
+ request = httpx.Request(
+ "GET", "http://example.com", headers={"Authorization": "empty"}
+ )
+
+ client = httpx.Client()
+ headers = client._redirect_headers(request, url, "GET")
+
+ assert "Authorization" in headers
def test_is_not_https_redirect():
- url = httpx.URL("http://example.com")
- location = httpx.URL("https://www.example.com")
- assert not is_https_redirect(url, location)
+ url = httpx.URL("https://www.example.com")
+ request = httpx.Request(
+ "GET", "http://example.com", headers={"Authorization": "empty"}
+ )
+
+ client = httpx.Client()
+ headers = client._redirect_headers(request, url, "GET")
+
+ assert "Authorization" not in headers
def test_is_not_https_redirect_if_not_default_ports():
- url = httpx.URL("http://example.com:9999")
- location = httpx.URL("https://example.com:1337")
- assert not is_https_redirect(url, location)
+ url = httpx.URL("https://example.com:1337")
+ request = httpx.Request(
+ "GET", "http://example.com:9999", headers={"Authorization": "empty"}
+ )
+
+ client = httpx.Client()
+ headers = client._redirect_headers(request, url, "GET")
+
+ assert "Authorization" not in headers
@pytest.mark.parametrize(
diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py
index a952da6a..0134bee8 100644
--- a/tests/test_wsgi.py
+++ b/tests/test_wsgi.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import sys
import typing
import wsgiref.validate
@@ -12,7 +14,7 @@ if typing.TYPE_CHECKING: # pragma: no cover
from _typeshed.wsgi import StartResponse, WSGIApplication, WSGIEnvironment
-def application_factory(output: typing.Iterable[bytes]) -> "WSGIApplication":
+def application_factory(output: typing.Iterable[bytes]) -> WSGIApplication:
def application(environ, start_response):
status = "200 OK"
@@ -29,7 +31,7 @@ def application_factory(output: typing.Iterable[bytes]) -> "WSGIApplication":
def echo_body(
- environ: "WSGIEnvironment", start_response: "StartResponse"
+ environ: WSGIEnvironment, start_response: StartResponse
) -> typing.Iterable[bytes]:
status = "200 OK"
output = environ["wsgi.input"].read()
@@ -44,7 +46,7 @@ def echo_body(
def echo_body_with_response_stream(
- environ: "WSGIEnvironment", start_response: "StartResponse"
+ environ: WSGIEnvironment, start_response: StartResponse
) -> typing.Iterable[bytes]:
status = "200 OK"
@@ -63,9 +65,9 @@ def echo_body_with_response_stream(
def raise_exc(
- environ: "WSGIEnvironment",
- start_response: "StartResponse",
- exc: typing.Type[Exception] = ValueError,
+ environ: WSGIEnvironment,
+ start_response: StartResponse,
+ exc: type[Exception] = ValueError,
) -> typing.Iterable[bytes]:
status = "500 Server Error"
output = b"Nope!"
@@ -90,41 +92,47 @@ def log_to_wsgi_log_buffer(environ, start_response):
def test_wsgi():
- client = httpx.Client(app=application_factory([b"Hello, World!"]))
+ transport = httpx.WSGITransport(app=application_factory([b"Hello, World!"]))
+ client = httpx.Client(transport=transport)
response = client.get("http://www.example.org/")
assert response.status_code == 200
assert response.text == "Hello, World!"
def test_wsgi_upload():
- client = httpx.Client(app=echo_body)
+ transport = httpx.WSGITransport(app=echo_body)
+ client = httpx.Client(transport=transport)
response = client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert response.text == "example"
def test_wsgi_upload_with_response_stream():
- client = httpx.Client(app=echo_body_with_response_stream)
+ transport = httpx.WSGITransport(app=echo_body_with_response_stream)
+ client = httpx.Client(transport=transport)
response = client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert response.text == "example"
def test_wsgi_exc():
- client = httpx.Client(app=raise_exc)
+ transport = httpx.WSGITransport(app=raise_exc)
+ client = httpx.Client(transport=transport)
with pytest.raises(ValueError):
client.get("http://www.example.org/")
def test_wsgi_http_error():
- client = httpx.Client(app=partial(raise_exc, exc=RuntimeError))
+ transport = httpx.WSGITransport(app=partial(raise_exc, exc=RuntimeError))
+ client = httpx.Client(transport=transport)
with pytest.raises(RuntimeError):
client.get("http://www.example.org/")
def test_wsgi_generator():
output = [b"", b"", b"Some content", b" and more content"]
- client = httpx.Client(app=application_factory(output))
+ transport = httpx.WSGITransport(app=application_factory(output))
+ client = httpx.Client(transport=transport)
response = client.get("http://www.example.org/")
assert response.status_code == 200
assert response.text == "Some content and more content"
@@ -132,7 +140,8 @@ def test_wsgi_generator():
def test_wsgi_generator_empty():
output = [b"", b"", b"", b""]
- client = httpx.Client(app=application_factory(output))
+ transport = httpx.WSGITransport(app=application_factory(output))
+ client = httpx.Client(transport=transport)
response = client.get("http://www.example.org/")
assert response.status_code == 200
assert response.text == ""
@@ -161,14 +170,15 @@ def test_wsgi_server_port(url: str, expected_server_port: str) -> None:
SERVER_PORT is populated correctly from the requested URL.
"""
hello_world_app = application_factory([b"Hello, World!"])
- server_port: typing.Optional[str] = None
+ server_port: str | None = None
def app(environ, start_response):
nonlocal server_port
server_port = environ["SERVER_PORT"]
return hello_world_app(environ, start_response)
- client = httpx.Client(app=app)
+ transport = httpx.WSGITransport(app=app)
+ client = httpx.Client(transport=transport)
response = client.get(url)
assert response.status_code == 200
assert response.text == "Hello, World!"
@@ -184,9 +194,19 @@ def test_wsgi_server_protocol():
start_response("200 OK", [("Content-Type", "text/plain")])
return [b"success"]
- with httpx.Client(app=app, base_url="http://testserver") as client:
+ transport = httpx.WSGITransport(app=app)
+ with httpx.Client(transport=transport, base_url="http://testserver") as client:
response = client.get("/")
assert response.status_code == 200
assert response.text == "success"
assert server_protocol == "HTTP/1.1"
+
+
+def test_deprecated_shortcut():
+ """
+ The `app=...` shortcut is now deprecated.
+ Use the explicit transport style instead.
+ """
+ with pytest.warns(DeprecationWarning):
+ httpx.Client(app=application_factory([b"Hello, World!"]))