Expose a local Express app
Expose a local Express app through a Horizon tunnel, and make req.hostname and req.protocol use the public URL with trust proxy.
Reach your Express app from outside your laptop, with an HTTPS URL, and make req.hostname return the public host.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. npm install express
Start your app
This app trusts the loopback address, then returns what Express reads from the request. The Horizon CLI runs on your machine and connects to your app from there, so loopback is the proxy to trust.
const express = require("express");
const app = express();
app.set("trust proxy", "loopback");
app.get("/", (req, res) => {
res.json({
hostname: req.hostname,
protocol: req.protocol,
host: req.get("host"),
forwardedHost: req.get("x-forwarded-host"),
forwardedProto: req.get("x-forwarded-proto"),
});
});
app.listen(3000, () => console.log("Listening on http://localhost:3000"));node server.jsStart a tunnel
Use -s with a subdomain you reserved. Reserved subdomains are a paid feature, see Pricing.
hrzn tunnel http://localhost:3000 -s my-appHORIZON: Tunnel connected
URL https://my-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-appKeep this terminal open.
Call the public URL
curl https://my-app.hrzn.run/Check it works
The Horizon terminal prints GET 200 /. The JSON shows which headers reached Express. host is localhost:3000, because the Horizon CLI sets Host to the address you tunnel to. forwardedHost is my-app.hrzn.run. With trust proxy set, hostname is the value of X-Forwarded-Host.
Without the app.set line, req.hostname is localhost. Express reads X-Forwarded-Host only when trust proxy allows it.
Choose a trust proxy value
Express's default is false: it treats the app as facing the client directly. Pick the narrowest value that fits.
| Value | Meaning |
|---|---|
"loopback" | Trust 127.0.0.1/8 and ::1/128. Use this for the Horizon CLI on your machine. |
1 | Trust the address one hop away. |
true | Trust the left-most X-Forwarded-For entry. Use it only if the last proxy overwrites the forwarded headers. |
Express warns that with true, the last trusted proxy must remove or overwrite X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto. Otherwise a client can send any value.
Build absolute URLs from the public host
Your app sees Host: localhost:3000. An OAuth redirect URI built from Host points at localhost. Build it from req.protocol and req.hostname after you set trust proxy, or set the public URL in an environment variable.
app.get("/login", (req, res) => {
const redirectUri = `${req.protocol}://${req.hostname}/auth/callback`;
res.json({ redirectUri });
});req.protocol reads X-Forwarded-Proto when trust proxy allows it. Check the forwardedProto value in the JSON above. If it is null, req.protocol is http, so hard-code https for tunnel URLs or read the public URL from an environment variable.
Troubleshooting
Error: listen EADDRINUSE: address already in use :::3000
Another process holds port 3000. Stop it, or change the port in app.listen and in the tunnel command.
req.hostname returns localhost
You didn't set trust proxy, or the request doesn't come from an address it trusts. Set app.set("trust proxy", "loopback") and check forwardedHost in the JSON.
Nothing reaches your app
Check that the Horizon terminal is still running. If its last line is Connection lost. Reconnecting…, wait for Reconnected.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app.
Next steps
- Read Express's guide to running behind proxies.
Share a Vite dev server
Share a Vite dev server through a Horizon tunnel to open it on a phone or send it to a teammate, with no allowedHosts change.
Use a Horizon tunnel with Expo
Point an Expo or React Native app on a phone at a local API through a Horizon tunnel, with an EXPO_PUBLIC_ environment variable.