GraphQL, Auto Persisted Queries, CDN Support and Getting it work on Native App.

Henry Hong
SCMP — Inside the Wonton
7 min readAug 1, 2019

The challenges of API Integration

To cater the rapidly changed requirements and short release cycle nowadays, we need a powerful version controlled API yet optimized to be high performance in terms of latency and data consumption.

App loading time and latency are known as performance metric, one of the key metric to measure your native app’s success.

The data requires on mobile depends on the UI on frontend. To optimize the data usage, there will be API works to be done for frontend changes and introduces work dependencies as well as project’s uncertainty.

Facebook’s Solution — GraphQL

Graphql was developed by Facebook internally and have been using on Facebook Apps since 2012. The Graphql specs is open source since 2015. It reduces the works need to be done on API by allowing client sending API requests along with query document that specify what data to be returned. It’s very suitable for agile environment that rapidly changes of requirements.

There was also a considerable amount of code to write on both the server to prepare the data and on the client to parse it. This frustration inspired a few of us to start the project that ultimately became GraphQL. GraphQL was our opportunity to rethink mobile app data-fetching from the perspective of product designers and developers. It moved the focus of development to the client apps, where designers and developers spend their time and attention.

ref. https://code.fb.com/core-data/graphql-a-data-query-language/

Overhead

However, there is one drawback using Graphql in production. Query document can be large, sending each request with query document increases API overhead and server loading.

Request can be expansive

Performance issues

It will be running into server performance or even availability issues eventually and surprise you with the upcoming server bills.

Especially for News industry like SCMP which will be constantly pushing Breaking News notifications to a large amount of users, creating sudden network spikes at anytime.

High Latency for Peaks due to Breaking News
Redundant traffic and data consumption on client

Persisted Queries

Persisted Queries is designed to address this issue. `sha256hash` value will be generated with the query document. The query document will be stored in a table in server side, it can be lookup by corresponding hash value.

// payload example for a GraphQL Query Request
{
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "53e3b...fba805ce"
}
},
"variables": {
"name": "2.....0"
}
}

Because the size of sha256hash is fixed regardless of the size of the query document, thus size of request body is tremendously reduced and restricted. From the above sample request of getting a list of topic data, the POST body size is reduced around 88%

Normal Queries vs Persisted Queries

There are two steps behind the scene.

  1. pre-config query docuement mappings on server manually
  2. request with corresponding hash value and variables

Auto Persisted Queries

Since the query document changes from time to time, maintaining the query mapping manually is not practical at all.
Auto Persisted Queries (aka. APQs) allows the query mapping record to be added from a normal request.

So, How does APQs work?

  1. Client sends APQs request with a hash value of the query document, server lookup the query table for corresponding query document.
  • if query is found, fetch data with the query document and return result.
  • otherwise, return a special error message

2. Client handle the special error by retrying request with the hash value together with the query document. Server process the query, return data and create a record in the mapping table.

3. In order to maintain a lightweight table, server has to periodically expiry the mapping records.

Graphql APQs interactive diagram

Let’s take a look at a payload sample

{
"query": "query HeroName($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n }\n}",
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671"
}
},
"variables": {
"episode": "EMPIRE"
}
}

`query` and `sha256Hash` are pre-defined after GraphQL run script is proceed and translated into API class file according to different language eg. swift, Java, Scala, typescript etc.

`variables` is parameter we construct in runtime.

Implementation of GraphQLOperation

To enabled Auto Persisted Query, we need to make sure the above data are available in generated class files. (Instruction will be provide below).

APQs on Native App

Despite the fact that GraphQL was originally designed for native app, APQs was not yet implemented or working well in Apollo Mobile SDK.

Ironically, web open-source project Apollo client is already fully support APQs.

The APQs status in Apollo OpenSource Projects

Apollo android: Support, but issues found when working with CDN. (#1055, #1317) ( fixed on release 1.0.1 16July2019)

Apollo iOS: Not implemented.

Ok, there is nothing can stop us from moving forward. To resolve the issues, getting APQs in place, we have to breakdown into two objectives and focus on one platform each time.

Two objectives

  1. Enable/ Implement APQs
  2. Integrate APQs with CDN

Android

  1. APQs works well with enableAutoPersistedQueries set to true
ApolloClient.builder()
.enableAutoPersistedQueries(true)
.build()

2. To integrate with CDN, we need to send GET Request to server referring to Cloudflare Integration

However, we found there is a bug preventing ApolloClient to send GET request. It’s still sending POST even useHttpGetMethodForQueries set to true

ApolloClient.builder()
.useHttpGetMethodForQueries(true)
.enableAutoPersistedQueries(true)
.build()

And even the bug is fixed, it will most likely introduce another issue of exceeding maximum URI length by sending large query document with HttpGET.

Various ad hoc limitations on request-line length are found in
practice. It is RECOMMENDED that all HTTP senders and recipients
support, at a minimum, request-line lengths of 8000 octets.

Extremely long URLs are usually a mistake. URLs over 2,000 characters will not work in the most popular web browsers. Don’t use them if you intend your site to work for the majority of Internet users.

ref. Credit to Paul Dixon’s answer and RFC 2616, RFC 7230

// GET query string sample
// When query is missing or Not support
https://<graphql_endpoint>?query=<the_very_large_query_document>&variable=<query variable>&extension=<extension>
// When query exists in table
https://<graphql_endpoint>?variable=<query variable>&extension=<extension>

Solution to CDN Integration

Cloudflare requires URI not exceeding 32KB

414 URI Too Long (RFC7231)

Refusal from the server that the URI was too long to be processed. For example, if a client is attempting a GETrequest with an unusually long URI after a POST, this could be seen as a security risk and a 414 gets generated.

Cloudflare will generate this response for a URI longer than 32KB

ref. https://support.cloudflare.com/hc/en-us/articles/115003014512-4xx-Client-Error#code_414

To prevent URI too long, we should only send Query document with POST Request.

  1. Sends GET for hashed APQs request
  2. If failed, retry with POST with full query document.

We could refer to useGETForHashedQueries in apollo-link-persisted-queries.

useGETForHashedQueries: set to true to use the HTTP GET method when sending the hashed version of queries (but not for mutations). GET requests require apollo-link-http 1.4.0 or newer, and are not compatible with apollo-link-batch-http.

ref. https://github.com/apollographql/apollo-link-persisted-queries

Coding for Android SDK

We’ve done a fix for CDN support and it can be found in SCMP Github and PR#1376 to Apollo official.

#updated: it’s merged and including in apollo-android 1.0.1 Release.

// usage
ApolloClient.builder()
.useHttpGetMethodForPersistedQueries(true)
.enableAutoPersistedQueries(true)
.build()

Coding for iOS SDK

We’ve implemeneted the APQs and have it support with CDN according to the solution mentioned above, it can be found in support_get_apqs_working and PR#608, #583 to Apollo official.

#warning it’s still under code review and not finalized to release, even we have ran some tests on our App, use on your own risk!

Usage

  1. Import the fork Apollo SDK above
  2. Update apollo run-script to generate operationIdentifier (sha256hash)

Import Apollo with `cocoapods`

// Podfile
pod ‘Apollo’, :git => ‘https://github.com/scmp-contributor/apollo-ios.git', :commit => ‘04e9ca58da9257e89294cc29fec2cacec3ea9e90’
// Usage
let networkTransport = HTTPNetworkTransport(
url: url,
configuration: sessionConfiguration,
useGETForQueries: false,
enableAutoPersistedQueries: true,
useGETForPersistedQueryRetry: true)

let client = ApolloClient(networkTransport: networkTransport)

Generating operationIdentifiers

operationIdentifier is missing in API.swift

To generate the operationIdentifiers, we need to modify the `apollo runscript` in xcode build phase to include operationIdsPath.

$(find $APOLLO_FRAMEWORK_PATH/ -name 'check-and-run-apollo-cli.sh') codegen:generate --queries="$(find . -name '*.graphql')" --schema=schema.json --operationIdsPath=operationIdsPath.json --mergeInFieldsFromFragmentSpreads API.swift
operationIdentifier is generated in API.swift

Now GraphQL APQs with CDN for native apps are good to go! Enjoy!

Charles logs

--

--

Henry Hong
SCMP — Inside the Wonton

Mobile Architect | Product @ SCMP | With great power comes great responsibility