I am trying to write up our httpService, it should have a post method that checks to see if a cookie exists with an auth token, if it does then it should append the auth header and make the post request.
However if the cookie doesn't exist I need to load a local json file that contains the token and use it to create the cookie, then append the auth header and make the post request.
The issue I'm having is that if the cookie doesn't exist I need to make a observable wait for another observable. I had thought the solution was to use switchMap, but that doesn't play well with .subscribe which is necessary for the http.post request to initialize.
private makePostRequest(address: string, payload: any, callback: any): Observable<any> {
return this.http.post(address, payload, { headers: this.headers })
.map(callback)
.catch(( error: any ) => this.handleError(error));
}
public post(address: string, payload: any, callback: any): Observable<any> {
if (this.hasOAuth2()) {
this.appendHeader(this.cookieService.get('oauth2'));
return this.makePostRequest(address, payload, callback);
} else if (this.isLocalhost()) {
return this.setAuthCookie()
.switchMap(() => this.makePostRequest(address, payload, callback));
} else {
return this.handleError('Could not locate oauth2 cookie');
}
}
private setAuthCookie(): Observable<any> {
return this.http.get('./json/token.json')
.map((res: Response) => {
let oauth2: any = res.json();
this.cookieService.set('oauth2', oauth2.access_token, oauth2.expiration);
this.appendHeader(oauth2.access_token);
})
.catch((error: any) => {
console.log('No dev token was found', error);
return Observable.throw(error);
});
}
Update: Where this gets weird is that more or less the exact game code works correctly with a get request.
private makeGetRequest(address: string, callback: any): Observable<any> {
return this.http.get(address, { headers: this.headers })
.map(callback)
.catch(( error: any ) => this.handleError(error));
}
public get(address: string, callback: any): Observable<any> {
if (this.hasOAuth2()) {
this.appendHeader(this.cookieService.get('oauth2'));
return this.makeGetRequest(address, callback);
} else if (this.isLocalhost()) {
return this.setAuthCookie()
.switchMap(() => this.makeGetRequest(address, callback));
} else {
return this.handleError('Could not locate oauth2 cookie');
}
}
Solution: I wasn't subscribing to the httpService.post observable so it wasn't ever being initialized.
Add an empty .subscribe() to your second case:
return this.setAuthCookie()
.map(() => { })
.switchMap(() => { // I need to switchMap due to the http.get request in the setAuthCookie method
this.makePostRequest(address, payload, callback).subscribe(); // Again I need this or the post request won't be made
}).subscribe(); // <--- here
It will activate the http call.
I had never subscribed to my httpService.post observable so it was never being initialized. Adding the later subscribe calls was causing it to be initialized incorrectly.
Related
I'm work on chat app between specific clients using Angular and SignalR. So far everything is working good except one thing - when I'm sending a message to specific user, I want to send it also to myself.
After a lot reading of reading, I've realized that I can't get the Context value unless I'm approaching directly to my hub. I found a way to go around it by invoking GetSenderName() method (to get the sender's name) and then send it back to the server's controller. The problem is when I've added the parameter fromUser to my request . Before I've added it the request, I've reached the server and after I've added it I'm getting-
Status code 400
I've tried to debugg the client and it seems like all the parametrs has a valid values so I don't understand what went worng. Can anyone help me please?
User component (only the relevant part)-
fromUser: string="";
sendMessageToUser(sendToUser: any): void {
this.sendToUser=sendToUser;
this.fromUser=this.signalRService.getName();
console.log("fromUser: ", this.fromUser);
console.log("check sendToUser: ", this.sendToUser);
this.signalRService.sendMessageToUser(
this.sendMessageToUserUrl,
this.chatMessage,
this.sendToUser,
this.fromUser
);
}
SignalR Service-
public fromUser: string="";
constructor(private http: HttpClient) { }
public startSignalrConnection(connectionUrl: any) {
return new Promise<any>((resolve, reject) => {
this.hubConnection = new HubConnectionBuilder()
.withUrl(connectionUrl, {
withCredentials: false,
accessTokenFactory: () => localStorage.getItem('jwt')!,
})
.configureLogging(LogLevel.Debug)
.build();
this.hubConnection.start()
.then(() => {
console.log('in');
resolve(this.hubConnection.connectionId);
})
.then(()=>this.getSenderName())
.catch((error) => {
reject(error);
});
public getSenderName=()=>{
this.hubConnection.invoke('getSenderName')
.then((data)=>{
console.log("this is the data: ", data);
this.fromUser=data;
})
}
public getName(): string{
return this.fromUser;
}
This is where the problem is (SignalR Service)-
public sendMessageToUser(sendMessageToUserUrl: string, message: string, sendToConnId: any, fromUser:string){
firstValueFrom(this.http.post(sendMessageToUserUrl, buildChatMessageModel(sendToConnId, message, fromUser)))
.then((response) => {
console.log(response);
})
.catch((error) => {
console.log("Failed to send message: ", error);
alert("Failed to send message: "+ error);
})
}
ChatMessageModel-
export interface ChatMessageModel {
ConnectionId: string,
Message: string
FromUser: string
}
Utils-
export const buildChatMessageModel = (hubConnectionId: string, message: string, fromUser: string): ChatMessageModel => {
return {
ConnectionId: hubConnectionId,
Message: message,
FromUser: fromUser
};
};
I'm using the following interceptors in a Vuejs v2 website to push a firebase token to my node backend. There in the backend, I detect/verify the token, pull some data using the uid from a database and then process any api calls.
Even though I am using the firebase onIdTokenChanged to automatically retrieve new ID tokens, sometimes, if the user is logged in, yet inactive for an hour, the token expires without refreshing. Now, this isn't a huge deal - I could check in the axios response interceptor and push them to a login page, but that seems annoying, if I can detect a 401 token expired, resend the axios call and have a refreshed token, the user won't even know it happened if they happen to interact with a component that requires data from an API call. So here is what I have:
main.js
Vue.prototype.$axios.interceptors.request.use(function (config) {
const token = store.getters.getSessionToken;
config.headers.Authorization = `Bearer ${token}`;
return config;
});
Vue.prototype.$axios.interceptors.response.use((response) => {
return response }, async function (error) {
let originalRequest = error.config
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
let user = auth.currentUser;
await store.dispatch("setUser", {user: user, refresh: true}).then(() => {
const token = store.getters.getSessionToken;
Vue.prototype.$axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
return Vue.prototype.$axios.request(originalRequest);
});
}
return Promise.reject(error); });
let app;
auth.onAuthStateChanged(async user => {
await store.dispatch("setUser", {user: user, refresh: false}).then(() => {
if (!app) {
app = new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount('#app')
}
})
.catch(error => {
console.log(error);
});
});
vuex
setUser({dispatch, commit}, {user, refresh}) {
return new Promise((resolve) => {
if(user)
{
user.getIdToken(refresh).then(token => {
commit('SET_SESSION_TOKEN', token);
this._vm.$axios.get('/api/user/session').then((response) => {
if(response.status === 200) {
commit('SET_SESSION_USER', response.data);
resolve(response);
}
})
.catch(error => {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: 'Server unavailable: '+error
});
resolve();
});
})
.catch(error => {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: 'Unable to verify auth token.'+error
});
resolve();
});
}
else
{
console.log('running logout');
commit('SET_SESSION_USER', null);
commit('SET_SESSION_TOKEN', null);
resolve();
}
})
},
I am setting the token in vuex and then using it in the interceptors for all API calls. So the issue I am seeing with this code is, I'm making an API call with an expired token to the backend. This returns a 401 and the axios response interceptor picks it up and goes through the process of refreshing the firebase token. This then makes a new API call with the same config as the original to the backend with the updated token and returns it to the original API call (below).
This all seems to work, and I can see in dev tools/network, the response from the API call is sending back the correct data. However, it seems to be falling into the catch of the following api call/code. I get an "undefined" when trying to load the form field with response.data.server, for example. This page loads everything normally if I refresh the page (again, as it should with the normal token/loading process), so I know there aren't loading issues.
vue component (loads smtp settings into the page)
getSMTPSettings: async function() {
await this.$axios.get('/api/smtp')
.then((response) => {
this.form.server = response.data.server;
this.form.port = response.data.port;
this.form.authemail = response.data.authemail;
this.form.authpassword = response.data.authpassword;
this.form.sendemail = response.data.sendemail;
this.form.testemail = response.data.testemail;
this.form.protocol = response.data.protocol;
})
.catch(error => {
console.log(error);
});
},
I have been looking at this for a few days and I can't figure out why it won't load it. The data seems to be there. Is the timing of what I'm doing causing me issues? It doesn't appear to be a CORS problem, I am not getting any errors there.
Your main issue is mixing async / await with .then(). Your response interceptor isn't returning the next response because you've wrapped that part in then() without returning the outer promise.
Keep things simple with async / await everywhere.
Also, setting common headers defeats the point in using interceptors. You've already got a request interceptor, let it do its job
// wait for this to complete
await store.dispatch("setUser", { user, refresh: true })
// your token is now in the store and can be used by the request interceptor
// re-run the original request
return Vue.prototype.$axios.request(originalRequest)
Your store action also falls into the explicit promise construction antipattern and can be simplified
async setUser({ dispatch, commit }, { user, refresh }) {
if(user) {
try {
const token = await user.getIdToken(refresh);
commit('SET_SESSION_TOKEN', token);
try {
const { data } = await this._vm.$axios.get('/api/user/session');
commit('SET_SESSION_USER', data);
} catch (err) {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: `Server unavailable: ${err.response?.data ?? err.message}`
})
}
} catch (err) {
dispatch('logout');
dispatch('setSnackbar', {
color: "error",
timeout: 4000,
text: `Unable to verify auth token. ${error}`
})
}
} else {
console.log('running logout');
commit('SET_SESSION_USER', null);
commit('SET_SESSION_TOKEN', null);
}
}
My provider makes available an API for an http.get request:
join(){
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('x-access-token',this.getToken());
return Observable.create(observer =>{
this.http.get('/localhost/chat/'+this.room,{headers : headers})
.map(res => res.json())
.subscribe(
data=>{
observer.next(data);
},
(err) =>{
observer.error(err);
}
);
})
}
My page.ts just use this API cyclically:
join(){
this.myProvider.join().subscribe(
(data)=>{
if(data.success){
... /* doing smt */ ....
this.join();
}else{
this.message=data.message;
//TBD sleep....
//this.join();
}
},
(err) => {
this.message="Connectivity with server Lost...";
});
}
My question is: I would like to write a function in page.ts in order to stop this cycle.
How can I kill a pending get request?
A solution that doesn't work was
I tried to keep a pointer to the observable object in my page.ts:
export class Page {
...
join_channel: any;
join(){
this.join_channel = this.myProvider.join().subscribe(
(data)=>{
...
this.join();
...
Then I by calling the this.join_channel.unsubscribe() I wanted to close the request, so in my case:
ionViewWillLeave() {
this.join_channel.unsubscribe();
delete this;
}
But even by unsubscribing, the get request is still there pending; so when I try to enter again in my page, a new join() can't receive a http.get response at the first step, because the answer will be used before for the previous request which is still pending.
Use timeout from rxjs
this.http.get(API)
.timeout(2000)
.map(res => res.json()).subscribe((data) => {
return data;
},
(err) => {
return err;
}
);
Don't forget to import import 'rxjs/add/operator/timeout';
If you are using angular 6 you have to use
pipe(timeout(2000))
this.http.get(API)
.pipe(timeout(2000))
.map(res => res.json()).subscribe((data) => {
return data;
},
(err) => {
return err;
}
);
How to cancel a HTTPRequest in Angular 2?
I know how to reject the request promise only.
return new Promise((resolve, reject) => {
this.currentLoading.set(url, {resolve, reject});
this.http.get(url, {headers: reqHeaders})
.subscribe(
(res) => {
res = res.json();
this.currentLoading.delete(url);
this.cache.set(url, res);
resolve(res);
}
);
});
You can use the following simple solution:
if ( this.subscription ) {
this.subscription.unsubscribe();
}
this.subscription = this.http.get( 'awesomeApi' )
.subscribe((res)=> {
// your awesome code..
})
You can call unsubscribe
let sub = this.http.get(url, {headers: reqHeaders})
.subscribe(
(res) => {
res = res.json();
this.currentLoading.delete(url);
this.cache.set(url, res);
resolve(res);
}
);
sub.unsubscribe();
More info here: http://www.syntaxsuccess.com/viewarticle/angular-2.0-and-http
You can use SwitchMap on the observable which will cancel any previous request's responses and only request the latest:
https://www.learnrxjs.io/operators/transformation/switchmap.html
A little late for the party, but here is my take:
import { Injectable } from '#angular/core'
import { Http } from '#angular/http'
import { Observable } from 'rxjs/Observable'
import { Subscriber } from 'rxjs/Subscriber'
#Injectable ()
export class SomeHttpServiceService {
private subscriber: Subscriber<any>
constructor(private http: Http){ }
public cancelableRequest() {
let o = new Observable(obs => subscriber = obs)
return this.http.get('someurl').takeUntil(o)
.toPromise() //I dont like observables
.then(res => {
o.unsubscribe
return res
})
}
public cancelRequest() {
subscriber.error('whatever')
}
}
This allows you to manually cancel a request. I sometimes end up with an observable or promise that will make changes to a result on the page. If the request was initiated automatically (user didn't type anyting in a field for x millis) being able to abort the request is nice (user is suddenly typing something again)...
takeUntil should also work with a simple timeout (Observable.timer) if that is what you are looking for
https://www.learnrxjs.io/learn-rxjs/operators/filtering/takeuntil
Use switchMap [docs], which will cancel all in-flight requests and use only the latest.
get(endpoint: string): Observable<any> {
const headers: Observable<{url: string, headers: HttpHeaders}> = this.getConfig();
return headers.pipe(
switchMap(obj => this.http.get(`${obj.url}${endpoint}`, { headers: obj.headers, params: params }) ),
shareReplay(1)
);
}
shareReplay will emit the latest value for any late subscribers.
This is a great thread, and I have a little more info to provide. I have an API call that could potentially go on for a very long time. So I needed the previous request to cancel with a timeout. I just figured out today that I can add a timeout operator to the pipe function. Once the timeout completes its count, that will cancel the previous HTTP request.
Example...
return this.exampleHttpRequest()
.pipe(
timeout(3000),
catchError(err => console.log(error)
)
I'm writing a small utility function that wrap a call to AngularJS http.get with the necessary authentication headers:
get(endpoint: string): Observable {
var headers = new Headers();
this._appendAuthentificationHeaders( headers, this.user.credentials);
return this.http.get(endpoint, { headers: headers })
.map(res => res.json());
}
The point here is that if this.user is null, the method will just crash.
So I have three options:
Return null and check that return value on every call...
Throw an exception
Find a way to also return an RxJS Observable object that will directly trigger the error handler.
I would like to implement the third method, as it would allow me unify this method's behavior: It always returns an observable no matter what happen.
Do you have an idea about how to do that?
Do I have to create a new Observable and kind of merge those two?
What can I do?
If the user is null, you can simply return a raw observable that triggers an error:
if (this.user == null) {
return Observable.create((observer) => {
observer.error('User is null');
});
}
(...)
or leverage the throw operator:
if (this.user == null) {
return Observable.throw('User is null');
}
(...)
This way the second method of the subscribe method will be called:
observable.subscribe(
(data) => {
(...)
},
(err) => {
// Will be called in this case
}
);
I think the cleanest way would be to wrap the whole function body to an observable, as it will turn any accidental error to an observable error. Something like this:
get(endpoint: string): Observable {
return Rx.Observable.defer(() => {
var headers = new Headers();
this._appendAuthentificationHeaders(headers, this.user.credentials);
return Rx.Observable.just(headers);
})
.flatMap(headers => this.http.get(endpoint, { headers: headers }))
.map(res => res.json());
}
However I still do not agree with http.get returning an observable instead of a promise. As these are single valued observables, your function could be a simple async function (sry, js instead of ts):
async get(endpoint) {
var headers = new Headers();
this._appendAuthentificationHeaders(headers, this.user.credentials);
const res = await this.http.get(endpoint, { headers })).toPromise();
return res.json();
}