Stream Server-Sent Events through a tunnel
Stream Server-Sent Events from a local Next.js route handler through a Horizon tunnel, and watch them arrive with curl -N.
Stream Server-Sent Events (SSE) from your laptop. Horizon streams responses through unbuffered, so each event arrives as you send it.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A Next.js app using the App Router
- Optional: a reserved subdomain for
-s. Reserve one on the Subdomains page.
Add a streaming route
An SSE response uses the text/event-stream content type. Each event is a block of text that ends with a blank line. A data: line carries the payload.
This route handler sends one event per second, ten times, then closes the stream.
export const dynamic = "force-dynamic";
const encoder = new TextEncoder();
export async function GET() {
let count = 0;
let timer: ReturnType<typeof setInterval>;
const stream = new ReadableStream({
start(controller) {
timer = setInterval(() => {
count += 1;
controller.enqueue(encoder.encode(`data: tick ${count}\n\n`));
if (count === 10) {
clearInterval(timer);
controller.close();
}
}, 1000);
},
cancel() {
clearInterval(timer);
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
});
}Start the app on port 3000.
npm run devStart a tunnel
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-appDrop -s my-app for a free random subdomain. It changes every run. Reserved subdomains are a paid feature, see Pricing.
Watch the stream with curl
Pass -N so curl doesn't buffer its output. curl sends a non-browser user agent, so it skips the Before you continue page.
curl -N https://my-app.hrzn.run/api/eventsRead it in a browser
In a browser, use EventSource. A browser that opens the tunnel for the first time sees the Before you continue page, so open the site once and select Continue to site first.
const source = new EventSource("https://my-app.hrzn.run/api/events");
source.onmessage = (event) => console.log(event.data);
source.onerror = () => source.close();If the page that runs this code is on another origin, your route must send CORS headers. See Share a local API.
Check it works
curl prints one line per second, not all ten at the end:
data: tick 1
data: tick 2
data: tick 3Your Horizon terminal prints one line for the request:
GET 200 /api/eventsTroubleshooting
Events arrive all at once
- Check that you passed
-Ntocurl. - Check that your route returns the stream without awaiting the whole response first.
- Check that the route is dynamic.
export const dynamic = "force-dynamic"keeps Next.js from caching it.
The browser shows an error and reconnects
EventSource reconnects when the stream closes. The example closes after ten events, so the browser reconnects. Close the source in onerror, as the example does.
Visitors see a Before you continue page
A browser sees it once every 7 days per IP address. They select Continue to site. Requests with an x-hrzn-skip-warning header or a non-browser user agent skip it.
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.
Next steps
- Expose a local WebSocket server
- Read MDN's guide to Using server-sent events.