You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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. 👍
The text was updated successfully, but these errors were encountered:
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?
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.
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:
I fixed the error by merging code from apollo-link-http with the code from
apollo-upload-client
. Here's what I got: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:
It wasn't returning a function before, now it is.
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. 👍
The text was updated successfully, but these errors were encountered: