extract both JSON and headers from fetch()
Asked Answered
R

4

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: '[email protected]', 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({ '[email protected]', 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!

Roemer answered 23/1, 2017 at 17:22 Comment(0)
R
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 (https://mcmap.net/q/629785/-how-to-handle-errors-in-fetch-responses-with-redux-thunk)

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

Roemer answered 14/6, 2017 at 15:58 Comment(3)
The second one seems more appropriate for general use.Assemble
Indeed that's what we ended up using in our codebase.Roemer
Thank you very much for Dan solution. Quite clean and understandable even by me. But I think it lacks parenthesis in the last arrow function. It should be .then( ({headers,status,json}) => ...) instead of .then( {headers,status,json} => ...)Sangsanger
R
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.

Rale answered 23/1, 2017 at 17:30 Comment(2)
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.Roemer
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.Rale
G
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)
  })
Goatsbeard answered 18/12, 2018 at 4:43 Comment(0)
I
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.

Indigested answered 23/6, 2019 at 18:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.