You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular连接Azure SignalR与v4版TypeScript函数的问题求助

Angular 12 + Azure SignalR + Functions v4 连接问题排查

问题概述

连接Angular 12前端与Azure SignalR、TypeScript无服务器Azure Functions后端时,遇到两个核心问题:

  1. Azure Functions v4编程模型的SignalR绑定语法存在什么错误?
  2. 当negotiate()函数返回硬编码值时,如何解决Angular连接时出现的**Error: None of the transports supported by the client are supported by the server.**错误?

核心现象

  • 无额外输入绑定时,negotiate()函数可执行,但Angular调用this.hubConnection.start()时返回传输兼容错误
  • 添加SignalR输入绑定后,函数启动前直接返回500状态码崩溃
  • 上游触发(POST请求)无法正常工作

技术栈

  • 无服务器Azure Functions(TypeScript,v4编程模型)
  • 免费层无服务器Azure SignalR
  • Angular 12前端(部署在Azure静态Web应用,带自定义域名)

客户端代码示例

const options: any = {
    endpoint: this.config?.signalRHostName,
    authType: 'azure.msi;Version=1.0;',
    // accessKey: 'ZYp9h52IfYouCanReadThisYouAreTooClosenweLQDoXMg='
};

let functionAppUrl = this.config?.functionHostName + '/api/';

this.hubConnection = new HubConnectionBuilder()
.configureLogging(LogLevel.Debug)
.withUrl(functionAppUrl, options)
.build();

this.hubConnection
.start()
.then(() => {
    console.log('************* SignalR connection started');
    this.sendHi();
})
.catch((err: any) => {
    console.log('************* Error while starting SignalR connection', err)
});

服务端negotiate()函数示例

import { app, input, output, HttpResponseInit, InvocationContext } from '@azure/functions';
import { Log, IRequest, AuthUtils, AzureADHelper } from '@wetware/viewdu-fxa';

export async function negotiate(request: IRequest, context: InvocationContext): Promise<HttpResponseInit> {
    await Log.setup(context);
    Log.debug(`>>> Http function processed request for url "${request.url}"`);
    context.log('************* signalR context.extraInputs', context.extraInputs);

    const connection = {
        hubName: 'deafult', // 拼写错误:deafult -> default
        // "connectionStringSetting": "Endpoint=https://sgr-xxxxx.service.signalr.net;AccessKey=ZYp9h52IfYouCanReadThisYouAreTooClosenweLQDoXMg=;Version=1.0;"
        connectionStringSetting: 'Endpoint=https://sgr-xxxxx.service.signalr.net;AuthType=azure.msi;Version=1.0;'
    };

    return {
        status: 200,
        body: JSON.stringify(connection, null, 2)
    };
};

const signalRInput = input.generic({
    type: 'signalRConnectionInfo',
    name: 'connectionInfo',
    hubName: 'default',
    // "connectionStringSetting": "AzureSignalRConnectionString",
    // "connectionStringSetting": "Endpoint=https://sgr-xxxxx.service.signalr.net;AccessKey=ZYp9h52IfYouCanReadThisYouAreTooClosenweLQDoXMg=;Version=1.0;"
    connectionStringSetting: 'Endpoint=https://sgr-xxxxx.service.signalr.net;AuthType=azure.msi;Version=1.0;'
})

app.http('negotiate', {
    methods: ['POST', 'GET'],
    authLevel: 'anonymous',
    handler: negotiate,
    extraInputs: [signalRInput]
});

服务端SignalR触发函数示例

import { app, input, output, HttpResponseInit, InvocationContext } from '@azure/functions';
import { Log, IRequest, AuthUtils, AzureADHelper } from '@wetware/viewdu-fxa';

export async function signalRTrigger(request: IRequest, context: InvocationContext): Promise<HttpResponseInit> {
    await Log.setup(context);
    Log.debug(`>>> Http function processed request for url "${request.url}"`);
    context.log('************* signalR context.extraInputs', context.extraInputs);

    const connection = {
        hubName: 'default',    
        // connectionStringSetting: 'Endpoint=https://sgr-xxxxx.service.signalr.net;AccessKey=ZYp9h52IfYouCanReadThisYouAreTooClosenweLQDoXMg=;Version=1.0;',
        connectionStringSetting: 'Endpoint=https://sgr-xxxxx.service.signalr.net;AuthType=azure.msi;Version=1.0;'
    };

    return {
        status: 200,
        body: JSON.stringify(connection, null, 2)
    };

};

// This should provide the signalR upstream target to trigge
const signalrInput = input.generic({
    type: 'signalRTrigger',
    name: 'invocation',
    hubName: 'default',
    category: 'messages',
    event: 'SendMessage',
    parameterNames: [
       'message'
    ]
});

/* 
"type": "signalRTrigger",
"name": "invocation",
"hubName": "SignalRTest",
"category": "messages",
"event": "SendMessage",
"parameterNames": [
    "message"
],
"direction": "in"
 */

app.http('signalr-trigger', {
    methods: ['POST', 'GET'],
    authLevel: 'anonymous',
    handler: signalRTrigger,
    extraInputs: [signalrInput]
});

问题解答

1. Azure Functions v4编程模型的SignalR绑定语法错误

核心错误点:

  • SignalRConnectionInfo绑定配置错误:connectionStringSetting应填写Function App应用设置的键名,而非直接硬编码连接字符串或MSI格式字符串,硬编码会导致绑定初始化失败触发500错误。
  • SignalRTrigger绑定类型错误:SignalRTrigger是触发型绑定,需通过app.signalR()注册,不能作为HTTP函数的extraInputs配置,当前写法完全不符合绑定逻辑,导致函数无法启动。
  • 拼写错误:negotiate函数中hubName拼写为deafult,与绑定中的default不一致,会导致连接不匹配。

修正后的negotiate函数示例:

import { app, input, HttpResponseInit, InvocationContext } from '@azure/functions';
import { Log, IRequest } from '@wetware/viewdu-fxa';

export async function negotiate(request: IRequest, context: InvocationContext): Promise<HttpResponseInit> {
    await Log.setup(context);
    Log.debug(`>>> Http function processed request for url "${request.url}"`);
    
    // 从绑定中获取生成的有效连接信息
    const connectionInfo = context.extraInputs.get('connectionInfo');

    return {
        status: 200,
        body: JSON.stringify(connectionInfo, null, 2),
        headers: {
            'Content-Type': 'application/json'
        }
    };
};

// 正确配置:connectionStringSetting指向应用设置键名
const signalRInput = input.generic({
    type: 'signalRConnectionInfo',
    name: 'connectionInfo',
    hubName: 'default',
    connectionStringSetting: 'AzureSignalRConnectionString'
})

app.http('negotiate', {
    methods: ['POST'], // SignalR客户端默认用POST请求协商
    authLevel: 'anonymous',
    handler: negotiate,
    extraInputs: [signalRInput]
});

修正后的SignalRTrigger函数示例:

import { app, InvocationContext } from '@azure/functions';
import { Log } from '@wetware/viewdu-fxa';

// 使用app.signalR()注册触发函数
app.signalR('signalRTrigger', {
    hubName: 'default',
    category: 'messages',
    event: 'SendMessage',
    handler: async (context: InvocationContext, invocation: any) => {
        await Log.setup(context);
        Log.debug(`Received SignalR message: ${invocation.args[0]}`);
        
        // 配置输出绑定广播消息
        context.extraOutputs.set('broadcast', {
            target: 'ReceiveMessage',
            arguments: [invocation.args[0]]
        });
    },
    extraOutputs: [
        output.generic({
            type: 'signalR',
            name: 'broadcast',
            hubName: 'default',
            connectionStringSetting: 'AzureSignalRConnectionString'
        })
    ]
});

2. 解决传输兼容错误(Error: None of the transports supported...)

错误原因:

硬编码返回的{hubName, connectionStringSetting}不是Azure SignalR客户端期望的有效格式,客户端需要包含url和accessToken的响应,错误格式会导致客户端无法获取正确的连接端点和令牌,进而无法协商兼容的传输方式。

解决方案:

  1. 使用SignalRConnectionInfo绑定生成有效响应:如上述修正后的negotiate函数,通过绑定自动生成包含url和accessToken的正确响应,而非手动构造内容。
  2. 修正客户端配置:确保withUrl传入的是negotiate函数的完整路径(例如${functionAppUrl}/negotiate),而非根路径;若使用MSI认证,需确保Function App已被授予Azure SignalR的SignalR App Server角色权限。
  3. 检查CORS配置:确保Function App的CORS允许前端域名访问,避免跨域拦截请求。
  4. 启用详细日志:客户端开启LogLevel.Trace,查看协商过程中的详细错误,确认是传输方式被拒绝还是令牌无效。

内容的提问来源于stack exchange,提问作者Aiden Dipple

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 14:36:06