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

huangapple go评论79阅读模式
英文:

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

问题

我正在使用 PEAN 栈(即 PostgreSQL - ExpressJS - Angular - NodeJS)构建身份验证应用程序。

在后端,我检查用户登录状态如下:

  1. 在后端,检查会话 cookie 是否存在 req.session 对象的 user 属性。

server.js

/* ... */

app.post('/api/get-signin-status', async (req, res) => {
  try {
    if (req.session.user) {
      return res.status(200).json({ message: '用户已登录' });
    } else {
      return res.status(400).json({ message: '用户已注销' });
    }
  } catch {
    return res.status(500).json({ message: '内部服务器错误' });
  }
});

/* ... */
  1. 使用 HTTP POST 请求发送到 api/get-signin-status 端点,包括一个 cookie 在请求中。

auth.service.ts

/* ... */

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

/* ... */
  1. 拦截任何 HTTP 请求并提供一个可观察的 interceptorResponse$ 以订阅拦截请求的响应。

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. 在前端,从 InterceptorService 订阅 interceptorResponse 可观察对象,并将响应记录到控制台。

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('拦截器响应成功:', response.response);
      } else {
        console.log('拦截器响应为空');
      }
    });
  }

  ngOnInit(): void {}
}

问题:

为什么 Angular 拦截器始终使用 BehaviorSubject 返回 null(即初始值),而不是更新后的值?

编辑 1:

我注意到您在 app.module.ts 中配置了拦截器,请确保在 providers 数组中正确提供 InterceptorService

编辑 2:

根据您的编辑,您似乎已经解决了问题。您移除了不必要的代码,并对拦截器进行了必要的更改以匹配新的端点。现在,您可以根据后端传递的登录状态在前端显示不同的元素。

英文:

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(&#39;/api/get-signin-status&#39;, async (req, res) =&gt; {
  try {
    if (req.session.user) {
      return res.status(200).json({ message: &#39;User logged in&#39; });
    } else {
      return res.status(400).json({ message: &#39;User logged out&#39; });
    }
  } catch {
    return res.status(500).json({ message: &#39;Internal server error&#39; });
  }
});

/* ... */
  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 + &#39;api/get-signin-status&#39;, 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 &#39;@angular/core&#39;;
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler } from &#39;@angular/common/http&#39;;
import { Observable, BehaviorSubject } from &#39;rxjs&#39;;
import { AuthService } from &#39;src/app/auth/services/auth.service&#39;;

@Injectable({
  providedIn: &#39;root&#39;,
})
export class InterceptorService implements HttpInterceptor {
  private interceptorResponse$: BehaviorSubject&lt;any&gt; = new BehaviorSubject&lt;any&gt;(null);

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

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

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

    return next.handle(httpRequest);
  }

  getInterceptorResponse(): Observable&lt;any&gt; {
    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 &#39;@angular/core&#39;;
import { InterceptorService } from &#39;../auth/services/interceptor.service&#39;;

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

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

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

  ngOnInit(): void {}
}

Problem

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

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

But if I console log next and error like this:

interceptor.service.ts

/* ... */

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

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

/* ... */

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

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

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 &#39;@angular/core&#39;;
import { BrowserModule } from &#39;@angular/platform-browser&#39;;
import { BrowserAnimationsModule } from &#39;@angular/platform-browser/animations&#39;;

import { HeaderComponent } from &#39;./header/header.component&#39;;
import { AppComponent } from &#39;./app.component&#39;;
import { FooterComponent } from &#39;./footer/footer.component&#39;;

import { AppRoutingModule } from &#39;./app-routing.module&#39;;
import { RoutingComponents } from &#39;./app-routing.module&#39;;

import { SharedModule } from &#39;./shared/shared.module&#39;;

import { HttpClientModule } from &#39;@angular/common/http&#39;;

import { MatMenuModule } from &#39;@angular/material/menu&#39;;
import { MatSidenavModule } from &#39;@angular/material/sidenav&#39;;

import { CodeInputModule } from &#39;angular-code-input&#39;;

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

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

@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(&#39;/api/get-signin-status&#39;, () =&gt; {}) 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(&#39;/api/get-signin-status&#39;, async (req, res) =&gt; {
  try {
    if (req.session.user) {
      return res.status(200).json({ message: &#39;User logged in&#39; });
    } else {
      return res.status(400).json({ message: &#39;User logged out&#39; });
    }
  } catch {
    return res.status(500).json({ message: &#39;Internal server error&#39; });
  }
});
*/

/* ... */
  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 + &#39;api/get-signin-status&#39;, data, {
    withCredentials: true,
  });
}
*/

/* Added */
private signInStatus$: BehaviorSubject&lt;any&gt; = new BehaviorSubject&lt;any&gt;(null);

getSignInStatusObserver(): Observable&lt;any&gt; {
  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 &#39;@angular/core&#39;;
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from &#39;@angular/common/http&#39;;
import { Observable, throwError } from &#39;rxjs&#39;;
import { tap, catchError } from &#39;rxjs/operators&#39;;
import { AuthService } from &#39;src/app/auth/services/auth.service&#39;;

@Injectable({
  providedIn: &#39;root&#39;,
})
export class InterceptorService implements HttpInterceptor {
  intercept(httpRequest: HttpRequest&lt;any&gt;, next: HttpHandler): Observable&lt;HttpEvent&lt;any&gt;&gt; {
    return next.handle(httpRequest).pipe(
      tap((event: HttpEvent&lt;any&gt;) =&gt; {
        if (event instanceof HttpResponse &amp;&amp; httpRequest.url.endsWith(&#39;api/get-user&#39;)) {
          this.authService.setSignInStatus({ success: true, response: event.body });
        }
      }),
      catchError((err: any) =&gt; {
        if (httpRequest.url.endsWith(&#39;api/get-user&#39;)) {
          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 &#39;@angular/core&#39;;
import { AuthService } from &#39;src/app/auth/services/auth.service&#39;;
import { Router } from &#39;@angular/router&#39;;
import { MatSnackBar } from &#39;@angular/material/snack-bar&#39;;

@Component({
  selector: &#39;app-header&#39;,
  templateUrl: &#39;./header.component.html&#39;,
  styleUrls: [&#39;./header.component.scss&#39;],
})
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) =&gt; {
      this.signInStatus = response;
      if (response) {
        console.log(&#39;Sign in status success:&#39;, response.response);
      } else {
        console.log(&#39;Sign in status is null&#39;);
      }
    });
  }

  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

&lt;div *ngIf=&quot;signInStatus&quot;&gt;Show this element if the user is signed in&lt;/div&gt;
&lt;div *ngIf=&quot;!signInStatus&quot;&gt;Show this element if the user is signed out&lt;/div&gt;

答案1

得分: 1

"interceptor response is null" 可能是预期的情况:您在拦截器中使用 RxJSBehaviorSubject 的方式可能导致了这个问题。

拦截器的目的是拦截 HTTP 请求并对这些请求或响应执行某些操作。
(请参见例如 Intro to Angular Http InterceptorsCory Rylan 编写的文章)

但是在您的拦截器中,您通过调用 this.authService.getSignInStatus().subscribe(signInStatusObserver); 创建了一个 新的 HTTP 请求,而不是拦截现有的请求。这意味着当您的组件订阅 getInterceptorResponse() 时,对这个请求的响应可能不会立即可用。
... 因此可能会出现 null 拦截器响应。


举个例子,您可以使用拦截器来检查用户的身份验证状态:

// 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);
      })
    );
  }
}

在这个更新后的版本中,我们订阅了 nexthandle 方法,它返回了一个 HTTP 响应的 Observable。它使用 tap 操作符来处理成功的响应,使用 catchError 操作符来处理错误。

我假设 setSignInStatus 是您的 AuthService 中的一个方法,用于更新存储在 BehaviorSubject 中的登录状态。
我还检查请求的 URL 是否以 '/api/get-signin-status' 结尾,以确保我们只为该特定请求设置登录状态。

这意味着您可以使用以下方式实现用户的身份验证状态:

// 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);
}

您需要更新 header.component.ts,以使用 authService 而不是 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');
    }
  });
}

最后,在您的组件中,您应该订阅 getSignInStatusObserver 而不是 getInterceptorResponse

这确保了每次从 '/api/get-signin-status' 收到 HTTP 响应时都会更新登录状态,并且组件可以订阅状态更新。

您的组件中的订阅代码如下:

// 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) => {
      // 每当 signInStatus 更新时,该代码块将运行
      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 {}
}

HeaderComponent 的构造函数中,我们注入了 AuthService 而不是 InterceptorService。我们订阅了 getSignInStatusObserver(),这是 AuthService 中的一个方法,返回一个登录状态的 Observable。当登录状态更新时,订阅的函数将以新状态作为参数被调用。

我们将登录状态存储在 signInStatus 属性中,并将其记录到控制台。如果响应存在,我们还会记录一个成功的消息,否则记录一个指示登录状态为空的消息。这与您的原始代码的行为相似。

英文:

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 &#39;rxjs/operators&#39;;

@Injectable({
  providedIn: &#39;root&#39;,
})
export class InterceptorService implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(httpRequest: HttpRequest&lt;any&gt;, next: HttpHandler): Observable&lt;HttpEvent&lt;any&gt;&gt; {
    return next.handle(httpRequest).pipe(
      tap((event: HttpEvent&lt;any&gt;) =&gt; {
        if (event instanceof HttpResponse &amp;&amp; httpRequest.url.endsWith(&#39;/api/get-signin-status&#39;)) {
          this.authService.setSignInStatus({ success: true, response: event.body });
        }
      }),
      catchError((err: any) =&gt; {
        if (httpRequest.url.endsWith(&#39;/api/get-signin-status&#39;)) {
          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&lt;any&gt; = new BehaviorSubject&lt;any&gt;(null);

getSignInStatusObserver(): Observable&lt;any&gt; {
  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) =&gt; {
    console.log(response);

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

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 &#39;@angular/core&#39;;
import { AuthService } from &#39;../auth/services/auth.service&#39;;

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

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

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

  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.

答案2

得分: 1

我猜问题可能是Angular创建了多个interceptorService的实例。一个用作拦截器,另一个被注入到你的组件/服务中。

我依稀记得我曾经遇到过相同的问题。当时我是这样解决的:

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

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

组件和拦截器都使用另一个服务来共享数据。

编辑

快速搜索实际上证实了这一点。请参见https://stackoverflow.com/questions/69268285/interceptor-create-two-instances-of-singleton-service

英文:

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 https://stackoverflow.com/questions/69268285/interceptor-create-two-instances-of-singleton-service

huangapple
  • 本文由 发表于 2023年7月12日 22:51:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/76671881.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定