šcloudflared tunnel doesn't work with websockets from Twilio, ngrok *does*
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:
- BROKEN:
cloudeflaredlocally +wrangler devserver + Twilio websocket = Twilio error "31920: Stream - WebSocket - Handshake Error ... server didn't respond with 101" - WORKS:
cloudeflared+wrangler dev+ manually connect to ws fromwebsocketking.com(no twilio) - WORKS:
ngrok+ samewrangler dev+ same Twilio websocket - 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):
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 = `<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<Say>Starting websocket</Say>
<Connect>
<Stream url="${webSocketUrl}" />
</Connect>
<Say>This TwiML instruction is unreachable unless the Stream is ended by your WebSocket server.</Say>
</Response>`;
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.
-
Tunnel ID :
tunnelID=1d6b2701-227a-42aa-8df6-0cccde21d322 -
cloudflared config: I've tried many options, but none change the game at all, so here's my minimal setup:
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
cloudflaredVersion: 2025.4.2 (latest)wranglerversion: 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:
- received response other than 101
- 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.
The code that fails in the wrangler-cloudflared environment works when deployed.
Confirming this is still a tooling problem on Cloudflare's end.
I encountered the same problem. The application I am using is Vaultwardan. The WSS connection does not work properly in HTTP2 mode.
I'm having the same issue with cloudflared tunnel and twilio websocket.
same
Also facing Websockets issue but with other tools.
I faced the same issue, and I could make it work by adding Content-Encoding: identity to the response header.
return new Response(null, {
status: 101,
webSocket: client,
headers: {
"Content-Encoding": "identity"
},
})
It seems that Cloudflare Tunnel overwrites the Accept-Encoding header with specific values (as far as Iāve confirmed, gzip, br) when sending requests from tunnel to origin, regardless of the original requestās value. miniflare respects that setting and automatically compresses the content accordingly. (cloudflare/workers-sdk#8004)
This mechanism works fine for normal HTTP requests, but in the case of WebSockets, there appear to be scenarios where things donāt line up properly.
After trying various cases, I found that with the combination of Cloudflare Tunnel + wrangler dev + WebSocket request without Accept-Encoding: gzip header, the connection never gets established after the status:101 response, and the client times out.
Most likely, the way the Cloudflare Tunnel and miniflare handle content compression introduces some inconsistency in the handshake process of WebSocket connections.
For example, browsers and Node.jsās native WebSocket client send headers that allow gzip during the request, so the connection succeeds. But the Node.js ws module does not include such a header unless explicitly specified, which leads to the same timeout issue. Probably the WebSocket requests from Twilio also donāt include the Accept-Encoding header.
By adding Content-Encoding: identity to the response header on the server side, we can explicitly disable compression in the response, which seems to resolve the problem.