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

本地React连接Azure Function SignalR时遭遇CORS问题求助

问题描述

按照《Azure SignalR Service Serverless 快速入门》教程操作:

  • 本地运行的Azure Function托管的HTML文件可正常连接SignalR
  • Postman及浏览器能正常访问negotiate接口
    但使用TypeScript+Vite搭建的React应用尝试连接时,出现以下错误:
HttpConnection.ts:350  Uncaught (in promise) Error: Failed to complete negotiation with the server: TypeError: Failed to fetch
    at HttpConnection._getNegotiationResponse (HttpConnection.ts:350:35)
    at async HttpConnection._startInternal (HttpConnection.ts:246:41)
    at async HttpConnection.start (HttpConnection.ts:136:9)
    at async _HubConnection._startInternal (HubConnection.ts:228:9)
    at async _HubConnection._startWithStateTransitions (HubConnection.ts:202:13)

排查后确认是CORS拦截问题,即使在local.settings.json中设置CORS为*或指定React应用URL,仍出现报错:

localhost/:1  Access to fetch at 'http://localhost:7071/api/negotiate?negotiateVersion=1' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
localhost/:1  Access to fetch at 'http://localhost:7071/api/negotiate?negotiateVersion=1' from origin 'http://localhost:5123' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

咨询:本地环境下,如何正确配置CORS以解决该问题?


附相关代码

Azure Function代码(C#隔离进程)

using System;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

namespace csharp_isolated;

public class Functions
{
    private static readonly HttpClient HttpClient = new();
    private static string Etag = string.Empty;
    private static int StarCount = 0;

    [Function("index")]
    public static HttpResponseData GetHomePage([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req)
    {
        var response = req.CreateResponse(HttpStatusCode.OK);
        response.WriteString(File.ReadAllText("content/index.html"));
        response.Headers.Add("Content-Type", "text/html");
        return response;
    }

    [Function("negotiate")]
    public static HttpResponseData Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req,
        [SignalRConnectionInfoInput(HubName = "serverless")] string connectionInfo)
    {
        var response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "application/json");
        response.WriteString(connectionInfo);
        return response;
    }

    [Function("broadcast")]
    [SignalROutput(HubName = "serverless")]
    public static async Task<SignalRMessageAction> Broadcast([TimerTrigger("*/5 * * * * *")] TimerInfo timerInfo)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/azure/azure-signalr");
        request.Headers.UserAgent.ParseAdd("Serverless");
        request.Headers.Add("If-None-Match", Etag);
        var response = await HttpClient.SendAsync(request);
        if (response.Headers.Contains("Etag"))
        {
            Etag = response.Headers.GetValues("Etag").First();
        }
        if (response.StatusCode == HttpStatusCode.OK)
        {
            var result = await response.Content.ReadFromJsonAsync<GitResult>();
            if (result != null)
            {
                StarCount = result.StarCount;
            }
        }
        int randomNum = Random.Shared.Next(1, 10); // helps to visualize that the messages are going through
        return new SignalRMessageAction("newMessage", [$"Current star count of https://github.com/Azure/azure-signalr is: {StarCount + randomNum}"]);
    }

    private class GitResult
    {
        [JsonPropertyName("stargazers_count")]
        public int StarCount { get; set; }
    }
}

React应用代码(TypeScript+Vite)

import * as React from 'react';
import './App.css';
import * as signalR from "@microsoft/signalr";

const URL = "http://localhost:7071/api";
const list: string[] = [];

interface MessageProps {
  HubConnection: signalR.HubConnection
}

const Messages: React.FC<MessageProps> = (props: MessageProps) => {
  const { HubConnection } = props;
  React.useEffect(() => {
    HubConnection.on("newMessage", message => list.push(message));
  }, []);

  return <> {list.map((message, index) => <p key={index}>{message}</p>)} </>
}

const App: React.FC = () => {
  const hubConnection = new signalR.HubConnectionBuilder()
    .withUrl(URL)
    .configureLogging(signalR.LogLevel.Information)
    .build();

  hubConnection.start()
    .catch(console.error);

  return <Messages HubConnection={hubConnection} />
}

export default App;

示例HTML中的JS代码

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7/signalr.min.js"></script>
<script>
    let messages = document.querySelector('#messages');
    const apiBaseUrl = window.location.origin;
    console.log('base url:', apiBaseUrl);
    const connection = new signalR.HubConnectionBuilder()
        .withUrl(apiBaseUrl + '/api')
        .configureLogging(signalR.LogLevel.Information)
        .build();
    connection.on('newMessage', (message) => {
        document.getElementById("messages").innerHTML = message;
    });

    connection.start()
        .catch(console.error);
</script>

解决方案

1. 修正local.settings.json的CORS配置

SignalR连接默认会携带credentials,此时不能使用*作为允许的源,必须指定React应用的具体地址(包含协议和端口)。

修改local.settings.json中的CORS字段:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureSignalRConnectionString": "你的SignalR连接字符串"
  },
  "Host": {
    "LocalHttpPort": 7071,
    "CORS": "http://localhost:5173" // 替换为你的React应用实际端口,比如5123
  }
}
  • 若有多个需要允许的源,用逗号分隔(如"http://localhost:5173,http://localhost:5123")
  • 修改完成后必须重启Azure Function本地运行时,配置才会生效。

2. 确保SignalR连接的credentials设置一致

React应用中,SignalR默认启用withCredentials,可显式声明以确保配置清晰:

const hubConnection = new signalR.HubConnectionBuilder()
  .withUrl(URL, { withCredentials: true }) // 显式启用credentials,与服务器配置匹配
  .configureLogging(signalR.LogLevel.Information)
  .build();

3. 避免手动添加CORS响应头

在Azure Function的negotiate函数中,不要手动添加Access-Control-Allow-Origin等CORS相关头,Azure Function平台会根据local.settings.json的配置自动处理这些头,手动添加可能导致冲突。

4. 验证配置生效

重启Azure Function和React应用后,打开浏览器开发者工具:

  • 查看negotiate请求的响应头,确认包含:
    • Access-Control-Allow-Origin: http://localhost:5173(与你的React源一致)
    • Access-Control-Allow-Credentials: true

若仍有问题,检查是否有本地代理、防火墙或浏览器插件拦截请求,可尝试使用无痕模式测试。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 03:12:45