apollo-client: if a Query is stopped using AbortController, it cannot be executed again.
Intended outcome:
i use react with apollo.
i want to cancel a running graphql query using the AbortController
approach to stop the fetch
request. later i want to run the query again.
Actual outcome:
i can cancel the query using an AbortController
.but, if i execute the query again, no http-request is sent. maybe some cleanup did not happen and apollo still considers the query running? no idea. i can start other queries. so for example, if this graphql query has a parameter, i can start the query with param1, cancel it, then start it with param2 and it works. but if i start it again with param1, no http request is sent.
How to reproduce the issue:
- create a react-component
- wrap it in
withApollo
- somewhere in the component do a handler to a button-click or something that you can do multiple times:
handleButtonClicked() {
const abortController = new AbortController();
const { signal } = abortController;
this.props.client
.query({
query: QUERY,
variables: {
.
.
.
},
context: {
fetchOptions: {
signal,
},
},
fetchPolicy: 'network-only',
})
.then(() => {console.log('done');});
setTimeout(() => {
abortController.abort();
}, 3000);
}
-
the
QUERY
should go to something slow (like, response in 20seconds) -
open the network-tab in the browser (i tried in google-chrome)
-
click the button, and verify that the query is first
pending
and then latercanceled
-
click the button again, and there will be no new line in the network-tab
Versions
System:
OS: macOS 10.14.1
Binaries:
Node: 8.12.0 - /usr/local/opt/node@8/bin/node
Yarn: 1.12.1 - /usr/local/bin/yarn
npm: 6.4.1 - /usr/local/opt/node@8/bin/npm
Browsers:
Chrome: 70.0.3538.102
Firefox: 63.0.1
Safari: 12.0.1
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 28
- Comments: 23 (5 by maintainers)
I spent some time this weekend debugging this problem, and I found it was necessary to set both
queryDeduplication: false
on the ApolloClient and use.watchQuery
instead of.query
+ an explicitAbortController.signal
.I wound up with something like…
Creating the Apollo client:
Then to issue/cancel a query…
That should successfully cancel a query, and enable re-sending, e.g. in this case where I canceled a query twice, and let it succeed the third time:
@gabor where did you read about the AbortController approach? I think it’s the wrong way to do this, because reaching into the link/fetch and cancelling the request breaks all the abstractions that ApolloClient and apollo link set up for you. If you ask ApolloClient to run a query, then you should tell ApolloClient if you no longer wish to run that query, so the stack can be unwound in an orderly fashion. If you called
client.query
to run it, you should use that same interface to cancel the query. Unfortunately that’s not possible at the moment, but it should be possible by making the promise cancellable.Under the hood
client.query
callswatchQuery
, which supports unsubscribing. If I’m not mistaken, unsubscribing from a watch query will propagate through the link stack and abort thefetch
request in browsers that support it, so this is the proper way to go.If you want to make
query
cancellable, you could consider making a PR to apollo client that adds acancel
function to the promise returned.As a workaround for the time being, here are two other options:
watchQuery
and unsubscribe from it when you want to cancel the request. You can turn an observable into a promise pretty easily. I’m not 100% sure if this will work, but it should work.client.link
directly to make the request by callingexecute(link, operation)
, as described here: https://www.apollographql.com/docs/link/#standalone If you unsubscribe from the observable there (by calling the function returned fromobservable.subscribe
), the link stack should be properly unwound, and your fetch request will get cancelled if it’s still pending.PS: The reason the second request hangs if the first one was aborted is precisely because assumptions were violated by breaking the abstraction. As you can see here, the authors originally assumed that the only way a request would get aborted is via unsubscribing. If you’re aborting it directly, that assumption is obviously violated, and the error doesn’t propagate through the stack. This leaves other links and subscribers in the stack hanging, because they received neither an error, nor were they unsubscribed from. In your specific case, the dedup link is left hanging, so when a second identical request comes in, it ends up being deduplicated and your second requests waits for the first one to complete, not knowing that it was aborted. The simplest workaround in that case is to just passing
queryDeduplication: false
when you initially instantiate ApolloClient. Keep in mind however that this might now result in multiple identical queries being in flight at the same time if your client makes the same query more than once in a short interval.PPS: Looking at your code, it seems what you really want is just a query timeout. If so, then the right solution would be to use a timeout link (eg. apollo-link-timeout. Although that particular implementation issues and also breaks assumptions, it should fix your problem for now).
@helfer thanks for the response. there is a lot of info in your comment, so i will try to react to it’s parts:
AbortController
approach is because:fetch
requests are cancelled using anAbortController
fetch
call usingfetchOptions
(https://www.apollographql.com/docs/link/links/http)watchQuery
approach should be fine for my use-case, i will try it if it works, and get back to you with the resultany updates on this?
@helfer i tried it out, and the
watchQuery
+unsubscribe
approach does work, thanks a lot! would be nice to have this mentioned somewhere in the documentation.Anyone come up with a solution in Apollo 3?
@AW-TaylorBett thank you for the info. unfortunately the situation is somewhat different. the problem-scenario is this:
and the request in [3] does not happen.
I used the solution proposed by @leebenson in apollo client 2 and it worked fine. But it’s not working in apollo client 3.
I am using
queryDeduplication: false
,fetchPolicy: 'network-only'
and I’ve checked thatunsubscribe
tears down the query, but somehow the query is not aborted.I have a similar problem. I made a search cancellation like this. However, there is a problem that’words that have already been canceled’ are not recalled.
EX) If “apple” is cancelled, it searches up to “appl”, but “apple” does not re call api.
[process] A. Whenever a new event occurs, cancel the previous abortController. B. Reassign a new abortController.
[reason]
[logic1]
[logic2]
[logic3]
[logic4]
[Method currently being considered]
I am thinking of a method of separating components only in the search box and fetching components whenever they are called…
sorry @francoisromain , i do not have access to that code anymore. it went roughly like this:
client.query(params).then(...)
const thing = client.watchQuery(params)
thing
there is a way to subscribe a callback-function to it. you will get the query-result that waything
, the ajax-request will stop.unfortunately i do not remember the exact api-calls, but there is some way to go from that
thing
to something that has the api like zen-observable ( https://github.com/zenparsing/zen-observable ). perhaps, if you use typescript the auto-complete can navigate you to it. or maybe someone else can link to a code-example.Same issue here. The third request never fires, even despite executing with a new, unaborted AbortController.
A ugly workaround I use for now is that I pass some additional variable
foo
withMath.random()
to distinguish the queries…You need to recreate a new AbortController for the second instance of the request. A single AbortController is associated with a single request via its AbortSignal. Hence your AbortController is still associated with the original request instance.
See https://developer.mozilla.org/en-US/docs/Web/API/AbortController
Edit: Seems like you should be doing that already with your code. There may be something in chrome that is actually recognising and reusing the same req. or AbortController/AbortSignal.
You may also consider that the browser could be automatically retrying the request on account of not receiving any status in response from the server. See https://blogs.oracle.com/ravello/beware-http-requests-automatic-retries