Origin

Websites delivered using HTTP/2 enjoys a wide range of new features including -

  • fully multiplexed connections: all requests and responses to a domain are fully multiplexed via a single connection, making best use of available bandwidth.

  • header compression: repeated headers are compressed with HPACK compression so that they are not resent with every request and response.

  • PUSH: resources can be pre-emptively pushed by the server to the client, speeding up page load times.

Node.js just launched support(v8.8.1) for HTTP/2 as part of their core. In this post, we will create a simple HTTP/2 server to serve static files and then demonstrate some cool features like HTTP/2 PUSH.

Get an SSL certificate

Even though the HTTP/2 spec does not mandate HTTPS, browsers have decided that they will only support HTTP/2 on a HTTPS connection. This would also mitigate interference from older proxies which may not understand newer protocol.

For your local server, a self-signed certificate will work. You can find how to setup a self-signed certificate here

Building a Static File Server

Let us start with a simple server which just serves static files. Note that if you are using node.js 8.7.0, you need to run node with the --expose-http2 flag.

We will be listening to the stream event and responding to it with the corresponding file from the server root(public, in this case) using the respondWithFile API. We are using the mime-type module to look up the correct mime type to send along with the response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const http2 = require('http2')
const fs = require('fs')
const path = require('path')
const mime = require('mime-types')

cosnt {
HTTP2_HEADER_PATH,
HTTP2_HEADER_METHOD,
HTTP_STATUS_NOT_FOUND,
HTTP_STATUS_INTERNAL_SERVER_ERROR
} = http2.constants

const options = {
key: fs.readFileSync('./selfsigned.key'),
cert: fs.readFileSync('./selfsigned.crt'),
}

const server = http2.createSecureServer(options)

const serverRoot = './public'

function respondToStreamError(err, stream) {
console.log(err)
if (err.code === 'ENOENT') {
stream.respond({
':stats': HTTPS_STATUS_NOT_FOUND,
})
} else {
stream.respond({
':status': HTTP_STATUS_INTERNAL_SERVER_ERROR,
})
}
stream.end()
}

server.on('stream', (stream, headers) => {
const reqPath = headers[HTTP2_HEADER_PATH]
const reqMethod = headers[HTTP2_METHOD]

const fullPath = path.join(serverRoot, reqPath)
const responseMimeType = mime.lookup(fullPath)

stream.respondWithFile(fullPath, {
'content-type': responseMimeType
}, {
onError: err => respondToStreamError(err, stream)
})
})

server.listen(443)

Server PUSH Example

Now we have simpel HTTP/2 server running, lets try to use one of the new featues in HTTP/2 - HTTP/2 PUSH. This can lead to significant performance improvements in high latency environments, if done correctly.

We are loading a simple HTML file pointing to style.css which references to our font file. The request to the font file is only made after css file is discovered in the HTML, downloaded and then parsed. This is how the waterfall would have usually looked like:

You can initate a new PUSH with the pushStream API. Since we know that the browser is going to be requesting the font file in the future, we can PUSH the font file as soon as the server receives the request for the HTML file.

When the actual request for the font file takes place, it is claimed from the PUSH cache, instead of making a network request then.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
const http2 = require('http2')
const fs = require('fs')
const path = require('path')
const mime = require('mime-types')

const {
HTTP2_HEADER_PATH,
HTTP2_HEADER_METHOD,
HTTP_STATUS_NOT_FOUND,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
} = http2.constants

const options = {
key: fs.readFileSync('./selfsigned.key'),
cert: fs.readFileSync('./selfsigned.crt'),
}

const server = http2.createSecureServer(options)

const serverRoot = './public'

function respondToStreamError (err, stream) {
console.log(err)
if (err.code === 'ENOENT') {
stream.respond({
':status': HTTP_STATUS_NOT_FOUND,
})
} else {
stream.respond({
':status': HTTP_STATUS_INTERNAL_SERVER_ERROR,
})
}
stream.end()
}

server.on('stream', (stream, headers) => {
const reqPath = headers[HTTP2_HEADER_PATH]
const reqMethod = headers[HTTP2_HEADER_METHOD]
const fullPath = path.join(serverRoot, reqPath)
const responseMimeType = mime.lookup(fullPath)

if (fullPath.endsWith('.html')) {
console.log('html')
// handle the html file
stream.respondWithFile(fullPath, {
'content-type': 'text/html'
}, {
onError: (err) => {
respondToStreamError(err, stream)
}
})

stream.pushStream({
':path': '/font.woff'
}, {
parent: stream.id,
}, pushStream => {
console.log('pushing')
pushStream.respondWithFile(path.join(serverRoot, '/font.woff'), {
'content-type': 'text/css',
}, {
onError: err => {
respondToStreamError(err, pushStream)
}
})
})
} else {
// handle the static file
console.log(fullPath)
stream.respondWithFile(fullPath, {
'content-type': responseMimeType,
}, {
onError: err => respondToStreamError(err, stream)
})
}
})

server.listen(443)