In React Apollo client, we have some links configured.
const apollo = new ApolloClient({
cache: new InMemoryCache(),
link: from([
onErrorLink,
afterwareLink,
httpLink
])
});
In our application, when session times out we return the response with 200 status and some response headers (timeout: true). By using these response headers we need to show some dialog on UI. The response header also has content-type: text/html. Also remember, sessions are maintained on federal level (SSO) in our organization and we do not control response context for that.
When session times out and we receive response from server, Apollo react client onError link fires up and we try to access response as below.
const context = operation.getContext();
const response = context.response;
const headers = response.headers;
In above code, context never has response property and hence response is undefined. Due to this, I am not able to access header values.
Analysis
const afterwareLink = new ApolloLink((operation, forward) => {
return forward(operation).map(response => {
const context = operation.getContext();
const response = context.response;
const headers = response.headers;
return response;
})
};
I have seen in case of error, only onError is resolved and afterwareLink map function never triggers.
response is populated in context only if comes via forward function given by Apollo.
In successful API calls, afterwareLink map function do resolves and I am able to find all headers just fine.
Solutions online are similar but just does not work.
I am stumped here. I can clearly see response headers in Dev Tools and Fiddler. It's for sure there. Just Apollo is not allowing me to access that. In my case, networkError or graphQLErrors are not of much use.
This was few steps problem to solve. Sharing my findings and solutions below.
Headers
Make sure the response from API has the following header value.
Access-Control-Allow-Origin: http://uiclient.com
Access-Control-Expose-Headers: *
The first header will make sure the browser is satisfied and allows UI to resolve the response. The second header will make sure UI can read the response headers.
Response Content Type
In my case response type was not as expected by Graph and cannot be changed as this was from federal layer (SSO). Others can face the same problem in the case of third party service, 401, 403 or other status codes.
To resolve the problem, I wrote a simple custom fetch function and attached it to HttpLink.
const httpFetch = (uri, options) => {
return fetch(uri, options).then(response => {
// ... read headers from response.headers.
if (response.status >= 500) { // or handle 400 errors
return Promise.reject(response.status);
}
return response;
}).catch(error => {
return Promise.reject(error);
});
};
const httpLink = new HttpLink({
uri: 'https://serviceapi.com',
fetch: httpFetch
});
Handle httpFetch
This technique can be used to read data in Apollo client when your response is not JSON (or is JSON). In my case, I checked the header first and if the header had session timeout value then resolved the promise using a custom object (as the main content will be HTML).
The above steps will mostly help you to customize any response in Apollo. Also note, I did not write any custom middleware (or link) to address this issue.
Related
I'm not getting an error but when I saw the logs of my server it prints an empty object {} whenever I send a request to the sever from my react app using axios. I double checked everything every other request in another components of my app works fine, but only in this particular request the data is not being sent! I have no CORS issue!
My react axios request
// PrivateAxios instance to send api request
const axiosPrivate = useAxiosPrivate();
const handleSearch = async () => {
const data = JSON.stringify({ from_company: keyWord });
try {
const response = await axiosPrivate.get(SEARCH_URL, data);
console.log(response);
setRecords(response?.data);
} catch (err) {
if (!err?.response) {
console.log("NO SERVER RESPONSE");
} else {
console.log("SOMETHING WRONG");
}
}
};
Server log
{} <-- Prints the request.data as an empty object
"GET /api/find_many/ HTTP/1.1" 200 6276
The django server responses with correct details when I send a request with Postman or Thunder Client. The server also prints the object that were sent with the Postman request. I don't know why the server is unable to get the object or data when I request from my react app.
Request sent from Postman returns
{'from_company': 'Jethmal Paliwal'} <-- Prints the request.data correctly
"GET /api/find_many/ HTTP/1.1" 200 2284
I have double checked everything, my headers are set correctly, Content-Type: application/json, withCredentials: true, and every other possible settings, Even every request from other components works great, but why this particular request doesn't reach the server?
Tried writing the data as an Object in the request funcion itself
const response = axiosPrivate.get(SEARCH_URL, { "from_company": "Jethmal Paliwal" }); which doesn't work as well. The same empty object gets printed.
Tried JSON.stringify the data, which doesn't work as well.
I believe that axios is omitting the data as it's not per REST standard to submit data in the GET request. HTTP allows that, but people and apparently libraries are not expecting it.
This is the API for axios get method:
axios.get(url[, config])
as you see there is no data in the method signature. And if we look at the POST method:
axios.post(url[, data[, config]])
I suggest if you have data to submit to server that you use a POST method instead.
I set up passport-local to login a user, and then once logged in, the user will be given a JWT token through passport-JWT. The JWTStrategy is set up to use
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() so that the JWT can sent with the Authorization header Authorization: `Token ${userContext.token}`}. In my react client side, I have set up a GET request using axios as shown here:
const fetchProfileDetails = async(config)=>{
const res = await axios.get("http://localhost:8080/users/me", config)
}
const config = {
method:"GET",
withCredentials: true,
headers: {Authorization: `Bearer ${userContext.token}`}
}
This request successfully authenticates and returns the user data from /me.
Now heres the kicker: when I use the exact same request structure, but switch the method to post in the axios request and in my express route in the backend, the request always responds with a 401 Unauthorized.
However, when I send the same request from POSTMAN, with the same Bearer Token used in the request that was Unauthorized, the request succeeds without any errors.
TLDR: GET requests work with the JWT token and return with a 200 status code, while the same request with a POST method returns with a 401 status code.
What am I missing here??!
You are probably using there GET and not using POST anywhere.
In your code, you have code only for get. You will need to write code for post as well.
Below is the code for post for your reference:
router.post('/', config, async(req, res, next) => {
const { error } = validateBody(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const newData= new passport({ name: req.body.name });
await newData.save();
console.log('saving the document');
res.send(newData);
})
Your code should have post as well. Writing single code will not work. You need to have to write code for every condition and every possibility. So like for get need code for post as well, also if you have condition for patch or delete or put you will have to write the axios method for that as well.
Hope this has helped you in any way.
I have come upon a solution for this issue. For some reason, axios was not maintaining the Authorization header I had set in my config variable, and deleted it upon making the request. To solve this, I just had to reshuffle my axios request to look like this:
const res = await axios({
method:'POST',
url:"http://localhost:8080/users/test",
headers:{'Authorization':`Bearer${token}`
}})
I feel cheated as I spent a ton of time on this and the solution was so underwhelming. Axios was trolling me the whole time :/
Was making a network call in getStaticProps to APIs of superhero.com which worked but when I tried making the same in useEffect it is throwing CORS error.
Access to fetch at 'https://superheroapi.com/api//1' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I have tried making network calls using fetch as well as axios but getting the same error. Does the problem or limitation lie with Next.js?
Edit: Tried using JSON placeholder APIs in useEffect and it is working.
CORS
CORS errors happen when you're trying to access resources from one domain on another domain. It only happens in the browser, and is a security feature.
So essentially, when you're fetching data from https://superheroapi.com/api/1 while on localhost:3000, the browser first asks superheroapi.com, "hey, can this domain fetch data from you?". superheroapi.com will then say, "I only accept requests from these domains". If localhost:3000 is not in that list, you'll get a CORS error.
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
You can change the domains that superheroapi.com accepts via the Access-Control-Allow-Origin header. You can do it manually, or there's a handy npm package that will take care of that for you in Next.js.
Fix CORS in Next.js
By default in Next.js, the CORS header is restricted to same-domain traffic only. However, you can change this.
Next.js actually has a guide in their docs on adding a CORS header to api routes.
https://nextjs.org/docs/api-routes/api-middlewares#connectexpress-middleware-support
In short, though, first install the CORS package.
npm i cors
# or
yarn add cors
# or
pnpm add cors
Then add it to the API route.
import Cors from 'cors'
// Initializing the cors middleware
const cors = Cors({
methods: ['GET', 'HEAD'],
})
// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) {
return reject(result)
}
return resolve(result)
})
})
}
async function handler(req, res) {
// Run the middleware
await runMiddleware(req, res, cors)
// Rest of the API logic
res.json({ message: 'Hello Everyone!' })
}
export default handler
Code snippets are taken from the Next.js docs. All credit goes to the makers of Next.js for them.
Code in useEffect runs in the frontend/browser therefore must obey the CORS. getStaticProps runs at build time, therefore do not have the CORS limitation.
If superheroapi.com API is under your control, you should add CORS response headers to fix the issue. Otherwise you need to create a reverse proxy, e.g., https://www.npmjs.com/package/cors-anywhere
You must run your code on a domain that is validated.
Some api give you cors error for localhost request.
Simple way is: Try to define a local domain (Change your local dns for that domain to 127.0.0.1 in host file) and write a server.js file like this.
Replace 'next.yourdomain.com' with your domain ;)
const { createServer, } = require('http');
const { parse, } = require('url');
const next = require('next');
// const dev = process.env.NODE_ENV !== 'production';
const dev = false;
const app = next({dev, });
const handle = app.getRequestHandler();
const HOST = 'next.yourdomain.com';
const PORT = 8090;
app.prepare().then(() => {
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
}).listen(PORT, HOST, (err) => {
if (err) throw err;
console.log(`Starting Next.js at http://${HOST}:${PORT}`);
});
});
Then run
next build && node server.js
Also you must discard this file on your final deploy.
I'm trying to get cookie or localStorage value in my API request. When I'm trying to access localStorage I get error that localStorage is not defined, cookies are undefined, I tried my luck with AsyncLocalStorage, but I'm getting error that window is undefined. There is any way to get saved value in this asynchronous function?
import AsyncLocalStorage from '#createnextapp/async-local-storage'
export default async (req, res) => {
try {
let data = await AsyncLocalStorage.getItem('#key')
console.log(data)
}catch(error){
console.log(error)
}
}
This is expected. When you process the data in your API it is actually running in the server. Localstorage, and cookies however are browser based storage (This is also why window is undefined, servers don't have windows). As a result they are not available to the API to leverage. Instead when you make the API request you need to add the data into a header which can then be parsed on your server.
Setting a header on your request:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
From there you should be able to read the headers through:
(req, res) => {
// look at all these great headers we have
const { headers } = req;
}
I have a redux store set up with actions to handle loading accounts. The action calls a service like so:
const requestOptions = {
method: 'GET',
headers: authHeader()
};
return fetch(`http://localapi.co.uk/api/account/load/${account_id}`, requestOptions)
.then(handleResponse)
.then(account => {
if(account.account.id) {
localStorage.setItem('account', JSON.stringify(account))
}
return account;
})
.catch(redirectToLogin)
Handle response is simply a function that checks the .status and .ok properties of the response and either displays an error or logs out if the response status is 401. This works perfectly fine for POST requests. When I hit my login route, any response hits the first .then(handleResponse) and deals with it.
When I send a GET request instead like above 404s, 401s, 500s.. etc all skip the .then(handleResponse) and instead jump to my catch. The problem that causes is that because catch doesn't actually give me a response object to work with I can't check the status - I want to do different things depending on whether the get was a 401 (I want to logout) or a 500 (I want to display a user error stating what went wrong) for example.
Is there a solution that will allow me to get a response or stop my GET requests hitting the .catch and instead hit the response handler I've written?
I'm using:
a Laravel 5.7.20 back-end
a React 16.7.0 front-end
running local node server with npm start
How about having a clean async/await function. Waiting till you JSON data becomes ready and then having the rest of your if logic or returning the account object from the function and moving the rest of the logic to the caller function. Something like this:
async myFunc({ account_id}) {
const url = `http://localapi.co.uk/api/account/load/${account_id}`;
const response = await fetch(url, { headers: headers: authHeader() });
const account = await response.json();
// return account (recommended);
// Place your if logic here (not recommended because its not clean)
}