Horizon

Share a local API

Share a local API with curl, Postman and browser fetch calls from another origin, using a Horizon tunnel and the x-hrzn-skip-warning header.

Give a teammate, a client or a frontend on another origin a public HTTPS URL for the API on your laptop.

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.
  • An API running on your machine, for example on http://localhost:3000

Start your API

This example is a plain Node.js server. It sends its own CORS headers, because Horizon doesn't add them.

server.mjs
import { createServer } from "node:http";

const corsHeaders = {
  "Access-Control-Allow-Origin": "https://my-frontend.example",
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, x-hrzn-skip-warning",
};

createServer((request, response) => {
  if (request.method === "OPTIONS") {
    response.writeHead(204, corsHeaders);
    response.end();
    return;
  }

  response.writeHead(200, { ...corsHeaders, "Content-Type": "application/json" });
  response.end(JSON.stringify({ message: "hello from my laptop" }));
}).listen(3000);

Replace https://my-frontend.example with the origin of the page that calls your API. Run it.

node server.mjs

Start a tunnel

Use -s with a subdomain you reserved. Without it the subdomain is random and changes every run. Reserved subdomains are a paid feature, see Pricing.

hrzn tunnel http://localhost:3000 -s my-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:3000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Call it from curl or Postman

curl sends a non-browser user agent, so it skips the Before you continue page.

curl https://my-app.hrzn.run/
Output
{"message":"hello from my laptop"}

Postman also sends a non-browser user agent. Create a request, set the URL to https://my-app.hrzn.run/, and select Send. If you get an HTML page, add a header named x-hrzn-skip-warning with any value.

Call it from a browser on another origin

A browser sends a browser user agent, so add the x-hrzn-skip-warning header to every fetch. Any value works.

src/api.ts
const response = await fetch("https://my-app.hrzn.run/", {
  headers: { "x-hrzn-skip-warning": "1" },
});
console.log(await response.json());

A custom header makes the browser send a CORS preflight first. That is an OPTIONS request. Horizon doesn't show the warning page for OPTIONS, so the preflight reaches your API.

Your API still answers the preflight. It must send its own CORS headers, including x-hrzn-skip-warning in Access-Control-Allow-Headers. The server in step 1 does this.

Check it works

Run the curl command from step 3. Your Horizon terminal prints:

Output
  GET     200  /

From a browser on another origin, the terminal prints the preflight first, then the call:

Output
  OPTIONS 204  /
  GET     200  /

Troubleshooting

My app gets an HTML page instead of JSON

The request got the Before you continue page. Add the x-hrzn-skip-warning header, with any value, to the request.

The browser blocks the request with a CORS error

Horizon doesn't add CORS headers. Your API must send Access-Control-Allow-Origin and answer OPTIONS. If you send x-hrzn-skip-warning from a browser, list it in Access-Control-Allow-Headers.

The preflight returns an error

Your API must answer OPTIONS with a success status, such as 204, and the three Access-Control-Allow-* headers. Check that no route or middleware rejects OPTIONS.

The URL changed after a restart

You started the tunnel without -s. Restart with -s my-app. -s needs a subdomain you reserved, see Pricing.

Nothing reaches your API

  • Check that the Horizon terminal is still running. If its last line is Connection lost. Reconnecting…, wait for Reconnected.
  • Check that your API runs on the port in the hrzn tunnel command.

Next steps

On this page