Reputation: 338
So I am trying to pass an authorization header to Apollo Client 3 to access a database. The recommended way to do this in the current docs is to create a HttpLink object
const httpLink = new HttpLink({
uri: "************************",
fetch
});
and then use the setContext method (from 'http-link-context' I think):
const authLink = setContext((_, { headers, ...context }) => {
const token = localStorage.getItem("userToken");
return {
headers: {
...headers,
...(token
? { Authorization: `Bearer ${token}` }
: `Bearer *************************`)
},
...context
};
});
then graft the objects together and pass them as a "link" object to the new ApolloClient :
const client = new ApolloClient({
cache: new InMemoryCache(),
link: authLink.concat(httpLink)
});
Unfortunately, though, when I do this I get an error message
Uncaught (in promise) Error: GraphQL error: Missing authorization header.
And when I inspect my request headers I cannot see an authorization header.
Has anyone else been able to get this up and running successfully?
Upvotes: 3
Views: 6124
Reputation: 823
The reason why the Authorization
header is not making it to the request despite you setting it in your authLink
has nothing to do with Apollo Client, but rather CORS compliance. It would be great for Apollo folks to add this as a gotcha in some of their docs to make it clear things won't just work off-the-shelf without proper CORS setup on both the server and client. The silent failure is really painful. Anyway, we're here for solutions. First, let's understand a few things:
Apollo Client CORS issue
Like I've noted, this is not really an Apollo Client issue. Under the hood, Apollo client uses the Fetch API which is CORS compliant(see below). Apollo can accept config values that it passes to the underlying fetch()
request, as we'll see.
CORS(Cross Origin Resource Sharing)
A mechanism used by a web server to control access to itself from browser based scripts. By default, web servers only allow requests from scripts that were loaded from the same origin as the server (AKA same-origin-policy). If the script is from a different origin, as is the case with SPA's many times(e.g. server runs on http://localhost:8080/graphql
and a React client on http://localhost:3000
), then the browser needs to first consult the server to understand its CORS policies before executing the request.
Preflight Request
How does a browser consult a server for its CORS policy? By sending what is called a Preflight request. Browsers do this automatically behind the scenes when they detect a cross-origin request.
Now, I know that doesn't really answer the question of why the Authorization
header is missing. This will be clearer when we understand what a server's CORS policy looks like. So here goes:
CORS Headers
The CORS policy is simply a set of HTTP headers that the server sends to the browser in the response to a Preflight request. Here are the headers:
http://localhost:3000
) needs to be hereAuthorization
, Content-Type
I think by now you are getting the juice of this. After a preflight request, the browser will basically analyse the headers and decide whether the request it shelved earlier is CORS compliant or not. If not, then you will likely see several CORS errors in the browser console, such as below:
Disabling CORS
It's probably time to mention that you can also tell the browser to by-pass CORS policies. With this option, the response is opaque, i.e. you can't access and process it. Maybe, if the CORS policies on the server are explicitly relaxed in advance as happens in this SO question, then the response will be usable. A Preflight request won't be sent, and the browser will just attempt to send the cross-origin request. Needless to say, it won't solve the auth header issue as is the case in that question, it will just calm the browser console errors.
Closing the loop
With all the above information, by now I believe you have the way forward. To be more explicit, below are the things that need to happen to cross the line to the solution side:
Server Side
Preflight request support - By protocol, you need to ensure the server accepts Preflight requests to the /graphql
endpoint without requiring authentication. These are typically OPTIONS
requests. See here. I use spring-graphql and my own is just a wildcard pattern like .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
CORS Policy configuration - you need to configure the CORS policy on your server to respond with appropriate headers, as we saw earlier. My spring boot one looks like this:
graphql:
servlet:
cors:
allowed-origins: http://localhost:3000
allowed-headers: content-type,authorization
allow-credentials: true
allowed-origins
is self-explanatory at this point. allowed-headers
is critical, as it's the config that seems to be quite closely related to the missing Authorization
header issue. However, it doesn't tie to it as closely as allow-credentials
. What makes this issue so painful is that the failure is silent, or at least the root cause is hard to arrive at. allow-credentials
is the config that will ultimately tell the browser whether to include sensitive header info or not. Authorization
falls in this category, so it will be omitted if this config value is not set. However, there's a client side config(credential
) that needs to work in tandem with allow-credentials
to close the loop, Phew!!!!!!
Client Side, Fetch()
Like I stated at the beginning, on the client side, the issues simply revolve around Fetch API and its CORS compliance. Let's have a look at this fetch request:
fetch("http://localhost:8080/api", {
method: "POST",
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer xxx"
},
body: JSON.stringify({})
})
.then((response) => response.json())
Take note of 3 options: mode
, credentials
and headers
:
mode - defines how to handle cross-origin requests. By default, its value is cors
, meaning the browser should seek for CORS compliance. When you set it to no-cors
, then it means the browser should not check for compliance and instead just call the API. The effect is that the results will be opaque, and you won't be able to process the response.
credentials - defines whether credentials or sensitive data(e.g. cookies and Auth header) will be included in the request. The default value is same-origin
(credentials are sent when origin is same and omitted when cross-origin, NO WONDER!!!). Other options are omit
which omits all, regardless of whether cross or same-origin. The last option is include
, which means credentials will be included for cross-origin requests.
headers - used to set custom headers in the request, e.g. Content-type
, Authorization
. We also need the server to support any headers we include here.
Client Side, Apollo Client
Now that we know Apollo uses fetch
and we know what options are critical for fetch as regards CORS, now we can see how to pass the same to Apollo client:
mode
and credentials
can go in HttpLink
;
const httpLink = new HttpLink({
uri: "/graphql",
fetchOptions: {
mode: "cors",
},
credentials: "include",
});
headers
can go in an authLink
as you did.
Upvotes: 1
Reputation: 13
first apollo-link-context is now @apollo/client/link/context
second: u can use headers in apolloclient
const client = new ApolloClient({
cache,
uri: 'http://localhost:4000/graphql',
headers: {
authorization: localStorage.getItem('token') || '',
'client-name': 'Space Explorer [web]',
'client-version': '1.0.0',
},
// or authLink because its return Headers
...authLink
});
Upvotes: 1
Reputation: 39
I think they updated there documentation that includes setContext:
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({
uri: '/graphql',
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
You don't need to install @apollo/link-context separately anymore, everything is now bundled with apollo 3.0. [Reference]: https://www.apollographql.com/docs/react/networking/authentication
Upvotes: 3
Reputation: 338
Very annoyingly the example they give in Apollo's docs do not show the import for setContext so the package required should not be imported from "http-link-context" but instead from "@apollo/link-context" when using the latest version of Apollo (or Apollo 3, anyway). Using the current package it works fine.
Upvotes: 2