Skip to content
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

AbortController TypeError. #70

Closed
marksauter opened this issue Feb 22, 2018 · 3 comments
Closed

AbortController TypeError. #70

marksauter opened this issue Feb 22, 2018 · 3 comments
Labels

Comments

@marksauter
Copy link

marksauter commented Feb 22, 2018

Hi,
I was getting the following error when trying to use this uploadLink with my apollo client:
TypeError: 'abort' called on an object that does not implement interface AbortController.

Here is my apollo client code:

import { ApolloClient } from 'apollo-client';
import { createUploadLink } from './apollo-upload-link';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { persistCache } from 'apollo-cache-persist';

const authLink = setContext((_, { headers }) => {
  // get the authentication token from session storage if it exists
  const token = window.sessionStorage.getItem('token');
  // return the headers to the context so httpLink can read them
  return {
    headers: {
      ...headers,
      authorization: token ? token : '',
    },
  };
});

const uploadLink = createUploadLink({
  uri: `${process.env.REACT_APP_BACKEND_URL}/graphql`,
});

// Custom InMemoryCache to remove __typename from 'id' variables, because I
// already do this server side.
const cache = new InMemoryCache({
  dataIdFromObject: o => o.id || null,
});

persistCache({
  cache,
  storage: sessionStorage,
});

const client = new ApolloClient({
  // By default, this client will send queries to the
  //  `/graphql` endpoint on the same host
  link: authLink.concat(uploadLink),
  cache,
});

export default client;

I fixed the error by merging code from apollo-link-http with the code from apollo-upload-client. Here's what I got:

import { ApolloLink, Observable, fromError } from 'apollo-link';
import {
  serializeFetchParameter,
  selectURI,
  parseAndCheckHttpResponse,
  checkFetcher,
  selectHttpOptionsAndBody,
  createSignalIfSupported,
  fallbackHttpConfig,
  UriFunction as _UriFunction,
} from 'apollo-link-http-common';
import extractFiles from 'extract-files';

export { ReactNativeFile } from 'extract-files';

export const createUploadLink = (linkOptions = {}) => {
  let {
    uri: fetchUri = '/graphql',
    // use default global fetch if nothing passed in
    credentials,
    fetch: fetcher,
    fetchOptions,
    headers,
    includeExtensions,
    useGETForQueries,
  } = linkOptions;

  // dev warnings to ensure fetch is present
  checkFetcher(fetcher);

  // fetcher is set here rather than the destructuring to ensure fetch is
  // declared before referencing it. Reference in the destructuring would cause a
  // ReferenceError
  if (!fetcher) {
    fetcher = fetch;
  }

  const linkConfig = {
    http: { includeExtensions },
    options: fetchOptions,
    credentials,
    headers,
  };

  return new ApolloLink(operation => {
    let chosenURI = selectURI(operation, fetchUri);

    const context = operation.getContext();

    const contextConfig = {
      http: context.http,
      options: context.fetchOptions,
      credentials: context.credentials,
      headers: context.headers,
    };

    // uses fallback, link, and then context to build options
    const { options, body } = selectHttpOptionsAndBody(
      operation,
      fallbackHttpConfig,
      linkConfig,
      contextConfig,
    );

    const { controller, signal } = createSignalIfSupported();
    if (controller) options.signal = signal;

    // If requested, set method to GET if there are no mutations.
    const definitionIsMutation = d => {
      return d.kind === 'OperationDefinition' && d.operation === 'mutation';
    };
    if (
      useGETForQueries &&
      !operation.query.definitions.some(definitionIsMutation)
    ) {
      options.method = 'GET';
    }

    if (options.method === 'GET') {
      const { newURI, parseError } = rewriteURIForGET(chosenURI, body);
      if (parseError) {
        return fromError(parseError);
      }
      chosenURI = newURI;
    } else {
      try {
        options.body = serializeFetchParameter(body, 'Payload');
      } catch (parseError) {
        return fromError(parseError);
      }
    }

    const files = extractFiles(body);

    // If we are sending files in the body of the request
    if (files.length !== 0) {
      // Automatically set by fetch when the body is a FormData instance.
      delete options.headers['content-type'];

      // GraphQL multipart request spec:
      // https://github.com/jaydenseric/graphql-multipart-request-spec
      options.body = new FormData();
      options.body.append('operations', serializeFetchParameter(body));
      options.body.append(
        'map',
        JSON.stringify(
          files.reduce((map, { path }, index) => {
            map[`${index}`] = [path];
            return map;
          }, {}),
        ),
      );
      files.forEach(({ file }, index) =>
        options.body.append(index, file, file.name),
      );
    } else options.body = serializeFetchParameter(body);

    return new Observable(observer => {
      fetcher(chosenURI, options)
        .then(response => {
          // Forward the response on the context.
          operation.setContext({ response });
          return response;
        })
        .then(parseAndCheckHttpResponse(operation))
        .then(result => {
          observer.next(result);
          observer.complete();
          return result;
        })
        .catch(err => {
          // fetch was cancelled so its already been cleaned up in the
          // unsubscribe
          if (err.name === 'AbortError') return;
          // if it is a network error, BUT there is graphql result info
          // fire the next observer before calling error
          // this gives apollo-client (and react-apollo) the `graphqlErrors` and
          // `networkErrors` to pass to UI
          if (err.result && err.result.errors)
            // if we dont' call next, the UI can only show networkError because AC didn't
            // get andy graphqlErrors
            // this is graphql execution result info (i.e errors and possibly data)
            // this is because there is no formal spec how errors should translate to
            // http status codes. So an auth error (401) could have both data
            // from a public field, errors from a private field, and a status of 401
            // {
            //  user { // this will have errors
            //    firstName
            //  }
            //  products { // this is public so will have data
            //    cost
            //  }
            // }
            //
            // the result of above *could* look like this:
            // {
            //   data: { products: [{ cost: "$10" }] },
            //   errors: [{
            //      message: 'your session has timed out',
            //      path: []
            //   }]
            // }
            // status code of above would be a 401
            // in the UI you want to show data where you can, errors as data where you can
            // and use correct http status codes
            observer.next(err.result);

          observer.error(err);
        });

      return () => {
        // Abort fetch as the optional cleanup function.
        if (controller) return controller.abort;
      };
    });
  });
};

// For GET operations, returns the given URI rewritten with parameters, or a
// parse error.
function rewriteURIForGET(chosenURI, body) {
  // Implement the standard HTTP GET serialization, plus 'extensions'. Note
  // the extra level of JSON serialization!
  const queryParams = [];
  const addQueryParam = (key, value) => {
    queryParams.push(`${key}=${encodeURIComponent(value)}`);
  };

  if ('query' in body) {
    addQueryParam('query', body.query);
  }
  if (body.operationName) {
    addQueryParam('operationName', body.operationName);
  }
  if (body.variables) {
    let serializedVariables;
    try {
      serializedVariables = serializeFetchParameter(
        body.variables,
        'Variables map',
      );
    } catch (parseError) {
      return { parseError };
    }
    addQueryParam('variables', serializedVariables);
  }
  if (body.extensions) {
    let serializedExtensions;
    try {
      serializedExtensions = serializeFetchParameter(
        body.extensions,
        'Extensions map',
      );
    } catch (parseError) {
      return { parseError };
    }
    addQueryParam('extensions', serializedExtensions);
  }

  // Reconstruct the URI with added query params.
  // XXX This assumes that the URI is well-formed and that it doesn't
  //     already contain any of these query params. We could instead use the
  //     URL API and take a polyfill (whatwg-url@6) for older browsers that
  //     don't support URLSearchParams. Note that some browsers (and
  //     versions of whatwg-url) support URL but not URLSearchParams!
  let fragment = '',
    preFragment = chosenURI;
  const fragmentStart = chosenURI.indexOf('#');
  if (fragmentStart !== -1) {
    fragment = chosenURI.substr(fragmentStart);
    preFragment = chosenURI.substr(0, fragmentStart);
  }
  const queryParamsPrefix = preFragment.indexOf('?') === -1 ? '?' : '&';
  const newURI =
    preFragment + queryParamsPrefix + queryParams.join('&') + fragment;
  return { newURI };
}

export class UploadLink extends ApolloLink {
  requester;
  constructor(opts) {
    super(createUploadLink(opts).request);
  }
}

Most of the changes probably aren't needed, but it's working and I don't want to mess with it at the moment. The key changes - I think - are the following:

return () => {
  // Abort fetch as the optional cleanup function.
  if (controller) return controller.abort;
};

It wasn't returning a function before, now it is.

const { controller, signal } = createSignalIfSupported();
if (controller) options.signal = signal;

I moved this out of the Observable.

I haven't done any formal testing, but it's working for me know. Just thought I'd let you know. 👍

@jaydenseric
Copy link
Owner

Working on this now, and pretty sure I fixed it, but how do you test aborting the fetch? Using the example app, which is what I develop and test with, how can I reproduce the error?

@jaydenseric
Copy link
Owner

Flying blind, can you please let me know if this is fixed in v7.1.0-alpha.2 🙏

@marksauter
Copy link
Author

I can confirm that your update fixed the bug. I don't know exactly why the bug was happening, but my best guess would be that it has something to do with my persisted cache in sessionStorage. I'd have to look into it more, which I don't have the time to do at the moment.

In terms of testing Abort Controller, I'm not very familiar with how it works. This may help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants