-
Notifications
You must be signed in to change notification settings - Fork 653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Try the experimental WebSocketNetworkTransport #5862
Comments
Great job, thanks! I don't observe
Will these issue be addressed in the future? |
Thanks for trying it out and for the details!
val apolloClient = ApolloClient.Builder()
.retryOnErrorInterceptor(MyRetryOnErrorInterceptor())
.build()
class MyRetryOnErrorInterceptor : ApolloInterceptor {
object RetryException: Exception()
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
var attempt = 0
return chain.proceed(request).onEach {
if (request.retryOnError == true && it.exception != null && it.exception is ApolloNetworkException) {
throw RetryException
} else {
attempt = 0
}
}.retryWhen { cause, _ ->
if (cause is RetryException) {
attempt++
delay(2.0.pow(attempt).seconds)
true
} else {
// Not a RetryException, probably a programming error, pass it through
false
}
}
}
} I'm aware this is significantly more verbose than the old way but the old way used to do a bunch of assumptions that didn't always work. At least this method should give you full control over what exceptions you want to retry, exponential backoff, etc... |
* Add WebSocketEngine(WebSocket.Factory) and fix documentation See #5862 * remove ApolloExperimental from internal function and expose ensureUniqueUuid()
@dmitrytavpeko PS: I also realized we can simplify renewing the uuid (see #6075) |
@martinbonnin Thank you for the quick response! |
Hey @martinbonnin 👋 it looks like there are some changes required when authenticating websockets which aren't mentioned in the migration guide, specifically setting connection payload and refreshing token. Would it be possible to add this to the migration doc? |
@jvanderwee Can you share your existing code here? I'll add the matching snippet to the doc |
|
Hi @jvanderwee thanks for sending this! Looks like you're calling class WebSocketReconnectException: Exception("The WebSocket needs to be reopened")
class RetryException: Exception("The WebSocket needs to be retried") class MyInterceptor: ApolloInterceptor {
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
return chain.proceed(request)
.onEach {
if (request.retryOnError == true && it.exception != null && it.exception is ApolloNetworkException) {
throw RetryException()
}
}
.retryWhen { cause, attempt ->
when (cause) {
is RetryException -> {
attempt < 3
}
is WebSocketReconnectException -> {
true
}
else -> false
}
}.catch {
if (it !is RetryException) {
throw it
}
}
}
} val apolloClient = ApolloClient.Builder()
.serverUrl("http://localhost:8080/graphql")
.subscriptionNetworkTransport(
WebSocketNetworkTransport.Builder()
.serverUrl("http://localhost:8080/subscriptions")
.protocol(
GraphQLWsProtocol(
connectionParams = {
mapOf("Authorization" to token)
},
),
)
.retryOnError { it.operation is Subscription }
.retryOnErrorInterceptor(MyInterceptor())
.build()
)
.build() apolloClient.subscriptionNetworkTransport.closeConnection(WebSocketReconnectException()) While the above should be working, it's pretty verbose and I'd love to take this opportunity to do things a bit better. Can you share what triggers the call to |
When we change the import for closeConnection -import com.apollographql.apollo.network.ws.closeConnection
+import com.apollographql.apollo.network.websocket.closeConnection the exception is expected to be of type
but then this means we need
could we add a specific subclass of if we don't update the import an we close the connection whenever we need to update the authorisation header in the payload |
Thanks for the follow up!
We can make a
That's the question. When do you need to update the authorization header? Ideally I would expect the server to notify its listeners directly in the websocket with some message ( This seems like the best way and would allow to handle websocket authentication in a single interceptor, just like for HTTP. That'd be a lot more symmetrical. But maybe your server doesn't signal that? In which case I'm curious how you can tell that you need to refresh the token (timeout, something else?) |
👌 thanks!
very good point! we've been closing the connection as a side-effect of our HTTP authorisation interceptor, but it makes way more sense for this to be handle my the websocket itself. I'll look into this! we will still need to update the authorisation header when the user logs in though |
Interesting! What happens if you're on a screen that listens to a subscription without ever doing a query/mutation? The subscription has no way to know when to renew its token in that case? |
correct! very much an oversight on our part 🙃 |
@jvanderwee Thanks for the follow up!
I did not mention This probably will require new APIs to update the token from an Right now I'm thinking something like this: class AuthorizationInterceptor : ApolloInterceptor {
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
return flow {
val token = getOrRefreshToken()
emit(
request.newBuilder()
// for HTTP transport
.addHttpHeader("Authorization", "Bearer $token")
// for WebSocket transport
//.webSocketConnectionPayload(mapOf("token" to token))
.build()
)
}.flatMapConcat {
chain.proceed(it)
}.onEach {
if (it.errors.contains("Token expired")) {
throw TokenExpired
}
}.retryWhen { cause, attempt ->
cause is TokenExpired
}
}
} I haven't tested it yet but assuming the server can return the 401 information is a regular GraphQL response, that would allow handling authentication consistently between HTTP and WebSocket. Any thoughts? Could that work for you? |
That would work for us! Thanks @martinbonnin |
👋 we're seeing the following crash occur on Android in 4.0.1 after migrating to experimental web sockets. unsure when this happens at the moment, will update if we narrow it down
|
@jvanderwee thanks for the feedback. This can happen if the For the record, would you have the rest of the stacktrace so we can see what triggers this error? |
Thanks!
|
So it dispatches from a separate thread but that thread executes concurrently with the constructor... While the fix should work, I'm not sure it's 100% correct. I'll revisit. |
Should fix a race condition when the onError is called concurrently with the SubscribableWebSocket constructor See #5862 (comment)
Should fix a race condition when the onError is called concurrently with the SubscribableWebSocket constructor See #5862 (comment)
New try here. If you get a chance to try the SNAPSHOTs, let us know how that goes. |
Thanks @martinbonnin! When do you plan on publishing the 4.0.2 release? |
Currently planned for next week |
Thanks @martinbonnin 🙏 just trying out the snapshot, unable to pull in
|
Ah this is interesting! Probably due to the moving of mockserver. Thanks for reporting it, I'll take a look! |
@jvanderwee the new SNAPSHOT is currently building. In the meantime, you can workaround with configurations.configureEach {
exclude(group= "apollo-kotlin", module = "apollo-mockserver")
} |
Description
Historically, WebSockets have been one of the most complex and error-prone parts of Apollo Kotlin.
Starting with
4.0.0-beta.6
,apollo-runtime
has an experimentalApolloWebSocketNetworkTransport
aiming at simplifying the WebSocket setup.If you have had issues with WebSockets, try it out. More documentation and migration guide is available at https://www.apollographql.com/docs/kotlin/v4/advanced/experimental-websockets
The text was updated successfully, but these errors were encountered: