:wave: Hey folks - I'm fishing for some ideas if a...
# apollo-kotlin
r
πŸ‘‹ Hey folks - I'm fishing for some ideas if anyone can help πŸ€” After upgrading to Apollo Kotlin 4 (KMP) we've found the value of our user(/`viewer`) key is set to
null
for some iOS users (Android is fine). This means there's no `CacheMissException`* when we
watch()
our
user():Flow
but we can't assert it being there anymore - we are 99% sure the server never returns
null
for this value/never cache
null
. Any ideas on what we could look into would be very much appreciated.
*I understand that unlike Apollo 3, Apollo 4 no longer throws
CacheMissException
to control flow, and now sets the
.exception
field of
ApolloResponse
- which has an impact on
watch()
since it now emits the response, causing
dataAssertNoErrors
to fail.
b
So this behaves as if it was stored as
null
in the cache, right?
r
Exactly! I'm just about to try pull the DB before/after from the Simulator.
I note also after the upgrade all we see is crashes on launch, no API calls are made (because we start observing
user()
/ this row) on launch. Hence thinking it's a migration thing.
b
All right. And the field is nullable in the schema right?
r
Yes πŸ™‚
Initially I thought it was returning a 200 with a
null
value, but if that's the case it would also be populating the
.errors
array in GQL... I don't think Apollo used to cache error responses πŸ€”
b
correct, unless setting
storePartialResponses(true)
βœ… 1
r
Yeah we try to keep it simple, no partial data. Our pattern is our data functions are either: 1. Fetching only -
NetworkOnly
2. Observing only -
CacheOnly
For queries (not mutations)
πŸ‘ 1
Sadly the upgrade path seems fine, pulled DB and it is not null. This makes me think we must be receiving a
null
viewer
and caching it. So I'll keep looking at how we avoid doing that, but any other ideas anyone has please share πŸ˜„ πŸ™‡
b
The type of the field is a regular type, not a custom scalar by any chance?
r
It's a regular type yep.
b
ok just making sure it wasn't an adapter issue
r
I appreciate it thank you. The strangest thing is this is only affecting iOS. I'm currently looking around anything that might mean it's trying to read from cache before the value is written or something πŸ€” i.e.
.dataAssertNoErrors
is fine, but
.dataAssertNoErrors.viewer
is null. In v3
.watch()
was not emitting into the Flow until
.viewer
was not
null
.
b
if there was no value, this would be a cache miss, not a
null
value
r
When debugging I can see it's not a
CacheMissException
- the generated
GetUserQuery.Data
is there, but the single field it has
viewer?
is null. I wondered if it still considers
null
(because
viewer
optional) as a successful cached value for
viewer
. Hence no cache miss.
b
well yes, null is a valid value, so no cache miss πŸ™‚ But all it indicates is that the value null was put in the cache
nod 1
do you have a way to reproduce the issue?
r
No πŸ™ƒ but I'm working on it - it's very slow going with the iOS build.
πŸ‘ 1
We add an interceptor before calling
store()
that filters out cache misses.
b
this removes interceptors implementing
ApolloStoreInterceptor
- is that the case for yours?
r
ah no, we are OK then, thanks for the spot
πŸ‘ 1
I thought I'd report back where we are on this since we have found the smoking gun, but we're still waiting to confirm exactly what is going wrong. TL;DR - a query that uses
CacheFirst
appears to "poison the well" by persisting a
viewer:null
on iOS only. β€’
GetWoFStatusQuery
fetches
viewer { isOptedIntoWoF }
β—¦ this is being called with
CacheFirst
- a mistake on our side anyway, we should be
NetworkOnly
-fetching, and observing via
CacheOnly
. β€’
GetUserQuery
fetches
viewer { name, etc }
β—¦ we observe this as the
user():Flow<User>
via
CacheOnly
- and assume
viewer:null
is never stored, since the API always returns
errors[]
if not authorised. *potentially this is only happening to users that trigger our
ReauthenticateInterceptor
(refresh bearer token in-line).