In my angular 4 app I'm using ngx-restangular
to handle all server calls. It returns observable as result, and this module has hooks to handle errors (like 401 etc).
But from documentation, i can handle 403 (401) so:
RestangularProvider.addErrorInterceptor((response, subject, responseHandler) => {
if (response.status === 403) {
refreshAccesstoken()
.switchMap(refreshAccesstokenResponse => {
//If you want to change request or make with it some actions and give the request to the repeatRequest func.
//Or you can live it empty and request will be the same.
// update Authorization header
response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)
return response.repeatRequest(response.request);
})
.subscribe(
res => responseHandler(res),
err => subject.error(err)
);
return false; // error handled
}
return true; // error not handled
});
and this is good for one request, which has broken with 403 error. how can i stack this calls using rxJs? Becouse now, for example, i have 3 requests, which have 403 and for each this broken request I'm refreshing token - this is not so good, i have to update my token and then repeat all my broken requests. How can I achive this using Observables?
In angular 1 it was pretty easy:
Restangular.setErrorInterceptor(function (response, deferred, responseHandler) {
if (response.status == 403) {
// send only one request if multiple errors exist
if (!refreshingIsInProgress) {
refreshingIsInProgress = AppApi.refreshAccessToken(); // Returns promise
}
$q.when(refreshingIsInProgress, function () {
refreshingIsInProgress = null;
setHeaders(response.config.headers);
// repeat request with error
$http(response.config).then(responseHandler, deferred);
}, function () {
refreshingIsInProgress = null;
$state.go('auth');
});
return false; // stop the promise chain
}
return true;
});
And all was working like a charm. But I'm new to rxJs & angular 4 and I don't have any idea how to achive this with observables and angular 4. Maybe somebody have an idea?
upd! here is my refreshAccesstoken method
const refreshAccesstoken = function () {
const refreshToken = http.post(environment.apiURL + `/token/refresh`,
{refreshToken: 'someToken'});
return refreshToken;
};
Reactive Extensions for JavaScript, or RxJS, is a reactive library used to implement reactive programming to deal with async implementation, callbacks, and event-based programs. It can be used in your browser or with Node. js. RxJS observables allow you to publish events.
React is a javascript library for building user interfaces whereas RxJS is a javascript library for reactive programming using Observables. Both these javascript libraries are not comparable to each other since it serve different purposes. Both these can be used together to create better single page applications.
Angular currently uses RxJs Observables in two different ways: as an internal implementation mechanism, to implement some of its core logic like EventEmitter. as part of its public API, namely in Forms and the HTTP module.
RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using observables that makes it easier to compose asynchronous or callback-based code.
One way I can see of doing this using ngx-restangular is to use the share operator. This way you don't have to implement complicated queueing logic. The idea is that if you have 3 requests all of the with a 403 response, they will all hit your interceptor and call your observable. If you share that observable, you will have only one token request for the 3 requests with a broken token.
You just have to use the share operator in your code like so:
refreshAccesstoken()
.share()
.switchMap(refreshAccesstokenResponse => {
//If you want to change request or make with it some actions and give the request to the repeatRequest func.
//Or you can live it empty and request will be the same.
// update Authorization header
response.request.headers.set('Authorization', 'Bearer ' + refreshAccesstokenResponse)
return response.repeatRequest(response.request);
})
.subscribe(
res => responseHandler(res),
err => subject.error(err)
);
I haven't checked that the code actually works but I have used this approach before for the same use case, but instead of interceptors, I was using the angular HTTP service.
EDIT to change refreshAccessToken:
You need to wrap your refreshAccessToken method in a deferred Observable and share it. This way you will reuse the same observable every time.
In the constructor:
this.source = Observable.defer(() => {
return this.refreshAccesstoken();
}).share();
Create another method that will invoke that observable:
refreshToken(): Observable<any> {
return this.source
.do((data) => {
this.resolved(data);
}, error => {
this.resolved(error);
});
}
EDIT2
I have created a git repository which uses angular2 with restangular. The scenario is the following:
Here is what I can see in my console:
If I remove the share operator I will get the following logs: meaning that the observable will be created every time.
In order for this to work, it is important that the source is declared and created in the RestangularConfigFactory. It will essentially become a singleton object and that's what allows the Share operator to work.
NOTE:
I created a simple web API hosted locally for this project just because it was faster for me.
EDIT3:Update to include code to refresh the token:
@Injectable()
export class TokenRefreshService {
source: Observable<any>;
pausedObservable: Observable<any>;
constructor(
private authenthicationStore: AuthenticationStore,
private router: Router,
private authenticationDataService: AuthenticationDataService,
private http: ObservableHttpService) {
this.source = Observable.defer(() => {
return this.postRequest();
}).share();
}
refreshToken(): Observable<any> {
return this.source
.do((data) => {
this.resolved(data);
}, error => {
this.resolved(error);
});
}
public shouldRefresh(): boolean {
if (this.getTime() < 0) {
return true;
}
return false;
}
private postRequest(): Observable<any> {
let authData = this.authenticationDataService.getAuthenticationData();
if (authData == null) {
return Observable.empty();
}
let data: URLSearchParams = new URLSearchParams();
data.append('grant_type', 'refresh_token');
let obs = this.http.postWithHeaders(
'token', data, { 'Content-Type': 'application/x-www-form-urlencoded' })
.map((response) => {
return this.parseResult(true, response, 'authenticateUserResult');
})
.catch((error) => {
let errorMessage = this.rejected(error);
return Observable.throw(errorMessage);
});
return obs;
}
private rejected(failure) {
let authenticateUserResult;
let response = failure;
let data = response.json();
if (response &&
response.status === 400 &&
data &&
data.error &&
data.error === 'invalid_grant') {
authenticateUserResult = this.parseResult(false, data, 'error_description');
return authenticateUserResult;
} else {
return failure;
}
}
private parseResult(success, data, resultPropertyName) {
let authenticateResultParts = data[resultPropertyName].split(':');
data.result = {
success: success,
id: authenticateResultParts[0],
serverDescription: authenticateResultParts[1]
};
return data;
}
private resolved(data): void {
let authenticationResult = data.result;
if (authenticationResult && authenticationResult.success) {
let authenticationData = this.createAuthenticationData(data);
this.authenthicationStore.setUserData(authenticationData);
} else {
this.authenthicationStore.clearAll();
this.router.navigate(['/authenticate/login']);
}
}
private createAuthenticationData(data: any): AuthenticationData {
let authenticationData = new AuthenticationData();
authenticationData.access_token = data.access_token;
authenticationData.token_type = data.token_type;
authenticationData.username = data.username;
authenticationData.friendlyName = data.friendlyName;
return authenticationData;
}
private getTime(): number {
return this.getNumberOfSecondsBeforeTokenExpires(this.getTicksUntilExpiration());
}
private getTicksUntilExpiration(): number {
let authData = this.authenticationDataService.getAuthenticationData();
if (authData) {
return authData.expire_time;
}
return 0;
}
private getNumberOfSecondsBeforeTokenExpires(ticksWhenTokenExpires: number): number {
let a;
if (ticksWhenTokenExpires === 0) {
a = new Date(new Date().getTime() + 1 * 60000);
} else {
a = new Date((ticksWhenTokenExpires) * 1000);
}
let b = new Date();
let utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate(), a.getHours(), a.getMinutes(), a.getSeconds());
let utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate(), b.getHours(), b.getMinutes(), b.getSeconds());
let timeInSeconds = Math.floor((utc1 - utc2) / 1000);
return timeInSeconds - 5;
}
}
I had a similar problem in my application and I solved it this way:
Subscribers keep waiting for source to emit a value and dont do anything till then
We can use this behavior to solve our problem.
What we can do is something like this :
All HTTP Calls :
public doSomeHttpCall(params){
return authService
.getToken()
.switchMap(token => httpcall())
}
With above code, the HTTP call wont happen until we get the token.
Inside auth service :
private token = new BehaviorSubject(null);
public getToken(){
return this.token
.filter( t => !!t );
}
Since I have a filter for token, if token is falsy
no HTTP call will be executed. As soon as token gets a good value all the HTTP calls continue to execute.
In your case, if there is a 403
, disable the token by setting it to null. After refresh set it to new token value.
private refreshInProgress= false;
public refreshToken(){
if(refreshInProgress){
//dont do anything
return;
}
refreshInProgress = true;
this.token.next(null);
// fetch new token
this.token.next(newToken);
refreshInProgress = false;
}
A very simple illustration here :
jsbin : https://jsbin.com/mefepipiqu/edit?html,js,console,output
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With