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:
Function should return 200 status with the set parameters
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;
}
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
~

{
"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?
Most helpful comment
@pragnagopa any chance we can get the PR for this pulled in?