Teitunnel

Use cases

Share a Vite, Next.js, Django or Rails dev server

Fix "Blocked request. This host is not allowed", "Invalid Host header", broken hot reload and CSRF errors when sharing a dev server through Cloudflare Tunnel.

In this tutorial you share a running dev server, first at a random address and then on your own domain with a name that follows your git branch, and fix the errors dev servers give when they're reached through a tunnel. It takes about ten minutes.

You need: Teitunnel installed, a dev server you can start (the examples use a Vite app on port 5173), and, for the second part, a Cloudflare account with a domain (first route).

Why the first visit often fails

Dev servers protect you from DNS-rebinding attacks by refusing requests for hostnames they don't know. Through a tunnel, requests arrive for demo.yourhost.com or something.trycloudflare.com, not localhost, so Vite answers Blocked request. This host is not allowed, webpack Invalid Host header, Rails Blocked hosts and Django DisallowedHost. Teitunnel recognises each of these and fixes them in place.

1. Share it at a random address

Start the dev server as usual:

npm run dev          # Vite: http://localhost:5173

In Quick Share, the dev server is listed under Running on this Mac. Choose it (or type 5173 in Port or address) and Share.

From a terminal instead:

teitunnel share 5173

Within a few seconds the card shows a https://….trycloudflare.com address and a QR code for your phone. Open it.

New shares of Vite, Nuxt, webpack-dev-server and Angular send the dev server's own address as the Host header from the start (localhost:5173), so they work at once; the card says "Sends Host: localhost:5173 so Vite accepts the public address." Those servers only read the Host header to stop DNS rebinding, so nothing else changes. To turn it off: Advanced ▸ Host header ▸ As the browser sends it in the share form, or --no-host-header.

Before sharing, the exposure check looks at the dev server for things that shouldn't be public, like a .env file it serves.

2. Share it on your domain, named after your branch

A random address changes every time. On your own domain the address stays the same, and it can carry server-sent events and long-lived streams.

cd ~/code/shop
teitunnel share 5173 --on "{branch}.yourhost.com"     # feat/login → feat-login.yourhost.com
teitunnel share 5173 --on                             # the same name as last time, in this folder

{branch}, {project} and {user} are filled in from the current folder. In the app, choose Address ▸ On yourhost.com in the share form and type the Subdomain; the form suggests names from the project and shows what they become ("It's feat-login.yourhost.com here.").

The share makes a temporary route and removes it again when you stop it, quit the app or end the command. Add --allow @yourhost.com to put a login in front of it.

3. When the dev server refuses the address

When a share or route goes live, Teitunnel fetches it once through Cloudflare, like a visitor. If the dev server refuses the address, the card or route says Your dev server rejects this address. ("Next.js only answers addresses it knows, and demo.yourhost.com isn't one of them.") and offers two fixes:

  • Send Host: localhost:5173: the quickest fix, with no change to your project. A Quick Share restarts with a new address; a route or domain share changes through a plan like any edit.
  • Show the Config Line: the exact line for your framework, ready to copy ("Add this to vite.config.js, then restart the dev server:").

After either, choose Check Again: it tells you when it works ("Vite accepts the address now.").

For Next.js, Rails, Django, SvelteKit and Astro the config line comes first: they also compare the Host header with the browser's address (server actions, CSRF protection, sign-in callbacks), so sending another one can break forms and logins. Next.js checks where requests come from rather than the Host header, so for it only the config line works.

teitunnel share prints the same finding and suggestion, and keeps sharing.

Option A: send localhost as the Host header

teitunnel share 5173 --host-header localhost:5173
teitunnel route add app.yourhost.com 5173 --host-header localhost:5173

In the app, for a route: Edit route ▸ Advanced ▸ Origin settings ▸ Host header. The dev server then sees every request as a request for itself. The catch: anything the app builds from the Host header (absolute links, redirects, OAuth callbacks) says localhost too, and frameworks that compare it with the browser's Origin reject form posts. If that matters, use option B.

Option B: allow the hostname in your project

These are the lines Teitunnel suggests. A leading dot (.yourhost.com) allows every subdomain, and for Quick Shares Teitunnel suggests .trycloudflare.com, since the address changes each time.

Vite 6.0.9 and later (and 5.4.12, 4.5.6) answer Blocked request. This host ("demo.yourhost.com") is not allowed. In vite.config.js (Astro: astro.config.mjs under vite:; Nuxt: nuxt.config.ts under vite:; SvelteKit: vite.config.js):

export default defineConfig({
  server: {
    allowedHosts: [".yourhost.com"], // or [".trycloudflare.com"] for Quick Shares
    hmr: { clientPort: 443 },         // hot reload through the tunnel
  },
});

hmr.clientPort: 443 makes the browser open the hot-reload WebSocket on the tunnel's HTTPS port instead of 5173, so edits show up live for everyone viewing it.

Streaming (Server-Sent Events) and MCP servers

Quick Shares (trycloudflare.com addresses) don't carry Server-Sent Events: the stream never reaches the browser. When the service answers with text/event-stream (a streaming UI, or an MCP server on the SSE transport), the card says "This service streams events (Server-Sent Events), which Quick Shares don't carry. Share it on your own domain instead, where streams work." Share it with --on, as in step 2. For a local MCP server, teitunnel share 3000 --on mcp.yourhost.com --mcp adds a bearer token and keeps streams alive.

Changes that don't show up

On your own domain, Cloudflare may cache files such as scripts, styles and images, so a change you just made can show the old copy. Turn on Bypass Cloudflare's cache in the hostname's Edit Protection… sheet, or run teitunnel protect dev.yourhost.com --bypass-cache. Cloudflare then never caches that hostname, and nothing changes for the rest of the domain (Edge protection).

HTTPS dev servers

If your dev server itself uses HTTPS with a self-signed certificate (https://localhost:8443), cloudflared refuses it by default. For a route, turn on Origin settings ▸ Accept self-signed certificates, or pass --no-tls-verify. Visitors still get Cloudflare's valid certificate.

Visitors reach your app over HTTPS, but your app receives plain HTTP from cloudflared, with X-Forwarded-Proto: https saying how the visitor came. Through the inspector (the default for Quick Shares) it also gets X-Forwarded-Host with the public address. An app that doesn't trust these headers builds http:// links, which browsers block as mixed content, or redirects HTTP to HTTPS forever. An app whose base URL is localhost links to this computer, which visitors can't reach.

The check after sharing reads the page and says so: The page loads http://[::1]:5173/@vite/client from this computer, which visitors can't reach. or The page loads http://shop.yourhost.com/build/app.css over plain HTTP, so browsers block it on the HTTPS address (mixed content)., with the fix for your framework. teitunnel share prints the same, and verify_route gives it to AI agents.

FrameworkLinks to localhostPlain http:// links
Laravel (and Livewire)Stop npm run dev and run npm run build while you share, and set APP_URL to the public address in .envIn bootstrap/app.php, $middleware->trustProxies(at: '*'); (Laravel 11+), or $proxies = '*' in the TrustProxies middleware
RailsLeave config.asset_host unset, or bin/rails assets:precompileconfig.assume_ssl = true (Rails 7.1+)
DjangoSTATIC_URL and MEDIA_URL as paths (/static/)SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https"); USE_X_FORWARDED_HOST = True when the Host header is rewritten
Flask / FastAPIBuild URLs with url_for / request.url_forWerkzeug's ProxyFix, or uvicorn --proxy-headers --forwarded-allow-ips='*'
Express and NodeUse relative linksapp.set("trust proxy", true)
Next.js, NuxtLeave assetPrefix / app.cdnURL unset in developmentUsually nothing: they use relative links

See too many redirects.

Where to go next

Edit on GitHub

Last updated on

On this page