craigcaulfield
craigcaulfield

Reputation: 3538

Returning Observables from chained HTTP call using concatMap

In an Angular 9 service class I'm chaining a couple of HTTP calls using RxJS's concatMap, using the output of the first call as input the second:

getUserDetails(authorisationCode: string): Observable<UserInfo> {

this.getAuthorisationTokens(authorisationCode)
  .pipe(
    concatMap(authorisationTokens =>  this.getUserInfo(authorisationTokens.access_token)))
    .subscribe(
      (data: UserInfo) => {
        this.userInfo = data;
        console.log(JSON.stringify(this.userInfo));
        console.log(JSON.stringify(data));
      },
      (err: any) => console.log('Error getting user info details : ' + err),
      () => {
        console.log('Got user information: ' + this.userInfo);
      }
    );

return of(this.userInfo);
}

I want to return this.userInfo to a caller and my first naive idea was to wrap it in an Observable (return of(this.userInfo)) and call it like this:

export class LandingComponent implements OnInit {

  username: string;
  userFirstName: string;
  email: string;


  constructor(private route: ActivatedRoute, private userService: UserDataService) {
    this.authorisationCode = route.snapshot.queryParamMap.get('code');
    console.log('Code was ' + this.authorisationCode);
  }

  ngOnInit(): void {

    this.userService.getUserDetails(this.authorisationCode)
      .subscribe((data: UserInfo) => {
        this.userFirstName = data.given_name;
        this.username = data.preferred_username;
        this.email = data.email;

        console.log('Got: ' + this.userFirstName + ', ' + this.username + ', ' + this.email);
      });

  }
}

I can see from the browser console that the calls to my services are succeeding and populating my object this.userInfo, but only after I've tried to use it and got an undefined error:

Code was 2ffa40f9-5e71-4f29-8ddd-318e8d0b99bc
main-es2015.8df8d853b157ca70b40a.js:1 Getting authorisation tokens in exchange for authorisation code 2ffa40f9-5e71-4f29-8ddd-318e8d0b99bc
main-es2015.8df8d853b157ca70b40a.js:1 Header: [object Object]
main-es2015.8df8d853b157ca70b40a.js:1 Body: grant_type=authorization_code&redirect_uri=https://xxx/landing/&client_id=xxx&code=2ffa40f9-5e71-4f29-8ddd-318e8d0b99bc&client_secret=xxx
main-es2015.8df8d853b157ca70b40a.js:1 TOKEN endpoint: https://xxx.amazoncognito.com/oauth2/token

TOKEN endpoint: https://xxx.amazoncognito.com/oauth2/token
main-es2015.8df8d853b157ca70b40a.js:1 ERROR TypeError: Cannot read property 'given_name' of undefined

    ...

USERINFO endpoint https://xxx.amazoncognito.com/oauth2/userInfo
main-es2015.8df8d853b157ca70b40a.js:1 USERINFO endpoint https://xxx.amazoncognito.com/oauth2/userInfo
main-es2015.8df8d853b157ca70b40a.js:1 {"sub":"4bfd88a4-5439-4ad6-a399-71b02034dfa1","email_verified":"true","given_name":"Craig","family_name":"Caulfield","email":"[email protected]","username":"4bfd88a4-5439-4ad6-a399-xxx"}
main-es2015.8df8d853b157ca70b40a.js:1 Got user information: [object Object]

I've tried to apply the following questions, but I can't find a way to apply them to my case:

So, my async thinking is wrong. Is there something obvious that I haven't done?

Upvotes: 0

Views: 276

Answers (2)

Barremian
Barremian

Reputation: 31105

You could be returning undefined because this.userInfo is assigned value asynchronously. One correct way would be to return the HTTP observable and subscribe to it in the controller.

I've also moved extracting the authorisationCode from the constructor to the ngOnInit() hook. While usually the ngOnInit() hook will be triggered after the constructor is called, it can't be guaranteed that the variable this.authorisationCode would have been assigned the value by the time the hook is triggered.

Try the following

Service

getUserDetails(authorisationCode: string): Observable<any> {
  return this.getAuthorisationTokens(authorisationCode)
    .pipe(
      concatMap(authorisationTokens =>  this.getUserInfo(authorisationTokens.access_token))
    );
}

Controller

constructor(private route: ActivatedRoute, private userService: UserDataService) { }

ngOnInit(): void {
  const authorisationCode = route.snapshot.queryParamMap.get('code');
  this.userService.getUserDetails(authorisationCode).subscribe(
    (data: UserInfo) => {
      this.userFirstName = data.given_name;
      this.username = data.preferred_username;
      this.email = data.email;

      console.log('Got: ' + this.userFirstName + ', ' + this.username + ', ' + this.email);
    },
    (err: any) => { console.log('Error getting user info details : ' + err) },
    () => { console.log('Got user information: ' + data); }
  );
}

Upvotes: 1

Andi Kleinbichler
Andi Kleinbichler

Reputation: 161

When you access this.UserInfo via the observable for the first time it is undefined. You need to wait until the API returns or you have to ignore the first value emitted by observable creted by of(this.userUnfo) (e.g.: apply skip(1) operator). A better solution wohl be to fire the this.user observable only after the API returns. You can create a subject, which fires once the API completes, and instead of returning of(this.userInfo) yiu simply create the observable from zhe above mentiined subject (e.g.: user ToObservable() operation)

Upvotes: 0

Related Questions