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

Use custom fetch handler if provided #174

Merged
merged 11 commits into from
Mar 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#170 is addressing the ability to customize "ok" behavior per-request, what's going on here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

res.ok is true when the status is 200-299, so this is just an example of setting that boolean with something aside from the actual response status code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. The issue I linked to is proposing a way to pass in your own 'ok' check instead of res.ok so I was just confused seeing this here. Carry on!

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);
});