2

I'm stuck in a wierd behaviour that I can't really debug.

The store dispatch the action that perform the login request passing username and password. Then when the response is ready I store the credentials in the redux store. When I need to perform an authorized request I set those parameters in the header request. When I receive the response I update the credentials in the store with the new ones that I get from the response. When I try to perform the third request it will respond unauthorized. I figured out that this is because all the parameters passed to my action generator setCredentials are null. I can't understand why also because if I add a debugger before the return statement of my setCredentials function and I wait some seconds before restart the execution I found out that the parameters aren't null anymore. I was thinking about the fact that the request is async but being inside a then statement the response should be ready right? I've also notice that fetch sent two request for each one. Here the code for more clarity.

import { combineReducers } from 'redux'
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const initialState = {
  currentUser: {
    credentials: {},
    user: {}
  },
  test: {},
  users: []
}

export const SUBMIT_LOGIN = 'SUBMIT_LOGIN'
export const SET_USER = 'SET_USER'
export const TEST = 'TEST'
export const SET_USERS = 'SET_USERS'
export const SET_CREDENTIALS = 'SET_CREDENTIALS'

//actions
const submitLogin = () => (dispatch) => {
  return postLoginRequest()
    .then(response => {
      dispatch(setCredentials(
        response.headers.get('access-token'),
        response.headers.get('client'),
        response.headers.get('expiry'),
        response.headers.get('token-type'),
        response.headers.get('uid')
      ));
      return response
    })
    .then(response => {
      return response.json();
    })
    .then(
      (user) => dispatch(setUser(user.data)),
    );
}

const performRequest = (api) => (dispatch) => {
  return api()
    .then(response => {
      dispatch(setCredentials(
        response.headers.get('access-token'),
        response.headers.get('client'),
        response.headers.get('expiry'),
        response.headers.get('token-type'),
        response.headers.get('uid')
      ));
      return response
    })
    .then(response => {return response.json()})
    .then(
      (users) => {
        dispatch(setUsers(users.data))
      },
    );
}

const setUsers = (users) => {
  return {
    type: SET_USERS,
    users
  }
}

const setUser = (user) => {
  return {
    type: SET_USER,
    user
  }
}

const setCredentials = (
  access_token,
  client,
  expiry,
  token_type,
  uid
) => {
  debugger
  return {
    type: SET_CREDENTIALS,
    credentials: {
      'access-token': access_token,
      client,
      expiry,
      'token-type': token_type,
      uid
    }
  }
}

//////////////
const currentUserInitialState = {
  credentials: {},
  user: {}
}

const currentUser = (state = currentUserInitialState, action) => {
  switch (action.type) {
    case SET_USER:
      return Object.assign({}, state, {user: action.user})
    case SET_CREDENTIALS:
      return Object.assign({}, state, {credentials: action.credentials})
    default:
      return state
  }
}

const rootReducer = combineReducers({
  currentUser,
  test
})

const getAuthorizedHeader = (store) => {
  const credentials = store.getState().currentUser.credentials
  const headers = new Headers(credentials)
  return headers
}

//store creation

const createStoreWithMiddleware = applyMiddleware(
  thunk
)(createStore);

const store = createStoreWithMiddleware(rootReducer);

const postLoginRequest = () => {
  return fetch('http://localhost:3000/auth/sign_in', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email: '[email protected]',
      password: 'password',
    })
  })
}

const getUsers = () => {
  const autorizedHeader = getAuthorizedHeader(store)
  return fetch('http://localhost:3000/users',
    {
      method: 'GET',
      headers : autorizedHeader
    }
  )
}

const getWorks = () => {
  const autorizedHeader = getAuthorizedHeader(store)
  return fetch('http://localhost:3000/work_offers',
    {
      method: 'GET',
      headers : autorizedHeader
    }
  )
}
// this request works fine
store.dispatch(submitLogin())

// this request works fine
setTimeout(() => {
  store.dispatch(performRequest(getUsers))
}, 3000)

// this fails
setTimeout(() => {
  store.dispatch(performRequest(getWorks))
}, 5000)
7
  • Have you verified that all your endpoints return those headers and not just the login one? Maybe when you performRequest(getUsers), it comes back with empty headers. Commented Feb 26, 2016 at 4:09
  • Yes. Verified. Every authorized request to my backend return those headers. @dan Commented Feb 27, 2016 at 14:21
  • Alas, hard to guess from the code. If you can create an isolated example that reproduces the issue in JSBin I can take a look. Commented Feb 27, 2016 at 16:37
  • Thank you very much. I'll do it. @dan Commented Feb 28, 2016 at 4:46
  • Hi Dan. I create this repo for the test. Thanks @DanAbramov Commented Mar 2, 2016 at 12:23

1 Answer 1

2

I should have clarified that when I asked

Have you verified that all your endpoints return those headers and not just the login one? Maybe when you performRequest(getUsers), it comes back with empty headers.

I didn’t just mean the server logic. I meant opening the Network tab in DevTools and actually verifying whether your responses contain the headers you expect. It turns out getUsers() headers do not always contain the credentials:

Now that we confirmed this happens, let’s see why.

You dispatch submitLogin() and performRequest(getUsers) roughly at the same time. In the cases when the error is reproduced, the problem is in the following sequence of steps:

  1. You fire off submitLogin()
  2. You fire off performRequest(getUsers) before submitLogin() comes back
  3. submitLogin() comes back and stores the credentials from the response headers
  4. performRequest(getUsers) comes back but since it started before credentials were available, the server responds with empty headers, and those empty credentials are stored instead of the existing ones
  5. performRequest(getWorks) is now requested without the credentials

There are several fixes for this problem.

Don’t Let Old Unauthorized Requests Overwrite the Credentials

I don’t think it really makes sense to overwrite existing good credentials with the empty ones, does it? You can either check that they are non-empty in performRequest before dispatching:

const performRequest = (api) => (dispatch, getState) => {
  return api()
    .then(response => {
      if (response.headers.get('access-token')) {
        dispatch(setCredentials(
          response.headers.get('access-token'),
          response.headers.get('client'),
          response.headers.get('expiry'),
          response.headers.get('token-type'),
          response.headers.get('uid')
        ));
      }
      return response
    })
    .then(response => {return response.json()})
    .then(
      (users) => {
        dispatch(setUsers(users.data))
      },
    );
}

Alternatively, you can do ignore invalid credentials in the reducer itself:

case SET_CREDENTIALS:
  if (action.credentials['access-token']) {
    return Object.assign({}, state, {credentials: action.credentials})
  } else {
    return state
  }

Both ways are fine and depend on the conventions that make more sense to you.

Wait Before Performing Requests

In any case, do you really want to fire getUsers() before you have the credentials? If not, fire off the requests only until the credentials are available. Something like this:

store.dispatch(submitLogin()).then(() => {
  store.dispatch(performRequest(getUsers))
  store.dispatch(performRequest(getWorks))
})

If it’s not always feasible or you would like more sophisticated logic like retrying failed requests, I suggest you to look at Redux Saga which lets you use powerful concurrency primitives to schedule this kind of work.

Sign up to request clarification or add additional context in comments.

2 Comments

I imagined this could be the problem but I don't understand why this happens since performRequest(getUsers) or performRequest(getUsers) are fired of in a setTimeout function. I supposed that in 3 seconds the login request should return the response. Anyway do you think this is a good way to manage the credentials (I mean saving them in the store)? Thanks you very much for your time. @dan
Well, relying on response coming exactly during some period of time is not very wise, at least in production code :-). You can put more logs and check whether my diagnosis is correct. Unfortunately I don’t have time right now to open your project again and do it for you :-). Yes, the approach seems fine.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.