Skip to content

Commit 91d933b

Browse files
authored
Merge pull request #159 from cloudflare/gv/egrss
experimental outbound interception
2 parents 9885a4b + b7a06f4 commit 91d933b

13 files changed

Lines changed: 1634 additions & 17 deletions

File tree

README.md

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ A class for interacting with Containers on Cloudflare Workers.
55
## Features
66

77
- HTTP request proxying and WebSocket forwarding
8+
- Outbound request interception by host or catch-all handler
89
- Simple container lifecycle management (starting and stopping containers)
910
- Event hooks for container lifecycle events (onStart, onStop, onError)
1011
- Configurable sleep timeout that renews on requests
@@ -110,7 +111,6 @@ See [this example](#http-example-with-lifecycle-hooks).
110111
- `onStart()`
111112

112113
Called when container starts successfully.
113-
114114
- called when states transition from `stopped` -> `running`, `running` -> `healthy`
115115

116116
- `onStop()`
@@ -150,7 +150,6 @@ See [this example](#http-example-with-lifecycle-hooks).
150150
Note: `containerFetch` does not work with websockets.
151151

152152
Sends an HTTP request to the container. Supports both standard fetch API signatures:
153-
154153
- `containerFetch(request, port?)`: Traditional signature with Request object
155154
- `containerFetch(url, init?, port?)`: Standard fetch-like signature with URL string/object and RequestInit options
156155

@@ -188,7 +187,6 @@ See [this example](#http-example-with-lifecycle-hooks).
188187
Starts the container, without waiting for any ports to be ready.
189188

190189
You might want to use this instead of `startAndWaitForPorts` if you want to:
191-
192190
- Start a container without blocking until a port is available
193191
- Initialize a container that doesn't expose ports
194192
- Perform custom port availability checks separately
@@ -249,10 +247,52 @@ See [this example](#http-example-with-lifecycle-hooks).
249247

250248
Manually renews the container activity timeout (extends container lifetime).
251249

250+
##### Outbound Interception
251+
252+
Use outbound interception when you want to control what the container can reach, or proxy outbound requests through Worker code.
253+
254+
- `setOutboundByHost(host: string, method: string): Promise<void>`
255+
256+
Routes a specific hostname to a named handler from `static outboundHandlers`.
257+
258+
- `setOutboundByHosts(handlers: Record<string, string>): Promise<void>`
259+
260+
Replaces all runtime host-specific overrides at once.
261+
262+
- `removeOutboundByHost(host: string): Promise<void>`
263+
264+
Removes a runtime host-specific override.
265+
266+
- `setOutboundHandler(method: string): Promise<void>`
267+
268+
Sets the catch-all outbound handler to a named handler from `static outboundHandlers`.
269+
270+
To configure interception on the class itself:
271+
272+
- `static outbound = (req, env, ctx) => Response`
273+
274+
Catch-all handler for outbound requests.
275+
276+
- `static outboundByHost = { [host]: handler }`
277+
278+
Per-host handlers for exact hostname matches such as `google.com` or an IP address.
279+
280+
- `static outboundHandlers = { [name]: handler }`
281+
282+
Named handlers that can be selected at runtime with `setOutboundHandler` and `setOutboundByHost`.
283+
284+
Matching order is:
285+
286+
1. Runtime `setOutboundByHost` override
287+
2. Static `outboundByHost`
288+
3. Runtime `setOutboundHandler` catch-all
289+
4. Static `outbound`
290+
291+
If nothing matches, the request goes out normally when `enableInternet` is `true`, and is blocked when `enableInternet` is `false`.
292+
252293
- `schedule<T = string>(when: Date | number, callback: string, payload?: T): Promise<Schedule<T>>`
253294

254295
Options:
255-
256296
- `when`: When to execute the task (Date object or number of seconds delay)
257297
- `callback`: Name of the function to call as a string
258298
- `payload`: Data to pass to the callback
@@ -371,6 +411,45 @@ export class ConfiguredContainer extends Container {
371411

372412
You can also set these on a per-instance basis with `start` or `startAndWaitForPorts`
373413

414+
### Outbound Interception
415+
416+
This lets you intercept requests the container makes to the outside world.
417+
418+
```typescript
419+
import { Container, OutboundHandlerContext } from '@cloudflare/containers';
420+
421+
export class MyContainer extends Container {
422+
defaultPort = 8080;
423+
enableInternet = false;
424+
425+
static outboundByHost = {
426+
'google.com': (_req: Request, _env: unknown, ctx: OutboundHandlerContext) => {
427+
return new Response('hi ' + ctx.containerId + ' i am google');
428+
},
429+
};
430+
431+
static outboundHandlers = {
432+
async github(_req: Request, _env: unknown, _ctx: OutboundHandlerContext) {
433+
return new Response('i am github');
434+
},
435+
};
436+
437+
static outbound = (req: Request) => {
438+
return new Response(`Hi ${req.url}, I can't handle you`);
439+
};
440+
441+
async routeGithubThroughHandler(): Promise<void> {
442+
await this.setOutboundByHost('github.com', 'github');
443+
}
444+
445+
async makeEverythingUseGithubHandler(): Promise<void> {
446+
await this.setOutboundHandler('github');
447+
}
448+
}
449+
```
450+
451+
Use `outboundByHost` for fixed host rules, `outbound` for a default catch-all, and `outboundHandlers` for reusable named handlers you want to switch on at runtime.
452+
374453
### Multiple Ports and Custom Routing
375454

376455
You can create a container that doesn't use a default port and instead routes traffic to different ports based on request path or other factors:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM golang:1.24-alpine AS builder
2+
WORKDIR /src
3+
COPY container_src/main.go ./main.go
4+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /server ./main.go
5+
6+
FROM scratch
7+
COPY --from=builder /server /server
8+
EXPOSE 8080
9+
CMD ["/server"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module server
2+
3+
go 1.23.2
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
"os"
10+
"os/signal"
11+
"syscall"
12+
"time"
13+
)
14+
15+
func handler(w http.ResponseWriter, r *http.Request) {
16+
message := os.Getenv("MESSAGE")
17+
deploymentId := os.Getenv("CLOUDFLARE_DEPLOYMENT_ID")
18+
19+
if p := r.URL.Query().Get("proxy"); p != "" {
20+
res, err := http.Get("http://" + p)
21+
if err != nil {
22+
w.WriteHeader(520)
23+
io.WriteString(w, "error connecting to proxy: "+err.Error())
24+
return
25+
}
26+
27+
w.WriteHeader(res.StatusCode)
28+
body, err := io.ReadAll(res.Body)
29+
if err != nil {
30+
w.WriteHeader(520)
31+
io.WriteString(w, "error connecting to proxy: "+err.Error())
32+
return
33+
}
34+
35+
w.Write(body)
36+
return
37+
}
38+
39+
fmt.Fprintf(w, "Hi, I'm a container and this is my message: %s, and my deployment ID is: %s", message, deploymentId)
40+
}
41+
42+
func errorHandler(w http.ResponseWriter, r *http.Request) {
43+
// panics
44+
panic("This is a panic")
45+
}
46+
47+
func main() {
48+
stop := make(chan os.Signal, 1)
49+
50+
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
51+
52+
router := http.NewServeMux()
53+
router.HandleFunc("/", handler)
54+
router.HandleFunc("/container", handler)
55+
router.HandleFunc("/error", errorHandler)
56+
57+
server := &http.Server{
58+
Addr: ":8080",
59+
Handler: router,
60+
}
61+
62+
go func() {
63+
log.Printf("Server listening on %s\n", server.Addr)
64+
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
65+
log.Fatal(err)
66+
}
67+
}()
68+
69+
<-stop
70+
log.Println("Shutting down server...")
71+
72+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
73+
defer cancel()
74+
75+
if err := server.Shutdown(ctx); err != nil {
76+
log.Fatal(err)
77+
}
78+
79+
log.Println("Server shutdown successfully")
80+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "example",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"deploy": "wrangler deploy",
7+
"dev": "wrangler dev",
8+
"start": "wrangler dev",
9+
"cf-typegen": "wrangler types"
10+
},
11+
"devDependencies": {
12+
"wrangler": "4.74.0",
13+
"@cloudflare/workers-types": "^4.20250506.0",
14+
"typescript": "^5.5.2"
15+
},
16+
"dependencies": {
17+
"@cloudflare/containers": "0.0.1"
18+
}
19+
}

0 commit comments

Comments
 (0)