diff --git a/packages/ra-data-simple-rest/README.md b/packages/ra-data-simple-rest/README.md index 46464bd554f..2164ad76e04 100644 --- a/packages/ra-data-simple-rest/README.md +++ b/packages/ra-data-simple-rest/README.md @@ -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 = () => ( + + + +); + +export default App; +``` + ## License This data provider is licensed under the MIT License, and sponsored by [marmelab](http://marmelab.com). diff --git a/packages/ra-data-simple-rest/src/index.spec.ts b/packages/ra-data-simple-rest/src/index.spec.ts index 85729a766f9..27dc38c6ed6 100644 --- a/packages/ra-data-simple-rest/src/index.spec.ts +++ b/packages/ra-data-simple-rest/src/index.spec.ts @@ -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); + }); }); }); diff --git a/packages/ra-data-simple-rest/src/index.ts b/packages/ra-data-simple-rest/src/index.ts index a715c0f5ea3..6377dcfb1ce 100644 --- a/packages/ra-data-simple-rest/src/index.ts +++ b/packages/ra-data-simple-rest/src/index.ts @@ -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; @@ -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())), }; }); }, @@ -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]), @@ -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())), }; }); },