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:5173In 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 5173Within 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:5173In 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.
Mixed content and links to localhost
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.
| Framework | Links to localhost | Plain 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 .env | In bootstrap/app.php, $middleware->trustProxies(at: '*'); (Laravel 11+), or $proxies = '*' in the TrustProxies middleware |
| Rails | Leave config.asset_host unset, or bin/rails assets:precompile | config.assume_ssl = true (Rails 7.1+) |
| Django | STATIC_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 / FastAPI | Build URLs with url_for / request.url_for | Werkzeug's ProxyFix, or uvicorn --proxy-headers --forwarded-allow-ips='*' |
| Express and Node | Use relative links | app.set("trust proxy", true) |
| Next.js, Nuxt | Leave assetPrefix / app.cdnURL unset in development | Usually nothing: they use relative links |
See too many redirects.
Where to go next
Last updated on
Expose localhost to the internet
Share a website or API running on localhost at a public HTTPS URL with Cloudflare Tunnel, free and without port forwarding, from the app or the command line. Stable URLs on your own domain too.
Test webhooks on localhost
Receive Stripe, GitHub, Slack, Shopify or Twilio webhooks on your computer through Cloudflare Tunnel, at a stable HTTPS URL, then inspect, check and replay every delivery.