https://kotlinlang.org logo
#apollo-kotlin
Title
# apollo-kotlin
d

dorche

01/20/2023, 10:50 PM
My backend is sending Unauthorised wrapped in a 200 response and the actual error in the body. So in my AuthInterceptor I have to read the body to determine if I should refresh my token and re-do the original request. What is the correct way to read the body?
HttpResponse.body
returns BufferedSource and I am not sure how to read it without closing the stream. The json parsing itself is pretty simple so I am fine with manually parsing but I get an exception with my current code
Copy code
body: BufferedSource?
...
body.use { source ->
    BufferedSourceJsonReader(source).use { jsonReader ->
        jsonReader.beginObject()
        ...
    }
}
is what I have now but I get
Copy code
com.apollographql.apollo3.exception.ApolloParseException: Failed to parse GraphQL http network response
	at com.apollographql.apollo3.network.http.HttpNetworkTransport$Companion.wrapThrowableIfNeeded(HttpNetworkTransport.kt:238)
	at com.apollographql.apollo3.network.http.HttpNetworkTransport$Companion.access$wrapThrowableIfNeeded(HttpNetworkTransport.kt:232)
	at com.apollographql.apollo3.network.http.HttpNetworkTransport.singleResponse(HttpNetworkTransport.kt:101)
	at com.apollographql.apollo3.network.http.HttpNetworkTransport.access$singleResponse(HttpNetworkTransport.kt:29)
	at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invokeSuspend(HttpNetworkTransport.kt:83)
	at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invoke(Unknown Source:12)
	at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invoke(Unknown Source:10)
	at kotlinx.coroutines.flow.SafeFlow.collectSafely(Builders.kt:61)
	at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:230)
	at kotlinx.coroutines.flow.internal.ChannelFlowOperatorImpl.flowCollect(ChannelFlow.kt:195)
	at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlow.kt:157)
	at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(Unknown Source:4)
	at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:60)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Caused by: java.lang.IllegalStateException: closed
	at okio.RealBufferedSource.request(RealBufferedSource.kt:203)
	at com.apollographql.apollo3.api.json.BufferedSourceJsonReader.nextNonWhitespace(BufferedSourceJsonReader.kt:808)
	at com.apollographql.apollo3.api.json.BufferedSourceJsonReader.doPeek(BufferedSourceJsonReader.kt:241)
	at com.apollographql.apollo3.api.json.BufferedSourceJsonReader.beginObject(BufferedSourceJsonReader.kt:97)
	at com.apollographql.apollo3.api.internal.ResponseParser.parse(ResponseParser.kt:25)
	at com.apollographql.apollo3.api.Operations.parseJsonResponse(Operations.kt:61)
	at com.apollographql.apollo3.network.http.HttpNetworkTransport.singleResponse(HttpNetworkTransport.kt:96)
	at com.apollographql.apollo3.network.http.HttpNetworkTransport.access$singleResponse(HttpNetworkTransport.kt:29) 
	at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invokeSuspend(HttpNetworkTransport.kt:83) 
	at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invoke(Unknown Source:12) 
	at com.apollographql.apollo3.network.http.HttpNetworkTransport$execute$1.invoke(Unknown Source:10) 
	at kotlinx.coroutines.flow.SafeFlow.collectSafely(Builders.kt:61) 
	at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:230) 
	at kotlinx.coroutines.flow.internal.ChannelFlowOperatorImpl.flowCollect(ChannelFlow.kt:195) 
	at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlow.kt:157) 
	at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(Unknown Source:4) 
	at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:60) 
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) 
	at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42) 
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95) 
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570) 
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750) 
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677) 
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
All the examples I can find use status code so they never have to touch the response body…
m

mbonnin

01/20/2023, 10:55 PM
Mmm that's a good one. You could use an
HttpInterceptor
that just buffers everything
For an example LoggingInterceptor does this here
Thinking about this a bit more, I don't think you can do it without buffering everything actually because trying to parse the Json is going to leave your source in an unknow state on the error case. I would try to see with your backend folks if they could either return a HTTP 401 or use the GraphQL error format (so that the default parser could still work)
s

Stylianos Gakis

01/20/2023, 11:13 PM
Might be interesting for you to read this discussion as well https://kotlinlang.slack.com/archives/C01A6KM1SBZ/p1671543387658819?thread_ts=1670864226.285099&cid=C01A6KM1SBZ I literally had the same problem with backend returning odd responses, so I ended up working with the expiration date of tokens locally and having an interceptor to refresh it pre-emptively before the backend receives a request with the wrong token and returning weird stuff. Is it by any chance possible that you can do the same if your backend folks can’t help you out?
d

dorche

01/20/2023, 11:30 PM
That’s exactly what I had which prompted me to get it changed Martin, I must have copied it from that interceptor earlier. It just hit me yesterday that this must be slowing down the whole interceptor chain because it needs to actually get the response. I tried countless times to get them to change to 401 but they won’t give in. Could you elaborate more on the “GraphQL error format” (sorry been a Rest user all my life)? Perhaps that’s something that they would agree to. Stylianos - problem for me is that this backend sends all errors like this - i.e. business rules or no permission etc. So I think maybe this “GraphQL error format” that Martin mentioned is the only way to go. Does either of you have any reading materials on that?
m

mbonnin

01/20/2023, 11:37 PM
Errors are specified there: https://spec.graphql.org/draft/#sec-Errors
Copy code
{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [{ "line": 6, "column": 7 }],
      "path": ["hero", "heroFriends", 1, "name"]
    }
  ],
If they have this format, then you can read them from
ApolloResponse.errors
It just hit me yesterday that this must be slowing down the whole interceptor chain because it needs to actually get the response.
I think there's no real way around the buffering at least some part of the response. I believe the best you can do if find the minimum amount of characters to read "eagerly" and then reconstruct some okio
Source
from that
Like if your error is not Json, you could just read the first char and decide based on that
d

dorche

01/20/2023, 11:40 PM
Re errors block above That would be for GraphQL specific errors about the data tho, right? How does one communicate errors like Unauthorised or Unauthenticated?
m

mbonnin

01/20/2023, 11:41 PM
You can put the error you want in
message
just send
data: null
:
Copy code
{
  "errors": [
    {
      "message": "Unauthorized",
      "locations": [],
      "path": []
    }
  ],
  "data": null
}
It's "abusing" the spec a bit but it should work
d

dorche

01/20/2023, 11:44 PM
Currently this is what I am getting which is why I attempted to parse it myself to get
type
Copy code
{
  "errors": [
    {
      "message": "Unauthorized: Access is denied.",
      "extensions": {
        "type": "Unauthorized"
      }
    }
  ],
  "data": {}
}
I guess it means I could switch to using
ApolloResponse.errors
and try infering it all from
message
m

mbonnin

01/20/2023, 11:45 PM
You should be able to read the extensions from
response.errors[0].extensions
(source)
d

dorche

01/20/2023, 11:47 PM
My data block actually is not empty, would that trip the parsing?
Copy code
{
  "errors": [
    {
      "message": "Unauthorized: Access is denied.",
      "extensions": {
        "type": "Unauthorized"
      }
    }
  ],
  "data": {
    "someVariable": null
  }
}
m

mbonnin

01/20/2023, 11:47 PM
That should work too, this is actually very valid GraphQL
GraphQL always send 200 because you can have partial responses, it's ok to have data and errors at the same time
d

dorche

01/20/2023, 11:51 PM
Thank you, I’ll rework this madness into using
ApolloResponse.errors
right away then, foolish me coming from rest world and not realising there’s a graphql way
m

mbonnin

01/20/2023, 11:51 PM
TBH the GraphQL 200 error is pretty confusing, it's become kind of a meme on the internet
But once I realized partial data is ok I embraced it 😄
d

dorche

01/20/2023, 11:54 PM
I guess this also means this (checking for 401 & refreshing token) can’t/shouldn’t be done on an Interceptor level, you’d need to do it on a wrapper around the ApolloClient to have access to the Apollo types like
ApolloResponse
m

mbonnin

01/20/2023, 11:55 PM
You can do it at the ApolloInterceptor level
d

dorche

01/20/2023, 11:56 PM
Right but this is coming back to what started this question if you do it there - I need to read the body, etc.. Not sure if I am missing something obvious here..
m

mbonnin

01/20/2023, 11:56 PM
Copy code
object authInterceptor: ApolloInterceptor {
  override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
    return chain.proceed(request).onEach { 
      if (it.errors?.get(0)?.extensions?.get("type") == "UnAuthorized") {
        // do something
      }
    }
  }
}
ApolloInterceptor
works at the GraphQL layer.
HttpInterceptor
at the HTTP layer. So with HTTP, you deal with response bodies while with GraphQL you deal with
ApolloResponse
d

dorche

01/21/2023, 12:06 AM
Right I see my problem now, should be an even simpler fix to switch to implementing
ApolloInterceptor
from
HttpInterceptor
that I had to the above parsing. Thanks again, it really wasn’t obvious that both existed, maybe I missed it somewhere in the docs. It should be somewhere in capitals haha
ApolloInterceptor: Use this one if you need access to the body of the response
m

mbonnin

01/21/2023, 12:08 AM
Yea looks like we don't have a doc page for
ApolloInterceptor
. Want to open an issue so we don't forget about this?
125 Views