A closer look at using HTTP/2, WebSockets and Server Sent Events with Fastify on Google Cloud Run

Google recently announced end-to-end HTTP/2, WebSockets and streaming support in their Cloud Run service offering. This is great news because our projects often rely on cloud architectures, and being able to use these features natively opens up scenarios that were not possible until now.

Fastify is NearForm’s web framework of choice and it has supported these features for a long time, but when running in the cloud it needs the service provider to support them.

This blog post will show you how to use Cloud Run to run Fastify applications that use HTTP/2, WebSockets and streaming of Server Sent Events. The source code accompanying this post is available at nearform/fastify-cloud-run.

For a primer about using HTTP/2 in Node.js, we recommend watching James Snell’s Having fun with HTTP/2 in Node.js.

HTTP/2

Cloud Run has always supported HTTP/2. Until now, all HTTP/2 connections were downgraded to HTTP/1 when they were sent to a container running your Cloud Run code.

The recent release of Cloud Run introduced end-to-end support for HTTP/2, meaning that we can fully leverage Fastify support for HTTP/2 when running in Cloud Run.

Below is a simple Fastify application to illustrate this. It can also be found in the repository accompanying this blog post.

const fastify = require('fastify')({
  http2: true,
  logger: true,
})

fastify.get('/', function (request, reply) {
  reply.code(200).send({
    hello: 'world',
    httpVersion: request.raw.httpVersion,
  })
})

fastify.listen(process.env.PORT || 3000, '0.0.0.0')

The main difference between a non-HTTP/2 application and the above example is the use of the http2 flag in the server option. Setting it to true enables HTTP/2 support in the server.

Running the example locally

To run this application locally, simply execute:

npm install
npm start

The server will start listening on port 3000, and you can check it works by making a request to the server via curl, for example:

curl -v --http2-prior-knowledge localhost:3000

We force curl to assume the server supports HTTP/2 using the --http2-prior-knowledge flag, which you can read more about in the curl documentation.

The response will look similar to the following:

< HTTP/2 200
< content-type: application/json; charset=utf-8
< content-length: 37
< date: Sat, 06 Feb 2021 08:42:31 GMT
< {"hello":"world","httpVersion":"2.0"} 

Because the application’s code included Node’s raw request httpVersion property, we can be sure that HTTP/2 was used to handle the request.

Running the example in the cloud

To run the application on Cloud Run you must open a Google Cloud Shell and execute the following commands:

git clone https://github.com/nearform/fastify-cloud-run.git
cd fastify-cloud-run/http2
gcloud beta run deploy fastify-http2 --use-http2 --source=.

We use the beta version of the gcloud program because the new features are not yet available in the stable release. Also, we explicitly enable support for HTTP/2 by using the --use-http2 flag.

You may need to provide additional input to prompts that Cloud Shell will output on the screen, or additional arguments to the command, depending on how your Cloud Shell is configured. A common mandatory argument is the Google Cloud project ID, which is specified using the --project argument and it needs to have billing enabled.

Cloud Shell will carry out a few steps to prepare the environment to run the application. It will upload the application’s source code to Cloud Run, it will build it, create a container inside which to run it, create a revision for the application and finally start serving traffic to it. When all the steps are complete, Cloud Shell will output a URL which can be used to send requests to the application.

Building using Buildpacks and deploying container to Cloud Run service [fastify-http2] in project [fastify-secrets] region [europe-west1]
✓ Building and deploying... Done.                                                           
  ✓ Uploading sources...
  ✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/180cd8a5-c98f-4fef-aa38-44bf1157262c?project=1027534643217].
  ✓ Creating Revision...
  ✓ Routing traffic...
Done.
Service [fastify-http2] revision [fastify-http2-00005-woz] has been deployed and is serving 100 percent of traffic.
Service URL: https://fastify-http2-p2vqyxuqoq-ew.a.run.app

We can then use the application’s url to send a request to it in the same way we did with the local version of the application.

curl --http2-prior-knowledge https://fastify-http2-p2vqyxuqoq-ew.a.run.app
{"hello":"world","httpVersion":"2.0"}

Because the Cloud Run version of the application is running over HTTPS, we can also use a browser to send requests to it. Most browsers support HTTP/2, but only when the server is running over HTTPS, which wasn’t the case with the locally running application.
Don’t forget to remove the fastify-http2 service from Cloud Run to avoid further billing. You can do so via the Cloud Run Console.

WebSockets

WebSockets are another useful feature that Cloud Run didn’t support until recently. Fastify supports WebSockets via the fastify-websocket plugin. A simple WebSocket echo server written with Fastify is shown below:

const fastify = require('fastify')({
  logger: true,
})

fastify.register(require('fastify-websocket'), {
  // echo server
  handle: conn => conn.pipe(conn),
})

fastify.listen(process.env.PORT || 3000, '0.0.0.0')

When it receives a message via WebSocket, this server will echo it back to the sender.

We can test it out locally by running the application:

npm install
npm start

We then connect to it with any WebSocket client. We used websocat in the example below:

websocat ws://localhost:3000
hello↵
hello
world↵
world

Note the use of the ws:// scheme to access the server via Websocket.

Typing any text and pressing ENTER will send the message to the server over WebSocket, and the server will echo it back.

Running this in Cloud Run is very similar to the HTTP/2 example, with the exception that WebSockets don’t run on HTTP/2 but over HTTP/1:

git clone https://github.com/nearform/fastify-cloud-run.git
cd fastify-cloud-run/ws
gcloud beta run deploy fastify-ws --source=.

To test it, simply change the URL provided to the websocat command. For example:

websocat wss://fastify-ws-p2vqyxuqoq-ew.a.run.app
hello↵
hello
world↵
world

Note the use of the wss:// scheme, as the WebSocket example runs over secure WebSocket in Cloud Run, which is the same as HTTPS but for WebSockets instead of HTTP.

Server Sent Events

Google Cloud Run now enables streaming via Server Sent Events over HTTP/2 , which are a mechanism that web servers can use to push messages to clients.

Server Sent Events existed and worked over HTTP/1 too, but they had limitations that were removed in HTTP/2. The example we show uses SSE over HTTP/2.

An example of using SSE over HTTP/2 with Fastify is shown below:

const fastify = require('fastify')({
  http2: true,
  logger: true,
})

fastify.get('/', function (request, reply) {
  const interval = setInterval(function () {
    reply.raw.write(`data:${new Date().toISOString()}\n`)
  }, 1000)

  request.raw.on('close', () => {
    clearInterval(interval)
    reply.raw.end()
  })
})

fastify.listen(process.env.PORT || 3000, '0.0.0.0')

When it receives an HTTP request, this simple application starts a timer that sends a SSE containing the current server date back to the client approximately every second. When the client connection is closed, the timer is stopped so we don’t leak memory, and we close the response stream.

Once the application starts, we can then make a request to it:

curl -N --http2-prior-knowledge http://localhost:3000
< HTTP/2 200
< date: Sat, 06 Feb 2021 09:36:00 GMT
<
data:2021-02-06T09:36:00.338Z
data:2021-02-06T09:36:01.348Z
data:2021-02-06T09:36:02.354Z
data:2021-02-06T09:36:03.366Z

The -N flag disables buffering of the response stream because we want to see the data as soon as the server sends it to us, without any buffering. This application can be run in Cloud Run in the same way as for the HTTP/2 and WebSocket examples:

git clone https://github.com/nearform/fastify-cloud-run.git
cd fastify-cloud-run/sse
gcloud beta run deploy fastify-sse --use-http2 --source=.

And tested in the same way:

curl -N --http2-prior-knowledge 
https://fastify-sse-p2vqyxuqoq-ew.a.run.app
data:2021-02-06T09:46:36.917Z
data:2021-02-06T09:46:37.918Z
data:2021-02-06T09:46:38.917Z 

A whiteboard with Fastify over WebSocket

We used WebSockets to create a fun whiteboard demo. Fastify can be used as a static web server via fastify-static and with WebSockets via the fastify-websocket plugin. A simple way to broadcast the messages is to get the list of WebSocket clients, iterate through them and check that the current connection client is not the same as one of the client’s.

fastify.register(require('fastify-websocket'), {
  handle: (connection, req) => {
    connection.socket.on('message', message => {
      fastify.websocketServer.clients.forEach((client) => {
        if (client.readyState === 1 && client !== connection.socket) {
          client.send(message)
        }
      })
    })
  }
})

The code above avoids delivering the same message to the sender by filtering the WebSockets client list and excluding the sending connection from the list of recipients.

Running this in Cloud Run over HTTP/1:

git clone https://github.com/nearform/fastify-cloud-run.git
cd fastify-cloud-run/whiteboard
gcloud beta run deploy fastify-websocket-whiteboard --source=.

We can test it out locally by running the application:

npm install
npm start

The application starts at port 3000 by default if no PORT environment variable is present. The picture below shows it in action using two browser windows to collaborate on the same whiteboard:

The picture below shows an example of a piece of artistic work done with the whiteboard:

This post was cowritten with Paul Isache.

View all posts  |  Technology  |  Business  |  Culture  |  Opinion  |  Design
Follow us for more information on this and other topics.