cloudflare / cloudflare/cloudflared
🐛cloudflared tunnel doesn't work with websockets from Twilio, ngrok *does*
- Dominant language
- Go
- Stars
- 15.6k
- Forks
- 1.4k
- PR merge metrics
- No merged PRs in 30d
Description
**Describe the bug**
I cannot connect a Twilio stream via websocket over `cloudflared` to my `wrangler dev` backend locally. This *specific* setup fails, but any other setup works fine!? I'm 50-60 hours into this bug, over 4 days, and my hair is all gray now :)
**TL;DR:** 4 setups:
1. BROKEN: `cloudeflared` locally + `wrangler dev` server + Twilio websocket = Twilio error "31920: Stream - WebSocket - Handshake Error ... server didn't respond with 101"
2. WORKS: `cloudeflared` + `wrangler dev` + manually connect to ws from `websocketking.com` (no twilio)
3. WORKS: `ngrok` + same `wrangler dev` + same Twilio websocket
4. WORKS: `cloudeflared` + `node server.js` (rewritten, non-CF Worker server) + Twilio websocket
**To Reproduce**
With a simple Hono router setup in a CF Worker, the following tries to construct a minimal websocket, but never receives any messages over it because the handshake fails. Twilio calls `POST /incoming-call` and receives a message to connect to the websocket on `/media-stream`. It tries, my server says it sent a `101` response, but Twilio says it never gets it (but the other setups *do* indeed work):
```typescript
router.post('/incoming-call', validateTwilioRequest(), (c) => {
const host = c.req.header('host');
if (!host) {
console.error('Cannot determine host for WebSocket URL in /incoming-call');
return c.text('Server configuration error: Cannot determine host', 500);
}
const webSocketUrl = `wss://${host}/twilio-ai/media-stream`;
const twimlResponse = `
Starting websocket
This TwiML instruction is unreachable unless the Stream is ended by your WebSocket server.
`;
return c.text(twimlResponse, 200, { 'Content-Type': 'text/xml' });
});
router.get(
"/media-stream",
async (c) => {
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
try {
const upgradeHeader = c.req.header("Upgrade");
if (!upgradeHeader || upgradeHeader !== "websocket") {
return c.text("Expected Upgrade: websocket", 426);
}
// Handle incoming messages from Twilio
server.addEventListener("message", async (event: MessageEvent) => {
console.log('Message Event: ', event); // No messages are ever received bc handshake failed
}
);
server.addEventListener("close", async () => {
console.log('Close');
});
} catch (e) {
console.error("WebSocket setup error:", e);
return c.text("Internal Server Error", 500);
}
console.log('before `accept`');
server.accept();
console.log('after `accept`');
return new Response(null, {
status: 101,
webSocket: client,
});
}
);
```
If I use a websocket dev tool, like the online `websocketking.com`, I can connect to and send messages to that `/media-stream` endpoint just fine, over `cloudflared`.
Or if I swap out `cloudflared` for `ngrok`, that too works.
But with my ideal setup using `cloudflared` twilio says it never gets the `101` response. If I try to write to the client socket, Twilio gives a warning "received response other than 101". If I never write to the socket, just return the `101`, it times out waiting for the handshake.
4. Tunnel ID : ` tunnelID=1d6b2701-227a-42aa-8df6-0cccde21d322`
6. cloudflared config: I've tried many options, but none change the game at all, so here's my minimal setup:
```yaml
tunnel: devtunnel
loglevel: trace
ingress:
- hostname: devtunnel.mysite.io
service: http://localhost:8787
- hostname: devtunnel.mysite.io
service: ws://localhost:8787
- service: http_status:404
```
**Expected behavior**
I expect to be able to stream audio bidirectionally with Twilio from a local machine, with a Cloudflare worker backend (`wrangler dev`) and a `cloudflared` tunnel.
**Environment and versions**
- OS: Ubuntu 22.04.5
- `cloudflared` Version: 2025.4.2 (latest)
- `wrangler` version: 4.14.4 (latest)
**Logs and errors**
I really regret that for all my debugging (over 4, now 5 days), I don't have good logs locally, just the Twilio responses:
1) received response other than 101
2) handshake timeout
Locally, nothing really gets reported. Eventually downstream things break when the ws closes from twilio's end - breaks immediately if I try to write to the client socket and get error `1)` , or after 10 seconds (twilio's timeout threshold) if I merely respond with the 101 and don't write anything, error `2)`
Again, `ngrok` works as a tunnel just fine. My suspicion is that `cloudeflared` is dropping responses under certain conditions, perhaps conditioned on the request headers from Twilio.
Contributor guide
Assessment
This issue has not been assessed yet.