Azure-functions-host: Returning a AcceptedAtRouteResult throws "System.Private.CoreLib: Index was out of range." exception

Created on 5 Apr 2019  路  8Comments  路  Source: Azure/azure-functions-host

A customer created a question on Stack Overflow that trying to return an AcceptedAtRouteResult from their function was throwing a "Index was out of range" exception in System.Private.CoreLib.

The code they provided does reproduce the issue on my local and in Azure.

You can pull the metrics from:

  • Timestamp: 2019-04-04 21:13:10.025
  • Function App version (1.0 or 2.0): 2.0, Microsoft.NET.Sdk.Functions 1.0.26
  • Function App name: sasoucyfunctionapp
  • Function name(s) (as appropriate): AcceptedAtRouteResult
  • Invocation ID: eff6a9ac-17e4-4dd5-a3bf-e5615fab45a0
  • Region: East US 2

Expected behavior

Function should return 200 status with the set parameters

Actual behavior

Function completes successfully but then returns a 500 error

Provide any related information

[FunctionName("AcceptedAtRouteResult")]
public static IActionResult AcceptedAtRouteResult(
    [HttpTrigger("GET")]HttpRequest req)
{
    // read query parameter if present else set to defualt value

   var rs = new AcceptedAtRouteResult(
        "acceptedatrouteresult", 
        new { someParameter = "value" }, 
        new { Result = "1" });

    return rs;
}

bug

Most helpful comment

@pragnagopa any chance we can get the PR for this pulled in?

All 8 comments

Thanks for reporting and providing a repro. Will look into this.

FWIW doing this unblocks:

class AcceptedObjectResult : ObjectResult
{
    private readonly string _location;

    public AcceptedObjectResult(string location, object value) : base(value)
    {
        _location = location;
    }

    public override Task ExecuteResultAsync(ActionContext context)
    {
        context.HttpContext.Response.StatusCode = 202;
        var uri = new UriBuilder(context.HttpContext.Request.Scheme, context.HttpContext.Request.Host.Host)
        {
            Path = $@"api/{_location}",
        };
        if (context.HttpContext.Request.Host.Port.HasValue)
        {
            uri.Port = context.HttpContext.Request.Host.Port.Value;
        }

        context.HttpContext.Response.Headers.Add(@"Location", uri.ToString());

        return base.ExecuteResultAsync(context);
    }
}

and using it like:

            return new AcceptedObjectResult(@"OtherFunction", new { foo = "bar" });

Created also has similar bug so here is the Created version of it.

public class CreatedObjectResult : ObjectResult
    {
        private readonly string _location;
        private readonly string _id;

        /// <summary>
        /// Initializes a new instance of the <see cref="CreatedObjectResult"/> class.
        /// </summary>
        /// <param name="location">Route location.</param>
        /// <param name="id">Id of the resource.</param>
        /// <param name="value">Object to return.</param>
        public CreatedObjectResult(string location, string id, object value)
            : base(value)
        {
            _location = location;
            _id = id;
        }

        /// <inheritdoc/>
        public override Task ExecuteResultAsync(ActionContext context)
        {
            context.HttpContext.Response.StatusCode = (int) HttpStatusCode.Created;
            var uri = new UriBuilder(context.HttpContext.Request.Scheme, context.HttpContext.Request.Host.Host)
            {
                Path = $@"api/{_location}/{_id}",
            };
            if (context.HttpContext.Request.Host.Port.HasValue)
            {
                uri.Port = context.HttpContext.Request.Host.Port.Value;
            }

            context.HttpContext.Response.Headers.Add(@"Location", uri.ToString());

            return base.ExecuteResultAsync(context);
        }
    }

The same seems to be the case for CreatedAtRouteResult.

Can you kindly confirm?

The same seems to be the case for CreatedAtRouteResult.

Can you kindly confirm?

yes see the reply directly above yours, @madsnb

RCA:

In AcceptedAtRouteResult, the OnFormatting overload uses UrlHelper to build the URL to set to the Location Header:
https://github.com/aspnet/AspNetCore/blob/7ab32c8411f40e63984c5963b79722c1f2fd9d8a/src/Mvc/Mvc.Core/src/AcceptedAtRouteResult.cs#L84

In .Net 2.2, the UrlHelper.Link() method uses the .Router property to get the VirtualPath for the location specified:
https://github.com/aspnet/AspNetCore/blob/7ab32c8411f40e63984c5963b79722c1f2fd9d8a/src/Mvc/Mvc.Core/src/Routing/UrlHelper.cs#L80

Unfortunately, in .Net 2.2, this Router property just hard-refs [0] on ActionContext's RouteData:
https://github.com/aspnet/AspNetCore/blob/7ab32c8411f40e63984c5963b79722c1f2fd9d8a/src/Mvc/Mvc.Core/src/Routing/UrlHelper.cs#L35

This is where our index out of range exception is coming from.

Interestingly enough, in .Net 3.0, we get a nicer exception for this scenario:
https://github.com/aspnet/AspNetCore/blob/0d37794d1880b779246e8456d6af342ca3f3814f/src/Mvc/Mvc.Core/src/Routing/UrlHelper.cs#L42

namely:

Could not find an IRouter associated with the ActionContext. If your application is using endpoint routing then you can get a IUrlHelperFactory with dependency injection and use it to create a UrlHelper, or use Microsoft.AspNetCore.Routing.LinkGenerator.

Unfortunately, that's exactly what AcceptedAtRouteResult does:

var urlHelper = UrlHelper;
if (urlHelper == null)
{
    var services = context.HttpContext.RequestServices;
    urlHelper = services.GetRequiredService<IUrlHelperFactory>().GetUrlHelper(context);
}

so I'm not entirely sure what to do to properly solve this. I think it has something to do with the HttpContext in which a Function is executing. I like my re-inherited fix for the problem better than messing w/ the func host or having to do some initial service registration/dependency injection to get the ActionContext's RouteData.Routers collection to be populated correctly.

Going to keep digging in to see what a "proper" solution might look like.

cc @brettsam

More RCA:

Problem is this line of FunctionInvocationMiddleware.cs where we are using new RouteData() when creating the ActionResult instead of passing the context's RouteData along:

ActionContext actionContext = new ActionContext(context, new RouteData(), new ActionDescriptor());
await result.ExecuteResultAsync(actionContext);

I believe the correct approach should be:

ActionContext actionContext = new ActionContext(context, context.GetRouteData(), new ActionDescriptor());
await result.ExecuteResultAsync(actionContext);

this appears to work as expected for me:

Function code:

[FunctionName("hello")]
public static IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
    ILogger log)
{
    var rs = new MyAcceptedAtRouteResult(
            "acceptedatrouteresult",
            new { someParameter = "value" },
            new { Result = "1" });

    return rs;
}

[FunctionName("acceptedatrouteresult")]
public static IActionResult RunAccepted(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
    ILogger log)
{
    return new OkResult();
}

postman result:
~
GET /api/hello HTTP/1.1
Host: localhost:54466
User-Agent: PostmanRuntime/7.19.0
Accept: /
Cache-Control: no-cache
Postman-Token: 0a4593bf-cefa-4edc-8d0e-6ad291ae9cff,13fa1a98-604f-4d6c-b95e-e1902230e60c
Host: localhost:54466
Accept-Encoding: gzip, deflate
Connection: keep-alive
cache-control: no-cache
~

image

{
    "result": "1"
}

However note that I had to add another function w/ the matching route. If I remove this function, I get the "No routes found" exception path in AcceptedAtRouteResult because the WebJobs WebJobsRouter's GetVirtualPath method doesn't find the route we specify for AcceptedAt.

@pragnagopa any chance we can get the PR for this pulled in?

Was this page helpful?
0 / 5 - 0 ratings