There is an sample for authorization (sign request with authorization header) on the docs.
However it is a little bit unclear to me, how to watch request for failed authorization,missing rights or login and handle accordingly?
Basically, it's not fully apollo-specific question. Imagine the same situation with Http service.
I don't think you can achieve it without overwriting Angular2Apollo service (the same for Http). As far as I know there's no _interceptor_ even in Http service.
It means you have to "catch" all the requests.
You could extend Angular2Apollo class and add this "interceptor" to catch (RxJS operator) then every request that has an error could be manipulated by this interceptor.
With AngularJS it works the same, you $provide new factory which is close to what I said.
And you cannot use, I don't know which method but let's say router.navigateByUrl() inside middleware of ApolloClient because it's outside Angular's Dependency Injection but please correct me if I'm wrong or if there is another solution :slightly_smiling_face:
You can take a look here:
https://github.com/angular/angular/issues/2684#issuecomment-195751854
@kamilkisiela I'm not sure if this is the best way to go about it, but I have implemented error catching/401 error handling by extending the Angular2Apollo class as follows:
import {Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {Angular2Apollo, ApolloQueryObservable} from 'angular2-apollo';
import {ApolloClient, ApolloQueryResult, createNetworkInterface} from 'apollo-client';
import {Auth} from './auth.service';
/**
* MyAppApolloFactoryLoader
*/
export function MyAppApolloFactoryLoader(auth: Auth, router: Router) {
return new MyAppApollo(auth, router);
}
/**
* setup apollo graphql client
*/
const networkInterface = createNetworkInterface({
uri: '/graphql'
});
networkInterface.use([{
/**
* applyMiddleware
*
* Add auth token to GraphQL requests.
*/
applyMiddleware(req, next) {
req.options.headers = {
authorization: 'Bearer: ' + localStorage.getItem('access_token') || null
};
next();
}
}]);
networkInterface.useAfter([{
/**
* applyAfterware
*
* Handle response error codes.
*/
applyAfterware(res, next) {
if (res.response.status === 401) {
throw new Error('Unauthorized');
}
if (res.response.status === 500) {
throw new Error('Server Error');
}
next();
}
}]);
const client = new ApolloClient({
networkInterface
});
@Injectable()
export class MyAppApollo extends Angular2Apollo {
constructor(private auth: Auth,
private router: Router) {
super(client);
}
/**
* watchQuery
*
* Extends Angular2Apollo watchQuery methods and catches
* 401 errors.
*
*/
watchQuery(options: any): ApolloQueryObservable<ApolloQueryResult> {
let subscription = super.watchQuery(options);
subscription
.subscribe(
() => {},
err => this.errorHandler(err)
);
return subscription;
}
/**
* errorHandler
*
* Handles errors thrown
*
* @param {Error} err
* @return {void}
*/
errorHandler(err: Error): void {
if (err.toString().indexOf('Unauthorized') !== -1) {
this.auth.logout(this.router.url);
}
}
}
Note that in order to catch 401 errors properly, I had to detect them in Apollo Client afterware and then throw them again. I had to do this, as I could not get access to the HTTP response body in the normal error handler.
Is this the kind of thing you were thinking of in the above comment? I would welcome any feedback
@conor-mac-aoidh can you give an sample how you load this service in app.module?
@manuelfink Yes, the service is loaded in the providers. The key is to specify the dependencies:
providers: [
{
provide: MyAppApollo,
useFactory: MyAppApolloFactoryLoader,
deps: [Auth, Router]
}
]
@conor-mac-aoidh I went the same way just also exported ApolloQueryObservable, ApolloModule with the service
const networkInterface = createBatchingNetworkInterface({
uri: '/graphql',
batchInterval: 10
});
networkInterface.use([{
applyMiddleware(req, next) {
// get cookie token:
let token = getCookie("token");
req.options.headers["authorization"] = token ? ("Bearer " + token) : null;
next();
}
}]);
// Handle response error codes.
networkInterface.useAfter([{
applyAfterware(res, next) {
if (res.response.status === 401) {
throw new Error('Unauthorized');
}
if (res.response.status === 500) {
throw new Error('Server Error');
}
next();
}
}]);
const apolloClient = new ApolloClient({
networkInterface,
dataIdFromObject: (o:any) => { return o.__typename + "-" + (o.uuid || o.id)},
});
export function getApolloClient() {
return apolloClient;
}
@Injectable()
export class ApolloService extends Angular2Apollo {
apolloClient: ApolloClient = apolloClient;
constructor( private router: Router ) {
super(apolloClient);
}
/**
* watchQuery
*
* Extends Angular2Apollo watchQuery methods and catches
* 401 errors.
*
*/
watchQuery(options: any): ApolloQueryObservable<ApolloQueryResult> {
let subscription = super.watchQuery(options);
subscription
.subscribe(
() => {},
err => this.errorHandler(err)
);
return subscription;
}
/**
* errorHandler
*
* Handles errors thrown
*
* @param {Error} err
* @return {void}
*/
errorHandler(err: Error): void {
if (err.toString().indexOf('Unauthorized') !== -1) {
// this.auth.logout(this.router.url);
console.log("ApolloService Unauthorized")
} else {
console.log(err);
throw err;
}
}
}
// export ApolloQueryObservable;
// export class ApolloQueryObservable;
export { ApolloQueryObservable, ApolloModule } from 'angular2-apollo';
and than load it in app.module.ts with:
import { ApolloModule, getApolloClient } from './shared/services/apollo.service';
@NgModule({
declarations: [
],
imports: [
BrowserModule,
ApolloModule.withClient(getApolloClient()),
...
@kamilkisiela I'm not sure if is good to subscribe within the service to the queries to catch and handle errors though. Are you having an opinion on this?
@manuelfink @conor-mac-aoidh do you have an updated version of your code with the latest version? Since you both posted your solutions they don't work now (and I can't find any clear documentation on how to handle the errors anywhere)
I have not yet migrated to latest version since I have to adopt breaking changes. Posts welcome ;-)
@manuelfink I already made the changes to my ApolloService extending from Apollo, and the main changes are the following...
export const apolloClient = new ApolloClient({
networkInterface
});
export const clients: any = {
default: apolloClient
};
export class ApolloService extends Apollo {
constructor(
) {
super(clients);
}
}
Notice that now you have to use a ClientMap var in the super contructor, that's the key to make all work again, and use ApolloModule.forRoot instead of old _withClient_.
It works great for me. Only got one question, is there a way to prevent the error from being logged to the console? I have removed all the console.log and yet it is still being printed:

That's my code currently:

So you guys using indexOf for determining if the error was a 401?
Why not creating a new error and then using instanceof?
// Error classes in services folder
class Error401 extends Error {
status = 401;
message = 'User is not authenticated';
}
// In the apollo interceptor
networkInterface.useAfter([{
applyAfterware(res, next) {
if (res.response.status === 401) {
throw new Error401();
}
if (res.response.status === 500) {
throw new Error500();
}
next();
}
}]);
// In the error handler
errorHandler(err: Error) {
if (err instanceof Error401) {
this.auth.logout();
} else {
throw err;
}
}
This approach worked very well for me following the apollo interceptor way described above in your comments.
@kadosh1000 Have you tried catching the error somewhere just like i do?
Oh my bad, seems like the apollo client catches errors and just grabs the error message out of it, damn.
I'm still having trouble figuring out how to tackle the situation where my Apollo-Client depends upon an Angular2 service middleware.
For example, my provideApollo() function cannot be injected into ApolloModule.forRoot becuase it has dependencies.
import { Injectable } from '@angular/core';
import { ApolloClient, createNetworkInterface } from 'apollo-client';
import { ApolloModule } from 'apollo-angular';
import { environment } from '../environments/environment';
import { Angular2TokenService } from 'angular2-token';
@Injectable()
export class ApolloClientProvider {
client: ApolloClient;
constructor(private angular2TokenService: Angular2TokenService) {
this.angular2TokenService.init({
apiBase: environment.apiBase,
apiPath: 'api',
signInPath: 'auth/sign_in'
});
const networkInterface = createNetworkInterface({ uri: `${environment.apiBase}/api/graphql` });
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // Create the header object if needed.
}
Object.assign(req.options.headers, angular2TokenService.currentAuthHeaders);
next();
}
}]);
// Apollo Client Initialization
this.client = new ApolloClient({
networkInterface: networkInterface
});
}
}
export function provideApollo() {
return {
provide: ApolloClient,
useFactory: (angular2TokenService: Angular2TokenService) => new ApolloClientProvider(angular2TokenService).client,
deps: [Angular2TokenService]
};
}
Since i'm not using any service to manage tokens, i don't have this kind of problem, though i'm tempted to use it since it offers a lot of nice features (specially for the Devise gem).
The only place those currentAuthHeaders exist is in the service? if not, you could work around it, but if yes idk how to, because usually you pass the client as a dependency of the .forRoot(), which is at configuration, not at runtime when the services are available.
If you find how let me know.
Actually after looking at the code the forRoot() uses, it calls a function and it uses the result as the providers of the module according to lines ApolloModule.ts#L41-L42, so in theory depending on what is there you could see if the client passed is actually provided to Angular as a service, let's take a look:
According to lines Apollo.ts#L99-L108, if you pass your service to the apollo forRoot() function, your service would already be available for the full app, however it's being aliased to another token stored in CLIENT_MAP_WRAPPER instead.
So you wouldn't be able to get the service in other places of your app unless you ask for that specific token when injecting in those (i think).
So maybe you may get a way to use the original token as an alias of the one you passed to the forRoot() thing, like a way to tell it to be a singleton no matter where it was injected, however idk how should that be done (don't have that amount of knowledge on Angular 2+), however i have seen how other modules export services and don't use the forRoot() thing, you could base the solution on those.
The one i'm talking about is Material 2, they have services, but no matter where you insert the module, or if you use Lazy-Loading with shared modules, all services are singletons, which i suspect happens in this lines:
src/lib/dialog/dialog-module.ts#L48-L51
src/lib/dialog/dialog.ts#L41
Maybe i'm reading this wrong but as far as angular and apollo is concerned, you can pass the forRoot() method a function that just returns the actual client, or you could just pass an object that has the client as one of it's variables, though the exact name i'm still trying to figure out:
src/types.ts#L3-L5
Oh wait i found it, you need to have in your ApolloClientProvider the client instance in the default variable according this line:
src/Apollo.ts#L62
So if you put in the default proprety the instance of the ApolloClient you make in the constructor, as well as in client (just as precaution, but probably unnecesary), and you give the class to the forRoot(ApolloClientProvider) like that, then provide the Angular2TokenService to the root of the app, in theory, ApolloClientProvider should be instantiated as a service by the apollo module, so it would get the token dependency, and as good as you have the instance of your client in the provider in the property default, when the Apollo service injects your provider it will get the client naturally.
PD: IN THEORY should work, please let me know if it does (i'll celebrate with a beer), if not we should ping the staff, maybe they'll be able to clarify.
@raysuelzer Any progress?
@luchillo17 Thanks for the advice. What I ended up doing was just grabbing the headers manually from localStorage, thus by passing the need to use Angular2Token.
export function provideApolloClient() {
const networkInterface = createNetworkInterface({ uri: `${environment.apiBase}/api/graphql` });
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // Create the header object if needed.
}
Object.assign(req.options.headers, {
'access-token': localStorage.getItem('accessToken'),
client: localStorage.getItem('client'),
expiry: localStorage.getItem('expiry'),
'token-type': localStorage.getItem('tokenType'),
uid: localStorage.getItem('uid')
});
next();
}
}]);
// Apollo Client Initialization
return new ApolloClient({
networkInterface: networkInterface
});
}
It seems like the method you indicated might work, but at this point it's probably not worth the effort of trying to get it working since my current solution is straight forward and works fine. But, it would be great if there was some documentation on how to use another Angular service as part of the middleware.
Good news everyone. It will be possible to use any service thanks to new API that's coming up in few days!
So we don't get to know if my suggestion does work or not, that's a shame.
@kamilkisiela I'm about to update to latest, which version of apollo-client do i need, does it includes the services thing you mentioned?
@luchillo17 v1.0.0
Thanks, hopefully this services thing should allow me to finish my project (lacked a better control over errors and reactions to them).
Most helpful comment
Good news everyone. It will be possible to use any service thanks to new API that's coming up in few days!