When using apollo federation, it is not possible to change the cookie via the services, only the cookie can be changed via gateway
This issue which you've filed here doesn't _appear_ to be a bug with Apollo Server, but rather a question or uncertainty about how to use it. (Alternatively, it's just too vague to tell if it's a bug, a feature request, or a question.)
I've tried to answer below, but please keep in mind that, as explained in the issue template when you submitted this issue, GitHub Issues should include:
If you want to provide the functionality for a downstream service to set a cookie, you could probably do this using the buildSchema method within RemoteGraphQLDatasource, which is documented here โ but you'd have to explicitly give that sort of permission to specific downstream services since the specific implementation and use-cases could vary. Though I think you should have the primitives you need!
For example, while it's not used within the example in the documentation, the willSendRequest method should also be receiving the response from the Gateway-enabled Apollo Server, which I believe should give you access to response.http.headers, which are the headers which will be returned to the client itself (and could, for example, include a Set-Cookie header).
const gateway = new ApolloGateway({
...
buildService({ name, url }) {
return new RemoteGraphQLDataSource({
url,
willSendRequest({ response }) {
// Can you look into this `response` object and see what's available?
},
});
},
});
I'll close this since it's not a bug, but more than happy to discuss it further.
@abernix thanks for your warnings and help, yes I can change cookies in this way on the gateway (also possible with res.cookie (..., ...)) but is it possible to do this in a resolver on federation, I couldn't succeed
The federated service's resolvers are running in a completely different server, so you need to provide the plumbing to expose this in the way that makes sense for your application. You can imagine that the downstream resolvers might not necessarily be "authorized" to set cookies into the response, which is why something explicit like this is ideal. I know that might not _sound_ ideal, but there's not exactly one way to do this and providing a one-size-fits-all solution might not be the best course of action. For example, not everyone is running GraphQL over HTTP, so cookies are not always something that's available!
@abernix I was just investigating how to do this myself, and there doesn't seem to be any response object in the willSendRequest arguments. Would you recommend just pulling in something like apollo-server-express and manually handling this with middleware?
@kotojo any luck finding a solution?
I can dig into the request from a service to see the cookie, but I have no access to the underlying response from the gateway itself to pass through the cookie.
I admitted defeat and pushed the responsibility to the client. ๐ญ
@abernix Do you think you can re-open this issue? At the very least we should be able to hook into the final response that is sent from the gateway, regardless of whether or not it involves cookies. willSendRequest({ response }) is not a valid signature either
I was able to set cookies to res as per below
in apolo server
context: (context) => {
const { req, res} = context;
return {
jwt: req.headers.authorization,
res: res
}
},
didReceiveResponse({ response, request, context }) {
if(response && response.data && response.data.login){
context.res.cookie("token", response.data.login.token, {httpOnly: true, domain: 'localhost'});
}
return response;
}
Hey folks, I've come up with a solution! For it to work, every service from which you want to pass cookies to the gateway must be using apollo-server-express and not the default apollo-server, you also MUST include the response object into the context of each service, like this:
const server = new ApolloServer({
schema,
tracing: false,
playground: true,
context: ({ res }) => ({ res })
});
I've done it using the RemoteGraphQLDataSource, which has a method didReceiveResponse that allows us to "hook" into the response sent from each service and do something with it, in this case, get the set-cookie header, parse the cookies from it and send them from our gateway as part of the response, here's how I did it:
class CustomDataSource extends RemoteGraphQLDataSource {
didReceiveResponse({ response, context }): typeof response {
const rawCookies = response.http.headers.get('set-cookie') as string | null;
if (rawCookies) {
const cookies = parseCookies(rawCookies);
cookies.forEach(({ cookieName, cookieValue, options }) => {
if (context && context.res) {
context.res.cookie(cookieName, cookieValue, { ...options });
}
});
}
return response;
}
}
const gateway = new ApolloGateway({
serviceList,
buildService({ url }): CustomDataSource {
return new CustomDataSource({ url });
},
});
This way I'm sending every cookie set from any service as part of the response, of course you could customize this more, but this fits my needs!
I also ran into some trouble when trying to parse the cookies coming from the set-cookie header so I could set them again with the res.cookie() from express, since I couldn't find a library which does this properly, I wrote my own little helper function to do this, here's the code for it, hopefully it helps! https://gist.github.com/gmencz/7d6973b8ab95f5adae1739e88f956f70
I'll leave the class I've created that will push your set-cookie headers back to the client, along with any headers when making a request to a service for those googling like me. You'll just need to include this in your buildService within your Apollo Gateway
import { RemoteGraphQLDataSource } from '@apollo/gateway'
import { GraphQLRequestContext, GraphQLResponse, ValueOrPromise } from 'apollo-server-types'
class CookieDataSource<TContext extends Record<string, any> = Record<string, any>> extends RemoteGraphQLDataSource<TContext> {
/**
* Processes set-cookie headers from the service back to the
* client, so the cookies are set within their browser
*/
async process({ request, context }: Pick<GraphQLRequestContext<TContext>, 'request' | 'context'>): Promise<GraphQLResponse> {
const response = await super.process({ request, context })
const cookie = response.http?.headers.get('set-cookie')
if (cookie) {
context.response.set('set-cookie', cookie)
}
return response
}
/**
* Sends any cookies found within the clients request headers then
* pushes them to the requested services context
*/
willSendRequest(requestContext: Pick<GraphQLRequestContext<TContext>, 'request' | 'context'>): ValueOrPromise<void> {
Object.entries(requestContext.context.headers || {} as Record<string, string | {}>).forEach(([key, value]) =>
requestContext.request.http?.headers.set(key, value as string),
);
}
}
export default CookieDataSource
Building on @gmencz answer above, we might not need to parse incoming cookies to set them back in response, with the help of res.append():
const rawCookies = response.http.headers.get('set-cookie') as string | null;
context?.res.append('Set-Cookie', rawCookies.split(/,\s?/))
should do the trick.
Incoming cookies are merged into a single value, comma separated, but to send them back in response we must send multiple Set-Cookie headers, which is why I split() the value.
Building on @gmencz answer above, we might not need to parse incoming cookies to set them back in response, with the help of
res.append():const rawCookies = response.http.headers.get('set-cookie') as string | null; context?.res.append('Set-Cookie', rawCookies.split(/,\s?/))should do the trick.
Incoming cookies are merged into a single value, comma separated, but to send them back in response we must send multipleSet-Cookieheaders, which is why Isplit()the value.
This does the trick at one exception, the .split(/,\s?/) won't split properly when cookies have an expiration date (there is a comma in it) or a comma in their value. I am using this function instead https://github.com/nfriedly/set-cookie-parser#splitcookiesstringcombinedsetcookieheader which works nicely ๐๐ป
Most helpful comment
Hey folks, I've come up with a solution! For it to work, every service from which you want to pass cookies to the gateway must be using apollo-server-express and not the default apollo-server, you also MUST include the response object into the context of each service, like this:
I've done it using the
RemoteGraphQLDataSource, which has a methoddidReceiveResponsethat allows us to "hook" into the response sent from each service and do something with it, in this case, get theset-cookieheader, parse the cookies from it and send them from our gateway as part of the response, here's how I did it:This way I'm sending every cookie set from any service as part of the response, of course you could customize this more, but this fits my needs!
I also ran into some trouble when trying to parse the cookies coming from the
set-cookieheader so I could set them again with theres.cookie()fromexpress, since I couldn't find a library which does this properly, I wrote my own little helper function to do this, here's the code for it, hopefully it helps! https://gist.github.com/gmencz/7d6973b8ab95f5adae1739e88f956f70