Slide 1

Slide 1 text

GraphQL Persisted Queries with HTTP Caching

Slide 2

Slide 2 text

Who Am I • Kevin Jalbert • Work at theScore • Team Lead for the Emerging Platform Team • Using Rails/React and GraphQL • See more of what I'm doing: • github.com/kevinjalbert • twitter/kevinjalbert • kevinjalbert.com Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 2

Slide 3

Slide 3 text

Agenda • Problems with GraphQL • GraphQL Persisted Queries • Implementing Persisted Queries • Start from scratch • Iterative approach • Using React/Apollo, Express, and Rails • Add HTTP Caching Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 3

Slide 4

Slide 4 text

Problems with GraphQL • GraphQL APIs present some security/performance concerns • Exploiting circular/nested queries • Sending inefficient queries • Potentially large query being sent over the network Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 4

Slide 5

Slide 5 text

Possible Solutions • Max Stoiber's article on Securing Your GraphQL API from Malicious Queries: • Depth Limiting: Rejecting queries which are too deeply nested • Amount Limiting: Rejecting queries which ask for too much information • Query Cost Analysis: Rejecting queries which are too expensive • Query Whitelisting: Rejecting queries that are not whitelisted • We are going to focus on Query Whitelisting -- a.k.a Persisted Queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 5

Slide 6

Slide 6 text

When to use Persisted Queries • Want to secure your API from bad queries • Gain efficiency on the network layer • Works best when you: • Can lock your production GraphQL API to only use persisted queries • Own both the server and client (i.e., not a public API) • You still have the GraphQL API flexible during development Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 6

Slide 7

Slide 7 text

• https://twitter.com/leeb/status/829434814402945026 Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 7

Slide 8

Slide 8 text

Using Persisted Queries • A client sends queryId instead of actual query string • The server does a lookup using queryId against set of persisted queries • If a query is found, executes it • If a query is not found, return error { operationName: "CompanyQuery", queryId: "a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80" } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 8

Slide 9

Slide 9 text

Let's Build it! • We will be using the following: • React with Apollo Client • Express with GraphQL Yoga • Rails with GraphQL Ruby • Follow along kevinjalbert/graphql-persisted-queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 9

Slide 10

Slide 10 text

Setup Express Server Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 10

Slide 11

Slide 11 text

// server.js const { GraphQLServer } = require('graphql-yoga') const { typeDefs } = require('./graphql/typeDefs') const { resolvers } = require('./graphql/resolvers') const server = new GraphQLServer({ typeDefs, resolvers }) const options = { port: 5000, endpoint: '/graphql', } server.start(options, ({ port }) => console.log( `Server started, listening on port ${port} for incoming requests.`, ), ) Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 11

Slide 12

Slide 12 text

Setup React Application Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 12

Slide 13

Slide 13 text

// index.js // ... Additional imports import { ApolloClient, InMemoryCache } from 'apollo-boost'; import { ApolloProvider } from 'react-apollo'; import { createHttpLink } from 'apollo-link-http'; const client = new ApolloClient({ link: createHttpLink({ uri: 'http://localhost:5000/graphql' }), cache: new InMemoryCache(), }); const AppWithProvider = () => ( ); ReactDOM.render(, document.getElementById('root')); Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 13

Slide 14

Slide 14 text

// components/ConsoleContainer.js import { Query } from 'react-apollo'; import QUERY from '../graphql/ConsolesByYear.graphql'; const ConsolesAndCompany = ({ afterYear, beforeYear }) => ( {({ data, error, loading }) => { if (error) return 'Error!'; if (loading) return 'Loading'; return ( { data.consoles.map(console => (
)) } ); }} ); Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 14

Slide 15

Slide 15 text

Refactor React Application to use Persisted Queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 15

Slide 16

Slide 16 text

// index.js import { createPersistedQueryLink } from 'apollo-link-persisted-queries'; // ... rest of file const client = new ApolloClient({ link: createPersistedQueryLink().concat( createHttpLink({ uri: 'http://localhost:5000/graphql' }) ), cache: new InMemoryCache(), }); // ... rest of file Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 16

Slide 17

Slide 17 text

{ extensions: { persistedQuery: { version: 1, sha256Hash: "a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80" } }, operationName: "ConsolesByYear", variables: { afterYear: 1990, beforeYear: 1999 } } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 17

Slide 18

Slide 18 text

Extract GraphQL Queries from Client Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 18

Slide 19

Slide 19 text

persistgraphql • An Apollo tool that extracts queries from a client and dumps them to a JSON file. { "": 1, "": 2, "": 3, } • Unfortunately, queries are identified by an auto-incrementing ID Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 19

Slide 20

Slide 20 text

Fixing persistgraphql • Wrap persistgraphql and calculates SHA256 for each query Processes .graphql files under the provided input path and outputs and/or syncs the extracted queries. The signature for each query is the result of SHA256 hashing the query's content and using the hex digest. node index.js --input-path=../react-graphql/src --output-file=./extracted_queries.json Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 20

Slide 21

Slide 21 text

Refactor Express Server to use Persisted Queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 21

Slide 22

Slide 22 text

const { GraphQLServer } = require('graphql-yoga') const bodyParser = require('body-parser'); const { typeDefs } = require('./graphql/typeDefs') const { resolvers } = require('./graphql/resolvers') const { persistedQueriesMiddleware } = require('./persistedQueriesMiddleware') const server = new GraphQLServer({ typeDefs, resolvers }) const options = { port: 5000, endpoint: '/graphql', } server.express.use(bodyParser.json()); server.express.post('/graphql', persistedQueriesMiddleware) server.start(options, ({ port }) => console.log(`Server started, on port ${port}`)) Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 22

Slide 23

Slide 23 text

// persistedQueriesMiddleware.js const { invert } = require('lodash'); const extractedQueries = invert(require('./extracted_queries.json')) persistedQueriesMiddleware = (req, res, next) => { console.log("Handling request to: " + req.url) const querySignature = req.body.extensions.persistedQuery.sha256Hash; const persistedQuery = extractedQueries[querySignature] if (!persistedQuery) { res.status(400).json({ errors: ['Invalid querySignature'] }) return next(new Error('Invalid querySignature')) } req.body.query = persistedQuery next() } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 23

Slide 24

Slide 24 text

Reflection • Persisted queries between React application and Express GraphQL API • Main issue is using a JSON file on the server for the extracted queries • Different clients will have different JSON • Tracking/merging/juggling JSON files • Redeploying server to see changes • We can do better if we synchronize (i.e., client registers) queries to a running server Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 24

Slide 25

Slide 25 text

Setup Rails Server Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 25

Slide 26

Slide 26 text

rails new rails-graphql rails generate graphql:install • Basic Rails setup • Setup Rails Database migration/models • Setup GraphQL types • Follow graph-ruby guides Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 26

Slide 27

Slide 27 text

# app/controllers/graphql_controller.rb class GraphqlController < ApplicationController def execute variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] result = RailsGraphqlSchema.execute(query, variables: variables, operation_name: operation_name) render json: result end private def ensure_hash(ambiguous_param) # ... Generated code provided by graphql-ruby's graphql:install end end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 27

Slide 28

Slide 28 text

Synchronize GraphQL Queries to Rails Server Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 28

Slide 29

Slide 29 text

# app/models/persisted_query.rb class PersistedQuery < ApplicationRecord end # db/migrate/20180617011135_create_persisted_queries.rb class CreatePersistedQueries < ActiveRecord::Migration[5.2] def change create_table :persisted_queries do |t| t.string :signature, index: { unique: true } t.string :query t.timestamps end end end # config/routes.rb Rails.application.routes.draw do post "/graphql_persist", to: "graphql_persist#execute" end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 29

Slide 30

Slide 30 text

# app/controllers/graphql_persist_controller.rb class GraphqlPersistController < ApplicationController def execute document = GraphQL.parse(params[:query]) if valid_query?(document) persisted_query = PersistedQuery.create(signature: params[:signature], query: params[:query]) render json: persisted_query.attributes else render json: { errors: @errors }, status: 500 end end private def valid_query?(document) query = GraphQL::Query.new(RailsGraphqlSchema, document: document) validator = GraphQL::StaticValidation::Validator.new(schema: RailsGraphqlSchema) results = validator.validate(query) errors = results[:errors] || [] @errors = errors.map(&:message) @errors.empty? end end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 30

Slide 31

Slide 31 text

persistgraphql-signature-sync It is possible to sync the persisted queries to a specified endpoint. The endpoint needs to accept a POST request with body parameters of the query and the signature. node index.js --input-path=../react-graphql/src --sync-endpoint=http://localhost:3000/graphql_persist Synching persisted query a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80 { id: 1, signature: 'a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80', query: 'query ConsolesByYear($afterYear: Int, $beforeYear: Int) { ......', created_at: '2018-07-03T19:52:54.717Z', updated_at: '2018-07-03T19:52:54.717Z' } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 31

Slide 32

Slide 32 text

Refactor Rails Server to use Persisted Queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 32

Slide 33

Slide 33 text

# app/controllers/graphql_controller.rb class GraphqlController < ApplicationController def execute variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] if query.present? result = RailsGraphqlSchema.execute(query, variables: variables, operation_name: operation_name) else signature = params.dig(:extensions, :persistedQuery, :sha256Hash) persisted_query = PersistedQuery.find_by!(signature: signature) result = RailsGraphqlSchema.execute(persisted_query.query, variables: variables, operation_name: operation_name) end render json: result end # ... rest of class end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 33

Slide 34

Slide 34 text

Reflection • Persisted queries are working with Rails GraphQL server • Ability to synchronize queries from clients • Future work: • Authentication on persisting endpoint • Persisted queries only in production • graphql-ruby has a pro version which comes with its own persisted query solution Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 34

Slide 35

Slide 35 text

Let's add some HTTP Caching • We need to move from POST to GET requests • Let's not worrying about complicated cache rules • 10-second blanket cache • Assuming we don't have personalized data in responses Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 35

Slide 36

Slide 36 text

Add HTTP Caching to React Application const client = new ApolloClient({ link: createPersistedQueryLink({ useGETForHashedQueries: true }).concat( createHttpLink({ uri: 'http://localhost:5000/graphql' }) ), cache: new InMemoryCache(), }); Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 36

Slide 37

Slide 37 text

Add HTTP Caching to Express Server Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 37

Slide 38

Slide 38 text

// server.js const { GraphQLServer } = require('graphql-yoga') const { typeDefs } = require('./graphql/typeDefs') const { resolvers } = require('./graphql/resolvers') const { persistedQueriesMiddleware } = require('./persistedQueriesMiddleware') const server = new GraphQLServer({ typeDefs, resolvers }) const options = { port: 5000, getEndpoint: true, endpoint: '/graphql', } server.express.get('/graphql', persistedQueriesMiddleware) server.start(options, ({ port }) => console.log(`Server started, on port ${port}`)) Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 38

Slide 39

Slide 39 text

// persistedQueriesMiddleware.js const { invert } = require('lodash'); const extractedQueries = invert(require('./extracted_queries.json')) persistedQueriesMiddleware = (req, res, next) => { console.log('Handling request to: ' + req.url) res.set('Cache-Control', 'public, max-age=10') const extensions = JSON.parse(req.query.extensions) const querySignature = extensions.persistedQuery.sha256Hash; const persistedQuery = extractedQueries[querySignature] if (!persistedQuery) { res.status(400).json({ errors: ['Invalid querySignature'] }) return next(new Error('Invalid querySignature')) } req.query.query = persistedQuery next() } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 39

Slide 40

Slide 40 text

Add HTTP Caching to Rails Server Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 40

Slide 41

Slide 41 text

class GraphqlController < ApplicationController def execute expires_in(10.seconds, public: true) variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] if query.present? result = RailsGraphqlSchema.execute(query, variables: variables, operation_name: operation_name) else extensions = JSON.parse(params[:extensions]) || {} signature = extensions.dig("persistedQuery", "sha256Hash") persisted_query = PersistedQuery.find_by!(signature: signature) result = RailsGraphqlSchema.execute(persisted_query.query, variables: variables, operation_name: operation_name) end render json: result rescue StandardError => e render json: { errors: [e.message] } end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 41

Slide 42

Slide 42 text

Demo • To see the advantage of HTTP caching we need either: • a CDN • a reverse proxy • I have deployed both Express and Rails servers • Both have simple NGINX cache in front of them Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 42

Slide 43

Slide 43 text

Extensions • Apollo Engine and Apollo Cache Control • FastQL • Automatic Persisted Queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 43

Slide 44

Slide 44 text

Questions? Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 44

Slide 45

Slide 45 text

GraphQL Persisted Queries with HTTP Caching Kevin Jalbert