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:

$ 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:

$ 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:

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:

$ 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:

$ 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:

$ 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:

with urllib3.PoolManager(ca_certs="root-ca.pem") as pool:
    response = pool.request("GET", "https://service.internal/")
$ 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:

$ 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:

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:

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:

$ 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():

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:

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.