Last active
September 18, 2020 15:55
-
-
Save wesleytodd/e5642c0d39fa71bdebf8ef31ddbd5e40 to your computer and use it in GitHub Desktop.
Just noodling on the future Node.js and http
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
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
andhttps
to thequic
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
andframework
apis? My nit pick on this is that the wordcore
is ambiguous, as in the end I think we would expose all but theframework
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.
transport | protocol | core | working group | user |
---|---|---|---|---|
net | http | low level generic api | high level generic api | framework api |
tls | https | |||
quic | http2 | |||
http3 |
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 |
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
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
andhttps
to thequic
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
andframework
apis? My nit pick on this is that the wordcore
is ambiguous, as in the end I think we would expose all but theframework
api as a part of "node core" which would lead the double usage of "core" to confuse people.