WASI
====
`WebAssembly System Interface (WASI) `_ lets an application
run in a sandbox with only the capabilities granted by its host. urllib3.future
supports HTTP from componentized Python applications, including synchronous
WASI Preview 2 sockets and asynchronous WASI Preview 3 sockets.
This guide builds a small command component and runs it with
`Wasmtime `_. The resulting ``.wasm`` file contains the
Python runtime, your application, and its Python dependencies. It does not get
network or filesystem access unless the host grants those capabilities.
Prerequisites
-------------
Install Python 3.10 or later, Wasmtime 47 or later, and create an isolated
Python environment:
.. code-block:: bash
$ python -m venv .venv
$ . .venv/bin/activate
$ python -m pip install urllib3.future "componentize-py==0.25.0"
Follow the `Wasmtime installation guide
`_ if the ``wasmtime`` command is
not already available.
``componentize-py`` needs the WIT definitions for the world being targeted.
Version 0.25.0 does not include those definitions in its wheel, so download the
matching source tree:
.. code-block:: bash
$ git clone --depth 1 --branch v0.25.0 \
https://github.com/bytecodealliance/componentize-py.git
$ export COMPONENTIZE_WIT="$PWD/componentize-py/wit"
Your First Sandboxed Request
----------------------------
Create ``app.py``:
.. code-block:: python
import urllib3
from wit_world import exports
class Run(exports.Run):
def run(self) -> None:
with urllib3.PoolManager() as pool:
response = pool.request("GET", "http://example.com/")
print(response.status, response.version)
``wit_world`` is generated by componentize-py from the selected WIT world. It
defines the Python side of the component's imports and exports. Here,
``exports.Run`` implements the ``wasi:cli/run`` interface used by a command.
Build the application against the synchronous Preview 2 command world:
.. code-block:: bash
$ componentize-py \
-d "$COMPONENTIZE_WIT" \
-w wasi:cli/command@0.2.0 \
componentize -p . app -o app.wasm
Run it with network access and DNS explicitly enabled:
.. code-block:: bash
$ wasmtime run \
-Sinherit-network \
-Sallow-ip-name-lookup=y \
app.wasm
200 11
The exact status and HTTP version depend on the server. The important part is
that the component can only open sockets because the host invocation granted
network access. Remove ``-Sinherit-network`` and the sandbox cannot connect;
remove ``-Sallow-ip-name-lookup=y`` and hostname lookup is denied.
HTTPS
-----
CPython's standard ``ssl`` extension is not available in a WASI component.
Install the optional ``rtls`` backend to add TLS support:
.. code-block:: bash
$ python -m pip install "urllib3.future[rtls]"
Change the URL in ``app.py`` to ``https://example.com/``, then componentize the
application again. Dependencies are embedded at build time, so installing
``rtls`` after creating ``app.wasm`` does not modify an existing component.
Certificate verification remains enabled. Public certificate authorities are
loaded by the TLS backend. To use a private CA, pass a path visible inside the
sandbox and preopen its host directory:
.. code-block:: python
with urllib3.PoolManager(ca_certs="root-ca.pem") as pool:
response = pool.request("GET", "https://service.internal/")
.. code-block:: bash
$ wasmtime run \
-Sinherit-network \
-Sallow-ip-name-lookup=y \
--dir ./certificates::certificates \
app.wasm
Plain HTTP does not require ``rtls``. An HTTPS request from a component without
a TLS backend raises an ``ImportError`` explaining that the SSL module is not
available.
Supported Features
------------------
WASI applications use the same urllib3 APIs as native Python applications. The
WASI socket facades support more than basic GET requests:
* HTTP/1.1 and HTTP/2, including persistent connection pools, concurrent
requests, and explicit HTTP/2 response promises.
* HTTPS through ``rtls``, including certificate verification, private CAs,
mutual TLS, hostname controls, and TLS version selection.
* HTTP, HTTPS, and SOCKS5 proxies. HTTPS destinations through an HTTPS proxy use
TLS-in-TLS and require ``rtls``. Install the ``socks`` extra for SOCKS proxy
support.
* Custom DNS resolvers, including the system resolver, DNS over UDP, DNS over
TLS (DoT), and DNS over HTTPS (DoH). Resolver configuration is identical to
native urllib3 usage. Encrypted DNS transports require ``rtls``.
* WebSocket connections and Server-Sent Events (SSE). Install the ``ws`` extra
when using WebSockets.
* Request streaming, byte and file-like request bodies, response streaming,
redirects, retries, and the usual HTTP methods.
* Connection inspection through ``ConnectionInfo``, including the negotiated
HTTP version, TLS details, and peer information.
For an application using TLS, SOCKS, and WebSockets, install the optional
dependencies before componentizing it:
.. code-block:: bash
$ python -m pip install "urllib3.future[rtls,socks,ws]"
Optional dependencies must be available when the component is built. Rebuild
the ``.wasm`` file after adding an extra or changing resolver and protocol
dependencies.
WIT, Sync, and Async
--------------------
WIT (WebAssembly Interface Types) describes the boundary between a component
and its host. A *world* is the complete set of interfaces a component imports
and exports. Selecting the right world matters because Preview 2 and Preview 3
socket resources are different types, even when they represent the same TCP
connection.
Use the following combinations:
.. list-table::
:header-rows: 1
* - urllib3 API
- WIT world
- Wasmtime options
* - ``PoolManager``
- ``wasi:cli/command@0.2.0``
- ``-Sinherit-network -Sallow-ip-name-lookup=y``
* - ``AsyncPoolManager``
- ``wasi:cli/command@0.3.0``
- ``-Sp3 -Wcomponent-model-async -Sinherit-network -Sallow-ip-name-lookup=y``
Synchronous Preview 1 Fallback
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The synchronous API can also operate when a component world does not import
the Preview 2 socket interfaces. In that situation, urllib3 falls back to
CPython's legacy WASI Preview 1 socket adapter. Plain HTTP and HTTPS with
``rtls`` remain available, provided the host grants network access.
This fallback exists for compatibility and is not recommended for new
components. Preview 1 exposes a less explicit, legacy interface and does not
provide the resource-oriented socket model used by the WebAssembly Component
Model. Prefer ``wasi:cli/command@0.2.0`` with its Preview 2 socket imports for
synchronous applications. The asynchronous API requires Preview 3 sockets and
does not have a Preview 1 fallback.
For an asynchronous component, change ``app.py`` to:
.. code-block:: python
import urllib3
from wit_world import exports
class Run(exports.Run):
async def run(self) -> None:
async with urllib3.AsyncPoolManager() as pool:
response = await pool.request("GET", "https://example.com/")
print(response.status, response.version)
print((await response.data)[:80])
Build and run it using Preview 3:
.. code-block:: bash
$ componentize-py \
-d "$COMPONENTIZE_WIT" \
-w wasi:cli/command@0.3.0 \
componentize -p . app -o app-async.wasm
$ wasmtime run \
-Sp3 \
-Wcomponent-model-async \
-Sinherit-network \
-Sallow-ip-name-lookup=y \
app-async.wasm
If one component needs both APIs, define one custom world that includes the
Preview 3 command world and imports the required Preview 2 socket interfaces.
Do not treat Preview 2 and Preview 3 resources as interchangeable.
Why componentize-py?
--------------------
A Python file is not itself a WebAssembly component. componentize-py performs
the integration work needed to run it as one:
* It embeds a WASI-compatible Python runtime and the discovered dependencies.
* It generates Python bindings for the selected WIT world.
* It connects Python implementations such as ``exports.Run`` to component
exports.
* It connects socket, clock, filesystem, random, and other Python operations to
capabilities imported from the host.
This is why the build needs both the Python module name (``app``) and a WIT
world. The world is the sandbox contract; Wasmtime decides which requested
capabilities are actually granted when the component starts.
Lazy Imports Must Be Avoided
----------------------------
componentize-py discovers and embeds Python dependencies while building the
component. It cannot fetch a missing module after the component has started.
Code intended for WASI must therefore give up lazy imports for dependencies
that will be needed at runtime: import them at module level before
componentizing the application.
For example, do not defer an optional implementation until ``run()``:
.. code-block:: python
class Run(exports.Run):
def run(self) -> None:
import my_optional_transport
my_optional_transport.send()
Make the dependency visible when the application module is loaded instead:
.. code-block:: python
import my_optional_transport
class Run(exports.Run):
def run(self) -> None:
my_optional_transport.send()
The same rule applies to plugin systems that construct module names and load
them through ``importlib``. Import each implementation that the component may
use at module level. This can feel unusual in Python applications that normally
optimize startup through lazy imports, but a component has a closed dependency
set and cannot discover arbitrary host packages at runtime.
urllib3 accounts for this limitation on WASI by eagerly loading its built-in
HTTP protocol, resolver, and web-extension implementations. Applications do
not need private urllib3 preload imports. They do need to expose their own
optional or third-party implementations to componentize-py and rebuild the
component whenever that dependency set changes.
Known Caveats
-------------
* WASI Preview 3 and component-model async support are still evolving. The
command-line flags shown here are for Wasmtime 47 and may change in later
runtimes.
* Async operation cancellation is not yet supported by componentize-py 0.25.0.
Consequently, urllib3 read and connect timeouts are not enforced by the
native Preview 3 socket facade.
* Lazy imports and runtime plugin discovery require explicit module-level
imports, as described above.
* Network access, DNS, and filesystem paths are denied unless the host grants
them. This is expected sandbox behavior, not an urllib3 connection failure.
* HTTPS requires a WASI-compatible TLS backend. Install the ``rtls`` extra and
rebuild the component; plain HTTP remains available without it. The rtls
WASI extension currently requires CPython.
* HTTP/1.1 and HTTP/2 are supported. HTTP/3 requires a WASI-compatible ``qh3``
build and is not included in this getting-started setup.
* Preview 2 and Preview 3 socket resources cannot be passed to one another.
Use the matching urllib3 API and WIT world, or deliberately compose both sets
of imports in a custom world.
For production deployments, pin componentize-py and Wasmtime together, review
the component's WIT imports, and grant only the network and filesystem access
the application needs.