Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions 16-sync-http-clients/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Sync HTTP Clients Example

This example demonstrates outbound HTTP from a Python Worker using synchronous Python HTTP clients known to work in the current runtime:

- [`requests`](https://pypi.org/project/requests/)
- [`urllib3`](https://pypi.org/project/urllib3/)
- [`httpx.Client`](https://www.python-httpx.org/)

It intentionally calls their normal blocking-style APIs from inside the Worker handler.

## How to Run

First ensure that `uv` is installed:
https://docs.astral.sh/uv/getting-started/installation/#standalone-installer

Run:

```sh
uv run pywrangler dev
```

Then try:

```sh
curl http://localhost:8787/
curl http://localhost:8787/sync
```

You can also deploy with:

```sh
uv run pywrangler deploy
```

## Endpoints

| Endpoint | Description |
|---|---|
| `GET /` | Endpoint index |
| `GET /sync` | Fetch with `requests.get()`, `urllib3.PoolManager().request()`, and `httpx.Client` |
| `GET /all` | Alias for `/sync` |

## Notes

This example is limited to synchronous package-backed clients that work in Python Workers. It does not include stdlib raw-socket clients such as `urllib.request` or `http.client`.
13 changes: 13 additions & 0 deletions 16-sync-http-clients/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "python-sync-http-clients",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "uv run pywrangler deploy",
"dev": "uv run pywrangler dev",
"start": "uv run pywrangler dev"
},
"devDependencies": {
"wrangler": "^4.46.0"
}
}
18 changes: 18 additions & 0 deletions 16-sync-http-clients/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[project]
name = "python-sync-http-clients"
version = "0.1.0"
description = "Synchronous HTTP clients in Python Workers"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28.0",
"requests>=2.32.0",
"urllib3>=2.5.0",
"workers-runtime-sdk>=1.1.1",
]

[dependency-groups]
dev = [
"workers-py",
"workers-runtime-sdk"
]
76 changes: 76 additions & 0 deletions 16-sync-http-clients/src/entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import httpx
import requests
import urllib3
from workers import Response, WorkerEntrypoint

TARGET_URL = "https://example.com/"
EXPECTED_TEXT = "Example Domain"


def summarize_result(client, status_code, text, headers=None):
"""Return a small JSON-safe summary for an outbound HTTP response."""
headers = headers or {}
return {
"client": client,
"status_code": int(status_code),
"ok": 200 <= int(status_code) < 300,
"saw_expected_text": EXPECTED_TEXT in text,
"content_type": headers.get("content-type") or headers.get("Content-Type"),
"body_preview": text[:80],
}


class Default(WorkerEntrypoint):
async def fetch(self, request):
path = request.url.split("/", 3)[-1]
path = "/" + path.split("?", 1)[0] if path else "/"

if path == "/":
return Response.json(
{
"example": "Synchronous HTTP clients in Python Workers",
"target": TARGET_URL,
"clients": ["requests", "urllib3", "httpx.Client"],
"endpoints": {
"GET /sync": "Fetch using requests, urllib3, and httpx.Client",
"GET /all": "Alias for /sync",
},
}
)

if path in ("/sync", "/all"):
return Response.json({"target": TARGET_URL, "results": self.fetch_sync()})

return Response.json({"error": "not found"}, status=404)

def fetch_sync(self):
"""Use blocking-style Python HTTP clients from a Python Worker."""
requests_response = requests.get(TARGET_URL, timeout=10)

pool = urllib3.PoolManager()
urllib3_response = pool.request("GET", TARGET_URL, timeout=10.0)
urllib3_text = urllib3_response.data.decode("utf-8")

with httpx.Client() as client:
httpx_response = client.get(TARGET_URL, timeout=10.0)

return [
summarize_result(
"requests",
requests_response.status_code,
requests_response.text,
requests_response.headers,
),
summarize_result(
"urllib3",
urllib3_response.status,
urllib3_text,
urllib3_response.headers,
),
summarize_result(
"httpx.Client",
httpx_response.status_code,
httpx_response.text,
httpx_response.headers,
),
]
12 changes: 12 additions & 0 deletions 16-sync-http-clients/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "python-sync-http-clients",
"main": "src/entry.py",
"compatibility_date": "2025-11-02",
"compatibility_flags": [
"python_workers"
],
"observability": {
"enabled": true
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Need to deploy your Worker to Cloudflare? Python Workers are in open beta and ha
- [**`13-js-api-pygments/`**](13-js-api-pygments) — shows how to use [Pygments](https://pygments.org/) to highlight code with Python Workers.
- [**`14-websocket-stream-consumer/`**](14-websocket-stream-consumer) — shows how to use [WebSocket](https://developers.cloudflare.com/workers/runtime-apis/websockets/) to consume a stream of data with Python Workers.
- [**`15-chatroom/`**](15-chatroom) - A real-time chatroom using WebSocket.
- [**`16-sync-http-clients/`**](16-sync-http-clients) — demonstrates outbound HTTP with synchronous Python clients (`requests`, `urllib3`, and `httpx.Client`).



Expand Down
18 changes: 18 additions & 0 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,24 @@ def test_07_durable_objects(dev_server):
assert response.text == "No messages"


def test_16_sync_http_clients(dev_server):
port = dev_server
response = requests.get(f"http://localhost:{port}/sync")
assert response.status_code == 200

results = response.json()["results"]
assert [result["client"] for result in results] == [
"requests",
"urllib3",
"httpx.Client",
]

for result in results:
assert result["status_code"] == 200
assert result["ok"] is True
assert result["saw_expected_text"] is True


def test_08_cron(dev_server):
port = dev_server
response = requests.get(f"http://localhost:{port}")
Expand Down