azure-webjobs-sdk icon indicating copy to clipboard operation
azure-webjobs-sdk copied to clipboard

Allow an IFunctionFilter to modify the HTTP status code/response

Open kzu opened this issue 4 years ago • 12 comments

IFunctionFilter (and in particular IFunctionInvocationFilter) seems to be the only mechanism by which the execution pipeline of a function can be modified in a cross-cutting way (since you can't really access the host builder to add arbitrary middleware).

As such, it would be very useful if the filters could affect the resulting status code of a request (say in an HTTP-triggered function).

A concrete example would be checking for certain headers or access token claims, to determine if the caller has the right permissions for the invocation, and return a 401 or 403 as appropriate.

Currently, the only way to stop/abort the processing of the current request is to throw an exception from the filter, but this results in a fixed 500 error reported to the client.

Potential solutions

  • Special-case a new HttpFunctionException which can provide a status code and optional status string/message, and in this case it would not be reported as a function failure.
  • Extend the FunctionExecutingContext to allow the filter to signal whether further processing should happen or if it should be stopped (and with that HTTP status code result)

Potential issues

It's obvious that not all function invocations are HTTP-triggered, so maybe this should only be applied to those. There would need to be a mechanism in the filter to check the type of function being invoked. Currently the FunctionFilterContext is not sufficiently expressive to accomodate general-purpose pipelines that can act differently depending on the current invocation features/metadata/descriptor. Perhaps the IFunctionExecutionFeature should be exposed somehow? Maybe just the FunctionDescriptor?

kzu avatar Aug 02 '20 21:08 kzu

@mhoeger should issues related to APIs implemented in the webjobs-sdk be reported in that repo? (such as this one: https://github.com/Azure/azure-webjobs-sdk/issues/1314)

kzu avatar Aug 04 '20 21:08 kzu

@mathewc / @fabiocav / @paulbatum - This is a request similar to https://github.com/Azure/azure-functions-host/issues/4927 and others on adding some sort of filters to simplify logic for cross-cutting concerns.

Marking as Triaged and as a feature

mhoeger avatar Aug 04 '20 22:08 mhoeger

You could inject IHttpContextAccessor into your IFunctionFilter implementation. The property HttpContext should allow you to modify the response. Kind regards

stephanruhland avatar Oct 06 '20 15:10 stephanruhland

@stephanruhland but then how would u cancel the execution of the function ina way that wouldn't override the response as throwing an exception makes it a 500

drdamour avatar Oct 08 '20 17:10 drdamour

  1. Implement a custom exception, like for example an AbortRequestException.
  2. Check your custom logic in the IFunctionInvocationFilter or IFunctionInvocationAttribute implementation and throw your abort exception.
  3. In the implementation of the interface IFunctionExceptionFilter it can be checked if it is a Http function (httpContextAccessor.HttpContext != null) that is the source of the exception and if it is your abort exception. If yes, you can modify the response.

In that example the main part of your function should not be executed. Its just an idea :)

Kind regards

stephanruhland avatar Oct 08 '20 20:10 stephanruhland

@stephanruhland , I would very much like to use your idea. However, isn't IFunctionInvocationFilter obsolete? I am running Azure Functions 3 and when I go to implement it, visual studio let's me know that is obsolete.

chrismcclure avatar Dec 16 '20 14:12 chrismcclure

Yes, its marked as obsolete but i think they want to update and remove the obsolete attribute. It says only "preview" and not outdated code. Im dont know another "better" solution for now, so that was my way to go.

[System.Obsolete("Filters is in preview and there may be breaking changes in this area.")]
public interface IFunctionInvocationFilter : Microsoft.Azure.WebJobs.Host.IFunctionFilter

stephanruhland avatar Dec 16 '20 15:12 stephanruhland

  1. Implement a custom exception, like for example an AbortRequestException.
  2. Check your custom logic in the IFunctionInvocationFilter or IFunctionInvocationAttribute implementation and throw your abort exception.
  3. In the implementation of the interface IFunctionExceptionFilter it can be checked if it is a Http function (httpContextAccessor.HttpContext != null) that is the source of the exception and if it is your abort exception. If yes, you can modify the response.

In that example the main part of your function should not be executed. Its just an idea :)

Kind regards

Can you suggest how can we abort the exception in IFunctionExceptionFilter?

litex1982 avatar Dec 23 '20 06:12 litex1982

Can you suggest how can we abort the exception in IFunctionExceptionFilter?

I realise that this is now almost 6 months later @litex1982 but I myself was struggling to work out how to essentially do the same thing. We were using a custom HTTP header [I'll spare you the details as to why!] and needed a way to short circuit out of all function invocations in an x-cutting way. This example is what @stephanruhland suggested. However, we will not be shipping this to prod as we've found a workaround and weren't entirely comfortable relying on the function filters as they're obsolete/preview status and have been for a couple of years. If this helps someone, then great!

public abstract class FunctionBase : IFunctionExceptionFilter, IFunctionInvocationFilter
    {
        private readonly IHttpContextAccessor httpContextAccessor;

        public FunctionBase(IHttpContextAccessor httpContextAccessor)
        {
            this.httpContextAccessor = httpContextAccessor;
        }

        public const string HeaderName = "<custom header name>";

        public async Task OnExceptionAsync(FunctionExceptionContext exceptionContext, CancellationToken cancellationToken)
        {            
            if (exceptionContext.Exception.InnerException is RequestAbortException)
            {
                var ex = exceptionContext.Exception.InnerException as RequestAbortException;
                httpContextAccessor.HttpContext.Response.StatusCode = (int) ex.StatusCode;
                httpContextAccessor.HttpContext.Response.Headers.Append("MyHeader", "Foo");
                await httpContextAccessor.HttpContext.Response.WriteAsync("Not Authorised");                
            }                        
        }

        public Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancellationToken)
        {            
            return Task.CompletedTask;
        }

        public Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken)
        {                        
            var req = executingContext.Arguments.ContainsKey("req") ? (HttpRequest) executingContext.Arguments["req"] : null;

            if(null != req)
            {
                var customHeader = req.Headers[HeaderName].FirstOrDefault();

                if (customHeader == null)
                    throw new RequestAbortException(HttpStatusCode.Unauthorized);                                
            }

            return Task.CompletedTask;
        }
    }

NeillCain-zz avatar Jun 16 '21 13:06 NeillCain-zz

Bump. I can't seem to find any solution for this short of having a catch block return from each of my many functions, which is a LOT of extra syntax for this basic need.

JasonKleban avatar Oct 16 '21 19:10 JasonKleban

@NeillCain care to share your workaround please?

maimar-sw avatar Jan 27 '22 17:01 maimar-sw

@maimar-sw for our workaround, we defined the x-cutters we needed in Azure APIM policies, which we recently decided to change to leverage Middleware. We've still had issues, but it's more to do with how Middleware currently works and we can see the team is looking to address the problems.

NeillCain-zz avatar Mar 23 '22 15:03 NeillCain-zz