1

I'm building an authentication app using the PEAN stack (i.e., PostgreSQL - ExpressJS - Angular - NodeJS).

I check for user sign-in status as follows:

  1. On the backend, check the session cookie to see if the user property exists in the req.session object.

server.js

/* ... */

app.post('/api/get-signin-status', async (req, res) => {
  try {
    if (req.session.user) {
      return res.status(200).json({ message: 'User logged in' });
    } else {
      return res.status(400).json({ message: 'User logged out' });
    }
  } catch {
    return res.status(500).json({ message: 'Internal server error' });
  }
});

/* ... */
  1. Send an HTTP POST request to the api/get-signin-status endpoint with optional data and include a cookie in the request.

auth.service.ts

/* ... */

getSignInStatus(data?: any) {
  return this.http.post(this.authUrl + 'api/get-signin-status', data, {
    withCredentials: true,
  });
}

/* ... */
  1. Intercept any HTTP request and provide an observable interceptorResponse$ for subscribing to the response of intercepted requests.

interceptor.service.ts

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { AuthService } from 'src/app/auth/services/auth.service';

@Injectable({
  providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
  private interceptorResponse$: BehaviorSubject<any> = new BehaviorSubject<any>(null);

  intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const signInStatusObserver = {
      next: (x: any) => {
        this.interceptorResponse$.next({ success: true, response: x });
      },

      error: (err: any) => {
        this.interceptorResponse$.next({ success: false, response: err });
      },
    };

    this.authService.getSignInStatus().subscribe(signInStatusObserver);

    return next.handle(httpRequest);
  }

  getInterceptorResponse(): Observable<any> {
    return this.interceptorResponse$.asObservable();
  }

  constructor(private authService: AuthService) {}
}
  1. On the frontend, subscribe to the interceptorResponse observable from the InterceptorService and log the response to the console.

header.component.ts

import { Component, OnInit } from '@angular/core';
import { InterceptorService } from '../auth/services/interceptor.service';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
  interceptorResponse: any;

  constructor(
    private interceptorService: InterceptorService
  ) {
    this.interceptorService.getInterceptorResponse().subscribe((response: any) => {
      console.log(response);

      this.interceptorResponse = response;
      if (response) {
        console.log('Interceptor response success:', response.response);
      } else {
        console.log('Interceptor response is null');
      }
    });
  }

  ngOnInit(): void {}
}

Problem

According to the StackOverflow answer, I should use BehaviorSubject. The problem is that in the console, I always get the following:

Screenshot

But if I console log next and error like this:

interceptor.service.ts

/* ... */

const signInStatusObserver = {
  next: (x: any) => {
    console.log(x);
    this.interceptorResponse$.next({ success: true, response: x });
  },

  error: (err: any) => {
    console.log(err.error.message);
    this.interceptorResponse$.next({ success: false, response: err });
  },
};

/* ... */

I see the expected {message: 'User logged in'} in the console, as shown in the screenshot below. This means that the backend correctly passes sign-in status to the frontend.

Screenshot

Question

Why does the Angular interceptor always return null (i.e., the initial value) using BehaviorSubject, not the updated value?


EDIT 1

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { HeaderComponent } from './header/header.component';
import { AppComponent } from './app.component';
import { FooterComponent } from './footer/footer.component';

import { AppRoutingModule } from './app-routing.module';
import { RoutingComponents } from './app-routing.module';

import { SharedModule } from './shared/shared.module';

import { HttpClientModule } from '@angular/common/http';

import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';

import { CodeInputModule } from 'angular-code-input';

import { IfSignedOut } from './auth/guards/if-signed-out.guard';
import { IfSignedIn } from './auth/guards/if-signed-in.guard';

import { InterceptorService } from './auth/services/interceptor.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

@NgModule({
  declarations: [HeaderComponent, AppComponent, FooterComponent, RoutingComponents],
  imports: [BrowserModule, BrowserAnimationsModule, AppRoutingModule, SharedModule, HttpClientModule, MatMenuModule, MatSidenavModule, CodeInputModule],
  providers: [IfSignedOut, IfSignedIn, { provide: HTTP_INTERCEPTORS, useClass: InterceptorService, multi: true }],
  bootstrap: [AppComponent],
})
export class AppModule {}

EDIT 2

With the help of @VonC I managed to get the whole thing working as expected. Here's what I did.

  1. I removed my initial code in server.js because the interceptor will now depend on the api/get-user endpoint, not api/get-signin-status like before. Consequently, I don't need app.post('/api/get-signin-status', () => {}) anymore. The reason why I now use the api/get-user endpoint is because both did the same thing (i.e., checked the session cookie to see if the user property exists in the req.session object), which means that only one is enough for my auth app. There's no need to check the session cookie twice.

server.js

/* ... */

/* Removed */
/*
app.post('/api/get-signin-status', async (req, res) => {
  try {
    if (req.session.user) {
      return res.status(200).json({ message: 'User logged in' });
    } else {
      return res.status(400).json({ message: 'User logged out' });
    }
  } catch {
    return res.status(500).json({ message: 'Internal server error' });
  }
});
*/

/* ... */
  1. I removed my initial code in auth.service.ts and added the code as @VonC suggested.

auth.service.ts

/* ... */

/* Removed */
/*
getSignInStatus(data?: any) {
  return this.http.post(this.authUrl + 'api/get-signin-status', data, {
    withCredentials: true,
  });
}
*/

/* Added */
private signInStatus$: BehaviorSubject<any> = new BehaviorSubject<any>(null);

getSignInStatusObserver(): Observable<any> {
  return this.signInStatus$.asObservable();
}

setSignInStatus(status: any): void {
  this.signInStatus$.next(status);
}

/* ... */
  1. I removed my initial code in interceptor.service.ts and added the code as @VonC suggested. Note: I changed the endpoint from api/get-signin-status to api/get-user.

interceptor.service.ts

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { AuthService } from 'src/app/auth/services/auth.service';

@Injectable({
  providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
  intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(httpRequest).pipe(
      tap((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse && httpRequest.url.endsWith('api/get-user')) {
          this.authService.setSignInStatus({ success: true, response: event.body });
        }
      }),
      catchError((err: any) => {
        if (httpRequest.url.endsWith('api/get-user')) {
          this.authService.setSignInStatus({ success: false, response: err });
        }
        return throwError(err);
      })
    );
  }

  constructor(private authService: AuthService) {}
}
  1. I removed my initial code in header.component.ts and added the code as @VonC suggested.

header.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from 'src/app/auth/services/auth.service';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
  signInStatus: any;

  constructor(private authService: AuthService, public publicAuthService: AuthService, private signOutRouter: Router, private snackBar: MatSnackBar) {
    this.authService.getSignInStatusObserver().subscribe((response: any) => {
      this.signInStatus = response;
      if (response) {
        console.log('Sign in status success:', response.response);
      } else {
        console.log('Sign in status is null');
      }
    });
  }

  ngOnInit(): void {}
}

Now I can finally show elements in header.component.html depending on sign-in status coming from the backend as follows:

header.component.html

<div *ngIf="signInStatus">Show this element if the user is signed in</div>
<div *ngIf="!signInStatus">Show this element if the user is signed out</div>
Rok Benko
  • 14,265
  • 2
  • 24
  • 49
  • Are you trying to solve some issue in general, or you want BehaviorSubject solution only? – Sergey Jul 14 '23 at 16:28
  • Could you create a StackBlitz sample? – Sergey Jul 14 '23 at 16:36
  • @Sergey What could be an alternative solution? I read that `BehaviorSubject` is the best option. Hm, I will try. – Rok Benko Jul 14 '23 at 18:09
  • If I'm not mistaken, in general you want to implement a basic auth for requests, right? I mean that it doesn't have anything extraordinary to it. – Sergey Jul 14 '23 at 19:05
  • 1
    Any chance you end up with two instances of the service? One gets injected in the header component and another one used as an interceptor? That would explain why you don't get notifications on observable updates. – Sergey Jul 14 '23 at 19:06
  • Can you show you app.compoennt and app.module? – Robin Dijkhof Jul 14 '23 at 19:06
  • @RobinDijkhof See the edit. My `app.component.ts` is empty. – Rok Benko Jul 14 '23 at 20:00
  • @Sergey Why would two instances cause the problem? – Rok Benko Jul 14 '23 at 20:03
  • Because as I explained you may be observing the changes on one, while updates receives the other. It's like opening a room, but checking the wrong door for being open. In another building. – Sergey Jul 14 '23 at 20:08
  • To be more precise, you should be asking not why the Interceptor has `null`, but why it doesn't get updated. `BehaviorSubject` gives you an initial value. It's being subscribed to before any requests are made. And this is evident from the logs. What's weird is that your HeaderComponent doesn't receive any updates after the requests are made. – Sergey Jul 14 '23 at 20:15
  • Without being able to play around the issue myself I can only guess that it happens because you subscribe to one instance and another instance receives the actual updates. That would explain this behavior. – Sergey Jul 14 '23 at 20:16
  • Lastly, as a last resort you could try removing `dist` and `.angular` folders and running the browser in an incognito mode. Sometimes cache manages to surprise – Sergey Jul 14 '23 at 20:17
  • @Sergey With the help of VonC (see his answer below), I managed to solve the problem. Thanks for the time and attention you gave to my question. I appreciate it very much. – Rok Benko Jul 16 '23 at 13:15

2 Answers2

1

The "interceptor response is null" might be expected: the way you are using the RxJS BehaviorSubject in your interceptor may be causing this issue.

The purpose of an interceptor is to intercept HTTP requests and do something with those requests or responses.
(See for instance "Intro to Angular Http Interceptors" from Cory Rylan)

But in your interceptor, you are creating a new HTTP request by calling this.authService.getSignInStatus().subscribe(signInStatusObserver); instead of intercepting an existing one. That means the response to this request might not be available immediately when your component subscribes to getInterceptorResponse().
... hence possibly the null interceptor response.


As an example, you could use an interceptor to check the user's authentication status:

// interceptor.service.ts

import { tap, catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class InterceptorService implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(httpRequest).pipe(
      tap((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse && httpRequest.url.endsWith('/api/get-signin-status')) {
          this.authService.setSignInStatus({ success: true, response: event.body });
        }
      }),
      catchError((err: any) => {
        if (httpRequest.url.endsWith('/api/get-signin-status')) {
          this.authService.setSignInStatus({ success: false, response: err });
        }
        return throwError(err);
      })
    );
  }
}

In this updated version, we are subscribing to the handle method of next, which returns an Observable of the HTTP response. It uses the tap operator to do something with the successful responses and the catchError operator to handle errors.

I am assuming setSignInStatus is a method in your AuthService that updates the sign-in status stored in a BehaviorSubject.
I am also checking if the request URL ends with '/api/get-signin-status' to make sure we are only setting the sign-in status for that specific request.

That means you might implement this user's authentication status with:

// auth.service.ts

private signInStatus$: BehaviorSubject<any> = new BehaviorSubject<any>(null);

getSignInStatusObserver(): Observable<any> {
  return this.signInStatus$.asObservable();
}

setSignInStatus(status: any): void {
  this.signInStatus$.next(status);
}

You would need to update your header.component.ts to use authService instead of interceptorService:

// header.component.ts

constructor(private authService: AuthService) {
  this.authService.getSignInStatusObserver().subscribe((response: any) => {
    console.log(response);

    this.interceptorResponse = response;
    if (response) {
      console.log('Interceptor response success:', response.response);
    } else {
      console.log('Interceptor response is null');
    }
  });
}

Finally, in your component, you would then subscribe to getSignInStatusObserver instead of getInterceptorResponse.

That ensures that the sign-in status is updated every time an HTTP response is received from '/api/get-signin-status' and that components can subscribe to the status updates.

The subscription in your component would look like:

// header.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../auth/services/auth.service';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss'],
})
export class HeaderComponent implements OnInit {
  signInStatus: any;

  constructor(private authService: AuthService) {
    this.authService.getSignInStatusObserver().subscribe((response: any) => {
      // That block of code will run every time the signInStatus updates
      console.log(response);

      this.signInStatus = response;
      if (response) {
        console.log('Sign in status success:', response.response);
      } else {
        console.log('Sign in status is null');
      }
    });
  }

  ngOnInit(): void {}
}

In the constructor of HeaderComponent, we are injecting AuthService instead of InterceptorService. We are subscribing to getSignInStatusObserver(), which is a method in AuthService that returns an Observable of the sign-in status. When the sign-in status is updated, the subscribed function will be called with the new status as the argument.

We are storing the sign-in status in the signInStatus property and logging it to the console. We are also logging a success message if the response exists and a message indicating the sign-in status is null otherwise. That mirrors the behavior of your original code.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • I spent some time implementing your suggestion, and it looks very promising. I correctly get sign-in status, but because I changed the endpoint from `api/get-signin-status` to `api/get-user`, I will continue tomorrow, and if everything works fine, I will of course accept your answer and give you the bounty. – Rok Benko Jul 16 '23 at 13:11
  • @RokBenko No problem. Let me know if you have any other question on this issue. – VonC Jul 16 '23 at 14:50
  • Everything works. Thanks for the time and attention you gave to my question. I appreciate it very much. – Rok Benko Jul 17 '23 at 19:01
  • @RokBenko No problem, glad you made it work! – VonC Jul 17 '23 at 19:01
  • I'm offering [another bounty](https://stackoverflow.com/q/76762998/10347145). Would you mind taking a look at my problem? I would appreciate it very much. – Rok Benko Jul 31 '23 at 14:15
1

I'm guessing the problem is that Angular creates multiple instances of your interceptorService. One actually used as an interceptor and one is injected into your components/services.

I vaguely remember having the same problem. Back then, I solved it like this:

class Interceptor{
    constructor(private final service: SharedDataService){}
}

class Component{
    constructor(private final service: SharedDataService){}
}

Both the component and interceptor use another service to share data.

EDIT

A quick search actually confirms this. See Interceptor create two instances of singleton service

Robin Dijkhof
  • 18,665
  • 11
  • 65
  • 116
  • With the help of VonC (see his answer above), I managed to solve the problem. Thanks for the time and attention you gave to my question. I appreciate it very much. – Rok Benko Jul 16 '23 at 13:03