Skip to content

Commit c4b5cc0

Browse files
authored
Add basic abortsignal example (#992)
Showing how to apply timeouts to fetch requests
1 parent 36d2e37 commit c4b5cc0

6 files changed

Lines changed: 194 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/abort-signal/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "abort-signal-on-workers"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[package.metadata.release]
7+
release = false
8+
9+
[lib]
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
worker.workspace = true
14+
futures-util = { workspace = true, features = ["async-await"] }

examples/abort-signal/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# AbortSignal example
2+
3+
Cancel in-flight fetch requests using `AbortController` and `AbortSignal` in a Rust Cloudflare Worker.
4+
5+
## Routes
6+
7+
| Route | Description |
8+
|---|---|
9+
| `GET /abort?url=<url>` | Fetches the URL and immediately aborts. Always returns an abort error. |
10+
| `GET /timeout?url=<url>&timeout=<ms>` | Fetches the URL with a timeout (default 2000ms). Cancels the request if the server doesn't respond in time. |
11+
12+
## Local slow server
13+
14+
A slow Node server is included for testing timeouts locally:
15+
16+
```sh
17+
node slow-server.mjs # port 3000, 5s delay
18+
node slow-server.mjs 9000 10 # port 9000, 10s delay
19+
```
20+
21+
The server has two endpoints:
22+
- `GET /` returns a response after the default delay
23+
- `GET /delay/:ms` returns a response after `:ms` milliseconds
24+
25+
## Testing
26+
27+
1. Start the slow server on a port (e.g. 3000):
28+
```sh
29+
node slow-server.mjs 3000
30+
```
31+
32+
2. Start the Worker (in the `abort-signal` example directory):
33+
```sh
34+
npx wrangler dev
35+
```
36+
37+
3. Test immediate abort:
38+
```sh
39+
curl "http://localhost:8787/abort?url=http://localhost:3000"
40+
# → "Aborted: ..."
41+
```
42+
43+
4. Test timeout (500ms timeout against a 5s delayed server):
44+
```sh
45+
curl "http://localhost:8787/timeout?url=http://localhost:3000&timeout=500"
46+
# → "Request timed out after 500ms"
47+
```
48+
49+
5. Test timeout where the server responds in time (using `/delay/100` for 100ms):
50+
```sh
51+
curl "http://localhost:8787/timeout?url=http://localhost:3000/delay/100&timeout=2000"
52+
# → "Got response: {\"delayed_ms\":100,\"message\":\"slow response\"}"
53+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// A minimal HTTP server that responds after a configurable delay.
2+
// Used to test AbortSignal timeouts against a real slow endpoint.
3+
//
4+
// Usage:
5+
// node slow-server.mjs # default 5s delay on port 3000
6+
// node slow-server.mjs 9000 10 # port 9000, 10s delay
7+
//
8+
// Endpoints:
9+
// GET / → responds after <delay> seconds
10+
// GET /delay/:ms → responds after :ms milliseconds
11+
12+
import { createServer } from "node:http";
13+
14+
const PORT = parseInt(process.argv[2] || "3000", 10);
15+
const DEFAULT_DELAY_S = parseInt(process.argv[3] || "5", 10);
16+
17+
const server = createServer((req, res) => {
18+
const url = new URL(req.url, `http://localhost:${PORT}`);
19+
const match = url.pathname.match(/^\/delay\/(\d+)$/);
20+
const delayMs = match
21+
? parseInt(match[1], 10)
22+
: DEFAULT_DELAY_S * 1000;
23+
24+
console.log(`${req.method} ${url.pathname} → will respond in ${delayMs}ms`);
25+
26+
const timer = setTimeout(() => {
27+
res.writeHead(200, { "Content-Type": "application/json" });
28+
res.end(JSON.stringify({ delayed_ms: delayMs, message: "slow response" }));
29+
}, delayMs);
30+
31+
req.on("close", () => clearTimeout(timer));
32+
});
33+
34+
server.listen(PORT, () => {
35+
console.log(`Slow server listening on http://localhost:${PORT}`);
36+
console.log(`Default delay: ${DEFAULT_DELAY_S}s`);
37+
});

examples/abort-signal/src/lib.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use std::time::Duration;
2+
3+
use futures_util::future::Either;
4+
use worker::{
5+
event, AbortController, AbortSignal, Context, Delay, Env, Fetch, Request, Response, Result,
6+
RouteContext, Router,
7+
};
8+
9+
fn get_target_url(req: &Request) -> Result<String> {
10+
req.url()?
11+
.query_pairs()
12+
.find(|(k, _)| k == "url")
13+
.map(|(_, v)| v.into_owned())
14+
.ok_or_else(|| worker::Error::RustError("Missing 'url' query param".into()))
15+
}
16+
17+
async fn abort_immediate(req: Request, _ctx: RouteContext<()>) -> Result<Response> {
18+
let target = get_target_url(&req)?;
19+
20+
let signal = AbortSignal::abort();
21+
let fetch = Fetch::Url(target.parse()?);
22+
23+
match fetch.send_with_signal(&signal).await {
24+
Ok(mut resp) => {
25+
let text = resp.text().await?;
26+
Response::ok(format!("Unexpected success: {text}"))
27+
}
28+
Err(e) => Response::ok(format!("Aborted: {e}")),
29+
}
30+
}
31+
32+
async fn abort_timeout(req: Request, _ctx: RouteContext<()>) -> Result<Response> {
33+
let target = get_target_url(&req)?;
34+
35+
let timeout_ms: u64 = req
36+
.url()?
37+
.query_pairs()
38+
.find(|(k, _)| k == "timeout")
39+
.and_then(|(_, v)| v.parse().ok())
40+
.unwrap_or(2000);
41+
42+
let url = target.parse()?;
43+
let controller = AbortController::default();
44+
let signal = controller.signal();
45+
46+
let fetch_fut = async {
47+
let mut resp = Fetch::Url(url).send_with_signal(&signal).await?;
48+
let text = resp.text().await?;
49+
Ok::<_, worker::Error>(text)
50+
};
51+
52+
let timeout_fut = async {
53+
Delay::from(Duration::from_millis(timeout_ms)).await;
54+
controller.abort();
55+
};
56+
57+
futures_util::pin_mut!(fetch_fut);
58+
futures_util::pin_mut!(timeout_fut);
59+
60+
match futures_util::future::select(timeout_fut, fetch_fut).await {
61+
Either::Left((_timed_out, _cancelled)) => {
62+
Response::ok(format!("Request timed out after {timeout_ms}ms"))
63+
}
64+
Either::Right((Ok(body), _)) => Response::ok(format!("Got response: {body}")),
65+
Either::Right((Err(e), _)) => Response::ok(format!("Fetch error: {e}")),
66+
}
67+
}
68+
69+
#[event(fetch)]
70+
pub async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
71+
Router::new()
72+
.get_async("/abort", abort_immediate)
73+
.get_async("/timeout", abort_timeout)
74+
.run(req, env)
75+
.await
76+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name = "abort-signal-on-workers"
2+
main = "build/worker/shim.mjs"
3+
compatibility_date = "2026-04-28"
4+
5+
[build]
6+
command = "cargo install \"worker-build@^0.8\" && worker-build --release"

0 commit comments

Comments
 (0)