Skip to content

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

@freckletonj

Description

@freckletonj

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):

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.

  1. Tunnel ID : tunnelID=1d6b2701-227a-42aa-8df6-0cccde21d322

  2. 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
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority: NormalMinor issue impacting one or more usersType: BugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions