Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: HTTP server overhaul, improved framework support #1361

Merged
merged 31 commits into from
Oct 28, 2020
Merged

feat: HTTP server overhaul, improved framework support #1361

merged 31 commits into from
Oct 28, 2020

Conversation

benjie
Copy link
Member

@benjie benjie commented Oct 2, 2020

Description

Adds support for restify, and overhauls the HTTP server in general, fixing a number of longstanding issues in the process.

Separate HTTP handlers for /graphql, /graphiql, /graphql/stream and /favicon.ico

Fixes #1354
Fixes #1220

We've broken all the handlers out so you can call them directly. This allows you to opt into the pieces of functionality that you care about, mounting just those things to the relevant routes. E.g. Restify support looks like this:

const restify = require('restify');
const { postgraphile, PostGraphileResponseNode } = require('postgraphile');

const middleware = postgraphile(process.env.DATABASE_URL, 'app_public', { ... });

const app = restify.createServer();

const handlerToMiddleware = handler => (req, res, next) =>
  handler(new PostGraphileResponseNode(req, res, next)).catch(next);

app.opts(middleware.graphqlRoute, handlerToMiddleware(middleware.graphqlRouteHandler));
app.post(middleware.graphqlRoute, handlerToMiddleware(middleware.graphqlRouteHandler));
if (middleware.graphiqlRouteHandler) {
  app.head(middleware.graphiqlRoute, handlerToMiddleware(middleware.graphiqlRouteHandler));
  app.get(middleware.graphiqlRoute, handlerToMiddleware(middleware.graphiqlRouteHandler));
}
if (middleware.faviconRouteHandler) {
  app.get('/favicon.ico', handlerToMiddleware(middleware.faviconRouteHandler));
}
if (middleware.eventStreamRouteHandler) {
  app.opts(middleware.eventStreamRoute, handlerToMiddleware(middleware.eventStreamRouteHandler));
  app.get(middleware.eventStreamRoute, handlerToMiddleware(middleware.eventStreamRouteHandler));
}

app.listen(5000);

The handlerToMiddleware converter takes one of the PostGraphile route handlers as an argument and returns a middleware suitable for Restify, Express, Connect or Node.js's built in HTTP. For Koa and some other servers you'd need a different adaptor; adaptors inherit from PostGraphileResponse and there are two built in adaptors: PostGraphileResponseNode and PostGraphileResponseKoa. The PostGraphile route handlers are:

  • middleware.graphqlRouteHandler - serves GraphQL requests (i.e. those typically to /graphql)
  • middleware.graphiqlRouteHandler - serves the GraphiQL interface (i.e. the HTML typically served from /graphiql)
  • middleware.faviconRouteHandler - serves the PostGraphile favicon
  • middleware.eventStreamRouteHandler - serves the PostGraphile watch-mode event stream pointed to at X-GraphQL-Event-Stream; typically /graphql/stream

URL-rewriting proxy support

Fixes #1232

Previously we confounded two separate concerns with the externalUrlBase option, which meant that people who have proxy servers that rewrite the URLs could not get GraphiQL to work reliably. We now add the externalGraphQLRoute option that explicitly states where a browser would find the GraphQL endpoint relative to the GraphiQL interface, so that GraphiQL can always talk to GraphQL (both over HTTP and websockets). Separately, we've added internal logic that derives the sub-path the middleware was mounted on and passes this detail through to the websocket server such that it should Just Work ™️ (e.g. if you have app.use("/subpath", postgraphile({graphqlRoute: "/gql"})) then PostGraphile will know to listen for websocket connections on /subpath/gql). If you're using enhanceHttpServerWithSubscriptions directly and you mount your middleware on a sub-route within express or similar framework, then you should figure out this path for yourself and pass it as the graphqlRoute parameter, e.g. enhanceHttpServerWithSubscriptions(server, middleware, { graphqlRoute: "/subpath/gql" }).

If the above is a bit confusing, you can see a working proxy + subpath setup in scripts/proxied-test-server.js.

Websocket

Performance impact

Negligible.

Security impact

No known issues.

@benjie
Copy link
Member Author

benjie commented Oct 2, 2020

This works as intended, however I'm currently planning to try and fix our Koa and Fastify support, and I think I can do it as part of this by rewriting these newly introduced middleware.*Handler functions to not depend on req/res (at least not as heavily) such that rather than writing headers/body, PostGraphile can return them to the relevant framework to handle. So it might be a good few days before this is ready for release.

@benjie
Copy link
Member Author

benjie commented Oct 2, 2020

Ref #1220, bb851da, e2fcfe2

@benjie benjie mentioned this pull request Oct 2, 2020
6 tasks
@PaulMcMillan
Copy link

Breaking out the handlers like this looks like an excellent change! It'll make some of our fronting proxy configurations simpler.

@benjie benjie marked this pull request as ready for review October 13, 2020 15:27
@benjie
Copy link
Member Author

benjie commented Oct 13, 2020

I believe this is ready for merge now. Before doing so I'd love some confirmation it works nicely; @PaulMcMillan are you able to test this on your Restify stack? Assuming you have a pretty vanilla setup you should be able to:

  • clone the repo
  • checkout this branch
  • yarn && yarn build && yarn link
  • then in your project, yarn link postgraphile

Let me know if you need more help.

I'd also appreciate any of our existing Koa/Fastify users test it in their setups. I don't intend this to be a breaking change, but it is pretty invasive! CC @madflow @jwickens @cdaringe @MaxDesiatov

@benjie
Copy link
Member Author

benjie commented Oct 13, 2020

In particular, I'm interested to know how it interacts with more complex setups with lots of middlewares (compress, etc)

@MaxDesiatov
Copy link
Contributor

I'm not currently using postgraphile right now, but split handlers are a welcome change. 👍

@PaulMcMillan
Copy link

@benjie I'll work on getting this running in our stack, should be able to give you feedback by mid next week.

@benjie
Copy link
Member Author

benjie commented Oct 15, 2020

@PaulMcMillan excellent 🙌

@madflow
Copy link
Contributor

madflow commented Oct 18, 2020

I guess I am in cc because I logged some Fastify issues in the past. I do not use Fastify anymore with Postgraphile - but I "tried" to make it work with a newly created handler https://github.com/madflow/postgraphile/blob/c74cda85d10220644a30b915524fe9dcb710b2be/src/postgraphile/http/frameworks.ts#L232

This does not work and my ideas what is expected end here ...

InternalServerError: stream encoding should not be set
    at readStream (/home/flo/Projects/Javascript/postgraphile/node_modules/raw-body/index.js:171:17)
    at getRawBody (/home/flo/Projects/Javascript/postgraphile/node_modules/raw-body/index.js:108:12)
    at read (/home/flo/Projects/Javascript/postgraphile/node_modules/body-parser/lib/read.js:77:3)
    at jsonParser (/home/flo/Projects/Javascript/postgraphile/node_modules/body-parser/lib/types/json.js:135:5)
    at /home/flo/Projects/Javascript/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:198:17
    at /home/flo/Projects/Javascript/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:201:30
    at /home/flo/Projects/Javascript/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:194:13
    at /home/flo/Projects/Javascript/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:194:13
    at /home/flo/Projects/Javascript/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:194:13
    at /home/flo/Projects/Javascript/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:204:9

I would assert that I more likely qualify as a Fastify user than a Fastify handler developer ;)

@benjie benjie mentioned this pull request Oct 21, 2020
2 tasks
@benjie
Copy link
Member Author

benjie commented Oct 21, 2020

So it turns out we only had fastify v2 support; and v3 removes the middleware support we were relying on, so I've done fastify v3 support in the same way that restify support will work (using an adaptor much like the one @madflow wrote above).

A fastify v3 PostGraphile server will look something like:

const fastify = require('fastify');
const fastifyFormBodyParser = require('fastify-formbody');
const { postgraphile, PostGraphileResponseFastify3 } = require('postgraphile');

const handler = postgraphile(...);

const app = fastify();

// Natively, Fastify only supports 'application/json' and 'text/plain' content types
// Add application/graphql:
app.addContentTypeParser(
  'application/graphql',
  { parseAs: 'string' },
  (request, payload, done) => done(null, payload),
);
// And application/x-www-data-urlencoded:
app.register(fastifyFormBodyParser);

// Takes a PostGraphile route handler and turns it into a fastify route handler
const convertHandler = handler => (request, reply) =>
  handler(new PostGraphileResponseFastify3(request, reply));

// Wrapping in a function allows you to register these routes under a subpath should you wish
const routes = (router, opts, done) => {
  router.options(handler.graphqlRoute, convertHandler(handler.graphqlRouteHandler));
  router.post(handler.graphqlRoute, convertHandler(handler.graphqlRouteHandler));
  if (handler.graphiqlRouteHandler) {
    router.head(handler.graphiqlRoute, convertHandler(handler.graphiqlRouteHandler));
    router.get(handler.graphiqlRoute, convertHandler(handler.graphiqlRouteHandler));
  }
  if (handler.faviconRouteHandler) {
    router.get('/favicon.ico', convertHandler(handler.faviconRouteHandler));
  }
  if (handler.eventStreamRouteHandler) {
    router.options(handler.eventStreamRoute, convertHandler(handler.eventStreamRouteHandler));
    router.get(handler.eventStreamRoute, convertHandler(handler.eventStreamRouteHandler));
  }
  done();
};

app.register(routes, { prefix: "/api" });

app.listen(3000);

(I wrote this in the GitHub comment box, so let me know if I made mistakes.)

@madflow
Copy link
Contributor

madflow commented Oct 21, 2020

(I wrote this in the GitHub comment box, so let me know if I made mistakes.)

Working!

const fastifyFormBodyParser = require('fastify-formbody');

should be added, if this was intended.

I tested with a few Fastify plugins (etag, cors, helmet) - all work. Very nice!

Subpath via app.register(routes, { prefix: '/api' }); works, too. Graphiql did not work properly with this setup. It was relying on /graphql instead of /api/graphql. This could be an issue with my setup or just unrelated. Letting you know, though.

👍 🥇

@benjie
Copy link
Member Author

benjie commented Oct 21, 2020

Missing line added; thanks! That GraphiQL isn't working means that the originalUrl trickery is not working; and that makes sense because I couldn't track down how to accomplish that in Fastify. I'll see if I can add that to the tests so we can detect whether or not GraphiQL should work. That'd be a neat trick!

@benjie
Copy link
Member Author

benjie commented Oct 21, 2020

(And thanks so much for taking the time to test this!)

@PaulMcMillan
Copy link

I got this working within our Restify framework, and the parts I tested all seemed to work well.

I didn't test the eventStreamRoute functionality since our ecosystem makes websockets tricky.

@benjie
Copy link
Member Author

benjie commented Oct 27, 2020

Thanks @PaulMcMillan! (Technically the eventStreamRoute is for event streams/server-sent events which are completely separate from WebSockets; but you only need them to tell GraphiQL that the GraphQL schema has updated in watch mode, so if you're not using that functionality you don't need it.)

@benjie
Copy link
Member Author

benjie commented Oct 27, 2020

Given we've had issues in the past with compression of requests, particularly relating to event streams, I correctly guessed that we would again. To track them down I've built servers for each of the main frameworks we now support:

  • Node (basic HTTP)
  • Express (should also cover connect)
  • Koa
  • Fastify (v3)
  • Restify

For all but node I've made two versions: vanilla and rum-and-raisin. The former is a basic setup, tasty but simple. The latter is full-flavoured, delicious, but with a few bumpy bits. I.e. I've enabled a bunch of plugins to try and cause issues. Among these is the compress plugin for each framework, which reliably made the Event Stream stop working. Where possible I've worked around this in PostGraphile itself by requesting that the event stream does not compress (this is so easy with Koa!) but for the others I've had to include demonstrations of how to do it in the rum-and-raisin.ts files for the eventStreamRoute for each framework.

I had to dig into the code of the various servers to try and figure out how to achieve this. I'd love if people more familiar with these frameworks would like to step forward and let me know how I can achieve this in PostGraphile core without users having to worry about it.

This examples/servers/ folder is great documentation, so it was worth spending a few hours on.

@benjie
Copy link
Member Author

benjie commented Oct 27, 2020

Before I can merge this PR, I need to go back through all these examples and see if they work for subscriptions. 😅

@benjie
Copy link
Member Author

benjie commented Oct 28, 2020

Amazingly subscriptions worked across all 9 servers (vanilla + rum&raisin) without any changes! 🙌

@benjie benjie changed the title feat: HTTP server overhaul / restify support feat: HTTP server overhaul, improved framework support Oct 28, 2020
@benjie benjie merged commit 317aa40 into v4 Oct 28, 2020
@benjie benjie deleted the restify branch October 28, 2020 11:30
@benjie
Copy link
Member Author

benjie commented Oct 28, 2020

These changes are released in [email protected] aka postgraphile@next

const externalUrlBase = options.externalUrlBase || '';
const graphqlRoute = options.graphqlRoute || '/graphql';
const graphqlRoute =
subscriptionServerOptions?.graphqlRoute ||
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This optional chaining syntax does not work for me in the Docker build. I get:

/postgraphile/build-turbo/postgraphile/http/subscriptions.js:40                                                           
    const graphqlRoute = subscriptionServerOptions?.graphqlRoute ||                                                       
                                                   ^
SyntaxError: Unexpected token '.'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for raising this! Fixed in #1376

In future, please file an issue rather than commenting on a closed PR; it makes it easier to track 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants