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:I created a Vite React app using command
npm create vite@Latest react_appusedtypescripttemplate.And i added code in my
App.tsxfileApp.tsx:OUTPUT: