Kzryzstof
Kzryzstof

Reputation: 8382

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

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?

Upvotes: 16

Views: 15470

Answers (3)

JakeD
JakeD

Reputation: 457

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

Upvotes: 1

Sil
Sil

Reputation: 1330

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

Upvotes: 21

Filip D. Pindej
Filip D. Pindej

Reputation: 156

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

Upvotes: 14

Related Questions