if a Query is stopped using AbortController, it cannot be executed again.

See original GitHub issue

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 later canceled

  • 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

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:26
  • Comments:23 (5 by maintainers)

github_iconTop GitHub Comments

59reactions
leebensoncommented, Jun 8, 2019

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 explicit AbortController.signal.

I wound up with something like…

Creating the Apollo client:

const client = new ApolloClient({
  // Regular options here, such as...
  cache,
  links: ApolloLink.from([links,go,here]),

  // This is what enables cancelation
  queryDeduplication: false
})

Then to issue/cancel a query…

// Issue the query
const query = client.watchQuery<SomeQuery, SomeQueryVariables>({
  // Usual stuff, no need for a custom `AbortController.signal`
  query: SomeQueryDocument,
  fetchPolicy: "network-only",
  variables,
});

// Subscribe to it, and do something with the data
const observable = query.subscribe(({ data }) => {
  // do something with `data`
  // ...
})

// Then somewhere you want to cancel it...
observable.unsubscribe(); // <-- will implicitly cancel the fetch

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:

Screenshot 2019-06-08 at 15 17 48
15reactions
helfercommented, Apr 28, 2019

@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 calls watchQuery, which supports unsubscribing. If I’m not mistaken, unsubscribing from a watch query will propagate through the link stack and abort the fetch 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 a cancel function to the promise returned.

As a workaround for the time being, here are two other options:

  1. Use 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.
  2. If #1 doesn’t work or if you don’t really need the query to go through ApolloClient’s cache, you can use client.link directly to make the request by calling execute(link, operation), as described here: https://www.apollographql.com/docs/link/#standalone If you unsubscribe from the observable there (by calling the function returned from observable.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).

Read more comments on GitHub >

github_iconTop Results From Across the Web

Can't make new fetch request after aborting previous
After calling .abort() on the AbortController you have updated the state of it's AbortSignal which then renders the second request void. You ...
Read more >
class ApolloClient - Apollo GraphQL Docs
This means that any components that might be mounted will execute their queries again using your network interface. If you do not want...
Read more >
Canceling API Requests Using fetch() And AbortController In ...
abort() method for canceling the underlying request. The fetch() API cannot do this type of injection because it returns a native Promise ....
Read more >
How to abort pending query requests? - GIS Stack Exchange
You need to create a new AbortController and reference its Signal signal: controller.signal in the options object in the queryFeatures ...
Read more >
AbortController.abort() - Web APIs | MDN
The abort() method of the AbortController interface aborts a DOM request before it has completed. This is able to abort fetch requests, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found