本地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
相关产品推荐
相关产品推荐

