How to get Angular SignalR hub connection to Azure Typescript Function with version 4.0 programming model

418 views Asked by At

I’m having an issue getting my Angular 12 front end connected to my Azure SignalR and Azure Functions (server less Typescript) backend. I have negotiate() firing but erroring back in the client, and haven’t gotten upstream triggering (posts) working.

I have a two part question:

  1. What is wrong with my Azure Function version 4 programming model binding syntax?
  2. Given my negotiate() function returns hard coded values (see 'The Preliminary Stuff - Client Side' and signalRConnectionInfo below) how do I solve the angular 'Error: None of the transports supported by the client are supported by the server.' when connecting?

TLDR: The negotiate() function firing works with no additional input bindings, but returns an error to the Angular call to this.hubConnection.start().then().catch() method, e.g.: 'Error: None of the transports supported by the client are supported by the server.'.

When I do try to add the input bindings formatted as described below the Function crashes returning status 500 before it starts execution.

The following is the walkthrough of what I found, and how I got to the binding I’m using at present. I outline all of this because I have not seen anywhere how to do this. In fact, according to Microsoft, they don’t even have examples on how to do this yet: “Example binding for Node.js model v4 not yet available.”.

enter image description here

To find that disclosure by Microsoft, select Programming Model version 4 (it’s a tab you can select halfway down the page). See https://learn.microsoft.com/en-us/azure/azure-functions/add-bindings-existing-function?tabs=python-v2%2Cisolated-process%2Cnode-v4&pivots=programming-language-typescript

My stack: Serverless Azure Function (Typescript) with version 4.0 programming model, Azure SignalR Free-tier Serverless. Angular 12 front end severed on Azure Static Web App (with custom domain).

The Preliminary Stuff - Client Side

My angular app is calling my Azure Function root level (e.g.: https://<FUNCTION-URL>/api/) in the construction of the HubConnectionBuilder() (see @microsoft/signalr doco). I’m passing the hard coded options of endpoint and accessKey (and authType: ‘azure.msi;Version=1.0;’) in the same way as shown in this example: See https://medium.com/medialesson/serverless-real-time-messaging-with-azure-functions-and-azure-signalr-service-c70e781ff3c3

In the above article (Step 3), note the format of the SignalRConnectionInfo object being passed as the options parameter. As mentioned above, I’ve tried it with both the accessKey syntax and my own guess at using an authType parameter to try and get Server Managed Identity working. NOTE: Yes I intend to have a secure Azure Function where I can request this prior to constructing the HubConnectionBuilder object, but for now that is unnecessary detail, and does not work being the negotiate() input binding issue.

This gives me something in my Angular code that looks like this (in class constructor):

    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)
    });

The Preliminary Stuff - Server Side

The server side needs an Azure Function called negotiate() that has the extra binding of signalRConnectionInfo and returns the connection string. This second binding being the topic of the part 1) question. To address the question I looked for examples of version 4.0 programming model binding to understand how binding works and how we might game this out, in the absence of quality tutorials and examples. The following are the articles I’ve found that describe how they might bind.

  1. I started with version 3 examples of the negotiate() function’s bindings (these examples are the old style per the other ‘answer’ and I add the here for prosperity of the full narrative). The example shows we need a signalRConnectionInfo input binding as mentioned above and the values I’m going to try and replicate using version 4 programming model. See https://medium.com/holisticon-consultants/serverless-real-time-application-with-azure-signalr-5300e46a8b4b

  2. We note the version 4 programming model of Azure Functions is different, and this article touches on how you might expect to pass those bindings now that we don’t have a functions.json file to play with. Particularly note section "Extra inputs and outputs“ and also not the import statement bringing in input and output. Also note the example shows output.storageBlob. When I try this, Visual Studio Code provides autocomplete for storageBlob, but if I try output.signalr there is nothing! Make sure you read the part “Generic inputs and outputs ” also (because I thought this might be needed per the next point). See https://learn.microsoft.com/en-au/azure/azure-functions/functions-reference-node?tabs=typescript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#supported-versions

  3. This link shows the Azure Functions version 4 programming model extensions. As you can see, SignalR is included (as is StorageBlob) and I would assume available Visual Studio Code autocomplete feature. However, SignalR is NOT found, so I might be (probably am) wrong on this. I’m open to input on why my assumption that SignalR should autocomplete is wrong. See https://github.com/Azure/azure-functions-extension-bundles/blob/v4.x/src/Microsoft.Azure.Functions.ExtensionBundle/extensions.json

  4. The following article seems to imply that SignalR is compatible with the new version 4 binding extensibility model. See https://learn.microsoft.com/en-au/azure/azure-functions/functions-versions?tabs=isolated-process%2Cv4&pivots=programming-language-typescript

The Hackathon

Given the inputs and outputs for the additional bindings per point number 2) above, I was hoping I could bind configuration input data for the negotiate() function like this:

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',
        // "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]
});

To try and have a signalR trigger an upstream Azure Function, I was hoping I could trigger bind the Azure Function like this: See https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-signalr-service-trigger?tabs=isolated-process&pivots=programming-language-javascript

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]
});

Has anyone had success using the Azure Function version 4.0 model with binding specifically to Azure SignalR (server-less)? Please help.

2

There are 2 answers

0
Aiden Dipple On

OK, I've solved part 1 and am sharing how I got v4 Programming Model binding working using generics. I'll add to this when I solve for the 'Error: None of the transports supported by the client are supported by the server.' error.

NOTE: Even though these bindings work and the values are available within the Function, the output binding appears to be sending a POST request to SignalR with a Bearer Token. The input appears to work (I'm not getting failed authentication errors in my Angular code) when I access options in the negotiate() function:

context.log('************* signalR context', context.options.extraInputs);

But this Microsoft site states that options from http.output() doesn't work and not sure what that means - per above the output binding itself seems to work. See https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-output?tabs=isolated-process%2Cnodejs-v4&pivots=programming-language-typescript

enter image description here

If anyone can help solve that part 2 question I'm happy to still give you the points!

Binding for the signalRTrigger Azure Function:

/* 

V3 SYNTAX BEING CONVERTED TO V4 MODEL

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

The new code:

import { app, HttpRequest, output, trigger, HttpResponseInit, InvocationContext } from '@azure/functions';

export async function signalRTrigger(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {

    // **** - Add your signalR Azure Function code here

};

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


app.generic('signalr-trigger', {
    trigger: signalTriggerConfig,
    return: output.generic({
        type: 'http'
    }),
    handler: signalRTrigger
});

For the input and output binding make sure you have your local.settings.json and/or Azure Function configurations present or you'll get a 500 response error.

For the input binding:

/* 

V3 SYNTAX BEING CONVERTED TO V4 MODEL

{
  "type": "signalRConnectionInfo",
  "name": "connectionInfo",
  "hubName": "default",
  "connectionStringSetting": "AzureSignalRConnectionString",
  "direction": "in"
}
 */

I'm using the following code:

import { app, HttpRequest, input, HttpResponseInit, InvocationContext } from '@azure/functions';

export async function negotiate(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {

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

};

const signalRInput = input.generic({
    type: 'signalRConnectionInfo',
    name: 'connectionInfo',
    hubName: 'default',
})

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

For the output binding:

/* 

V3 SYNTAX BEING CONVERTED TO V4 MODEL

{
  "type": "signalR",
  "name": "signalRMessages",
  "hubName": "default",
  "connectionStringSetting": "AzureSignalRConnectionString",
  "direction": "out"
}
 */

Inspiration from here: https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-table-output?tabs=isolated-process%2Cnodejs-v4%2Ctable-api&pivots=programming-language-typescript

I'm using the following code:

import { app, HttpRequest, output, HttpResponseInit, InvocationContext } from '@azure/functions';

export async function signalRNotify(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {


    const message = {
        target: 'newMessage',
        arguments: ['This is Aiden calling.']
    };

    context.extraOutputs.set(signalrOutput, message);
    return { status: 201 };

};

// This should provide the signalR upstream target to trigge
const signalrOutput = output.generic({
    type: 'signalR',
    name: 'signalRMessages',
    hubName: 'default',
    connectionStringSetting: "AzureSignalRConnectionString"
});


app.http('signalr-notify', {
    methods: ['POST', 'GET'],
    authLevel: 'anonymous',
    handler: signalRNotify,
    extraOutputs: [signalrOutput]
});
3
Eliel Aguilera On

have you been able to solve this issue? I was able to solve it by using some inspiration from here. As it state, the answer from the negotiate event should be one of three options, and as the Azure Function is just use to redirect the client to the actual hub, so I send back { url: <ENDPOINT>/client/?hub=<HUBNAME>, accessToken: <ACCESS-TOKEN> }. The accessToken can be created using (from the same article in the section Using ConnectionString:

  let endpoint = /Endpoint=(.*?);/.exec(connectionString)[1];
  let accessKey = /AccessKey=(.*?);/.exec(connectionString)[1];
  let url = `${endpoint}/client/?hub=${hub}`;
  let token = jwt.sign({ aud: url }, accessKey, { expiresIn: 3600 });
  return ({ url: url, accessToken: token });