Skip to content

Commit

Permalink
Merge pull request #174 from alanfriedman/custom-fetch
Browse files Browse the repository at this point in the history
Use custom fetch handler if provided
  • Loading branch information
nason authored Mar 3, 2018
2 parents af94473 + b830a6f commit 72906ea
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 6 deletions.
75 changes: 73 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,72 @@ It must be one of the following strings:
- `same-origin` only sends cookies for the current domain;
- `include` always send cookies, even for cross-origin calls.

#### `[RSAA].fetch`

A custom [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) implementation, useful for intercepting the fetch request to customize the response status, modify the response payload or skip the request altogether and provide a cached response instead.

If provided, the fetch option must be a function that conforms to the Fetch API. Otherwise, the global fetch will be used.

Example of modifying a request payload and status:

```js
{
[RSAA]: {
...
fetch: async (...args) => {
// `fetch` args may be just a Request instance or [URI, options] (see Fetch API docs above)
const res = await fetch(...args);
const json = await res.json();

return new Response(
JSON.stringify({
...json,
// Adding to the JSON response
foo: 'bar'
}),
{
// Custom success/error status based on an `error` key in the API response
status: json.error ? 500 : 200,
headers: {
'Content-Type': 'application/json'
}
}
);
}
...
}
}
```
Example of skipping the request in favor of a cached response:
```js
{
[RSAA]: {
...
fetch: async (...args) => {
const cached = await getCache('someKey');

if (cached) {
// where `cached` is a JSON string: '{"foo": "bar"}'
return new Response(cached,
{
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
);
}

// Fetch as usual if not cached
return fetch(...args);
}
...
}
}
```
### Bailing out
In some cases, the data you would like to fetch from the server may already be cached in your Redux store. Or you may decide that the current user does not have the necessary permissions to make some request.
Expand Down Expand Up @@ -563,11 +629,12 @@ The `[RSAA]` property MAY
- have a `headers` property,
- have an `options` property,
- have a `credentials` property,
- have a `bailout` property.
- have a `bailout` property,
- have a `fetch` property.
The `[RSAA]` property MUST NOT
- include properties other than `endpoint`, `method`, `types`, `body`, `headers`, `options`, `credentials`, and `bailout`.
- include properties other than `endpoint`, `method`, `types`, `body`, `headers`, `options`, `credentials`, `bailout` and `fetch`.
#### `[RSAA].endpoint`
Expand Down Expand Up @@ -599,6 +666,10 @@ The optional `[RSAA].credentials` property MUST be one of the strings `omit`, `s
The optional `[RSAA].bailout` property MUST be a boolean or a function.
#### `[RSAA].fetch`
The optional `[RSAA].fetch` property MUST be a function.
#### `[RSAA].types`
The `[RSAA].types` property MUST be an array of length 3. Each element of the array MUST be a string, a `Symbol`, or a type descriptor.
Expand Down
10 changes: 8 additions & 2 deletions src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ function apiMiddleware({ getState }) {

// Parse the validated RSAA action
const callAPI = action[RSAA];
var { endpoint, body, headers, options = {} } = callAPI;
var {
endpoint,
body,
headers,
options = {},
fetch: doFetch = fetch
} = callAPI;
const { method, credentials, bailout, types } = callAPI;
const [requestType, successType, failureType] = normalizeTypeDescriptors(
types
Expand Down Expand Up @@ -148,7 +154,7 @@ function apiMiddleware({ getState }) {

try {
// Make the API call
var res = await fetch(endpoint, {
var res = await doFetch(endpoint, {
...options,
method,
body: body || undefined,
Expand Down
12 changes: 10 additions & 2 deletions src/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ function validateRSAA(action) {
'headers',
'credentials',
'bailout',
'types'
'types',
'fetch'
];
const validMethods = [
'GET',
Expand Down Expand Up @@ -103,7 +104,8 @@ function validateRSAA(action) {
options,
credentials,
types,
bailout
bailout,
fetch
} = callAPI;
if (typeof endpoint === 'undefined') {
validationErrors.push('[RSAA] must have an endpoint property');
Expand Down Expand Up @@ -186,6 +188,12 @@ function validateRSAA(action) {
}
}

if (typeof fetch !== 'undefined') {
if (typeof fetch !== 'function') {
validationErrors.push('[RSAA].fetch property must be a function');
}
}

return validationErrors;
}

Expand Down
134 changes: 134 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,32 @@ test('validateRSAA/isValidRSAA must identify conformant RSAAs', t => {
);
t.ok(isValidRSAA(action26), '[RSAA].options may be a function (isRSAA)');

const action27 = {
[RSAA]: {
endpoint: '',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
fetch: () => {}
}
};
t.ok(
isValidRSAA(action27),
'[RSAA].fetch property must be a function (isRSAA)'
);

const action28 = {
[RSAA]: {
endpoint: '',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE'],
fetch: {}
}
};
t.notOk(
isValidRSAA(action28),
'[RSAA].fetch property must be a function (isRSAA)'
);

t.end();
});

Expand Down Expand Up @@ -1921,3 +1947,111 @@ test('apiMiddleware must dispatch a failure FSA on an unsuccessful API call with
t.plan(8);
actionHandler(anAction);
});

test('apiMiddleware must use a [RSAA].fetch custom fetch wrapper when present', t => {
const asyncWorker = async () => 'Done!';
const responseBody = {
id: 1,
name: 'Alan',
error: false
};
const api = nock('http://127.0.0.1')
.get('/api/users/1')
.reply(200, responseBody);
const anAction = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
fetch: async (endpoint, opts) => {
t.pass('custom fetch handler called');

// Simulate some async process like retrieving cache
await asyncWorker();

const res = await fetch(endpoint, opts);
const json = await res.json();

return new Response(
JSON.stringify({
...json,
foo: 'bar'
}),
{
// Example of custom `res.ok`
status: json.error ? 500 : 200,
headers: {
'Content-Type': 'application/json'
}
}
);
},
types: ['REQUEST', 'SUCCESS', 'FAILURE']
}
};
const doGetState = () => {};
const nextHandler = apiMiddleware({ getState: doGetState });
const doNext = action => {
switch (action.type) {
case 'SUCCESS':
t.deepEqual(
action.payload,
{
...responseBody,
foo: 'bar'
},
'custom response passed to the next handler'
);
break;
}
};

const actionHandler = nextHandler(doNext);

t.plan(2);
actionHandler(anAction);
});

test('apiMiddleware must dispatch correct error payload when custom fetch wrapper returns an error response', t => {
const anAction = {
[RSAA]: {
endpoint: 'http://127.0.0.1/api/users/1',
method: 'GET',
fetch: async (endpoint, opts) => {
return new Response(
JSON.stringify({
foo: 'bar'
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
},
types: ['REQUEST', 'SUCCESS', 'FAILURE']
}
};
const doGetState = () => {};
const nextHandler = apiMiddleware({ getState: doGetState });
const doNext = action => {
switch (action.type) {
case 'FAILURE':
t.ok(action.payload instanceof Error);
t.pass('error action dispatched');
t.deepEqual(
action.payload.response,
{
foo: 'bar'
},
'custom response passed to the next handler'
);
break;
}
};

const actionHandler = nextHandler(doNext);

t.plan(3);
actionHandler(anAction);
});

0 comments on commit 72906ea

Please sign in to comment.