I’m building an authentication app using the PEAN stack (i.e., PostgreSQL – ExpressJS – Angular – NodeJS). For authentication, I’m using express-session
on the backend.
I check for user sign-in status as follows:
- On the backend, check the session cookie to see if the
user
property exists in thereq.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' });
}
});
/* ... */
- 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,
});
}
/* ... */
- 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) {}
}
- On the frontend, subscribe to the
interceptorResponse
observable from theInterceptorService
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:
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.
Question
Why does the Angular interceptor always return null using BehaviorSubject
?
EDIT
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 {}
2
Answers
The "
interceptor response is null
" might be expected: the way you are using the RxJSBehaviorSubject
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 togetInterceptorResponse()
.… hence possibly the null interceptor response.
As an example, you could use an interceptor to check the user’s authentication status:
In this updated version, we are subscribing to the
handle
method ofnext
, which returns anObservable
of the HTTP response. It uses thetap
operator to do something with the successful responses and thecatchError
operator to handle errors.I am assuming
setSignInStatus
is a method in yourAuthService
that updates the sign-in status stored in aBehaviorSubject
.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:
You would need to update your
header.component.ts
to useauthService
instead ofinterceptorService
:Finally, in your component, you would then subscribe to
getSignInStatusObserver
instead ofgetInterceptorResponse
.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:
In the constructor of
HeaderComponent
, we are injectingAuthService
instead ofInterceptorService
. We are subscribing togetSignInStatusObserver()
, which is a method inAuthService
that returns anObservable
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.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:
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