I have this function:

  /**
   * Attempt login with provided credentials
   * @returns {Observable<number>} the status code of HTTP response
   */
  login(username: string, password: string): Observable<number> {
    let body = new URLSearchParams();
    body.append('grant_type', 'password');
    body.append('username', username);
    body.append('password', password);

    return this.http.post(Constants.LOGIN_URL, body, null)
      .do(res => {
        if (res.status == 200) {
          this.authTokenService.setAuthToken(res.json().auth_token);
        }
      })
      .map(res => res.status)
      .catch(err => { return Observable.throw(err.status) });
  }

This function attempts to perform login and returns Observable<number> that the caller can use in order to be notified of the HTTP code of the response.

This seems to work, but I have one problem: if the caller just calls the function without subscribing to the returned Observable, do function is not called and the received auth token is not being saved.

Documentation of do states:

Note: this is different to a subscribe on the Observable. If the Observable returned by do is not subscribed, the side effects specified by the Observer will never happen. do therefore simply spies on existing execution, it does not trigger an execution to happen like subscribe does.

What I'd like to achieve, is to have this side effect regardless of whether the caller of login() subscribed or not. What should I do?

upvote
  flag
This method returns an observable. What's the point of calling it if you are not going to subscribe to it's response? (you won't be making this request if you don't subscribe) – echonax
upvote
  flag
@echonax, this method is in service. There might be multiple clients - some of them will subscribe, others won't. I don't want the functionality of the service to depend on the semantics of external usage. – Vasiliy
upvote
  flag
you can call subscribe() inside the login() method – martin

2 Answers 11

In my answer below I assume that you got used to behavior provided by Promises in previous versions of HTTP in AngularJS:

 login(username: string, password: string): Observable<number> {
    return this.http.post(Constants.LOGIN_URL, body, null)
      .then(res => {
        if (res.status == 200) {
          this.authTokenService.setAuthToken(res.json().auth_token);
        }
      })
      .then(res => res.status)
      .catch(err => { return Observable.throw(err.status) });

The observable returned from this.http.get() call is cold observable. It means that it will not start doing anything unless someone subscribes to it. That is by design. All operators that you chain to the returned observables don't do anything as well since there's no subscription.

You need to subscribe to make a request and then share the result with future subscribers. And I think AsyncSubject is a good candidate here:

  sent = false;
  s = new AsyncSubject();

  login(username: string, password: string): Observable<number> {   
    if (!this.sent) {
      this.http.post(Constants.LOGIN_URL, body, null)
        .do(res => {
          if (res.status == 200) {
            this.authTokenService.setAuthToken(res.json().auth_token);
          }
        })
        .map(res => res.status)
        .catch(err => { return Observable.throw(err.status) })
        .subscribe(s);

      this.sent = true;
    }

    return s;
  }

In this way only one http call will be made and all operators include do will run only once. After that, the returned result will be cached in AsyncSubject and it will pass it along to all future subscribers.

upvote
  flag
downvoter care to explain? – AngularInDepth.com
upvote
  flag
You don't need a subject. Plus your code is wrong. How a constant set to false can become true ? – n00dl3
1 upvote
  flag
no idea why the downvote, this really solves his problem. Even though I still have difficulties understanding what the issue is. – PierreDuc
upvote
  flag
@n00dl3, how come I don't need a subject? Why make multiple calls for authentication if one would do? Also you probably realize that I don't put the code exactly as it should be. I write pseudocode where typos are expected things – AngularInDepth.com
upvote
  flag
@PierreDuc, I believe that the OP worked with promises before where if you wrote this.http.get().then(do)... would make request instantly without requiring any subscription to then. This is hot behavior and that is what I think OP expects from the existing http.get() – AngularInDepth.com
upvote
  flag
You still don't need a subject, you can convert to hot observable. Your sent variable will always be false when the method is called so it is useless. – n00dl3
upvote
  flag
@n00dl3, how can I convert to hot observable? I don't understand why sent will always be false, I change it to true when the call is made. Why don't you provide your solution? – AngularInDepth.com
2 upvote
  flag
You can convert using publish() and connect(). it will always be false when the method is called as it is not a class property, but a simple variable. Whenever you call that method, sent is false, so what is the point in keeping it ? – n00dl3
upvote
  flag
so make it a class property, what's the problem? again, I dont post tested solutions, I expect OP to do that – AngularInDepth.com
upvote
  flag
I'm sorry if I offended you, that wasn't my intention. But the problem is your solution is wrong. That little difference makes your solution pointless. You don't know OP's code, you imagine that you can handle the whole session state with a single boolean ? You don't even reset it to false when the request has completed. What if there is another call to login with different parameters ? you will send back the same Observable with old login and password ? If you only talked about AsyncSubject I wouldn't have downvoted. – n00dl3
upvote
  flag
I was answering having in mind a certain pattern from previous HTTP clients. I added this information to the answer. – AngularInDepth.com
up vote 2 down vote accepted

As explained by @Maximus, by design, cold Observable (like http call) will not emit any data before you subscribe to them. So your .do() callback will never get called.

Hot Observables on the other hand will emit data without caring if there is a subscriber or not.

You can convert your cold Observable to a ConnectableObservable using the publish() operator. That Observable will start emitting when its connect() method get called.

login(username: string, password: string): Observable < number > {
  let body = new URLSearchParams();
  body.append('grant_type', 'password');
  body.append('username', username);
  body.append('password', password);
  let request = this.http.post(Constants.LOGIN_URL, body, null)
    .do(res => {
      if (res.status == 200) {
        this.authTokenService.setAuthToken(res.json().auth_token);
      }
    })
    .map(res => res.status)
    .catch(err => {
      return Observable.throw(err.status)
    }).publish();
  request.connect();
  // type assertion because nobody needs to know it is a ConnectableObservable
  return request as Observable < number > ; 
}

As @Maximus stated. If the subscription happen after the ajax call as completed, you won't get notified of the result. To handle this case you can use publishReplay(1) instead of simple publish(). PublishReplay(n) will repeat the last n-th elements emitted by the source Observable to new subscribers.

upvote
  flag
you will have a new request for each call to login – AngularInDepth.com
upvote
  flag
That's not OP's question. (BTW: so does your solution.) – n00dl3
upvote
  flag
no, my solution doesn't send the request each time. that is why I use the variable sent. – AngularInDepth.com
upvote
  flag
also bear in mind that with publish().connect() all subscriptions to login() will not be notified once the http.get() completes – AngularInDepth.com
upvote
  flag
@n00dl3, this seems to work, but I have one issue. authTokenService.setAuthToken() is always called with undefined argument. Looks like res.json().auth_token doesn't resolve to the actual data I'm receiving. Do you happen to know what the problem is? – Vasiliy
upvote
  flag
@Vasiliy no idea. Are you sure your response has that auth_token property ? – n00dl3
upvote
  flag
@n00dl3, nop :( Probably some issue with the test driver. Accepting your answer as it seems to work. Thanks – Vasiliy

Not the answer you're looking for? Browse other questions tagged or ask your own question.