Skip to content

Commit

Permalink
feat(http2): basic HTTP2 support for event-stream (#1009)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjie committed Feb 28, 2019
1 parent d0c4674 commit 3ebea16
Show file tree
Hide file tree
Showing 9 changed files with 624 additions and 66 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"prettier": "^1.15.2",
"source-map-support": "^0.4.6",
"style-loader": "^0.23.0",
"supertest": "^2.0.1",
"superagent": "^4.1.0",
"ts-node": "^2.0.0",
"tslint": "^5.10.0",
"tslint-config-prettier": "^1.14.0",
Expand All @@ -112,7 +112,8 @@
},
"moduleFileExtensions": [
"ts",
"js"
"js",
"json"
],
"setupFiles": [
"<rootDir>/resources/jest-setup.js"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,54 @@ Array [
]
`;

exports[`fastify-http2 will report an extended error when extendedErrors is enabled 1`] = `
Array [
Object {
"detail": "test detail",
"errcode": "12345",
"extensions": Object {
"exception": Object {
"detail": "test detail",
"errcode": "12345",
"hint": "test hint",
},
"testingExtensions": true,
},
"hint": "test hint",
"locations": Array [
Object {
"column": 2,
"line": 1,
},
],
"message": "test message",
"path": Array [
"testError",
],
},
]
`;

exports[`fastify-http2 will report standard error when extendedErrors is not enabled 1`] = `
Array [
Object {
"extensions": Object {
"testingExtensions": true,
},
"locations": Array [
Object {
"column": 2,
"line": 1,
},
],
"message": "test message",
"path": Array [
"testError",
],
},
]
`;

exports[`http will report an extended error when extendedErrors is enabled 1`] = `
Array [
Object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';
import { $$pgClient } from '../../../postgres/inventory/pgClientFromContext';
import createPostGraphileHttpRequestHandler from '../createPostGraphileHttpRequestHandler';
import request from './supertest';

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const http = require('http');
const request = require('supertest');
const http2 = require('http2');
const connect = require('connect');
const express = require('express');
const compress = require('koa-compress');
const koa = require('koa');
const koaMount = require('koa-mount');
const fastify = require('fastify');
// tslint:disable-next-line variable-name
const EventEmitter = require('events');

const shortString = 'User_Running_These_Tests';
// Default bodySizeLimit is 100kB
Expand Down Expand Up @@ -130,33 +135,61 @@ const serverCreators = new Map([
return server;
},
],
[
'koa',
(handler, options = {}, subpath) => {
const app = new koa();
if (options.onPreCreate) options.onPreCreate(app);
if (subpath) {
app.use(koaMount(subpath, handler));
} else {
app.use(handler);
}
return http.createServer(app.callback());
},
],
[
'fastify-http2',
async (handler, _options, subpath) => {
let server;
function serverFactory(fastlyHandler, opts) {
if (server) throw new Error('Factory called twice');
server = http2.createServer({}, (req, res) => {
fastlyHandler(req, res);
});
return server;
}
const app = fastify({ serverFactory, http2: true });
if (subpath) {
throw new Error('Fastify does not support subpath at this time');
} else {
app.use(handler);
}
await app.ready();
if (!server) {
throw new Error('Fastify server not created!');
}
server._http2 = true;
return server;
},
],
]);

serverCreators.set('koa', (handler, options = {}, subpath) => {
const app = new koa();
if (options.onPreCreate) options.onPreCreate(app);
if (subpath) {
app.use(koaMount(subpath, handler));
} else {
app.use(handler);
}
return http.createServer(app.callback());
});

const toTest = [];
for (const [name, createServerFromHandler] of Array.from(serverCreators)) {
toTest.push({ name, createServerFromHandler });
if (name !== 'http' && name !== 'fastify') {
if (name !== 'http' && name !== 'fastify' && name !== 'fastify-http2') {
toTest.push({ name, createServerFromHandler, subpath: '/path/to/mount' });
}
}

for (const { name, createServerFromHandler, subpath = '' } of toTest) {
const createServer = (handlerOptions, serverOptions) =>
createServerFromHandler(
const createServer = async (handlerOptions, serverOptions) => {
const _emitter = new EventEmitter();
const server = await createServerFromHandler(
createPostGraphileHttpRequestHandler(
Object.assign(
{},
{ _emitter },
subpath
? {
externalUrlBase: subpath,
Expand All @@ -169,6 +202,9 @@ for (const { name, createServerFromHandler, subpath = '' } of toTest) {
serverOptions,
subpath,
);
server._emitter = _emitter;
return server;
};

describe(name + (subpath ? ` (@${subpath})` : ''), () => {
test('will 404 for route other than that specified 1', async () => {
Expand Down Expand Up @@ -248,6 +284,7 @@ for (const { name, createServerFromHandler, subpath = '' } of toTest) {
const server = await createServer({ enableCors: true });
await request(server)
.post(`${subpath}/graphql`)
.expect(400)
.expect('Access-Control-Allow-Origin', '*')
.expect('Access-Control-Allow-Methods', 'HEAD, GET, POST')
.expect('Access-Control-Allow-Headers', /Accept, Authorization, X-Apollo-Tracing/)
Expand Down Expand Up @@ -852,6 +889,22 @@ for (const { name, createServerFromHandler, subpath = '' } of toTest) {
.expect(405);
});

test('will return an event-stream', async () => {
const server = await createServer({ graphiql: true, watchPg: true });
const promise = request(server)
.get(`${subpath}/graphql/stream`)
.set('Accept', 'text/event-stream')
.expect(200)
.expect('event: open\n\nevent: change\ndata: schema\n\n')
.then(res => res); // Trick superagent into finishing
await sleep(200);
server._emitter.emit('schemas:changed');
await sleep(100);
server._emitter.emit('test:close');

return await promise;
});

test('will render GraphiQL if enabled', async () => {
const server1 = await createServer();
const server2 = await createServer({ graphiql: true });
Expand Down Expand Up @@ -1010,12 +1063,14 @@ for (const { name, createServerFromHandler, subpath = '' } of toTest) {
.expect('Content-Type', /json/)
.expect({ data: { hello: 'foo' } });
expect(additionalGraphQLContextFromRequest).toHaveBeenCalledTimes(1);
expect(additionalGraphQLContextFromRequest.mock.calls[0][0]).toBeInstanceOf(
http.IncomingMessage,
);
expect(additionalGraphQLContextFromRequest.mock.calls[0][1]).toBeInstanceOf(
http.ServerResponse,
);
const [req, res] = additionalGraphQLContextFromRequest.mock.calls[0];
if (req.httpVersionMajor > 1) {
expect(req).toBeInstanceOf(http2.Http2ServerRequest);
expect(res).toBeInstanceOf(http2.Http2ServerResponse);
} else {
expect(req).toBeInstanceOf(http.IncomingMessage);
expect(res).toBeInstanceOf(http.ServerResponse);
}
});

if (name === 'koa') {
Expand Down
35 changes: 35 additions & 0 deletions src/postgraphile/http/__tests__/supertest/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// tslint:disable no-any
import * as http from 'http';
import Test from './lib/test';
import agent from './lib/agent';

const methods = http.METHODS.map(m => m.toLowerCase());

/**
* Test against the given `app`,
* returning a new `Test`.
*
* @param {Function|Server} app
* @return {Test}
* @api public
*/
export default (app: any) => {
const obj: any = {};

if (typeof app === 'function') {
app = http.createServer(app); // eslint-disable-line no-param-reassign
}

methods.forEach(method => {
obj[method] = (url: string) => {
return new Test(app, method, url);
};
});

// Support previous use of del
obj.del = obj.delete;

return obj;
};

export { Test, agent };
65 changes: 65 additions & 0 deletions src/postgraphile/http/__tests__/supertest/lib/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// tslint:disable
/**
* Module dependencies.
*/

import { agent as Agent } from 'superagent';
import * as http from 'http';
import Test from './test';

const methods = http.METHODS.map(m => m.toLowerCase());

/**
* Initialize a new `TestAgent`.
*
* @param {Function|Server} app
* @param {Object} options
* @api public
*/

function TestAgent(app: any, options: any) {
// @ts-ignore
if (!(this instanceof TestAgent)) return new TestAgent(app, options);
if (typeof app === 'function') app = http.createServer(app); // eslint-disable-line no-param-reassign
if (options) {
this._ca = options.ca;
this._key = options.key;
this._cert = options.cert;
}
Agent.call(this);
this.app = app;
}

/**
* Inherits from `Agent.prototype`.
*/

Object.setPrototypeOf(TestAgent.prototype, Agent.prototype);

// set a host name
TestAgent.prototype.host = function(host: any) {
this._host = host;
return this;
};

// override HTTP verb methods
methods.forEach(method => {
TestAgent.prototype[method] = function(url: any, _fn: any) {
// eslint-disable-line no-unused-vars
var req = new Test(this.app, method.toUpperCase(), url, this._host);
req.ca(this._ca);
req.cert(this._cert);
req.key(this._key);

req.on('response', this._saveCookies.bind(this));
req.on('redirect', this._saveCookies.bind(this));
req.on('redirect', this._attachCookies.bind(this, req));
this._attachCookies(req);

return req;
};
});

TestAgent.prototype.del = TestAgent.prototype.delete;

export default TestAgent;
Loading

0 comments on commit 3ebea16

Please sign in to comment.