Azure Function Middleware: How to return a custom HTTP response?
Asked Answered
P

3

13

I am exploring Azure Function running on .net 5 and I found out about the new middleware capabilities.

I have built a dummy middleware like this one:

public sealed class ExceptionLoggingMiddleware : IFunctionsWorkerMiddleware
{
    private readonly ILogger<ExceptionLoggingMiddleware> m_logger;

    public ExceptionLoggingMiddleware(ILogger<ExceptionLoggingMiddleware> logger)
    {
        m_logger = logger;
    }

    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception unhandledException)
        {
            m_logger.LogCritical(unhandledException, "Unhandled exception caught: {UnhandledException}", unhandledException.Message);
        }
    }
}

In my use case, the Azure Function is an HTTP triggered function:

public sealed class StorageAccountsFunction
{
    private readonly ILogger<StorageAccountsFunction> m_logger;

    public StorageAccountsFunction
    (
        ILogger<StorageAccountsFunction> logger
    )
    {
        m_logger = logger;
    }

    [Function("v1-post-storage-account")]
    public async Task<HttpResponseData> CreateAsync
    (
        [HttpTrigger(AuthorizationLevel.Anonymous, "POST", Route = "v1/storage-accounts")] 
        HttpRequestData httpRequestData, 
        FunctionContext context
    )
    {
        m_logger.LogInformation("Processing a request to create a new storage account");

        throw new Exception("Oh no! Oh well..");
    }
}

In my Function App running in-process on .net core 3.1, each Function had the responsibility of catching the unhandled exception (via a base class) and returned the appropriate HTTP status code.

I would like to have that logic sit in a middleware instead to have it centralized and avoid any future mistakes.

Question

The exception is caught by the middleware properly. However, I do not see how I can alter the response and return something more appropriate, instead of a 500 Internal Server Error that I get right now?

Pah answered 12/7, 2021 at 15:27 Comment(0)
B
14

According to this issue, there is currently no official implementation regarding this, but they also mention a "hacky workaround" until the proper functionality is implemented directly into Azure functions

We created an extension method for FunctionContext:

internal static class FunctionUtilities
{
    internal static HttpRequestData GetHttpRequestData(this FunctionContext context)
    {
        var keyValuePair = context.Features.SingleOrDefault(f => f.Key.Name == "IFunctionBindingsFeature");
        var functionBindingsFeature = keyValuePair.Value;
        var type = functionBindingsFeature.GetType();
        var inputData = type.GetProperties().Single(p => p.Name == "InputData").GetValue(functionBindingsFeature) as IReadOnlyDictionary<string, object>;
        return inputData?.Values.SingleOrDefault(o => o is HttpRequestData) as HttpRequestData;
    }

    internal static void InvokeResult(this FunctionContext context, HttpResponseData response)
    {
        var keyValuePair = context.Features.SingleOrDefault(f => f.Key.Name == "IFunctionBindingsFeature");
        var functionBindingsFeature = keyValuePair.Value;
        var type = functionBindingsFeature.GetType();
        var result = type.GetProperties().Single(p => p.Name == "InvocationResult");
        result.SetValue(functionBindingsFeature, response);
    }
}

The usage in the middleware looks like this:

public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
   try
   {
       await next(context);
   }
   catch (Exception ex)
   {
       if (ex.InnerException is *NameOfExceptionYouNeed* e)
       {
           var req = context.GetHttpRequestData();
           var res = await req.ErrorResponseAsync(e.Message);
           context.InvokeResult(res);
           return;
       }

       throw;
   }
}
Burial answered 9/8, 2021 at 21:1 Comment(0)
C
20

This is natively supported now as of version 1.8.0 of Microsoft.Azure.Functions.Worker.

The FunctionContextHttpRequestExtensions class was introduced so now you can just

using Microsoft.Azure.Functions.Worker;


public class MyMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        // To access the RequestData
        var req = await context.GetHttpRequestDataAsync();

        // To set the ResponseData
        var res = req!.CreateResponse();
        await res.WriteStringAsync("Please login first", HttpStatusCode.Unauthorized);
        context.GetInvocationResult().Value = res;
    }
}

Cytotaxonomy answered 22/7, 2022 at 12:18 Comment(5)
I did notice that if you are using the WriteAsJsonAsync method that you need to specify the status code as the 2nd argument as in: await res.WriteAsJsonAsync(myMessageClass, HttpStatusCode.Unauthorized); otherwise, it always sends back 200 Ok even if you set the status code on the response class in previous lines as shown above.Skippet
That is a bug. You can track is here: github.com/Azure/azure-functions-dotnet-worker/issues/776Foreconscious
See for a working sample the exception handling middleware sample of Microsoft: github.com/Azure/azure-functions-dotnet-worker/blob/main/…Foreconscious
This works. But likes to know why we have to get request and create response. Why not directly invoke GetHttpResponseData method from FunctionContextHttpRequestExtensions?Locarno
GetHttpResponseData gives you the opportunity to modify the response of the actual function invocation (like adding headers). The example above illustrates how to return a custom HTTP response from a middleware, which requires creating the custom response in order to return it.Cytotaxonomy
B
14

According to this issue, there is currently no official implementation regarding this, but they also mention a "hacky workaround" until the proper functionality is implemented directly into Azure functions

We created an extension method for FunctionContext:

internal static class FunctionUtilities
{
    internal static HttpRequestData GetHttpRequestData(this FunctionContext context)
    {
        var keyValuePair = context.Features.SingleOrDefault(f => f.Key.Name == "IFunctionBindingsFeature");
        var functionBindingsFeature = keyValuePair.Value;
        var type = functionBindingsFeature.GetType();
        var inputData = type.GetProperties().Single(p => p.Name == "InputData").GetValue(functionBindingsFeature) as IReadOnlyDictionary<string, object>;
        return inputData?.Values.SingleOrDefault(o => o is HttpRequestData) as HttpRequestData;
    }

    internal static void InvokeResult(this FunctionContext context, HttpResponseData response)
    {
        var keyValuePair = context.Features.SingleOrDefault(f => f.Key.Name == "IFunctionBindingsFeature");
        var functionBindingsFeature = keyValuePair.Value;
        var type = functionBindingsFeature.GetType();
        var result = type.GetProperties().Single(p => p.Name == "InvocationResult");
        result.SetValue(functionBindingsFeature, response);
    }
}

The usage in the middleware looks like this:

public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
   try
   {
       await next(context);
   }
   catch (Exception ex)
   {
       if (ex.InnerException is *NameOfExceptionYouNeed* e)
       {
           var req = context.GetHttpRequestData();
           var res = await req.ErrorResponseAsync(e.Message);
           context.InvokeResult(res);
           return;
       }

       throw;
   }
}
Burial answered 9/8, 2021 at 21:1 Comment(0)
D
1

This code works for me. It is based on the example here: https://github.com/Azure/azure-functions-dotnet-worker/blob/main/samples/CustomMiddleware/ExceptionHandlingMiddleware.cs

public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        // Simple example which always fails. Use the following in an error condition
        var httpReqData = await context.GetHttpRequestDataAsync();
        if (httpReqData != null)
        {
            var newHttpResponse = httpReqData.CreateResponse(HttpStatusCode.InternalServerError);
            await newHttpResponse.WriteAsJsonAsync(new { ResponseStatus = "Invocation failed!" }, newHttpResponse.StatusCode);
            context.GetInvocationResult().Value = newHttpResponse;
        }
    }
Duodenary answered 6/9, 2022 at 19:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.