Skip to content

Commit

Permalink
Merge pull request #5224 from marmelab/simple-rest-count-http-header
Browse files Browse the repository at this point in the history
simpleRestProvider: using a custom http header to count items in a collection
  • Loading branch information
djhi authored Sep 7, 2020
2 parents bf4097b + 6ba4e8a commit c023d3d
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 26 deletions.
34 changes: 34 additions & 0 deletions packages/ra-data-simple-rest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,40 @@ const httpClient = (url, options = {}) => {

Now all the requests to the REST API will contain the `Authorization: SRTRDFVESGNJYTUKTYTHRG` header.

## Note about Content-Range

Historically, Simple REST Data Provider uses the http `Content-Range` header to retrieve the number of items in a collection. But this is a *hack* of the [primary role of this header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range).

However this can be problematic, for example within an infrastructure using a Varnish that may use, modify or delete this header. We also have feedback indicating that using this header is problematic when you host your application on [Vercel](https://vercel.com/).

The solution is to use another http header to return the number of collection's items. The other header commonly used for this is `X-Total-Count`. So if you use `X-Total-Count`, you will have to :

* Whitelist this header with an `Access-Control-Expose-Headers` [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) header.

```
Access-Control-Expose-Headers: X-Total-Count
```

* Use the third parameter of `simpleRestProvider` to specify the name of the header to use :

```jsx
// in src/App.js
import * as React from "react";
import { Admin, Resource } from 'react-admin';
import { fetchUtils } from 'ra-core';
import simpleRestProvider from 'ra-data-simple-rest';

import { PostList } from './posts';

const App = () => (
<Admin dataProvider={simpleRestProvider('http://path.to.my.api/', fetchUtils.fetchJson, 'X-Total-Count')}>
<Resource name="posts" list={PostList} />
</Admin>
);

export default App;
```

## License

This data provider is licensed under the MIT License, and sponsored by [marmelab](http://marmelab.com).
32 changes: 32 additions & 0 deletions packages/ra-data-simple-rest/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,37 @@ describe('Data Simple REST Client', () => {
}
);
});

it('should use a custom http header to retrieve the number of items in the collection', async () => {
const httpClient = jest.fn(() =>
Promise.resolve({
headers: new Headers({
'x-total-count': '42',
}),
json: [{ id: 1 }],
status: 200,
body: '',
})
);
const client = simpleClient(
'http://localhost:3000',
httpClient,
'X-Total-Count'
);

const result = await client.getList('posts', {
filter: {},
pagination: {
page: 1,
perPage: 10,
},
sort: {
field: 'title',
order: 'desc',
},
});

expect(result.total).toEqual(42);
});
});
});
79 changes: 53 additions & 26 deletions packages/ra-data-simple-rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import { fetchUtils, DataProvider } from 'ra-core';
*
* export default App;
*/
export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
export default (
apiUrl: string,
httpClient = fetchUtils.fetchJson,
countHeader: string = 'Content-Range'
): DataProvider => ({
getList: (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
Expand All @@ -47,27 +51,34 @@ export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
filter: JSON.stringify(params.filter),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
const options =
countHeader === 'Content-Range'
? {
// Chrome doesn't return `Content-Range` header if no `Range` is provided in the request.
headers: new Headers({
Range: `${resource}=${rangeStart}-${rangeEnd}`,
}),
}
: {};

return httpClient(url, {
// Chrome doesn't return `Content-Range` header if no `Range` is provided in the request.
headers: new Headers({
Range: `${resource}=${rangeStart}-${rangeEnd}`,
}),
}).then(({ headers, json }) => {
if (!headers.has('content-range')) {
return httpClient(url, options).then(({ headers, json }) => {
if (!headers.has(countHeader)) {
throw new Error(
'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?'
`The ${countHeader} header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare ${countHeader} in the Access-Control-Expose-Headers header?`
);
}
return {
data: json,
total: parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
),
total:
countHeader === 'Content-Range'
? parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
)
: parseInt(headers.get(countHeader.toLowerCase())),
};
});
},
Expand All @@ -88,6 +99,10 @@ export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
getManyReference: (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;

const rangeStart = (page - 1) * perPage;
const rangeEnd = page * perPage - 1;

const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
Expand All @@ -97,22 +112,34 @@ export default (apiUrl, httpClient = fetchUtils.fetchJson): DataProvider => ({
}),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
const options =
countHeader === 'Content-Range'
? {
// Chrome doesn't return `Content-Range` header if no `Range` is provided in the request.
headers: new Headers({
Range: `${resource}=${rangeStart}-${rangeEnd}`,
}),
}
: {};

return httpClient(url).then(({ headers, json }) => {
if (!headers.has('content-range')) {
return httpClient(url, options).then(({ headers, json }) => {
if (!headers.has(countHeader)) {
throw new Error(
'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?'
`The ${countHeader} header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare ${countHeader} in the Access-Control-Expose-Headers header?`
);
}
return {
data: json,
total: parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
),
total:
countHeader === 'Content-Range'
? parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
)
: parseInt(headers.get(countHeader.toLowerCase())),
};
});
},
Expand Down

0 comments on commit c023d3d

Please sign in to comment.