Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can middleware access/alter what was returned from the function? #340

Closed
godefroi opened this issue Mar 22, 2021 · 22 comments
Closed

Can middleware access/alter what was returned from the function? #340

godefroi opened this issue Mar 22, 2021 · 22 comments
Assignees
Labels
enhancement New feature or request

Comments

@godefroi
Copy link

I'm putting together a global exception-handling middleware, and I'd like to be able to alter what will be returned for the output binding. The example middleware only puts data into the Items collection; I'm wondering if there's an example anywhere for accessing or manipulating the output binding data?

@fabiocav
Copy link
Member

@godefroi that's the goal, but some of that information is not yet exposed in the context that flows with the invocation. We have upcoming API reviews to cover items like binding data (which will be accessible in the binding context), function activation and other items are included in that list as well.

@fabiocav fabiocav added enhancement New feature or request and removed Needs: Triage (Functions) labels Mar 22, 2021
@godefroi
Copy link
Author

Thanks, I appreciate the answer. It's looking good, and while we're clearly not at a final product yet, I like the direction.

@fabiocav
Copy link
Member

Thank you! We'll definitely be lighting a lot these things up as we go, and feedback like like helps us prioritize what should come first.

@Jooraz
Copy link

Jooraz commented Mar 24, 2021

@godefroi that's the goal, but some of that information is not yet exposed in the context that flows with the invocation. We have upcoming API reviews to cover items like binding data (which will be accessible in the binding context), function activation and other items are included in that list as well.

It's a shame that modification of output was possible in the preview4 and is not in the 1.0.0, I've prepared a code which does that in the preview. And that is rendered completely useless since it's not even a matter or rewriting it, rather than inability to perform such action at all.

Unless there's some kind of way of doing so by any chance after the middleware was called, even via (ugh) reflection? (want to solve this at least for a now) As I was not able to detect one as such.
Ok, well I was able to change the return data object on the HttpTrigger but then I was not able to change StatusCode nor Content-type on the response, which is not good enough for my needs.
I guess that complete change of the output will be possible in the future?

@josere
Copy link

josere commented Apr 1, 2021

It will be good if we can also access the input parameters from the middleware.

@fabiocav
Copy link
Member

fabiocav commented Apr 1, 2021

@josere that is part of the plan. The goal is to enable scenarios where middleware can inspect and modify input and output data associated with the context.

@jasonshsoftware
Copy link

+1 on this one! I had the same use case of global error handling, etc.

@david-peden-q2
Copy link

I was super excited to update to the new out-of-process model simply for the sake of being able to run middlewares. We largely use(d) FunctionInvocationFilterAttribute to handle cross-cutting concerns but those appear to be doa. When I started to port our filters to middlewares, I quickly realized it was impossible to do because we don't have access to the HttpRequestData (#414) nor are we able to short circuit the pipeline and terminate early.

Another use-case I was hoping to convert was the addition of a /health endpoint. Currently, we implement this by copying/pasting the same function to each function project:

[Function(nameof(Health))]
public async Task<HttpResponseData> Health
(
    [HttpTrigger(AuthorizationLevel.Anonymous, nameof(HttpMethods.Get), Route = "health")]
    HttpRequestData request
)
{
    HttpResponseData response = request.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync(new {DateTime.UtcNow});
    return response;
}

This is perfect for a middleware so we can eliminate the code redundancy but, alas, we can't terminate the pipeline.

Please prioritize gaining access to HttpRequestData and HttpResponseData asap. Middlewares are largely useless without such features.

@defunky
Copy link

defunky commented Jun 23, 2021

Has there been any movement for this, or is there any workaround for this?

@lohithgn
Copy link

@defunky there is work around to read the HttpResponseData returned from the Fx in middleware. check this out #414 (comment)

@defunky
Copy link

defunky commented Jun 23, 2021

@lohithgn that seems to be related to HttpRequestData not HttpResponseData.

Edit: although you can do a similar thing and set the InvocationResult via reflection and it seems to work. Thanks for the heads up

@rhollamby
Copy link

@lohithgn that seems to be related to HttpRequestData not HttpResponseData.

Edit: although you can do a similar thing and set the InvocationResult via reflection and it seems to work. Thanks for the heads up

Any idea how to do this? I can only see the incoming http headers, not the out going.

@defunky
Copy link

defunky commented Jun 29, 2021

@rhollamby
My use-case is a little different as I wanted to modify the response data so in the case of GlobalExceptionHandler I wanted to set the status code to be internal server error. It something like this:

// Use reflection to grab HttpRequestData
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>;
var httpreq = inputData?.Values.SingleOrDefault(o => o is HttpRequestData) as HttpRequestData;

// Create response from the request
var res = httpreq.CreateResponse(HttpStatusCode.InternalServerError);

// Use reflection to set InvocationResult
var result = type.GetProperties().Single(p => p.Name == "InvocationResult");
result.SetValue(functionBindingsFeature, res);

You can probably to similar thing but instead of setting InvocationResult you can try retrieving it with reflection if you want to see what the result was but I haven't tried it.

@BrandonSchreck
Copy link

BrandonSchreck commented Sep 13, 2021

@lohithgn that seems to be related to HttpRequestData not HttpResponseData.
Edit: although you can do a similar thing and set the InvocationResult via reflection and it seems to work. Thanks for the heads up

Any idea how to do this? I can only see the incoming http headers, not the out going.

#530 (comment)

@RomanAlberdaSoftserveInc

Is this still in progress >?

@kshyju kshyju self-assigned this Feb 14, 2022
@kshyju
Copy link
Member

kshyju commented Feb 14, 2022

Yes, we are currently working on this item. The new APIs to support reading & altering input and output data from middleware will be available in the next few weeks. I will share an update to this thread as we make progress. Thanks!

@godefroi
Copy link
Author

@kshyju So, your last comment was more than two months ago, do you have a status update?

@stephanruhland
Copy link

Couldn't you change the HttpResponse with the IFunctionFilters?

e.g. Azure/azure-webjobs-sdk#2546

@NeillCain-zz
Copy link

@stephanruhland @godefroi hope you don't mind me chiming in here...

I would personally be hesitant to use function Filters as they're marked deprecated/Not GA and also it doesn't look like they can, yet: Azure/azure-webjobs-sdk#1314

I might be speaking out of turn here as I regularly get mixed up with worker versus hosted and the sdks etc. =)

@SeanFeldman
Copy link
Contributor

SeanFeldman commented Apr 26, 2022

And, not all functions are HTTP triggered. So the semantics have to work with triggers other than HTTP.

@kshyju
Copy link
Member

kshyju commented May 5, 2022

Version 1.8.0-preview1 of Microsoft.Azure.Functions.Worker package introduces a few new APIs which can be used to read & update input binding, output binding and invocation result data. Please give it a try and let us know what you think.

public static ValueTask<InputBindingData<T>> BindInputAsync<T>(BindingMetadata bindingMetadata);
public static InvocationResult<T> GetInvocationResult<T>();;
public static InvocationResult GetInvocationResult();
public static IEnumerable<OutputBindingData<T>> GetOutputBindings<T>();

// Http trigger specific
public static ValueTask<HttpRequestData?> GetHttpRequestDataAsync();
public static HttpResponseData? GetHttpResponseData();

Usage samples:

  1. Global exception handling middleware for http functions. This example uses the API to read HttpRequestData and updating the invocation result.
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error processing invocation");

        var httpReqData = await context.GetHttpRequestDataAsync();

        if (httpReqData != null)
        {
            var newResponse = httpReqData.CreateResponse();
            await newResponse.WriteAsJsonAsync(new { Status = "Failed" });

            // Update invocation result.
             context.GetInvocationResult().Value = newResponse;
        }
    }
}
  1. To get input data inside a middleware for a specific input binding entry.
BindingMetadata blobBindingMetaData = context.FunctionDefinition
                                             .InputBindings.Values
                                             .Where(a => a.Type == "blob")
                                             .FirstOrDefault();

if (blobBindingMetaData != null)
{
    var bindingResult = await context.BindInputAsync<MyBlob>(blobBindingMetaData);
    // Update a property value. 
    // This will be reflected in the function parameter value.
    bindingResult.Value.Name = "Edited name";
   
   // or you could even replace the entire object
   bindingResult.Value = new MyBlob { Name = "Totally different object" };
}

  1. Middleware using these APIs to update HttpResponseData. Below sample code stamps an http header to the response.
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
    var logger = context.GetLogger<StampHttpHeadersMiddleware>();
    var requestId = "azf-" + Guid.NewGuid();

    using (logger.BeginScope("azfunc-requestid:{requestId}", requestId))
    {
        await next(context);
    }

    var httpRequestData = await context.GetHttpRequestDataAsync();
    if (httpRequestData != null)
    {
        var httpResponseData = context.GetHttpResponseData();
        if (httpResponseData != null)
        {
            httpResponseData.Headers.Add("x-azfunc-requestid", requestId);
        }
    }
}

Hope this helps. Let us know if you have any questions.

@beukerz
Copy link

beukerz commented May 16, 2022

If I test the first sample in an attempt to build an authroization middleware I get this response back from the HTTP trigger:

{
"method": "",
"query": {},
"statusCode": "200",
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"enableContentNegotiation": false,
"cookies": [],
"body": "eyJTdGF0dXMiOiJGYWlsZWQifQ=="
}

The HTTP status code is also 200. Trying to set the status code on the newResponse has only an affect on the body. Not on the status code of the http response.

`
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
var httpReqData = await context.GetHttpRequestDataAsync();

    if (httpReqData != null)
    {
        var newResponse = httpReqData.CreateResponse();
        await newResponse.WriteAsJsonAsync(new { Status = "Failed" });
        newResponse.StatusCode = HttpStatusCode.Unauthorized;
        
        // Update invocation result.
        context.GetInvocationResult().Value = newResponse;
        return;
    }

`

What am I doing wrong?

@ghost ghost locked as resolved and limited conversation to collaborators Jun 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests