Add cookie to axios interceptor request handler - reactjs

I'm configuring Axios to always make a request with header Authorization with a value which is in user cookie.
My code:
import axios, { AxiosRequestConfig, AxiosResponse} from 'axios';
import {useCookies} from "react-cookie";
const [cookies] = useCookies(["myToken"]);
const customAxios = axios.create({
baseURL: 'http://localhost/storeApi/',
timeout: 10000,
});
const requestHandler = (request: AxiosRequestConfig) => {
request.headers.Authorization = `Bearer ${cookies.jwtToken}`;
return request;
};
customAxios.interceptors.request.use(
(request) => requestHandler(request)
);
export default customAxios;
But I have an error:
Line 3:19: React Hook "useCookies" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function
How to avoid that?

Since it is a React Hook, you can't use useCookies outside a React component function: to access a cookie, you'll need to read it from document.cookie, or install another package, like cookie.
If you're only using the one cookie, you can probably get away by using w3School's cookie example, (which I've turned into an npm package):
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
Then just do:
const cookie: string = getCookie('myToken');

Related

Rails API + React Frontend - how to make CSRF cookie NOT httponly?

I have a Rails 6 API and a React frontend and I would like to keep verify_authenticity_token on all controller actions.
Backend sets CSRF token as follows in application_controller.rb:
class ApplicationController < ActionController::Base
...
include ActionController::Cookies
after_action :set_csrf_cookie
...
protected
def verified_request?
super || request.headers['X-CSRF-Token'] === cookies['X-CSRF-Token']
end
def set_csrf_cookie
if protect_against_forgery? && current_user
cookies['X-CSRF-Token'] = {
value: form_authenticity_token,
httponly: false
}
end
end
end
Frontend is attempting to use js-cookie to retrieve cookies. I have the following in a cookies.js file:
import Cookies from 'js-cookie'
const getCSRFToken = () => {
window.Cookies = Cookies;
const token = Cookies.get('X-CSRF-Token')
return token
}
export default getCSRFToken
and I call this function when I create an Axios request. The function to build the request takes params like method, url, data, etc.:
export const newAxiosIns = params => {
// params will be a hash of various headers
const defaultParams = {
baseURL: baseUrl,
withCredentials: true,
headers: {
common: {
'X-CSRF-TOKEN': getCSRFToken()
}
}
}
const axiosIns = axios.create(defaultParams)
return axiosIns(params)
}
But the cookies end up being httponly in Chrome:
I wondered if it had to do with the form_authenticity_token, so I made a fake token with a value of 'faker' but that was also not httponly.
Thanks!

Why doesn't this update of an axios interceptor work?

I have an axios client, defined in a provider context and accessed throughout my app with a useAxios hook.
The provider uses an access token, which gets updated every few minutes using a token refresh. This is definitely updating correctly.
I use an interceptor to add the token to the request headers. But of course, I want the latest token. So, each time the token updates, I change the interceptor to use the new one. I do this in a useEffect which only fires if the token changes, and I have verified that this effect is firing correctly.
But, any use of the axios client subsequent to a token refresh still uses the original interceptors - the ones that were defined when the app was first loaded. Consequently, after the original token times out, all my requests are unauthenticated even though I've added an interceptor with an up to date token, because teh original interceptor is being used...
THE QUESTION
How do I correctly update this interceptor so the latest one is used on every query?
THE CODE
import React, { useEffect } from 'react'
import axios from 'axios'
import { useAuth } from './useAuth'
import { apiErrors } from 'errors'
export const AxiosContext = React.createContext(undefined)
const AxiosProvider = ({ children }) => {
const { accessToken } = useAuth()
const axiosClient = React.useMemo(() => {
return axios.create({
headers: {
'Content-Type': 'application/json',
},
})
}, [])
// Attach request interceptor to add the auth headers
useEffect(() => {
console.log(
'VERIFIED TO WORK - THIS SHOWS PERIODICALLY ON TOKEN UPDATE, WITH THE NEW TOKEN',
accessToken
)
axiosClient.interceptors.request.use((request) => {
if (accessToken) {
request.headers.Authorization = `JWT ${accessToken}`
}
return request
})
}, [axiosClient, accessToken])
return (
<AxiosContext.Provider value={axiosClient}>
{children}
</AxiosContext.Provider>
)
}
// A hook for accessing this authenticated axios instance
function useAxios() {
return React.useContext(AxiosContext)
}
export { AxiosProvider, useAxios }

Why does my Axios Response return a 404 even though I am setting the mock response?

I am trying to mock my REST requests for a react/ts project when testing in Storybook using Axios. Even though I am setting the response to an array object, it still seems to be responding with a "Request failed with status code 404" status.
Here is my component making the REST call: TestPrompt.tsx
const onClickHandler = () => {
requestOrMock("http://localhost:9002/projectPlan/projectTasks?project=FAKEWID")
}
Here is the method my TestPrompt component is using to make the request: UtilityFunctions.ts
import axios from 'axios';
export const axiosMock = axios.create();
export const requestOrMock = async (uri: string) => {
const response = axiosMock.get(uri);
return response;
}
Here is my test that is mocking the response: Prompt.stories.tsx
import * as React from "react";
import {storiesOf} from '#storybook/react';
import TestPrompt from "../components/common/Prompt";
import MockAdapter from 'axios-mock-adapter';
import { axiosMock } from "../utils/utilityFunctions";
const mock = new MockAdapter(axiosMock);
const blankPromptRequestUri = "http://localhost:9002/projectPlan/projectTasks?project=FAKEWID";
const footballTeams = [
{
"descriptor": "New England Patriots",
"id": "NewEnglandPatriots"
},
{
"descriptor": "Seattle Seahawks",
"id": "SeattleSeahawks"
}
];
storiesOf('Components/MultiSelect', module)
.add('Prompt1', () => {
mock.onGet(blankPromptRequestUri).reply(200, footballTeams);
return (
<TestPrompt/>
);
})
When I click on this component in storybook, it sends out the request to the designated url, but it gets the 404 response rather than the footballTeams object I have specified. Any idea what I have done wrong? Thanks for your help.
If I get your problem correctly, you need to call onGet() of mock to setup the mock end point and then send a request to that end point.
mock.onGet("/teams").reply(200, footballTeams);
storiesOf('Components/MultiSelect', module)
.add('Prompt1', () => {
axios.get("/teams")
.then(res => console.log(res.data))
return (
<TestPrompt/>
);
})
The requests that were being made were being made relative to the host, so rather than "http://localhost:9002/projectPlan/projectTasks?project=FAKEWID" being sent, it was actually "/projectPlan/projectTasks?project=FAKEWID". It is likely that you will only need to pass in the routes here.

Cancel requests on route change (React/Redux/Axios)

Is there convenient way to cancel all sending request on any route changes using axios, redux-thunk, redux? I know that axios has cancellation token which should be added to every request and I can call source.cancel(/* message */) to cancel it.
P.S. Currently I handle this in componentWillUnmount. Maybe there is something better?
The easiest way which I found is to store source in state, and use source.cancel if request is sending.
componentWillUnmount() { if(isLoading) { source.cancel() } }
Very simple solution would be to declare isMounted variable and set it to false when component unmounts.
Other way of handling this issue is, (I'm not sure about axios but) XMLHttpRequest has abort method on it. You could call xhr.abort() on componentWillUnmount.
Check it here: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort
And finally there is an industry solution :) from Netflix UI engineers. They have written a redux middleware for RxJS and using RxJS operators to fire up and cancel the requests.
The library is called redux-observable. I recommend to take a look at examples: https://redux-observable.js.org/docs/recipes/Cancellation.html
You can watch the talk about it here: https://youtu.be/AslncyG8whg
I found a simple solution (without redux) to do that. All you need is a cancelToken in your axios requests. After, use the useHook to detect route changes. Then, cancel the requests with the TOKEN when the route is unmounted and to generate a new TOKEN to make a new request. See the Axios Doc to more details (https://github.com/axios/axios).
Route.tsx file
import React, { useEffect } from 'react';
import { Route, RouteProps, useLocation } from 'react-router-dom';
import API from 'src/services/service';
const CustomRoute = (props: RouteProps) => {
const location = useLocation();
// Detect Route Change
useEffect(() => {
handleRouteChange();
return () => {
handleRouteComponentUnmount();
};
}, [location?.pathname]);
function handleRouteChange() {
// ...
}
function handleRouteComponentUnmount() {
API.finishPendingRequests('RouteChange');
}
return <Route {...props} />;
};
export default CustomRoute;
Service.ts file
import { Response } from 'src/models/request';
import axios, {AxiosInstance, AxiosResponse } from 'axios';
const ORIGIN_URL = 'https://myserver.com'
const BASE_URL = ORIGIN_URL + '/api';
let CANCEL_TOKEN_SOURCE = axios.CancelToken.source();
function generateNewCancelTokenSource() {
CANCEL_TOKEN_SOURCE = axios.CancelToken.source();
}
export const axiosInstance: AxiosInstance = axios.create({
baseURL: BASE_URL,
});
const API = {
get<DataResponseType = any>(
endpoint: string,
): Promise<AxiosResponse<Response<DataResponseType>>> {
return axiosInstance.get<Response<DataResponseType>>(endpoint, {
cancelToken: CANCEL_TOKEN_SOURCE.token,
});
},
// ...Another Functions
finishPendingRequests(cancellationReason: string) {
CANCEL_TOKEN_SOURCE.cancel(cancellationReason);
generateNewCancelTokenSource();
},
};
export default API;

React apollo default query variable from redux store

I am trying to make a multilingual single page application using react-apollo. The сurrent locale is stored in redux store. The problem is that I have to connect every component to redux just to pass locale variable to the query. Such repetition of code looks redundant. I cannot hardcode locale to the query because it can be different for different users. I need to pass default value dynamically. Any ideas?
You could add the locale as a header to every request you send to your server using the apollo client's middleware. Store the locale in localStorage if you are using a browser, or asyncStorage if you are using react native.
import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { GRAPHQL_API_DEV } from './api';
import { checkForLocale } from '../../utils/locale';
const networkInterface = createNetworkInterface({
uri: GRAPHQL_API_DEV
});
networkInterface.use([{
applyMiddleware(req, next) {
// Create the header object if needed.
if (!req.options.headers) {
req.options.headers = {};
}
// get the locale token from Async storage
// and assign it to the request object
checkForLocale()
.then(LOCALE => {
req.options.headers.custom-header = LOCALE;
next();
})
.catch(error => {
req.options.headers.custom-header = null;
next();
})
}
}]);
const client = new ApolloClient({
networkInterface,
dataIdFromObject: o => o.id
});
export default client;

Resources