6

I am modelling the auth layer for a simple react/redux app. On the server side I have an API based on the devise_token_auth gem.

I am using fetch to post a sign in request:

const JSON_HEADERS = new Headers({
  'Content-Type': 'application/json'
});

export const postLogin = ({ email, password }) => fetch(
  `${API_ROOT}/v1/auth/sign_in`, {
    method: 'POST',
    headers: JSON_HEADERS,
    body: JSON.stringify({ email, password })
});

// postLogin({ email: 'test@test.it', password: 'whatever' });

This works, and I get a 200 response and all the data I need. My problem is, information is divided between the response body and headers.

  • Body: user info
  • Headers: access-token, expiration, etc.

I could parse the JSON body this way:

postLogin({ 'test@test.it', password: 'whatever' })
  .then(res => res.json())
  .then(resJson => dispatch(myAction(resJson))

But then myAction would not get any data from the headers (lost while parsing JSON).

Is there a way to get both headers and body from a fetch Request? Thanks!

nerfologist
  • 761
  • 10
  • 23

4 Answers4

11

I thought I'd share the way we finally solved this problem: by just adding a step in the .then chain (before parsing the JSON) to parse the auth headers and dispatch the proper action:

fetch('/some/url')
  .then(res => {
    const authHeaders = ['access-token', 'client', 'uid']
      .reduce((result, key) => {
        let val = res.headers.get(key);
        if (val) {
          result[key] = val;
        }
      }, {});
    store.dispatch(doSomethingWith(authHeaders)); // or localStorage
    return res;
  })
  .then(res => res.json())
  .then(jsonResponse => doSomethingElseWith(jsonResponse))

One more approach, inspired by the mighty Dan Abramov (http://stackoverflow.com/a/37099629/1463770)

fetch('/some/url')
  .then(res => res.json().then(json => ({
    headers: res.headers,
    status: res.status,
    json
  }))
.then({ headers, status, json } => goCrazyWith(headers, status, json));

HTH

nerfologist
  • 761
  • 10
  • 23
1

Using async/await:

const res = await fetch('/url')
const json = await res.json()
doSomething(headers, json)

Without async/await:

fetch('/url')
  .then( res => {
    const headers = res.headers.raw())
    return new Promise((resolve, reject) => {
      res.json().then( json => resolve({headers, json}) )
    })
  })
  .then( ({headers, json}) => doSomething(headers, json) )

This approach with Promise is more general. It is working in all cases, even when it is inconvenient to create a closure that captures res variable (as in the other answer here). For example when handlers is more complex and extracted (refactored) to separated functions.

oklas
  • 7,935
  • 2
  • 26
  • 42
  • Hi, thanks for your answer. Unfortunately the `myAction` would need to take a promise as an argument since res.json() returns one. We'd want to pass plain data to non-thunk action creators. – nerfologist Jun 14 '17 at 16:13
  • Someone starred my answer and caught my attention, so I am adding a modern async / await approach and fixing the previous answer. Hope this helps the readers. – oklas Apr 07 '21 at 06:34
0

My solution for the WP json API

fetch(getWPContent(searchTerm, page))
  .then(response => response.json().then(json => ({
    totalPages: response.headers.get("x-wp-totalpages"),
    totalHits: response.headers.get("x-wp-total"),
    json
  })))
  .then(result => {
    console.log(result)
  })
hyp0thetical
  • 300
  • 6
  • 19
0

If you want to parse all headers into an object (rather than keeping the Iterator) you can do as follows (based on Dan Abramov's approach above):

fetch('https://jsonplaceholder.typicode.com/users')
    .then(res => (res.headers.get('content-type').includes('json') ? res.json() : res.text())
    .then(data => ({
        headers: [...res.headers].reduce((acc, header) => {
            return {...acc, [header[0]]: header[1]};
        }, {}),
        status: res.status,
        data: data,
    }))
    .then((headers, status, data) => console.log(headers, status, data)));

or within an async context/function:

let response = await fetch('https://jsonplaceholder.typicode.com/users');

const data = await (
    response.headers.get('content-type').includes('json')
    ? response.json()
    : response.text()
);

response = {
    headers: [...response.headers].reduce((acc, header) => {
        return {...acc, [header[0]]: header[1]};
    }, {}),
    status: response.status,
    data: data,
};

will result in:

{
    data: [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}],
    headers: {
        cache-control: "public, max-age=14400"
        content-type: "application/json; charset=utf-8"
        expires: "Sun, 23 Jun 2019 22:50:21 GMT"
        pragma: "no-cache"
    },
    status: 200
}

depending on your use case this might be more convenient to use. This solution also takes into account the content-type to call either .json() or .text() on the response.

exside
  • 3,736
  • 1
  • 12
  • 19