Use case
Let's say we have a graphql server that is setup using apollo-server
.
In our app, the user arrives at the page and the page will do a request for a cart (e-commerce site). The cart is personalized that means it depends on a user. The user can be identified by a unique sessionId, which we store in a cookie. Therefore, we would have to access the response headers that are returned from a graphql call.
All our requests from the client are made via graphql using apollo-client. In case there is no sessionId, the graphql-server will issue a new sessionId and send it back in the "Authorization" header. We need to parse this header on the client, see if the Authorization header is set and if yes, store the sessionId in our cookie.
Problem
The Apollo client does not expose the response headers to client. We can use HttpLink to send custom headers to in our requests (which we will do to authorize via 'Authorization' header), but there is no direct way to handle the response headers.
Solution
First, we need to setup our apollo-link with a combination of HttpLink and a so-called afterware link. Afterware is a fancy term that I personally don't like very much, but basically it runs after the request is finished (as oposed to middleware which intercepts each request).
'Afterware' is very similar to a middleware, except that an afterware runs after a request has been made, that is when a response is going to get processed.
https://www.apollographql.com/docs/react/networking/network-layer/#afterware
This is how we instantiate the ApolloClient using our custom afterware to access the response headers:
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link";
const ENDPOINT = "https://your-gql-server.com";
function createApolloClient(initialState = {}) {
const sessionID = getSessionID(); // get sessionID, e.g. from a cookie
// create HttpLink with custom headers if we have sessionID
const httpLink = new HttpLink({
uri: "http://localhost:4000/graphql",
headers: sessionID
? {
Authorization: `Bearer ${sessionID}`
}
: {}
});
// our custom "afterware" that checks each response and saves the sessionID
// if it contains an 'Authorization' header
const afterwareLink = new ApolloLink((operation, forward) => {
forward(operation).map(response => {
const context = operation.getContext();
const authHeader = context.response.headers.get("Authorization");
// We would see this log in the SSR logs in the terminal
// but in the browser console it would always be null!
console.log(authHeader);
if (authHeader) {
// cut off the 'Bearer ' part from the header
SESSION_ID = authHeader.replace("Bearer ", "");
setSessionID(SESSION_ID); // save sessionID, e.g. in a cookie
}
return response;
});
});
// this is how we combine middlewares in Apollo client
const link = afterwareLink.concat(httpLink);
return new ApolloClient({
link, // use combined version for our final client
cache: new InMemoryCache().restore(initialState)
});
}
This looks pretty good so far. The problem that we faced now was that we would see the authHeader on the SSR logs but not in the browser console. Yikes!
'Access-Control-Expose-Headers' to the rescue
After an hour or so of tearing our hairs out we finally found the solution. As these graphql requests are CORS requests, we had to set the 'Access-Control-Expose-Headers'
headers.
By default, only the 6 CORS-safelisted response headers are exposed (source):
The default ApolloServer from apollo-server
can only take a boolean attribute cors
. If set to true, apollo-server will add the cors middleware but with default values.
This is not enough in our case! As we learned this means it would just expose the 6 headers above.
In order to expose the header correctly we had to use the apollo-server-express
package and set our custom corsOptions
to the cors middleware (code below).
import { ApolloServer } from 'apollo-server-express'
import express from 'express'
import { resolvers, typeDefs } from './schema'
// we need custom cors options in order to pass through our Auth header to the client
// this works only using apollo-server-express
const app = express()
const corsOptions = {
origin: '*',
credentials: true,
exposedHeaders: ['Authorization'],
}
const server = new ApolloServer({
typeDefs,
resolvers,,
context: (ctx) => {
// get session, implementation omitted for clarity
const session = getOrCreateSession()
ctx.res.setHeader('Authorization', `Bearer ${session.id}`)
return {
session,
}
},
})
// apply cors middleware with our custom corsOptions
server.applyMiddleware({ app, cors: corsOptions, path: '/' })
app.listen({ port: 4000 }, () => {
console.log("🚀 Server ready on port 4000")
})
Conclusion
The key takeaway here is that apollo-server
comes with a default options for cors, which works for most standard cases. If you want to have a custom cors configuration, you need to use apollo-server-express. And we learned that the CORS specification allows only the 6 CORS-safelisted response headers by default.
Top comments (0)