Angular14基于WebSocket+Ngrok集成SignalR遇TypeError: Failed to fetch错误
问题背景
我需要开发一个服务,用来展示后端(.Net开发)任务完成后推送的消息。后端定义了3种消息类型,要根据完成的功能展示对应消息。但在Angular应用中实现SignalR服务时遇到了以下错误:
[2024-03-12T09:38:41.384Z] Warning: Error from HTTP request. TypeError: Failed to fetch.
我的代码实现
SignalR服务代码
import { Injectable } from '@angular/core'; import { HubConnectionBuilder, HubConnection, LogLevel, } from '@microsoft/signalr'; import { Subject } from 'rxjs'; import { IPonudaKreiranaNotifikacijaDto } from 'app/models/IPonudaKreiranaNotifikacijaDto'; import { environment } from 'environments/environment'; import { IIzvrsenaAkcijaNotifikacijaDto } from 'app/models/IIzvrsenaAkcijaNotifikacijaDto'; import { IZahtjevKreiranPorukaDto } from 'app/models/IZahtjevKreiranPorukaDto'; @Injectable({ providedIn: 'root', }) export class SignalRService { private connection: HubConnection; public ponudaKreirana$ = new Subject<IPonudaKreiranaNotifikacijaDto>(); public izvrsenaAkcija$ = new Subject<IIzvrsenaAkcijaNotifikacijaDto>(); public kreiranZahtjev$ = new Subject<IZahtjevKreiranPorukaDto>(); constructor() {} startConnection() { console.log('start connection'); const signalRUrl = `${environment.APS_API_URL}messageHub`; this.connection = new HubConnectionBuilder() .withUrl(signalRUrl) .build(); this.connection .start() .then(() => { console.log('SignalR connection started'); this.registerEventHandlers(); }) .catch((err) => console.error('Error while starting SignalR connection: ', err), ); } private registerEventHandlers() { this.connection.on( 'PonudaKreirana', (operaterIme: string, poruka: IPonudaKreiranaNotifikacijaDto) => { console.log( 'Received PonudaKreirana message:', operaterIme, poruka, ); this.ponudaKreirana$.next(poruka); }, ); this.connection.on( 'IzvrsenAkcija', (operaterIme: string, poruka: IIzvrsenaAkcijaNotifikacijaDto) => { console.log( 'Received IzvrsenAkcija message:', operaterIme, poruka, ); this.izvrsenaAkcija$.next(poruka); }, ); this.connection.on( 'KreiranZahtjev', (operaterIme: string, poruka: IZahtjevKreiranPorukaDto) => { console.log( 'Received KreiranZahtjev message:', operaterIme, poruka, ); this.kreiranZahtjev$.next(poruka); }, ); } stopConnection() { this.connection .stop() .then(() => console.log('SignalR connection stopped')) .catch((err) => console.error('Error while stopping SignalR connection: ', err), ); } }
AppComponent代码
import { Component } from '@angular/core'; import { Subscription } from 'rxjs'; import { SignalRService } from './services/signalr.service'; import { IPonudaKreiranaNotifikacijaDto } from './models/IPonudaKreiranaNotifikacijaDto'; import { IIzvrsenaAkcijaNotifikacijaDto } from './models/IIzvrsenaAkcijaNotifikacijaDto'; import { IZahtjevKreiranPorukaDto } from './models/IZahtjevKreiranPorukaDto'; import { ToastrService } from 'ngx-toastr'; @Component({ selector: 'app-root', template: ` <router-outlet></router-outlet> `, }) export class AppComponent { public title = $localize`Učitavam Smart Aps sistem...`; public ponudaKreiranaSubscription: Subscription; public izvrsenaAkcijaSubscription: Subscription; public kreiranZahtjevSubscription: Subscription; constructor(private signalRService: SignalRService, private toastr: ToastrService) {} ngOnInit() { this.signalRService.startConnection(); this.ponudaKreiranaSubscription = this.signalRService.ponudaKreirana$.subscribe( (poruka: IPonudaKreiranaNotifikacijaDto) => { this.toastr.success(`Primljena poruka: ${poruka}`); }, ); this.izvrsenaAkcijaSubscription = this.signalRService.izvrsenaAkcija$.subscribe( (poruka: IIzvrsenaAkcijaNotifikacijaDto) => { this.toastr.success(`Primljena poruka: ${poruka}`); }, ); this.kreiranZahtjevSubscription = this.signalRService.kreiranZahtjev$.subscribe( (poruka: IZahtjevKreiranPorukaDto) => { this.toastr.success(`Primljena poruka: ${poruka}`); }, ); } ngOnDestroy() { this.signalRService.stopConnection(); this.ponudaKreiranaSubscription.unsubscribe(); this.izvrsenaAkcijaSubscription.unsubscribe(); this.kreiranZahtjevSubscription.unsubscribe(); } }
需求
我已经参照Angular官方文档及相关技术资料实现,但仍未解决问题,期望SignalR服务能正常建立连接,AppComponent可成功订阅服务事件。
检查SignalR端点URL正确性
确认environment.APS_API_URL是否包含正确的协议(http/https)、域名和端口,且末尾是否带有斜杠。比如如果API地址是http://localhost:5000/,拼接后的地址应为http://localhost:5000/messageHub,若API地址末尾无斜杠,会变成无效地址http://localhost:5000messageHub。直接在浏览器访问http://your-api-url/messageHub/negotiate,若返回404或错误,说明后端Hub配置有误。跨域(CORS)配置检查
Angular前端和.Net后端通常跨端口运行,需确保后端正确配置CORS:- 在.Net后端
Program.cs中添加允许前端源的CORS策略,并开启AllowCredentials(SignalR必需):builder.Services.AddCors(options => { options.AddPolicy("AllowAngularApp", policy => { policy.WithOrigins("http://localhost:4200") // 替换为你的Angular地址 .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); app.UseCors("AllowAngularApp"); // 放在app.UseRouting()之后 - 确认SignalR Hub路由配置正确:
app.MapHub<MessageHub>("/messageHub");
- 在.Net后端
浏览器网络请求排查
打开浏览器开发者工具(F12),切换到Network标签,过滤Fetch/XHR请求,查看SignalR的negotiate请求:- 若状态码403:检查身份验证配置,比如JWT令牌是否正确携带,后端是否允许SignalR Hub匿名访问(未配置权限时)。
- 若状态码0或CORS错误:重新检查跨域配置。
增强SignalR服务的错误处理和日志
在SignalR服务中添加调试日志和连接关闭监听,便于定位问题:this.connection = new HubConnectionBuilder() .withUrl(signalRUrl) .configureLogging(LogLevel.Debug) .build(); this.connection.onclose(error => { console.error('SignalR连接关闭:', error); // 可选:自动重连 setTimeout(() => this.startConnection(), 5000); });检查Dto模型兼容性
确保Angular中的IPonudaKreiranaNotifikacijaDto等模型与.Net后端Dto字段完全匹配(包括大小写,JavaScript大小写敏感),字段不匹配会导致消息解析失败。身份验证配置(若需)
若后端SignalR Hub需要身份验证,建立连接时携带令牌:this.connection = new HubConnectionBuilder() .withUrl(signalRUrl, { accessTokenFactory: () => localStorage.getItem('authToken') || '' }) .build();
内容的提问来源于stack exchange,提问作者Irfan Bučan

