UNPKG

7.72 kBPlain TextView Raw
1import { ApolloLink, Observable, RequestHandler, fromError } from 'apollo-link';
2import {
3 serializeFetchParameter,
4 selectURI,
5 parseAndCheckHttpResponse,
6 checkFetcher,
7 selectHttpOptionsAndBody,
8 createSignalIfSupported,
9 fallbackHttpConfig,
10 Body,
11 HttpOptions,
12 UriFunction as _UriFunction,
13} from 'apollo-link-http-common';
14import { DefinitionNode } from 'graphql';
15
16export namespace HttpLink {
17 //TODO Would much rather be able to export directly
18 export interface UriFunction extends _UriFunction {}
19 export interface Options extends HttpOptions {
20 /**
21 * If set to true, use the HTTP GET method for query operations. Mutations
22 * will still use the method specified in fetchOptions.method (which defaults
23 * to POST).
24 */
25 useGETForQueries?: boolean;
26 }
27}
28
29// For backwards compatibility.
30export import FetchOptions = HttpLink.Options;
31export import UriFunction = HttpLink.UriFunction;
32
33export const createHttpLink = (linkOptions: HttpLink.Options = {}) => {
34 let {
35 uri = '/graphql',
36 // use default global fetch is nothing passed in
37 fetch: fetcher,
38 includeExtensions,
39 useGETForQueries,
40 ...requestOptions
41 } = linkOptions;
42
43 // dev warnings to ensure fetch is present
44 checkFetcher(fetcher);
45
46 //fetcher is set here rather than the destructuring to ensure fetch is
47 //declared before referencing it. Reference in the destructuring would cause
48 //a ReferenceError
49 if (!fetcher) {
50 fetcher = fetch;
51 }
52
53 const linkConfig = {
54 http: { includeExtensions },
55 options: requestOptions.fetchOptions,
56 credentials: requestOptions.credentials,
57 headers: requestOptions.headers,
58 };
59
60 return new ApolloLink(operation => {
61 let chosenURI = selectURI(operation, uri);
62
63 const context = operation.getContext();
64
65 const contextConfig = {
66 http: context.http,
67 options: context.fetchOptions,
68 credentials: context.credentials,
69 headers: context.headers,
70 };
71
72 //uses fallback, link, and then context to build options
73 const { options, body } = selectHttpOptionsAndBody(
74 operation,
75 fallbackHttpConfig,
76 linkConfig,
77 contextConfig,
78 );
79
80 const { controller, signal } = createSignalIfSupported();
81 if (controller) (options as any).signal = signal;
82
83 // If requested, set method to GET if there are no mutations.
84 const definitionIsMutation = (d: DefinitionNode) => {
85 return d.kind === 'OperationDefinition' && d.operation === 'mutation';
86 };
87 if (
88 useGETForQueries &&
89 !operation.query.definitions.some(definitionIsMutation)
90 ) {
91 options.method = 'GET';
92 }
93
94 if (options.method === 'GET') {
95 const { newURI, parseError } = rewriteURIForGET(chosenURI, body);
96 if (parseError) {
97 return fromError(parseError);
98 }
99 chosenURI = newURI;
100 } else {
101 try {
102 (options as any).body = serializeFetchParameter(body, 'Payload');
103 } catch (parseError) {
104 return fromError(parseError);
105 }
106 }
107
108 return new Observable(observer => {
109 fetcher(chosenURI, options)
110 .then(response => {
111 operation.setContext({ response });
112 return response;
113 })
114 .then(parseAndCheckHttpResponse(operation))
115 .then(result => {
116 // we have data and can send it to back up the link chain
117 observer.next(result);
118 observer.complete();
119 return result;
120 })
121 .catch(err => {
122 // fetch was cancelled so its already been cleaned up in the unsubscribe
123 if (err.name === 'AbortError') return;
124 // if it is a network error, BUT there is graphql result info
125 // fire the next observer before calling error
126 // this gives apollo-client (and react-apollo) the `graphqlErrors` and `networErrors`
127 // to pass to UI
128 // this should only happen if we *also* have data as part of the response key per
129 // the spec
130 if (err.result && err.result.errors && err.result.data) {
131 // if we dont' call next, the UI can only show networkError because AC didn't
132 // get andy graphqlErrors
133 // this is graphql execution result info (i.e errors and possibly data)
134 // this is because there is no formal spec how errors should translate to
135 // http status codes. So an auth error (401) could have both data
136 // from a public field, errors from a private field, and a status of 401
137 // {
138 // user { // this will have errors
139 // firstName
140 // }
141 // products { // this is public so will have data
142 // cost
143 // }
144 // }
145 //
146 // the result of above *could* look like this:
147 // {
148 // data: { products: [{ cost: "$10" }] },
149 // errors: [{
150 // message: 'your session has timed out',
151 // path: []
152 // }]
153 // }
154 // status code of above would be a 401
155 // in the UI you want to show data where you can, errors as data where you can
156 // and use correct http status codes
157 observer.next(err.result);
158 }
159 observer.error(err);
160 });
161
162 return () => {
163 // XXX support canceling this request
164 // https://developers.google.com/web/updates/2017/09/abortable-fetch
165 if (controller) controller.abort();
166 };
167 });
168 });
169};
170
171// For GET operations, returns the given URI rewritten with parameters, or a
172// parse error.
173function rewriteURIForGET(chosenURI: string, body: Body) {
174 // Implement the standard HTTP GET serialization, plus 'extensions'. Note
175 // the extra level of JSON serialization!
176 const queryParams = [];
177 const addQueryParam = (key: string, value: string) => {
178 queryParams.push(`${key}=${encodeURIComponent(value)}`);
179 };
180
181 if ('query' in body) {
182 addQueryParam('query', body.query);
183 }
184 if (body.operationName) {
185 addQueryParam('operationName', body.operationName);
186 }
187 if (body.variables) {
188 let serializedVariables;
189 try {
190 serializedVariables = serializeFetchParameter(
191 body.variables,
192 'Variables map',
193 );
194 } catch (parseError) {
195 return { parseError };
196 }
197 addQueryParam('variables', serializedVariables);
198 }
199 if (body.extensions) {
200 let serializedExtensions;
201 try {
202 serializedExtensions = serializeFetchParameter(
203 body.extensions,
204 'Extensions map',
205 );
206 } catch (parseError) {
207 return { parseError };
208 }
209 addQueryParam('extensions', serializedExtensions);
210 }
211
212 // Reconstruct the URI with added query params.
213 // XXX This assumes that the URI is well-formed and that it doesn't
214 // already contain any of these query params. We could instead use the
215 // URL API and take a polyfill (whatwg-url@6) for older browsers that
216 // don't support URLSearchParams. Note that some browsers (and
217 // versions of whatwg-url) support URL but not URLSearchParams!
218 let fragment = '',
219 preFragment = chosenURI;
220 const fragmentStart = chosenURI.indexOf('#');
221 if (fragmentStart !== -1) {
222 fragment = chosenURI.substr(fragmentStart);
223 preFragment = chosenURI.substr(0, fragmentStart);
224 }
225 const queryParamsPrefix = preFragment.indexOf('?') === -1 ? '?' : '&';
226 const newURI =
227 preFragment + queryParamsPrefix + queryParams.join('&') + fragment;
228 return { newURI };
229}
230
231export class HttpLink extends ApolloLink {
232 public requester: RequestHandler;
233 constructor(opts?: HttpLink.Options) {
234 super(createHttpLink(opts).request);
235 }
236}