spdy-http2 / node-spdy

SPDY server on Node.js
2.81k stars 196 forks source link

Koa support? #277

Closed kristianmandrup closed 7 years ago

kristianmandrup commented 7 years ago

Is spdy compatible with Koa (express for ES6/7) or what would be required for this to work.

indutny commented 7 years ago

It should be! Please let me know if it doesn't.

kasperlewau commented 7 years ago

Just thought I'd stop by and share some observations I've made in trying to integrate spdy into koa@2.

First, I generate a manifest of assets to be pushed to the client using http2-push-manifest.

{
  "/static/main-9a42a4188a.css": {
    "weight": 1,
    "type": "style"
  },
  "/app/common_2-3574809b05.js": {
    "weight": 1,
    "type": "script"
  },
  "/static/js/primus.js": {
    "weight": 1,
    "type": "script"
  },
  "/bower_components/polymer/polymer.html": {
    "weight": 1,
    "type": "document"
  }
}

Now, tack on a couple o' more assets in there (we've got a total of 150 in this manifest).

Then we move into the koa part where we have a middleware that sets up a Link header containing multiple 'push instructions'(?).

function manifestMw (opts) {
    const manifest = require(path.resolve('.', opts.manifest));

    return function (ctx, next) {
        // If we receive a request for a file _inside_ the manifest, move on to the next middleware.
        if (manifest[ctx.req.url]) {
            return next();
        }

        const links = [];

        Object.keys(manifest).forEach(key => {
            const u = url.resolve(`${ctx.protocol}://${ctx.host}`, key);
            const link = `<${u}>; rel=preload; as=${manifest[key].type}`;
            links.push(link);
        });

        ctx.set('Link', links.join(', '));
        // Link: <https://localhost:3000/static/main-9a42a4188a.css>; rel=preload; as=style, <https://localhost:3000/app/common_2-3574809b05.js>; rel=preload; as=script, <https://localhost:3000/static/js/primus.js>; rel=preload; as=script, <https://localhost:3000/bower_components/polymer/polymer.html>; rel=preload; as=document
        ctx.state.h2 = manifest;

        /**
         * ctx.state.h2 = 
         *    { '/static/main-9a42a4188a.css': { weight: 1, type: 'style' },
         *      '/app/common_2-3574809b05.js': { weight: 1, type: 'script' },
         *      '/static/js/primus.js': { weight: 1, type: 'script' },
         *      '/bower_components/polymer/polymer.html': { weight: 1, type: 'document' } } }
         */

        return next();
    }
}

koa.use(manifestMw('push_manifest.json'));

Then we move on to the next middleware that starts pushing things out to the client:

function pusher () {
    return function (ctx, next) {
        function push (file) {
            const opts = {
                request: {
                    accept: '*/*'
                },
                response: {
                    'content-type': mime.lookup(file)
                }
            };

            const p = ctx.res.push(file, opts, (_, stream) => {
                // cleanup function borrowed from spdy-push
                function cleanup (err) {
                    if (err) {
                        console.error(chalk.red(err.stack));
                    }

                    stream.removeListener('error', cleanup);
                    stream.removeListener('close', cleanup);
                    stream.removeListener('finish', cleanup);

                    destroy(content);
                }

                stream.on('error', cleanup);
                stream.on('close', cleanup);
                stream.on('finish', cleanup);
            });

            const content = fs.createReadStream(process.cwd() + '/dist' + file);
            content.pipe(p);
        }

        if (ctx.req.url === '/') {
            Object.keys(ctx.state.h2.manifest).map(push);
        }

        return next();
    }
}

koa.use(pusher());

And the server itself:

    // construct the koa app with env & CLI flags
    const koa = buildKoa(env, flags);

    const spdyConf = {
        key: fs.readFileSync(`${__dirname}/f.dev.key`),
        cert: fs.readFileSync(`${__dirname}/f.dev.crt`)
    };

    const server = spdy.createServer(spdyConf, koa.callback());

I'm quite new to SPDY Push Streams and how I should be incorporating them into Koa, this is/was my best guess after looking at similar projects that didn't work "out of the box".

I'm quite certain there's a bug or two in here that's causing the following issues that start pouring in on the initial index request: Got RST: PROTOCOL_ERROR's in my cleanup handler.

Stack trace looks as such:

    at Stream._handleRST (/Users/kasperlewau/code/falcon/html2/node_modules/spdy-transport/lib/spdy-transport/stream.js:280:24)
    at Stream._handleFrame (/Users/kasperlewau/code/falcon/html2/node_modules/spdy-transport/lib/spdy-transport/stream.js:126:10)
    at Connection._handleFrame (/Users/kasperlewau/code/falcon/html2/node_modules/spdy-transport/lib/spdy-transport/connection.js:329:12)
    at Parser.<anonymous> (/Users/kasperlewau/code/falcon/html2/node_modules/spdy-transport/lib/spdy-transport/connection.js:161:10)
    at emitOne (events.js:96:13)
    at Parser.emit (events.js:188:7)
    at readableAddChunk (/Users/kasperlewau/code/falcon/html2/node_modules/readable-stream/lib/_stream_readable.js:213:18)
    at Parser.Readable.push (/Users/kasperlewau/code/falcon/html2/node_modules/readable-stream/lib/_stream_readable.js:172:10)
    at Parser.Transform.push (/Users/kasperlewau/code/falcon/html2/node_modules/readable-stream/lib/_stream_transform.js:123:32)
    at next (/Users/kasperlewau/code/falcon/html2/node_modules/spdy-transport/lib/spdy-transport/protocol/base/parser.js:52:12)

packages & versions used;

"koa": "^2.0.0-alpha.5",
"spdy": "^3.4.0"

I followed the code and I got as far as the FLOW_CONTROL_ERROR at _onWindowOverflow - and now my brain is toast. 😆 I'll get back to cleaning the apartment for a couple o' hours and see if I can get any further with this.