mune diary

Hello, World!

ApolloClient(GraphQL) ApolloInterceptor での token の更新処理

概要

GraphQL で ApolloClient を用いている場合の token の更新処理は ApolloInterceptor を利用して実行させることができます。

kotlin: 1.6.0
apollo-kotlin(apollo3): 3.2.2

Interceptor(ApolloInterceptor) とは

「送出されるリクエストとそれに対応するレスポンスが戻ってくるのを観察し、変更し、そして短絡させることができる」(OkHttp のドキュメントより)OkHttp または Apollo クライアントにひとつまたは複数のインターセプターを追加すると、リクエストとレスポンスはそれらを経由するようになります。つまり Interceptor でレスポンスを購読して AUTHENTICATION_ERROR などの Error code が含まれていれば token の更新処理をその都度実行することができます。ApolloInterceptor も役割として同じです。

ApolloInterceptor の実装例

以下のような GraphQL のドキュメント通りのエラーレスポンスが返ることを前提に紹介します。

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["hero", "heroFriends", 1, "name"],
      "extensions": {
        "code": "AUTHENTICATION_ERROR",
        "timestamp": "Fri Feb 9 14:33:09 UTC 2018"
      }
    }
  ]
}

次のコードは ApolloInterceptor の実装例です。リクエストにAuthorization Headerで token を付与する必要がある API を想定しています。(token 更新は REST API )

class ApolloRefreshTokenInterceptor(
  private val authorizationRepository: AuthorizationRepository
) : ApolloInterceptor {

  private val mutex = Mutex()

  private var retryCount = 0

  @OptIn(FlowPreview::class)
  override fun <D : Operation.Data> intercept(
    request: ApolloRequest<D>,
    chain: ApolloInterceptorChain
  ): Flow<ApolloResponse<D>> {
    return chain.proceed(request).flatMapConcat { response ->
      when {
        retryCount >= MAX_RETRY_COUNT -> flowOf(response)
        response.hasAuthenticationError() -> {
          val token = mutex.withLock {
            // 更新処理
            authorizationRepository.refreshToken()
          }
          retryCount += 1

          chain.proceed(
          request.newBuilder()
            .addHttpHeader("Authorization", "Bearer $token")
            .build()
          )
        }
        else -> flowOf(response)
      }
    }
  }

  private fun <D : Operation.Data> ApolloResponse<D>.hasAuthenticationError(): Boolean =
    errors?.any { error ->
      error.extensions.orEmpty().containsValue(AUTHENTICATION_ERROR)
    } ?: false

  companion object {
    private const val MAX_RETRY_COUNT = 3
    private const val AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR"
  }
}

errors の extensions の code が認証エラーであるかどうかを確認します。

private fun <D : Operation.Data> ApolloResponse<D>.hasAuthenticationError(): Boolean =
  errors?.any { error ->
    error.extensions.orEmpty().containsValue(AUTHENTICATION_ERROR)
  } ?: false

authorizationRepository.refreshToken() は suspend 関数という体なので suspend 関数である Mutex.withLock を使用して排他制御で実行します。

val token = mutex.withLock {
  // 更新処理
  authorizationRepository.refreshToken()
}

使い方

Apollo Clientを作成するときに addInterceptor に実装した ApolloInterceptor をセットすることで使用できます。

@Provides
@Singleton
fun provideApolloClient(
  okHttpClient: OkHttpClient,
  serverUrl: String,
  apolloRefreshTokenInterceptor: ApolloInterceptor
): ApolloClient {
return ApolloClient.Builder().apply { 
    okHttpClient(okHttpClient)
    serverUrl(serverUrl)
    addInterceptor(apolloRefreshTokenInterceptor)
  }.build()
}

参考