Skip to content

Commit f9ee5bf

Browse files
authored
Add sync HTTP clients example (#79)
1 parent 15545c5 commit f9ee5bf

7 files changed

Lines changed: 183 additions & 0 deletions

File tree

16-sync-http-clients/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Sync HTTP Clients Example
2+
3+
This example demonstrates outbound HTTP from a Python Worker using synchronous Python HTTP clients known to work in the current runtime:
4+
5+
- [`requests`](https://pypi.org/project/requests/)
6+
- [`urllib3`](https://pypi.org/project/urllib3/)
7+
- [`httpx.Client`](https://www.python-httpx.org/)
8+
9+
It intentionally calls their normal blocking-style APIs from inside the Worker handler.
10+
11+
## How to Run
12+
13+
First ensure that `uv` is installed:
14+
https://docs.astral.sh/uv/getting-started/installation/#standalone-installer
15+
16+
Run:
17+
18+
```sh
19+
uv run pywrangler dev
20+
```
21+
22+
Then try:
23+
24+
```sh
25+
curl http://localhost:8787/
26+
curl http://localhost:8787/sync
27+
```
28+
29+
You can also deploy with:
30+
31+
```sh
32+
uv run pywrangler deploy
33+
```
34+
35+
## Endpoints
36+
37+
| Endpoint | Description |
38+
|---|---|
39+
| `GET /` | Endpoint index |
40+
| `GET /sync` | Fetch with `requests.get()`, `urllib3.PoolManager().request()`, and `httpx.Client` |
41+
| `GET /all` | Alias for `/sync` |
42+
43+
## Notes
44+
45+
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`.

16-sync-http-clients/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "python-sync-http-clients",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"deploy": "uv run pywrangler deploy",
7+
"dev": "uv run pywrangler dev",
8+
"start": "uv run pywrangler dev"
9+
},
10+
"devDependencies": {
11+
"wrangler": "^4.46.0"
12+
}
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[project]
2+
name = "python-sync-http-clients"
3+
version = "0.1.0"
4+
description = "Synchronous HTTP clients in Python Workers"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = [
8+
"httpx>=0.28.0",
9+
"requests>=2.32.0",
10+
"urllib3>=2.5.0",
11+
"workers-runtime-sdk>=1.1.1",
12+
]
13+
14+
[dependency-groups]
15+
dev = [
16+
"workers-py",
17+
"workers-runtime-sdk"
18+
]

16-sync-http-clients/src/entry.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import httpx
2+
import requests
3+
import urllib3
4+
from workers import Response, WorkerEntrypoint
5+
6+
TARGET_URL = "https://example.com/"
7+
EXPECTED_TEXT = "Example Domain"
8+
9+
10+
def summarize_result(client, status_code, text, headers=None):
11+
"""Return a small JSON-safe summary for an outbound HTTP response."""
12+
headers = headers or {}
13+
return {
14+
"client": client,
15+
"status_code": int(status_code),
16+
"ok": 200 <= int(status_code) < 300,
17+
"saw_expected_text": EXPECTED_TEXT in text,
18+
"content_type": headers.get("content-type") or headers.get("Content-Type"),
19+
"body_preview": text[:80],
20+
}
21+
22+
23+
class Default(WorkerEntrypoint):
24+
async def fetch(self, request):
25+
path = request.url.split("/", 3)[-1]
26+
path = "/" + path.split("?", 1)[0] if path else "/"
27+
28+
if path == "/":
29+
return Response.json(
30+
{
31+
"example": "Synchronous HTTP clients in Python Workers",
32+
"target": TARGET_URL,
33+
"clients": ["requests", "urllib3", "httpx.Client"],
34+
"endpoints": {
35+
"GET /sync": "Fetch using requests, urllib3, and httpx.Client",
36+
"GET /all": "Alias for /sync",
37+
},
38+
}
39+
)
40+
41+
if path in ("/sync", "/all"):
42+
return Response.json({"target": TARGET_URL, "results": self.fetch_sync()})
43+
44+
return Response.json({"error": "not found"}, status=404)
45+
46+
def fetch_sync(self):
47+
"""Use blocking-style Python HTTP clients from a Python Worker."""
48+
requests_response = requests.get(TARGET_URL, timeout=10)
49+
50+
pool = urllib3.PoolManager()
51+
urllib3_response = pool.request("GET", TARGET_URL, timeout=10.0)
52+
urllib3_text = urllib3_response.data.decode("utf-8")
53+
54+
with httpx.Client() as client:
55+
httpx_response = client.get(TARGET_URL, timeout=10.0)
56+
57+
return [
58+
summarize_result(
59+
"requests",
60+
requests_response.status_code,
61+
requests_response.text,
62+
requests_response.headers,
63+
),
64+
summarize_result(
65+
"urllib3",
66+
urllib3_response.status,
67+
urllib3_text,
68+
urllib3_response.headers,
69+
),
70+
summarize_result(
71+
"httpx.Client",
72+
httpx_response.status_code,
73+
httpx_response.text,
74+
httpx_response.headers,
75+
),
76+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "node_modules/wrangler/config-schema.json",
3+
"name": "python-sync-http-clients",
4+
"main": "src/entry.py",
5+
"compatibility_date": "2025-11-02",
6+
"compatibility_flags": [
7+
"python_workers"
8+
],
9+
"observability": {
10+
"enabled": true
11+
}
12+
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Need to deploy your Worker to Cloudflare? Python Workers are in open beta and ha
3030
- [**`13-js-api-pygments/`**](13-js-api-pygments) — shows how to use [Pygments](https://pygments.org/) to highlight code with Python Workers.
3131
- [**`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.
3232
- [**`15-chatroom/`**](15-chatroom) - A real-time chatroom using WebSocket.
33+
- [**`16-sync-http-clients/`**](16-sync-http-clients) — demonstrates outbound HTTP with synchronous Python clients (`requests`, `urllib3`, and `httpx.Client`).
3334

3435

3536

tests/test_examples.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,24 @@ def test_07_durable_objects(dev_server):
110110
assert response.text == "No messages"
111111

112112

113+
def test_16_sync_http_clients(dev_server):
114+
port = dev_server
115+
response = requests.get(f"http://localhost:{port}/sync")
116+
assert response.status_code == 200
117+
118+
results = response.json()["results"]
119+
assert [result["client"] for result in results] == [
120+
"requests",
121+
"urllib3",
122+
"httpx.Client",
123+
]
124+
125+
for result in results:
126+
assert result["status_code"] == 200
127+
assert result["ok"] is True
128+
assert result["saw_expected_text"] is True
129+
130+
113131
def test_08_cron(dev_server):
114132
port = dev_server
115133
response = requests.get(f"http://localhost:{port}")

0 commit comments

Comments
 (0)