Following along with the Azure SignalR Service Serverless Quick Start tutorial I was able to run the hosted html file just fine while also pinging the negotiate function from postman or a browser works as well. The problem I'm having is getting a React JS application to do the exact same thing. Instead of connecting to SignalR like the html's JS block, I get this error:
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)
I understand that the fetch is failing, but I don't understand why. There shouldn't be a CORS problem since Postman and the Browser can access it just fine, however, I'm at a loss for what else could be wrong since the example index.html file works fine, but not the React one.
Here's the Azure Function:
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; }
}
}
and here is the React app (using 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;
What really confuses me is how the JS in the html file works just fine:
<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>
but what looks like essentially the same thing in React is DOA due to a fetch failure from the SignalR JS.
The only thing I can figure is that there is in fact a CORS problem from running the react app on localhost:5173 and the Azure Function on localhost:7071, but the browser and Postman can each hit the negotiate route without any access problems, it's just the react app that fails.
network tab output from React page:
Request URL: http://localhost:7071/api/negotiate?negotiateVersion=1
Referrer Policy: strict-origin-when-cross-origin
-- Edit --
I should also point out that the local.settings.json file contains:
{
...
"Host": {
"LocalHttpPort": 7071,
"CORS": "*",
"CORSCredentials": false
},
...
}
Ok, so it is a CORS issue, now I'm seeing:
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'.
Also tried to specify the urls in the host settings to address the above * comment:
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.
Even with host set to * or specific URLs CORS is still saying no. How exactly is one supposed to set CORS for a local setup when local.settings.json is not enough?
This Worked for me.
I created a SignalR Function for negotiation.
Function1.cs
:
using System.Collections.Generic;
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace FunctionApp7
{
public class Function
{
private readonly ILogger _logger;
public Function(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger("negotiate");
}
[Function("negotiate")]
public async Task<HttpResponseData> Negotiate(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req,
[SignalRConnectionInfoInput(HubName = "serverless")] MyConnectionInfo connectionInfo)
{
_logger.LogInformation($"SignalR Connection URL = '{connectionInfo.Url}'");
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
response.WriteStringAsync($"Connection URL = '{connectionInfo.Url}'");
return response;
}
}
public class MyConnectionInfo
{
public string Url { get; set; }
public string AccessToken { get; set; }
}
}
I created a Vite React app using command npm create vite@Latest react_app
used typescript
template.
And i added code in my App.tsx
file
App.tsx
:
import { useState, useEffect, useRef } from 'react';
import { HubConnectionBuilder } from '@microsoft/signalr';
import reactLogo from './assets/react.svg';
import viteLogo from "./vite.svg";
import './App.css';
function App() {
const [count, setCount] = useState(0);
const hubConnection = useRef<any>(null); // Ref to hold the SignalR connection instance
useEffect(() => {
const startSignalRConnection = async () => {
try {
const hubUrl = 'http://localhost:7146/api/negotiate';
const response = await fetch(hubUrl, { method: 'POST' });
const connectionInfo = await response.json();
const connection = new HubConnectionBuilder()
.withUrl(connectionInfo.url, {
accessTokenFactory: () => connectionInfo.accessToken,
})
.withAutomaticReconnect()
.build();
connection.start();
connection.on('someEvent', (data) => {
console.log('Received data from SignalR:', data);
});
hubConnection.current = connection; // Assign the connection instance to the ref
} catch (error) {
console.error('Error setting up SignalR connection:', error);
}
};
startSignalRConnection();
return () => {
if (hubConnection.current) {
hubConnection.current.off('someEvent'); // Remove event listener
hubConnection.current.stop(); // Stop the SignalR connection
}
};
}, []);
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
);
}
export default App;
OUTPUT
: