cloudflare / cloudflare/cloudflared

🐛cloudflared tunnel doesn't work with websockets from Twilio, ngrok *does*

Open
#1,465 7 comments 2 reactions 0 assignees View on GitHub
Priority: Normal Type: Bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.