Deno logoDeno

Web Streams at the Edge

Ryan Dahl, Luca Casonato


At Deno we take web standards very seriously. A consequence of this is that Deno Deploy has excellent support for Web Streams (also called "Standard Streams"). With Deno Deploy it's possible to build a streaming, event-driven server in a few lines of JavaScript (or TypeScript) and deploy it to data centers in 28 world-wide regions instantly.

Let's take a look at how far browser standards have come server-side...

Basic HTTP Proxy

When building an HTTP proxy, it's important to not buffer the body. That would induce both more memory usage and slower response times. Instead you want to stream the HTTP message's body through the server back to the client.

This is a straightforward example:

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

async function handler(req: Request): Promise<Response> {
  const url = new URL(req.url);
  url.protocol = "https:";
  url.hostname = "example.com";
  url.port = "443";
  return await fetch(url.href, {
    headers: req.headers,
    method: req.method,
    body: req.body,
  });
}

serve(handler);

You can access this proxy server at https://example-proxy-requests.deno.dev/ or fork the code at https://dash.deno.com/playground/example-proxy-requests

HTTP Proxy with Transform

What if we wanted to modify the data passing through the proxy? In the following example we process the body, packet by packet, making text upper case with the aid of TransformStream, TextDecoderStream, and TextEncoderStream.

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

serve(async (req) => {
  const url = new URL(req.url);
  url.protocol = "https:";
  url.hostname = "example.com";
  url.port = "443";
  const resp = await fetch(url.href);

  const bodyUpperCase = resp.body
    .pipeThrough(new TextDecoderStream())
    .pipeThrough(
      new TransformStream({
        transform: (chunk, controller) => {
          controller.enqueue(chunk.toUpperCase());
        },
      }),
    )
    .pipeThrough(new TextEncoderStream());

  return new Response(bodyUpperCase, {
    status: resp.status,
    headers: resp.headers,
  });
});

You can access this server at https://example-proxy-upper-case.deno.dev/ or fork the code at https://dash.deno.com/playground/example-proxy-upper-case

Server-Sent Events

Of course, you don't need a proxy to make use of streams. What if one wanted to build a server which responded with a message every second? This can be achieved by combining ReadableStream with setInterval.

Additionally, by setting the content-type to text/event-stream and prefixing each message with "data: ", Server-Sent Events make for easy processing using the EventSource API.

Access this live at https://server-sent-events.deno.dev/ or fork the code at https://dash.deno.com/playground/server-sent-events

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

const msg = new TextEncoder().encode("data: hello\r\n\r\n");

serve(async (_) => {
  let timerId: number | undefined;
  const body = new ReadableStream({
    start(controller) {
      timerId = setInterval(() => {
        controller.enqueue(msg);
      }, 1000);
    },
    cancel() {
      if (typeof timerId === "number") {
        clearInterval(timerId);
      }
    },
  });
  return new Response(body, {
    headers: {
      "Content-Type": "text/event-stream",
    },
  });
});

Note that because Deno Deploy uses HTTP/2, SSE does not suffer from the browsers maximum open connections (6) limit that makes SSE over HTTP/1.1 unwise.

WebSockets

Deno Deploy also has support for WebSocket connections. WebSockets are not part of the Stream API, but the use-cases have a large overlap.

There is not yet a standard API for server-side websockets, so for this you must reach inside the Deno namespace for Deno.upgradeWebSocket:

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

serve((req) => {
  const upgrade = req.headers.get("upgrade") || "";
  if (upgrade.toLowerCase() != "websocket") {
    return new Response("request isn't trying to upgrade to websocket.");
  }
  const { socket, response } = Deno.upgradeWebSocket(req);
  socket.onopen = () => console.log("socket opened");
  socket.onmessage = (e) => {
    console.log("socket message:", e.data);
    socket.send(new Date().toString());
  };
  socket.onerror = (e) => console.log("socket errored:", e.message);
  socket.onclose = () => console.log("socket closed");
  return response;
});

Access this live at https://websocket.deno.dev/ or fork the code at https://dash.deno.com/playground/websocket

What's next?

Check out the examples gallery and documentation for more.

Deno Deploy is currently in beta and free to all. If you do try it out, please help by sending us some feedback.