use-http
use-http copied to clipboard
[Request Interceptor] Refresh token and update global option
Hi,
I have been struggling for two days on this. I'm not sure if it's a bug or me not getting the art of this Provider feature.
I have this Provider :
const FetchProvider = ({ children }) => {
let [token, setToken] = useState(localStorage.getItem("token"))
let [refresh, setRefresh] = useState(localStorage.getItem("refresh"))
const url = 'http://127.0.0.1:8000'
const [request_refresh_token, response_refresh_token] = useFetch(url);
const getToken = async () => {
console.log("is_executed")
let bodyFormData = new FormData();
bodyFormData.append('refresh', refresh );
let newToken = await request_refresh_token.post('/api/user/token_jwt/refresh/',bodyFormData)
if (response_refresh_token.ok) {
return newToken.access
}
else {
console.error('there was a problem authenticating the user')
}
}
const globalOptions = {
interceptors: {
async request(options) {
options = {headers: {Authorization: `Bearer ${token}`}};
console.log("OPTION_ENTREE=",options)
if (isExpired(token)) {
const new_token = await getToken()
options.headers.Authorization = `Bearer ${new_token}`
setToken(new_token)
}
return options
}
}
}
if (isExpired(refresh)){
return <Redirect to="/login" />
}
return <Provider url={url} options={globalOptions}> {children} </Provider>
}
export default FetchProvider
The child component is as follow :
export default function DietHistory(props) {
const [request, response] = useFetch(); //no argument there, eveyrhting is passed with the interceptor
const [recipes, updateRecipes] = useState([])
const [sent_recipes,updateSentRecipes] = useState([])
const [spinner_on_off,setSpinner] = useState(false)
const [open_snackbar,setSnackBar]= useState(false)
let [request_send, response_send] = useFetch(); //no argument there, eveyrhting is passed with the interceptor
const [recipe_being_sent,setRecipe_being_sent] = useState(['owner_email',0])
const { state: authState } = useContext(AuthContext);
useEffect(() => {
initializeRecipes()
}, [])
async function initializeRecipes() {
const initialRecipes = await request.get('/api/recipe/recipe_list/')
if (response.ok) updateRecipes(initialRecipes)
}
async function sendPDF (recipe_id,recipe_email){
setRecipe_being_sent([recipe_email,recipe_id])
setSpinner(true)
setSnackBar(true)
const send_pdf = await request_send.get('/api/recipe/send-email?id='+recipe_id)
if (response_send.ok) updateSentRecipes([...sent_recipes,recipe_id])
setSpinner(false)
setTimeout(() => {
setSnackBar(false)
}, 2000)
};
if (request.loading) return <CircularProgress/>
if (request.error) return <p>Error!</p>
if (recipes) {
return (
<div>
<div style={{maxWidth: "100%"}}>
<MaterialTable
icons={tableIcons}
columns={[
{title : "Print PDF", field:'PDF', render: rowData => {return (<ButtonPrintPdf recipe_id = {rowData.id} />)} },
{title : "Send PDF", field:'PDF', render : (rowData) => {
return(<ButtonLoadding
email = {rowData.owner_email}
loading = {spinner_on_off}
sent_recipes = {sent_recipes}
recipeid = {rowData.id}
recipeid_being_sent = {recipe_being_sent[1]}
tool_to_sendPDF = {sendPDF}
/>)
}
},
]}
data={recipes}
/>
</div>
</div>
);
}
}
So when I hit sendPDF after the token has expired, the provider doesn't seem to update the globalOption and just send back the previous token (which has now expired), causing a 401 error.
However If I just re-render the whole children component, then the token is nicely refreshed and pass to all of my global option.
It seems like global option cannot be updated on the go (conditionally, when the token has expired for exemple).
Am I missing something ?
- Please make a codesandbox
- try using react-use-localstorage
- what version of use-http are you using?
Here is a cleaned up version below, but I need some runnable code in a codesandbox
import useLocalStorage from 'react-use-localstorage'
const FetchProvider = props => {
const [token, setToken] = useLocalStorage('token') // try using this package
const [refresh, setRefresh] = useLocalStorage('refresh')
const url = 'http://127.0.0.1:8000'
const [request, response] = useFetch(url);
const getToken = async () => {
console.log("is_executed")
let bodyFormData = new FormData();
bodyFormData.append('refresh', refresh);
let newToken = await request.post('/api/user/token_jwt/refresh/', bodyFormData)
if (response.ok) return newToken.access
console.error('there was a problem authenticating the user')
}
const globalOptions = {
interceptors: {
async request({ options }) { // <- probably need to add curly braces here
options.headers = {
...(options.headers || {}),
Authorization: `Bearer ${token}`
}
console.log("OPTION_ENTREE=", options)
if (isExpired(token)) {
const new_token = await getToken()
options.headers.Authorization = `Bearer ${new_token}`
setToken(new_token)
}
return options
}
}
}
if (isExpired(refresh)) return <Redirect to="/login" />
return <Provider {...props} url={url} options={globalOptions} />
}
export default FetchProvider
and
export default function DietHistory(props) {
const [request, response] = useFetch(); // no need for 2 of these
const [recipes, updateRecipes] = useState([]);
const [sent_recipes, updateSentRecipes] = useState([]);
const [spinner_on_off, setSpinner] = useState(false);
const [open_snackbar, setSnackBar] = useState(false);
const [recipe_being_sent, setRecipe_being_sent] = useState([
"owner_email",
0,
]);
const { state: authState } = useContext(AuthContext);
useEffect(() => {
initializeRecipes();
}, []);
async function initializeRecipes() {
const initialRecipes = await request.get("/api/recipe/recipe_list/");
if (response.ok) updateRecipes(initialRecipes);
}
async function sendPDF(recipe_id, recipe_email) {
setRecipe_being_sent([recipe_email, recipe_id]);
setSpinner(true);
setSnackBar(true);
const send_pdf = await request.get(
"/api/recipe/send-email?id=" + recipe_id
);
if (request.ok) updateSentRecipes([...sent_recipes, recipe_id]);
setSpinner(false);
setTimeout(() => {
setSnackBar(false);
}, 2000);
}
if (request.loading) return <CircularProgress />;
if (request.error) return <p>Error!</p>;
if (recipes) {
return (
<div>
<div style={{ maxWidth: "100%" }}>
<MaterialTable
icons={tableIcons}
columns={[
{
title: "Print PDF",
field: "PDF",
render: (rowData) => {
return <ButtonPrintPdf recipe_id={rowData.id} />;
},
},
{
title: "Send PDF",
field: "PDF",
render: (rowData) => {
return (
<ButtonLoadding
email={rowData.owner_email}
loading={spinner_on_off}
sent_recipes={sent_recipes}
recipeid={rowData.id}
recipeid_being_sent={recipe_being_sent[1]}
tool_to_sendPDF={sendPDF}
/>
);
},
},
]}
data={recipes}
/>
</div>
</div>
);
}
}
-
Alright, can you tell me how I can mimick in SandBox token refresh or delivery ?
-
Got it.
-
"use-http": "^0.4.5",
-
On a side note, if you merge the two request object from my component
DietHistory, then you cannot split the loading. I didn't understand why you wanted to merge them.
start by forking this codesandbox and try to reproduce. Then put a link to your reproduced bug here.
Hey @alex-cory, Thanks to find some time to help. So here is a codesandbox
Behaviour :
- Initialize a list of users with a given token.
- Wait 30s. The token expired.
- Fire
<SendIcon/>to get a post. - This trigger a
get_tokenasync call which is supposed to return a new token. - The
headersis not changed, token isundefined.
That's the closest I'm getting from the real bug. The real bug I'm just getting the old expired token.
I would say it's a problem with await or a state update problem.
What's your take on this ?
@lgm-dev Hi, I think I have the same problem with you, and here is my sandbox example , it may be more simple than yours. I found the Provider can not read the real time state value, it may cache some thing.
Hi, @alex-cory , Am I missing something ?
I will take a look tomorrow. Heading to sleep. It's 2:13am.
I think I know what happened... I don't know if it's a bug... @alex-cory
Here is the reason: https://github.com/ava/use-http/blob/master/src/Provider.tsx#L14
Because you used useMemo to set the provider context value, and if we set globalOptions only include 2 functions, so, even token is update, but globalOptions will not change(because function is a pointer), so it still use the previous value.
For now, we can do this for avoid that :
const App = () => {
const [token, setToken] = useState()
useEffect(() => {
setTimeout(() => {
setToken("new token")
}, 1000)
}, [])
const globalOptions = {
headers:{
token:token,
},
interceptors: {
request: ({ options }) => {
options.headers = {
Authorization: `Bearer ${token}`
}
console.log("interceptors token", token)
return options
},
response: ({ response }) => {
console.log("initial resopnse.data", response.data)
return response
}
}
}
return (
<Provider options={globalOptions}>
<TestUseFetch token={token} />
</Provider>
)
}
@lgm-dev
Thanks for the idea @theowenyoung , but I fear that if we proceed this way, the point of intercepting the request sounds a bit useless. What do you think @alex-cory ?
This issue is similar (or likely the same) as mine, and I've been hesitating to suggest this, but I've tried using a different means of determining deep equality on the following line and it seems to fix the issue:
https://github.com/ava/use-http/blob/0656ec547b805ecaadac035b986a5e0a4ec8bfd5/src/utils.ts#L235
At the risk of coming across as ignorant, I believe the reason this works is because the interceptor references change here:
https://github.com/ava/use-http/blob/0656ec547b805ecaadac035b986a5e0a4ec8bfd5/src/useFetchArgs.ts#L36
...and while a deep equality function can pick up on the changes to function references, JSON.stringify cannot.
The options are also memoized, and the interceptor function references should change each time the options change. This should happen when the provider gets re-rendered, so the function references shouldn't change an indefinite number of times.
Comparing function references might cause other issues outside of interceptors, but since the interceptors are already memoized, and I don't think there are any other functions in the dependency array of makeFetch (as far as I can see) that aren't memoized in some way, it should work.
Hey Guys,
Does someone have.a nice workaround for this issue?
I don't understand exactly @theowenyoung solution.
Thanks for your help,
Kind regards
Apologies guys. I've been dealing with a lot of personal/family issues recently. I will get to this asap.
@theowenyoung : give a desperate try to your solution today. No success (again). Even when you pass the token down to the children component, the globablOptionare keeping the previous token as a bearer. Would you be kind enough to provide a SandBox ?
@CaveSeal : in another issue you seemed to have found a simpler solution to this token-refreshing problem ? anything to help :) ?
@alex-cory : good luck with your issues :'(
@lgm-dev The way I solved my issue won't work for you, because I didn't need to do any checks (i.e. for expiry) on each request. However, the issue of being unable to see an updated token in the interceptor is (I believe) due to the object comparison that happens when the various options are memoized, which doesn't take account for changes in function references. The function references never change, so you always see the same outer environment, which means you'll only see the token as it was initially. Wouldn't call myself a Javascript expert though, so this is all wild speculation.
One thing that might work as a workaround (though not ideal) is if you retrieved your token from local storage inside the interceptor. It's also likely that you'll have to do the same with the refresh token.
interceptors: {
async request(options) {
const token = localStorage.getItem("token")
options = {headers: {Authorization: `Bearer ${token}`}};
console.log("OPTION_ENTREE=",options)
if (isExpired(token)) {
const new_token = await getToken()
options.headers.Authorization = `Bearer ${new_token}`
setToken(new_token)
}
return options
}
}
I've got a PR up that I believe solves this issue and all similar issues, but we'll see what happens.
I'm facing this issue too, any update about when this will be solved/published? I can help with a PR or anything to delivery this bug fix, let me know how I can help
I search in the Pull requests and I found this one submitted by @CaveSeal https://github.com/ava/use-http/issues/268 just adding here to let more information about the status of this issue
I am having the same issue right know and I was wondering, if there is any solution to this. Thank you