Skip to content

Instantly share code, notes, and snippets.

@wesleytodd
Last active September 18, 2020 15:55
Show Gist options
  • Save wesleytodd/e5642c0d39fa71bdebf8ef31ddbd5e40 to your computer and use it in GitHub Desktop.
Save wesleytodd/e5642c0d39fa71bdebf8ef31ddbd5e40 to your computer and use it in GitHub Desktop.
Just noodling on the future Node.js and http
'use strict'
// Notice no certs, one thing I have thought
// for a long time is that frameworks should
// directly have support for spinning up with
// a cert if none was provided.
require('h3xt')()
.get('/', (req) => {
// Access the associated session
// req.session
// No need to check if can push, frameworks does this
// The framework has something like a ROOT so you can
// resolve static files, but the content type negotiation
// is something I think belongs in the underlying core api
req.pushFile('favicon.ico')
req.pushJSON('/foo.json', {
message: await req.body()
})
// Frameworks could do whatever templating they saw fit,
// delivering the resulting string/buffer to `req.respond()`
// In this example I am assuming a "return response" approach,
// and the `.sendFile` call would return a `Response`
// object, and the middleware/routing layer would use that
// object to actually send
return req.sendFile('index.html', {
// template data
})
})
.listen(8443)
'use strict'
const http = require('http-next')
const fs = require('fs')
http.createServer({
allowHTTP1: true,
allowHTTP2: true,
allowHTTP3: true,
key: fs.readFileSync('localhost-privkey.pem'),
cert: fs.readFileSync('localhost-cert.pem')
})
.on('session', (session) => {
// I am not even sure what an end user would do with session.
// Is it ok to store application state on? Like if a user cookie
// was present in a request, could you load the user metadata onto
// the session object?
// Seems like a very useful feature with session pinning on your LB
// and something like session.locals = Object.create(null) where users
// could load on to.
// And none of this would apply in http1, so there is also that to consider.
session.on('stream', (stream, headers, flags) => {
// Do a push stream if allowed
// This covers the cases for no support in http1, http2 settings
// and could even go from true to false when http3's max push is hit
if (stream.pushAllowed) {
stream.pushStream({
path: '/favicon.ico'
}, (err, pushStream, headers) => {
if (err) {
throw err
}
// Even in HTTP1 noone liked the way statusCode works!
// I honeslty think we can do it all in one method signature
// again with this, it is like 90% uf use cases
pushStream.respond(200, {
'content-type': 'image/x-icon'
}, ourPreloadedFaviconBuffer)
})
}
// A conviencence method for consuming the entire body
// This covers like 90% of use cases
// Also, we could add .consumeAsJSON to map to the new .respondWithJSON?
stream.consume((body) => {
// Always check again, because if we hit the max streams this would change
// Actually, I wonder if it would be better for `stream.pushStream` to do
// this check internally then just return false if it was not allowed?
if (stream.pushAllowed) {
stream.pushStream({
path: '/foo.json'
}, (err, pushStream, headers) => {
if (err) {
throw err
}
// Again with this, it is like 90% uf use cases
pushStream.respondWithJSON({
message: body
})
})
}
// The basic response with html
stream.respondWithFile('index.html', {
// This should be infered from the file name if not specified
// 'content-type': 'text/html; charset=utf-8'
})
})
})
})
.listen(8443)
const http = require('http-next')
module.exports = function (opts) {
return new Application()
}
class Application {
constructor () {
this.stack = []
for (const method of http.METHODS) {
this[method] = this.use.bind(this, method)
}
this.server = http.createServer()
this.server.onRequest(this.onRequest)
}
use (method, path, fnc) {
this.stack.push({ path, method, fnc })
}
async onRequest (ctx) {
// Accept and parse bodies
if (ctx.method !== 'HEAD' && ctx.method !== 'GET') {
// Read body
const _body = []
for await (const chunk of ctx.body) {
_body.push(chunk)
}
let body = Buffer.from(..._body)
if (ctx.headers['content-type'] === 'application/json') {
body = JSON.parse(body.toString())
}
ctx.body = body
}
for (const layer of this.stack) {
if (layer.method && ctx.method !== layer.method) {
continue
}
if (layer.path && ctx.path !== layer.path) {
continue
}
let statusCode = 200
let headers = {}
let body = null
const ret = await layer.fnc(ctx)
if (ret === undefined) {
continue
}
if (Number.isFinite(ret)) {
statusCode = ret
let msg = ''
switch (statusCode) {
case 200:
msg = 'Success'
break
case 404:
msg = 'Not Found'
break
case 500:
msg = 'Server Error'
break
}
body = Buffer.from(msg)
headers = {
'content-type': 'text/plain',
'content-length': body.length
}
} else if (typeof ret !== 'undefined' && typeof ret === 'object') {
body = Buffer.from(JSON.stringify(ret))
headers = {
'content-type': 'application/json',
'content-length': body.length
}
} else if (typeof ret === 'string') {
body = Buffer.from(ret, 'utf8')
headers = {
'content-type': 'text/plain',
'content-length': body.length
}
}
return ctx.respondWith(statusCode, headers, body)
}
}
listen (...args) {
return this.server.listen(...args)
}
}
@wesleytodd
Copy link
Author

This is a great diagram! And totally aligns with how I have grown to think about it lately. One addition I would make is to add http1 and https to the quic protocol layer so we understand what the future will look like in a complete sense.

I might also think about naming a bit more, maybe: transport, protocol, foundation, user and framework apis? My nit pick on this is that the word core is ambiguous, as in the end I think we would expose all but the framework api as a part of "node core" which would lead the double usage of "core" to confuse people.

@ronag
Copy link

ronag commented Sep 7, 2020

This is a great diagram! And totally aligns with how I have grown to think about it lately. One addition I would make is to add http1 and https to the quic protocol layer so we understand what the future will look like in a complete sense.

I might also think about naming a bit more, maybe: transport, protocol, foundation, user and framework apis? My nit pick on this is that the word core is ambiguous, as in the end I think we would expose all but the framework api as a part of "node core" which would lead the double usage of "core" to confuse people.

I agree with all of the above.

@ronag
Copy link

ronag commented Sep 7, 2020

transport protocol core working group user
net http low level generic api high level generic api framework api
tls https
quic http2
http3

@ronag
Copy link

ronag commented Sep 7, 2020

Applying this to undici it would look something like:

transport protocol core working group user
net http dispatch request, stream, connect, upgrade got
tls https

@wesleytodd
Copy link
Author

A few proposed updates from our call:

transport protocol core working group user
tcp http quic compat api high level generic api framework api
tls https
quic
httpNext.createServer({})

http.createServer({
  protocol: 'quic+http1',
  port: 1234
}).on('session', (sess) => fmw.emit('session', sess))

http.createServer({
  protocol: 'https'
}).on('session', (sess) => fmw.emit('session', sess)) 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment