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
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions examples/abort-signal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "abort-signal-on-workers"
version = "0.1.0"
edition = "2021"

[package.metadata.release]
release = false

[lib]
crate-type = ["cdylib"]

[dependencies]
worker.workspace = true
futures-util = { workspace = true, features = ["async-await"] }
53 changes: 53 additions & 0 deletions examples/abort-signal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# AbortSignal example

Cancel in-flight fetch requests using `AbortController` and `AbortSignal` in a Rust Cloudflare Worker.

## Routes

| Route | Description |
|---|---|
| `GET /abort?url=<url>` | Fetches the URL and immediately aborts. Always returns an abort error. |
| `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. |

## Local slow server

A slow Node server is included for testing timeouts locally:

```sh
node slow-server.mjs # port 3000, 5s delay
node slow-server.mjs 9000 10 # port 9000, 10s delay
```

The server has two endpoints:
- `GET /` returns a response after the default delay
- `GET /delay/:ms` returns a response after `:ms` milliseconds

## Testing

1. Start the slow server on a port (e.g. 3000):
```sh
node slow-server.mjs 3000
```

2. Start the Worker (in the `abort-signal` example directory):
```sh
npx wrangler dev
```

3. Test immediate abort:
```sh
curl "http://localhost:8787/abort?url=http://localhost:3000"
# → "Aborted: ..."
```

4. Test timeout (500ms timeout against a 5s delayed server):
```sh
curl "http://localhost:8787/timeout?url=http://localhost:3000&timeout=500"
# → "Request timed out after 500ms"
```

5. Test timeout where the server responds in time (using `/delay/100` for 100ms):
```sh
curl "http://localhost:8787/timeout?url=http://localhost:3000/delay/100&timeout=2000"
# → "Got response: {\"delayed_ms\":100,\"message\":\"slow response\"}"
```
37 changes: 37 additions & 0 deletions examples/abort-signal/slow-server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// A minimal HTTP server that responds after a configurable delay.
// Used to test AbortSignal timeouts against a real slow endpoint.
//
// Usage:
// node slow-server.mjs # default 5s delay on port 3000
// node slow-server.mjs 9000 10 # port 9000, 10s delay
//
// Endpoints:
// GET / → responds after <delay> seconds
// GET /delay/:ms → responds after :ms milliseconds

import { createServer } from "node:http";

const PORT = parseInt(process.argv[2] || "3000", 10);
const DEFAULT_DELAY_S = parseInt(process.argv[3] || "5", 10);

const server = createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`);
const match = url.pathname.match(/^\/delay\/(\d+)$/);
const delayMs = match
? parseInt(match[1], 10)
: DEFAULT_DELAY_S * 1000;

console.log(`${req.method} ${url.pathname} → will respond in ${delayMs}ms`);

const timer = setTimeout(() => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ delayed_ms: delayMs, message: "slow response" }));
}, delayMs);

req.on("close", () => clearTimeout(timer));
});

server.listen(PORT, () => {
console.log(`Slow server listening on http://localhost:${PORT}`);
console.log(`Default delay: ${DEFAULT_DELAY_S}s`);
});
76 changes: 76 additions & 0 deletions examples/abort-signal/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::time::Duration;

use futures_util::future::Either;
use worker::{
event, AbortController, AbortSignal, Context, Delay, Env, Fetch, Request, Response, Result,
RouteContext, Router,
};

fn get_target_url(req: &Request) -> Result<String> {
req.url()?
.query_pairs()
.find(|(k, _)| k == "url")
.map(|(_, v)| v.into_owned())
.ok_or_else(|| worker::Error::RustError("Missing 'url' query param".into()))
}

async fn abort_immediate(req: Request, _ctx: RouteContext<()>) -> Result<Response> {
let target = get_target_url(&req)?;

let signal = AbortSignal::abort();
let fetch = Fetch::Url(target.parse()?);

match fetch.send_with_signal(&signal).await {
Ok(mut resp) => {
let text = resp.text().await?;
Response::ok(format!("Unexpected success: {text}"))
}
Err(e) => Response::ok(format!("Aborted: {e}")),
}
}

async fn abort_timeout(req: Request, _ctx: RouteContext<()>) -> Result<Response> {
let target = get_target_url(&req)?;

let timeout_ms: u64 = req
.url()?
.query_pairs()
.find(|(k, _)| k == "timeout")
.and_then(|(_, v)| v.parse().ok())
.unwrap_or(2000);

let url = target.parse()?;
let controller = AbortController::default();
let signal = controller.signal();

let fetch_fut = async {
let mut resp = Fetch::Url(url).send_with_signal(&signal).await?;
let text = resp.text().await?;
Ok::<_, worker::Error>(text)
};

let timeout_fut = async {
Delay::from(Duration::from_millis(timeout_ms)).await;
controller.abort();
};

futures_util::pin_mut!(fetch_fut);
futures_util::pin_mut!(timeout_fut);

match futures_util::future::select(timeout_fut, fetch_fut).await {
Either::Left((_timed_out, _cancelled)) => {
Response::ok(format!("Request timed out after {timeout_ms}ms"))
}
Either::Right((Ok(body), _)) => Response::ok(format!("Got response: {body}")),
Either::Right((Err(e), _)) => Response::ok(format!("Fetch error: {e}")),
}
}

#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
Router::new()
.get_async("/abort", abort_immediate)
.get_async("/timeout", abort_timeout)
.run(req, env)
.await
}
6 changes: 6 additions & 0 deletions examples/abort-signal/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name = "abort-signal-on-workers"
main = "build/worker/shim.mjs"
compatibility_date = "2026-04-28"

[build]
command = "cargo install \"worker-build@^0.8\" && worker-build --release"
Loading