I’ve collected some links about how the native HTTP/2 module doesn’t work with Express.
- However, this SPDY module does: https://www.npmjs.com/package/spdy
- How to create a SPDY server: https://webapplog.com/http2-node/
- Should be fixed in 5.0: https://github.com/molnarg/node-http2/issues/100#issuecomment-98520102
- Express 5.0 alpha release: https://github.com/expressjs/express/releases
- Comment: For now (while we are waiting for Express v5) you can use spdy with express.
- If using steal-tools live-reload, you will need to pass along the key/cert: see source code.
- You may also consider using fastify instead of express: https://github.com/SAP/ui5-server/issues/77
Since I’m using Feathers, I’ll be sticking to using @feathersjs/express
with spdy
until express@5.0
is used and supports the native http2
module, as it seems fastify in feathers might not be straightforward.
Here is what my server code looks like (seems to work as expected):
config/development.json
{
"port": 3030,
"host": "DEV_SERVER_HOST",
"client": "../../client/",
"mongodb": "mongodb://localhost:27017/database",
"key": "localhost+2-key.pem",
"cert": "localhost+2.pem"
}
Note: key/cert generated using https://github.com/FiloSottile/mkcert.
index.js
'use strict';
// ENVIRONMENT
if (process.argv.indexOf('--develop') !== -1) {
process.env.DONE_SSR_DEBUG = 1;
process.env.NODE_ENV = 'development';
}
console.log(`process.env.NODE_ENV = '${process.env.NODE_ENV}'`);
// app server
const fs = require('fs');
const path = require('path');
const spdy = require('spdy');
const app = require('./app');
const host = app.get('host');
const port = app.get('port');
const key = path.resolve(__dirname, app.get('key'));
const cert = path.resolve(__dirname, app.get('cert'));
const server = spdy
.createServer(
{
key: fs.readFileSync(key),
cert: fs.readFileSync(cert),
},
app
)
.listen(port);
app.setup(server);
server.on('listening', () =>
console.log(
`DoneJS and Feathers application started on https://${host}:${port}`
)
);
// live-reload
if (process.argv.indexOf('--develop') !== -1) {
const exec = require('child_process').exec;
const killOnExit = require('kill-on-exit');
const stealTools = path.join('node_modules', '.bin', 'steal-tools');
const child = exec(
`${stealTools} live-reload --ssl-key ${key} --ssl-cert ${cert}`,
{
cwd: app.get('client'),
}
);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
killOnExit(child);
}
app.js
'use strict';
const path = require('path');
const cors = require('cors');
const services = require('./services');
const favicon = require('serve-favicon');
const compression = require('compression');
const middleware = require('./middleware');
const express = require('@feathersjs/express');
const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio');
const configuration = require('@feathersjs/configuration');
const app = express(feathers());
app
.configure(configuration())
.options('*', cors())
.use(cors())
.use(compression())
.use(favicon(path.join(app.get('client'), 'favicon.ico')))
.use(express.json())
.use(express.urlencoded({ extended: true }))
.configure(express.rest())
.configure(socketio())
.configure(services)
.use(express.static(app.get('client')))
.use('/node_modules', (req, res) => {
res.sendStatus(404);
})
.configure(middleware);
app.on('connection', connection => {
app.channel('anonymous').join(connection);
});
app.publish((data, context) => {
return app.channel('anonymous');
});
module.exports = app;
middleware/index.js
'use strict';
const errorHandler = require('./error');
module.exports = function(app) {
if (process.argv.indexOf('--static') !== -1) {
console.log('Configuring server app with static middleware.');
app.configure(require('./static'));
} else {
console.log('Configuring server app with SSR middleware.');
app.configure(require('./ssr'));
}
app.configure(errorHandler);
};
middleware/static.js
'use strict';
const fs = require('fs');
const path = require('path');
module.exports = function(app) {
app.get('*', (req, res, next) => {
const urlParts = path.parse(req.url);
const isPushstateRoute = !urlParts.ext || urlParts.name.includes('?');
if (isPushstateRoute) {
const env = process.env.NODE_ENV || 'production';
const htmlPath = path.join(app.get('client'), `${env}.html`);
if (fs.existsSync(htmlPath)) {
return res.sendFile(htmlPath);
}
}
return next();
});
};
middleware/ssr.js
'use strict';
const path = require('path');
const ssrMiddleware = require('done-ssr-middleware');
module.exports = function(app) {
app.use(
ssrMiddleware(
{
config: path.join(app.get('client'), 'package.json!npm'),
liveReload: process.env.NODE_ENV === 'development',
},
{
strategy: 'incremental',
debug: process.env.NODE_ENV === 'development',
}
)
);
};
Note: using strategy: 'incremental'
for incremental rendering works.
middleware/error.js
'use strict';
const errorFormat = require('donejs-error-format');
module.exports = function(app) {
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
const html = errorFormat.html(errorFormat.extract(err), {
liveReload: process.env.NODE_ENV === 'development',
});
console.error(err.stack);
res
.status(500)
.type('html')
.end(html);
});
};
services/index.js
'use strict';
const mongoose = require('mongoose');
// require services
const contributor = require('./contributor');
module.exports = function(app) {
mongoose.Promise = global.Promise;
// connect to mongodb
if (process.env.TESTING !== 'true') {
mongoose.connect(
app.get('mongodb'),
{
useCreateIndex: true,
useNewUrlParser: true,
reconnectTries: Number.MAX_VALUE,
}
);
}
// configure endpoints
app.configure(contributor);
};
services/contributor/index.js
'use strict';
const Model = require('./model');
const service = require('feathers-mongoose');
module.exports = function(app) {
app.use(
'/api/contributors',
service({
Model: Model,
lean: true,
})
);
app.service('/api/contributors').hooks({
after(context) {
context.data && delete context.data.__v;
},
});
};
services/contributor/model.js
'use strict';
const mongoose = require('mongoose');
const Schema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
active: { type: Boolean },
});
const Model = mongoose.model('Contributor', Schema);
module.exports = Model;
Result
Hope this helps someone else trying to get h2 working with feathers express.
More links about server push (for future self reference):
- https://docs.google.com/document/d/1K0NykTXBbbbTlv60t5MyJvXjqKGsCVNYHyLEXIxYMv0/edit
- https://devcenter.heroku.com/articles/http-routing#http-versions-supported
- https://github.com/google/node-h2-auto-push
- https://stackoverflow.com/a/34319871
- https://github.com/rmurphey/chrome-http2-log-parser
- https://bugs.chromium.org/p/chromium/issues/detail?id=464501
- https://medium.com/totally-tooling-tears/issue-5-http-2-push-9c6eba8d6d7c
- https://github.com/fastify/fastify/blob/master/docs/HTTP2.md
- https://http2-push.appspot.com/
- https://github.com/donejs/done-ssr-middleware/issues/69