
how to handle errors and prevent exposure of information when using outputbinding in .net isolated azure functions
The solution in this blogpost serves to goals: handle error situations when using azure functions output bindings and prevent the exposure of to much information. In this specific situation a Azure function was created to send a SignalR message to a specific userspecific user. When posting a message to '/api/SendMessage/
The default example
The example below accepts an empty POST to api/SendMessage/<userid>. It creates a SignalRMessage for the specific userid and sends this to the configured signalR service. As this is also a HTTPTrigger input binding, the output is also send back to the calling system.
[Function("SendMessage")]
[SignalROutput(HubName = "chat", ConnectionStringSetting = "SignalRConnection")]
    {
public static SignalRMessageAction SendMessage([HttpTrigger(AuthorizationLevel.Function, "post", Route = "SendMessage/{userid}" )] HttpRequestData req, string userid)
{    
    return new SignalRMessageAction("newMessage")
    {
        Arguments = new[] { bodyReader.ReadToEnd() },
        UserId = userid,
    };
}
        This causes two issues:
- every kind of userid is accepted, and thus being send to signalR:
- information about the SignalR connection is being exposed
userid issues
See the request and response below:
### send malformed request
POST http://localhost:7072/api/SendMessage/*$(%&@#_)$#%&#$&# HTTP/1.1
content-type: application/json
        HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain; charset=utf-8
Date: Wed, 03 May 2023 14:38:13 GMT
Server: Kestrel
Transfer-Encoding: chunked
{
  "ConnectionId": null,
  "UserId": "*$(%&@",
  "GroupName": null,
  "Target": "newNba",
  "Arguments": [
    ""
  ],
  "Endpoints": null
}
        There is an obious malformed userid input. This should always be prevented, from a security perspective, but also from a costs perspective: every signalR request may lead to additional costs, so these should be filtered. Using a very rudimentary validation, for example a regular expression, will enable the userid validation. The SignalRMessageAction is required for the SignalROutput, and it could be made <nullable> When the same request will be sent, the output is as follows:
HTTP/1.1 204 No Content
Content-Length: 0
Connection: close
Date: Wed, 03 May 2023 14:49:24 GMT
Server: Kestrel
        But this doesn't solve the issue of exposing to much information:
### send correct request
POST http://localhost:7072/api/SendMessage/henk HTTP/1.1
content-type: application/json
        HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain; charset=utf-8
Date: Wed, 03 May 2023 14:51:10 GMT
Server: Kestrel
Transfer-Encoding: chunked
{
  "ConnectionId": "connection-id-information",
  "UserId": "henk",
  "GroupName": null,
  "Target": "newNba",
  "Arguments": [
    ""
  ],
  "Endpoints": null
}
        This happens due to the behaviour of the output binding, in combination with the HttpTrigger input binding. When there is a HTTPTriggerBinding, the output is automatically send as HttpResponseData.
The solution
This can be solved by using a custom ReturnObject and moving the Outputbinding into this object. Take note of the SignalROutput binding attribute above the SignalRMessageAction. This Action has been made nullable, for the case where the userid is invalid and SignalR shouldn't be triggered.
namespace Models 
{
    public class ReturnObject {
        public HttpResponseData response {get;set;}
        
        [SignalROutput(HubName = "serverless", ConnectionStringSetting = "AzureSignalRConnectionString")]
        public SignalRMessageAction? message {get;set;}
    }
}
        This enables us to write 'proper' validation within the actual function and send our own custom HttpResponse, based on the outcome of the validation:
// rudimentary validation on userid
private static readonly Regex regex = new Regex("^[a-zA-Z0-9-]*$"); 
    [Function("SendMessage")]    
    public static ReturnObject SendMessage([HttpTrigger(AuthorizationLevel.Function, "post", Route = "SendMessage/{userid}" )] HttpRequestData req, string userid)
    {        
        // validate userid
        var result = regex.IsMatch(userid);         
        var returnObject = new ReturnObject();
        if(!result)
            returnObject.response = req.CreateResponse(HttpStatusCode.BadRequest);
        else
        {
            returnObject.response = req.CreateResponse(HttpStatusCode.Accepted);
            returnObject.message = new SignalRMessageAction("newNba")
            {
                Arguments = new[] {string.Empty},
                UserId = userid
            };
        }
        return returnObject;        
    }
        This leads to the following responses. The Http Response code has been modfied (202/400 vs the default 200/201 code) and the body doesn't contain any information anymore.
HTTP/1.1 202 Accepted
Content-Length: 0
Connection: close
Date: Wed, 03 May 2023 15:08:16 GMT
Server: Kestrel
HTTP/1.1 400 Bad Request
Content-Length: 0
Connection: close
Date: Wed, 03 May 2023 15:08:38 GMT
Server: Kestrel
        Summary
As I was new to isolated Azure Functions, Figuring out how to return a proper, custom HttpResponse and do some neat validation, costed me a bit of my time to figure out. It turned out that creating a custom ReturnObject which can be used with the Output bindings solved my issue.