Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

Commit

Permalink
apollo-link-http: support GET requests natively
Browse files Browse the repository at this point in the history
Fixes #236.
  • Loading branch information
glasser committed Feb 16, 2018
1 parent 512fd3c commit 600bc69
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 55 deletions.
4 changes: 2 additions & 2 deletions packages/apollo-link-batch-http/src/batchHttpLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
fromError,
} from 'apollo-link';
import {
serializeFetchBody,
serializeFetchParameter,
selectURI,
parseAndCheckHttpResponse,
checkFetcher,
Expand Down Expand Up @@ -109,7 +109,7 @@ export class BatchHttpLink extends ApolloLink {
}

try {
(options as any).body = serializeFetchBody(body);
(options as any).body = serializeFetchParameter(body, 'Payload');
} catch (parseError) {
return fromError<FetchResult[]>(parseError);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/apollo-link-http-common/src/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
checkFetcher,
selectHttpOptionsAndBody,
selectURI,
serializeFetchBody,
serializeFetchParameter,
fallbackHttpConfig,
} from '../index';

Expand Down Expand Up @@ -191,19 +191,19 @@ describe('Common Http functions', () => {
});
});

describe('serializeFetchBody', () => {
describe('serializeFetchParameter', () => {
it('throws a parse error on an unparsable body', () => {
const b = {};
const a = { b };
(b as any).a = a;

expect(() => serializeFetchBody(b)).toThrow();
expect(() => serializeFetchParameter(b, 'Label')).toThrow(/Label/);
});

it('returns a correctly parsed body', () => {
const body = { no: 'thing' };

expect(serializeFetchBody(body)).toEqual('{"no":"thing"}');
expect(serializeFetchParameter(body, 'Label')).toEqual('{"no":"thing"}');
});
});

Expand Down
20 changes: 14 additions & 6 deletions packages/apollo-link-http-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export interface UriFunction {
(operation: Operation): string;
}

// The body of a GraphQL-over-HTTP-POST request.
export interface Body {
query?: string;
operationName?: string;
variables?: Record<string, any>;
extensions?: Record<string, any>;
}

export interface HttpOptions {
/**
* The URI to use when fetching operations.
Expand Down Expand Up @@ -222,7 +230,7 @@ export const selectHttpOptionsAndBody = (

//The body depends on the http options
const { operationName, extensions, variables, query } = operation;
const body = { operationName, variables };
const body: Body = { operationName, variables };

if (http.includeExtensions) (body as any).extensions = extensions;

Expand All @@ -235,18 +243,18 @@ export const selectHttpOptionsAndBody = (
};
};

export const serializeFetchBody = body => {
let serializedBody;
export const serializeFetchParameter = (p, label) => {
let serialized;
try {
serializedBody = JSON.stringify(body);
serialized = JSON.stringify(p);
} catch (e) {
const parseError = new Error(
`Network request failed. Payload is not serializable: ${e.message}`,
`Network request failed. ${label} is not serializable: ${e.message}`,
) as ClientParseError;
parseError.parseError = e;
throw parseError;
}
return serializedBody;
return serialized;
};

//selects "/graphql" by default
Expand Down
1 change: 1 addition & 0 deletions packages/apollo-link-http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### vNEXT
- move logic to apollo-link-http-core [PR#364](https://github.com/apollographql/apollo-link/pull/364)
- follow the spec properly for GET requests [PR#490](https://github.com/apollographql/apollo-link/pull/490)

### 1.3.3
- ApolloLink upgrade
Expand Down
27 changes: 6 additions & 21 deletions packages/apollo-link-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Link ecosystem and how to use this link with libraries like Apollo Client and
graphql-tools, or as a standalone client.

The http link is a terminating link that fetches GraphQL results from a GraphQL
endpoint over an http connection. The http link support both POST and GET
endpoint over an http connection. The http link supports both POST and GET
requests with the ability change the http options on a per query basis. This
can be used for authentication, persisted queries, dynamic uris, and other
granualar updates.
Expand Down Expand Up @@ -42,7 +42,9 @@ The HTTP Link relies on having `fetch` present in your runtime environment. If y

<h2 id="context">Context</h2>

The Http Link uses the `headers` field on the context to allow passing headers to the HTTP request. It also supports the `credentials` field for defining credentials policy, `uri` for changing the endpoint dynamically, and `fetchOptions` to allow generic fetch overrides (i.e. method: "GET"). These options will override the same key if passed when creating the the link.
The Http Link uses the `headers` field on the context to allow passing headers to the HTTP request. It also supports the `credentials` field for defining credentials policy, `uri` for changing the endpoint dynamically, and `fetchOptions` to allow generic fetch overrides (i.e. `method: "GET"`). These options will override the same key if passed when creating the the link.

Note that if you set `fetchOptions.method` to `GET`, the http link will follow the [standard GraphQL HTTP GET encoding](http://graphql.org/learn/serving-over-http/#get-request): the query, variables, operation name, and extensions will be passed as query parameters rather than in the HTTP request body.

This link also attaches the response from the `fetch` operation on the context as `response` so you can access it from within another link.

Expand Down Expand Up @@ -146,32 +148,15 @@ All error types inherit the `name`, `message`, and nullable `stack` properties f

<h2 id="custom">Custom fetching</h2>

You can use the `fetch` option when creating an http-link to do a lot of custom networking. This is useful if you want to modify the request based on the headers calculated, send the request as a 'GET' via a query string, or calculate the uri based on the operation:

<h3 id="get-request">Sending a GET request</h3>

```js
const customFetch = (uri, options) => {
const { body, ...newOptions } = options;
// turn the object into a query string, try `object-to-querystring` package
const queryString = objectToQuery(JSON.parse(body));
requestedString = uri + queryString;
return fetch(requestedString, newOptions);
};
const link = createHttpLink({
uri: "data",
fetchOptions: { method: "GET" },
fetch: customFetch
});
```
You can use the `fetch` option when creating an http-link to do a lot of custom networking. This is useful if you want to modify the request based on the headers calculated or calculate the uri based on the operation:

<h3 id="custom-auth">Custom auth</h3>

```js
const customFetch = (uri, options) => {
const { header } = Hawk.client.header(
"http://example.com:8000/resource/1?b=1&a=2",
"GET",
"POST",
{ credentials: credentials, ext: "some-app-data" }
);
options.headers.Authorization = header;
Expand Down
55 changes: 39 additions & 16 deletions packages/apollo-link-http/src/__tests__/httpLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ describe('HttpLink', () => {

beforeEach(() => {
fetchMock.restore();
fetchMock.post('begin:data', makePromise(data));
fetchMock.get('begin:data', makePromise(data));
fetchMock.post('begin:http://data/', makePromise(data));
fetchMock.get('begin:http://data/', makePromise(data));
});

afterEach(() => {
Expand All @@ -47,7 +47,7 @@ describe('HttpLink', () => {

it('constructor creates link that can call next and then complete', done => {
const next = jest.fn();
const link = new HttpLink({ uri: 'data' });
const link = new HttpLink({ uri: 'http://data/' });
const observable = execute(link, {
query: sampleQuery,
});
Expand All @@ -63,27 +63,45 @@ describe('HttpLink', () => {

it('supports using a GET request', done => {
const variables = { params: 'stub' };
const extensions = { myExtension: 'foo' };

let requestedString;
const customFetch = (uri, options) => {
const { body, ...newOptions } = options;
const queryString = objectToQuery(JSON.parse(body));
requestedString = uri + queryString;
return fetch(requestedString, newOptions);
};
const link = createHttpLink({
uri: 'data',
uri: 'http://data/',
fetchOptions: { method: 'GET' },
fetch: customFetch,
includeExtensions: true,
});

execute(link, { query: sampleQuery, variables }).subscribe({
execute(link, { query: sampleQuery, variables, extensions }).subscribe({
next: makeCallback(done, result => {
const [uri, options] = fetchMock.lastCall();
const { method, body, ...rest } = options;
const { method, body } = options;
expect(body).toBeUndefined();
expect(method).toBe('GET');
expect(uri).toBe(
'http://data/?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D',
);
}),
error: error => done.fail(error),
});
});

it('supports using a GET request with search and fragment', done => {
const variables = { params: 'stub' };

const link = createHttpLink({
uri: 'http://data/?foo=bar#frag',
fetchOptions: { method: 'GET' },
});

execute(link, { query: sampleQuery, variables }).subscribe({
next: makeCallback(done, result => {
const [uri, options] = fetchMock.lastCall();
const { method, body } = options;
expect(body).toBeUndefined();
expect(method).toBe('GET');
expect(uri).toBe(
'http://data/?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D#frag',
);
}),
error: error => done.fail(error),
});
Expand All @@ -92,7 +110,7 @@ describe('HttpLink', () => {
it('supports using a GET request on the context', done => {
const variables = { params: 'stub' };
const link = createHttpLink({
uri: 'data',
uri: 'http://data/',
});

execute(link, {
Expand All @@ -103,8 +121,13 @@ describe('HttpLink', () => {
},
}).subscribe(
makeCallback(done, result => {
const method = fetchMock.lastCall()[1].method;
const [uri, options] = fetchMock.lastCall();
const { method, body } = options;
expect(body).toBeUndefined();
expect(method).toBe('GET');
expect(uri).toBe(
'http://data/?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D%0A&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D',
);
}),
);
});
Expand Down
69 changes: 63 additions & 6 deletions packages/apollo-link-http/src/httpLink.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApolloLink, Observable, RequestHandler, fromError } from 'apollo-link';
import {
serializeFetchBody,
serializeFetchParameter,
selectURI,
parseAndCheckHttpResponse,
checkFetcher,
Expand Down Expand Up @@ -41,7 +41,7 @@ export const createHttpLink = (linkOptions: HttpLink.Options = {}) => {
};

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

const context = operation.getContext();

Expand All @@ -63,10 +63,67 @@ export const createHttpLink = (linkOptions: HttpLink.Options = {}) => {
const { controller, signal } = createSignalIfSupported();
if (controller) (options as any).signal = signal;

try {
(options as any).body = serializeFetchBody(body);
} catch (parseError) {
return fromError(parseError);
if (options.method === 'GET') {
// Implement the standard HTTP GET serialization, plus 'extensions'. Note
// the extra level of JSON serialization!
const queryParams = [];
const addQueryParam = (key: string, value: string) => {
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 fromError(parseError);
}
addQueryParam('variables', serializedVariables);
}
if (body.extensions) {
let serializedExtensions;
try {
serializedExtensions = serializeFetchParameter(
body.extensions,
'Extensions map',
);
} catch (parseError) {
return fromError(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 ? '?' : '&';
chosenURI =
preFragment + queryParamsPrefix + queryParams.join('&') + fragment;
} else {
try {
(options as any).body = serializeFetchParameter(body, 'Payload');
} catch (parseError) {
return fromError(parseError);
}
}

return new Observable(observer => {
Expand Down

0 comments on commit 600bc69

Please sign in to comment.