I have a problem with using @aspnet/signalr-client, I'm still just starting out and I don't understand where
SignalR connection error: Error: WebSocket failed to connect comes from. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.
So I would like to refresh the page every time the token signaler expires. Here's my setup in my Azure Function App. that's my negociate function.json file
{
"disabled": false,
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get"]
},
{
"type": "http",
"direction": "out",
"name": "res"
},
{
"type": "signalRConnectionInfo",
"name": "connectionInfo",
"userId": "",
"hubName": "measurements",
"direction": "in"
}
]
}
here it's the negociate index.js file
module.exports = async function (context, req, connectionInfo) {
context.res.body = connectionInfo;
};
my local.settings.json file
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=mcdashboardfunctionsprd;AccountKey=????",
"AzureWebJobsCosmosDBConnectionString": "AccountEndpoint=https://prd-cosmos-db-account.documents.azure.com:443/;AccountKey=???",
"AzureSignalRConnectionString": "Endpoint=https://signalr-dashboard-prd.service.signalr.net;AccessKey=?????;Version=1.0;",
"FUNCTIONS_WORKER_RUNTIME": "node"
},
"Host": {
"LocalHttpPort": 7071,
"CORS": "*"
}
}
I decide to replace any key with ??.
this is my environment variable:
// The list of file replacements can be found in `angular.json`.
export const environment = {
authentication: {
stsServerUrl: 'https://dev-sf-ztc2f.us.auth0.com',
clientId: 'GoPIRYXScZWIMLxqfVuj2p4OwhSabh0l',
},
bff: {
serverUrl: 'http://localhost:7071',
basePath: '/api',
},
googleAnalyticsTagId: `G-TLMFNVQ6XY`,
instrumentationKey: '',
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
Client side with AngularJS, I created a service file: signalr.service.ts. that's my service class:
import * as signalR from '@microsoft/signalr';
import { BehaviorSubject, Observable, from, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { delay, map, retryWhen, switchMap, take } from 'rxjs/operators';
import { environment } from '@env/environment';
@Injectable()
export class SignalRService {
private hubConnection: signalR.HubConnection;
private connectionAttempts = 0;
private negociateUrl!: string;
connection!: signalR.HubConnection;
hubMessage: BehaviorSubject<string>;
constructor(private http: HttpClient) {
this.negociateUrl = `${environment.bff.serverUrl}${environment.bff.basePath}/negociate`;
this.hubMessage = new BehaviorSubject<string>(null);
}
private setSignalrClientMethods(): void {
this.connection.on('DisplayMessage', (message: string) => {
this.hubMessage.next(message);
});
}
public async initiateSignalrConnection(): Promise<void> {
try {
this.connection = this.hubConnectionBuilder(
this.negociateUrl,
{
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
}
);
await this.connection.start();
this.setSignalrClientMethods();
console.log(
`SignalR connection success! connectionId: ${this.connection.connectionId}`
);
} catch (error) {
console.log(`SignalR connection error: ${error}`);
}
}
private hubConnectionBuilder(url: string, options: any = null) {
return new signalR.HubConnectionBuilder()
.withUrl(url, options)
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
if (retryContext.elapsedMilliseconds < 6000) {
// If we've been reconnecting for less than 60 seconds so far,
// wait between 0 and 10 seconds before the next reconnect attempt.
return Math.random() * 1000;
} else {
// If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
return null;
}
},
})
.configureLogging(
environment.production ? signalR.LogLevel.None : signalR.LogLevel.Debug
)
.build();
}
public hubReconnection() {
this.connection = this.hubConnectionBuilder(this.negociateUrl);
if (this.connection.state === signalR.HubConnectionState.Disconnected) {
console.log(`Connection lost due to error. Reconnecting.....`);
this.connection.onreconnecting((error: any) => {
console.log(`Connection lost due to error "${error}". Reconnecting.`);
});
this.connection.onreconnected((connectionId: any) => {
console.log(
`Connection reestablished. Connected with connectionId "${connectionId}".`
);
});
} else {
console.log(this.connection.state);
}
}
public getHubConnection = (): Observable<any> => {
this.connectionAttempts++;
if (
(this.hubConnection !== undefined && this.isConnected()) ||
this.connectionAttempts > 1
) {
return of(true);
}
// TODO save token in local storage and handle token expiration
// const accessToken = localStorage.getItem('socketsToken')
// const url = localStorage.getItem('socketsUrl')
// if(accessToken && url && !this.isConnected()) {
// console.log('Using local storage')
// return this.startConnection(url, accessToken)
// }
return this.getAccessToken();
};
public isConnected() {
if (this.hubConnection) console.log('state', this.hubConnection.state);
return this.hubConnection && this.hubConnection.state !== 'Disconnected';
}
public getAccessToken() {
if (this.isConnected()) {
return of(true);
}
return this.http
.post(
`${environment.bff.serverUrl}${environment.bff.basePath}/negociate`,
{}
)
.pipe(
map((resp) => {
return this.startConnection(resp['url'], resp['accessToken']);
})
);
}
public startConnection(url, accessToken) {
const options = {
accessTokenFactory: () => accessToken,
};
this.hubConnection = this.hubConnectionBuilder(url, options);
this.hubConnection.serverTimeoutInMilliseconds = 1000 * 60 * 10;
this.hubConnection.keepAliveIntervalInMilliseconds = 1000 * 60 * 10;
return from(this.hubConnection.start());
}
public getRealtimeData(eventName): Observable<any> {
return this.getHubConnection().pipe(
switchMap(() => {
return this.onEvent(eventName).pipe(
retryWhen((errors) => errors.pipe(delay(1000), take(10)))
);
})
);
}
public onEvent(eventName): Observable<any> {
const that = this;
return new Observable((observer) => {
this.hubConnection.on(eventName, (data) => {
return observer.next({
name: data.process_time,
value: data.sensor_value,
});
});
// Function executed when the unsubscribe is called
return function () {
if (that.hubConnection) that.hubConnection.off(eventName);
};
});
}
public closeConnection() {
this.hubConnection.stop().then(() => {
console.log('connection closed');
});
}
}
and I import my service at my home.component.ts
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { SignalRService } from '@app/core/signalr/signalr.service';
import { ClockService } from './clock.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class HomeComponent implements OnInit {
dayIcon = 'assets/images/icon-day.svg';
nightIcon = 'assets/images/icon-night.svg';
themeIcon = this.dayIcon;
clock: Observable<Date>;
measurementCollection = 'real';
hubMessage: string;
constructor(
private clockService: ClockService,
private signalrService: SignalRService
) {
this.hubMessage = '';
}
ngOnInit(): void {
this.setClock(this.measurementCollection);
this.signalrService.getHubConnection().pipe(
tap((value: any) => console.log(value))
)
this.signalrService
.initiateSignalrConnection()
.then((reason: any) => {
console.log(reason);
})
.catch((err) => {
console.error(err);
});
this.signalrService.hubReconnection()
}
toggleTheme() {
const bodyEl = document.getElementsByTagName('body')[0];
bodyEl.classList.toggle('light-theme');
this.themeIcon =
this.themeIcon === this.dayIcon ? this.nightIcon : this.dayIcon;
}
setClock(collection) {
this.clock = this.clockService.getClock(collection);
}
toggleSimulated() {
this.measurementCollection =
this.measurementCollection === 'real' ? 'simulated' : 'real';
this.setClock(this.measurementCollection);
}
}
There I am really stuck for two weeks on this problem, I hope I will find an answer here. Here is the launch of the server and client.
The URL that you pass to the signalR.HubConnectionBuilder().withUrl()
should be the base URL to your hub, so without the negotiate
part.
Currently, it looks like the SDK is connecting to the negotiate endpoint directly instead of connecting to Azure SignalR Service, likely because the /negotiate
call that it makes is actually going to /negociate/negotiate
instead and failing.