Skip to content

Persisted Queries

Maxwell edited this page Jun 28, 2020 · 2 revisions

Automatic Persisted Queries

Persisted queries allows clients to use HTTP GET instead of HTTP POST, making it easier to cache in a CDN (which might not allow caching of HTTP POST). The automated part is that the protocol allows clients to register new query id:s on the fly, so they do not have to be known by the server beforehand.

Apollo outlines the protocol in a blog post: Improve GraphQL Performance with Automatic Persisted Queries. And the exact protocol can be found here: Persisted Query support with Apollo Link

Due to the declarative nature of Retrofit services and that the details of the protocol can differ depending on the backend, It would not make sense if this library did the persisted query negotiation under the hood. Therefore it requires some setup on your part.

Implementation

Since one query potentially require two web requests, there will also be two methods in the Retrofit service declaration

    @GET("graphql")
    @Headers("Content-Type: application/json")
    fun getTrendingPersistedQuery(@Query("extensions") String extensions, @Query("operationName") String operationName, @Query("variables") String variables): Call<GraphContainer<TrendingFeed>>

    @POST("graphql")
    @GraphQuery("Trending")
    @Headers("Content-Type: application/json")
    fun getTrending(@Body QueryContainerBuilder queryContainerBuilder): Call<GraphContainer<TrendingFeed>>

Additionally, the protocol relies on the client sending a SHA256 hash calculated from the query. Only the contents of the query, not the whole web request body. You can use the PersistedQueryHashCalculator for this.

    private fun persistedQueryRequest() {
        val queryName = "Trending"
        val persistedQueryHashCalculator = PersistedQueryHashCalculator(context?.applicationContext)
        val queryContainerBuilder = QueryContainerBuilder()
                .setOperationName(queryName)
                .putVariable("type", feedType)
                .putPersistedQueryHash(persistedQueryHashCalculator.getOrCreateAPQHash(queryName).orEmpty())

        val container = queryContainerBuilder.build()
        val queryParameters: PersistedQueryUrlParameters = PersistedQueryUrlParameterBuilder(container, Gson()).build()

        val indexModel = WebFactory.getInstance(context)
                .createService(IndexModel::class.java)

        val persistedQueryRequest = indexModel.getTrendingUsingPersistedQuery(queryParameters.extensions, queryParameters.operationName, queryParameters.variables)
        val fallbackRequest = indexModel.getTrending(queryContainerBuilder)

        doPersistedQueryNegotiation(
                persistedQueryRequest,
                fallbackRequest,
                result = { trendingFeed: TrendingFeed? ->
                    // do something with TrendingFeed
                },
                error = { error: Throwable? ->
                    // handle error
                }
        )
    }

Assuming that you use retrofit2.Call as your way of performing asynchronous web requests using Retrofit, an implementation of the negotiation part could look as such:

    private fun <T> doPersistedQueryNegotiation(request: Call<GraphContainer<T>>,
                                                fallbackRequest: Call<GraphContainer<T>>,
                                                result: (T?) -> Unit,
                                                error: (Throwable?) -> Unit) {

        request.enqueue(object : retrofit2.Callback<GraphContainer<T>> {
            override fun onResponse(call: Call<GraphContainer<T>>, response: Response<GraphContainer<T>>) {
                if (shouldUseFallbackRequest(response.body())) {
                    // using fallback request
                    fallbackRequest.enqueue(object : retrofit2.Callback<GraphContainer<T>> {
                        override fun onResponse(call: Call<GraphContainer<T>>, response: Response<GraphContainer<T>>) {
                            // persisted Query successfully registered
                            handleResponse(response, result, error)
                        }
                        override fun onFailure(call: Call<GraphContainer<T>>, t: Throwable) {
                            error.invoke(t)
                        }
                    })
                } else {
                    // persisted Query responded successfully
                    handleResponse(response, result, error)
                }
            }
            override fun onFailure(call: Call<GraphContainer<T>>, t: Throwable) {
                // Error during initial persisted query request
                error.invoke(t)
            }
        })
    }

    private fun <T> shouldUseFallbackRequest(graphContainer: GraphContainer<T>?): Boolean {
        if (graphContainer!!.errors != null) {
            for ((message) in graphContainer.errors!!) {
                if (PersistedQueryErrors.APQ_QUERY_NOT_FOUND_ERROR.equals(message!!, ignoreCase = true)) {
                    // Persisted query not found
                    return true
                } else if (PersistedQueryErrors.APQ_NOT_SUPPORTED_ERROR.equals(message, ignoreCase = true)) {
                    // Persisted query not supported
                    return true
                }
            }
        }
        return false
    }

    private fun <T> handleResponse(response: Response<GraphContainer<T>>, result: (T?) -> Unit, error: (Throwable?) -> Unit) {
        if (response.isSuccessful && response.body()?.errors.isNullOrEmpty()) {
            result.invoke(response.body()?.data)
        } else {
            error.invoke(HttpException(response))
        }
    }
Clone this wiki locally