Concurrent requests mixes up context
See original GitHub issueHi!
We’re having a couple of issues with apollo-server-express and apollo-rest-datasource.
We do authentication and authorization based on a header. After validating the user, we fetch some user details and set them on the context, together with the auth header (to be used by datasources).
Our issue is that during simultaneous requests that context seems to be mixed up together with other request contexts, making it so that we supply underlying services with the wrong auth header.
async updateUserDetails(args) {
// First request made, context is for the RIGHT user
await this.post(`v1/user-details`, args);
// Second request made, context is SOMETIMES for the WRONG user
return this.getProfileInformation();
}
This means that we will, now and then, return information from the wrong user. Are we missing something obvious? We’ve looked through the documentation but we’re failing to find any solution to the problem. We’re kind of guessing due to apollo-servers popularity that we’re missing something.
Our context function:
...
dataSources: () => services,
context: async ({ req }) => {
try {
const authHeader = req.headers.authorization.replace('Bearer ', '');
const userToken = await services.userValidationService.getUserToken(authHeader);
const jwt = jwtUtils.verifyToken(userToken);
const expires = jwt.exp * 1000;
const userDetails = await services.partService.getUserDetails(
jwt.sub,
userToken
);
return {
userToken: userToken,
expires: expires,
userDetails: userDetails
};
} catch (error) {
throw new AuthenticationError('Could not authenticate');
}
},
...
The datasource functions:
async willSendRequest(request) {
const {
context: { userToken }
} = this.context;
if (userToken) {
request.headers.set('authorization', `Bearer ${userToken}`);
}
}
async updateUserDetails(args) {
try {
// First request made, context is for the RIGHT user
await this.post(`v1/user-details`, args);
// Second request made, context is SOMETIMES for the WRONG user
return this.getProfileInformation();
} catch (e) {
throw new Error("Could not update user details");
}
}
async getProfileInformation() {
try {
const res = await this.get('v1/user-details');
return contactInformationModel(res);
} catch (e) {
throw new Error("Could not fetch user details");
}
}
Our test setup:
We’ve got a curl and a small bash script that continuously hits the api with an auth poll. They’re authenticated as two different users. We start the poll script and start executing the other curl request and keep watch of the results. Every now and then we get data from the wrong user.
curl 'http://localhost:3001/graphql' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-alive' -H 'DNT: 1' -H 'Origin: http://localhost:3001' -H 'Authorization: Bearer dd0c6de0-8fe2-404e-98eb-d2cb628dd76a' --data-binary '{"query":"mutation($input: UserDetailsInput!) {\n updateUserDetails(input: $input) {\n phoneNumber\n mobileNumber\n emailAddress\n wantsInformation\n notificationPreference {\n email\n sms\n }\n }\n }","variables":{"input":{"notificationPreference":{"sms":true,"email":true},"mobileNumber":"123123123","phoneNumber":"123123123","emailAddress":"asodh@odhf.se","wantsInformation":true}}}' --compressed
#!/bin/bash
for i in {1..100}
do
curl 'http://localhost:3001/graphql' -H 'Authorization: Bearer 2a212708-af36-43bd-a92a-5834a65f29db' -H 'content-type: application/json' --data-binary '{"operationName":null,"variables":{},"query":"{\n auth {\n expires {\n at\n }\n profile {\n id\n }\n }\n}\n"}' --compressed
done
Any help would be appreciated!
Issue Analytics
- State:
- Created 4 years ago
- Comments:7 (2 by maintainers)
Top GitHub Comments
@ruizmarc We found out the hard way that this isn’t actually a problem with apollo (however, they could solve it if they wanted to). Turns out this is an issue in node and express overall. The underlying
request
object is actually overwritten.Our solution was to move away from relying on passing data in apollo’s context and instead use a context based on cls (continous local storage). From Continuation Local Storage for easy context passing in Node.js:
Since we had
apollo-server-express
we could basically write a middleware to replace the apollo context creation, and set the user context via the npm package express-http-context that basically exposes the store. Mind that using CLS does mean a small performance hit.Apollos own context is basically exclusively used for us to pass along datasources now.
(To be more explicit: the problem was not that context itself was shared across requests, but that the
services
object above was shared across requests (which “magically” ended up oncontextValue.dataSources
), and that our docs didn’t make it clear enough that you shouldn’t do that.)