AspNetCoreServer incorrectly sets IHttpRequestBodyDetectionFeature.CanHaveBody that leads to breaking change after net 7 upgrade
MadSciencist opened this issue · 7 comments
Describe the bug
Hi,
It appears that the behavior of the APIGatewayProxyFunction is inconsistent with the behavior of Kestrel when handling requests with an empty body directed to endpoints with a parameter marked [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)].
In the InvokeFeatures class, the CanHaveBody property is set based on requestFeature.Body != null. This is problematic because the IHttpRequestFeature.Body property is initialized with a new MemoryStream() value, therefor CanHaveBody is never false.
As a result, this change introduces a breaking behavior when upgrading from .NET 6.0 to .NET 7.0 in scenarios where requests with an empty body are sent from AWS Lambda. ASP.NET Core attempts to deserialize the request (due to the CanHaveBody property), and because the request body is an empty stream, it causes the API to throw an error.
Regression Issue
- Select this option if this issue appears to be a regression.
Expected Behavior
Endpoints with parameters marked with [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] attribute can handle request both with and without body.
Current Behavior
AspNetCore tries to deserialize the body and throws The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
Reproduction Steps
Create PUT endpoint with optional body:
public IActionResult Test([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] Body request = default)
{
return Accepted();
}
public class Body
{
public string Prop { get; set; }
}and send requests with and without body.
Possible Solution
No response
Additional Information/Context
No response
AWS .NET SDK and/or Package version used
Amazon.Lambda.AspNetCoreServer 9.0.1
Targeted .NET Platform
NET 8.0
Operating System and version
Windows 10
@MadSciencist Good morning. Could you please share if you are using Amazon.Lambda.Annotations.APIGateway.FromBodyAttribute attribute or Microsoft.AspNetCore.Mvc.FromBodyAttribute class? The one provided by Amazon.Lambda.Annotations package does not support EmptyBodyBehavior property.
EDIT: Ignore my comment. This has nothing to do with annotations. Will try to reproduce it at my end.
Thanks,
Ashish
@MadSciencist Good afternoon. Somehow, I'm unable to reproduce the issue using the below code (used AWS Serverless Web API template):
.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<AWSProjectType>Lambda</AWSProjectType>
<!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- Generate ready to run images during publishing to improve cold start time. -->
<PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Amazon.Lambda.AspNetCoreServer" Version="9.0.1" />
</ItemGroup>
</Project>Controllers\ValuesController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace AWSServerlessApiNET8.Controllers
{
[Route("api/[controller]")]
public class ValuesController : ControllerBase
{
[HttpPut("test")]
public IActionResult Test([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] Body request = default)
{
return Accepted();
}
public class Body
{
public string Prop { get; set; }
}
}
}Below are the results:
- Executing locally works fine.
- Deploying to Lambda and testing using API Gateway endpoint (e.g.
https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test) works fine with or without request Body (used Postman withContent-Typeheader asapplication/jsonset).
curl requestwithBodycurl requestcurl --location --request PUT 'https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test' \ --header 'Content-Type: application/json' \ --data '{"Prop": "test"}'withoutBodyORcurl --location --request PUT 'https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test' \ --header 'Content-Type: application/json' \ --data ''curl --location --request PUT 'https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test' \ --header 'Content-Type: application/json' - Below are CloudWatch logs:
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| timestamp | message |
|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1731965976403 | INIT_START Runtime Version: dotnet:8.v29 Runtime Version ARN: arn:aws:lambda:us-east-2::runtime:a80f5bd0010789587efde8cc8718de53e230fba733dc31ed8ba53bf0d0b8d6f0 |
| 1731965976745 | 2024-11-18T21:39:36.739Z trce [Information] Microsoft.Hosting.Lifetime: Application started. Press Ctrl+C to shut down. |
| 1731965976746 | 2024-11-18T21:39:36.746Z trce [Information] Microsoft.Hosting.Lifetime: Hosting environment: Production |
| 1731965976746 | 2024-11-18T21:39:36.746Z trce [Information] Microsoft.Hosting.Lifetime: Content root path: /var/task |
| 1731965976805 | START RequestId: 26489e3b-9702-430b-b1e7-7008601b6195 Version: $LATEST |
| 1731965976974 | 2024-11-18T21:39:36.974Z 26489e3b-9702-430b-b1e7-7008601b6195 trce [Information] Microsoft.AspNetCore.Hosting.Diagnostics: Request starting PUT https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test - application/json 0 |
| 1731965977018 | 2024-11-18T21:39:37.018Z 26489e3b-9702-430b-b1e7-7008601b6195 trce [Information] Microsoft.AspNetCore.Routing.EndpointMiddleware: Executing endpoint 'AWSServerlessApiNET8.Controllers.ValuesController.Test (AWSServerlessApiNET8)' |
| 1731965977058 | 2024-11-18T21:39:37.058Z 26489e3b-9702-430b-b1e7-7008601b6195 trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Route matched with {action = "Test", controller = "Values"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult Test(Body) on controller AWSServerlessApiNET8.Controllers.ValuesController (AWSServerlessApiNET8). |
| 1731965977120 | 2024-11-18T21:39:37.120Z 26489e3b-9702-430b-b1e7-7008601b6195 trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor: Executing AcceptedResult, writing value of type 'null'. |
| 1731965977138 | 2024-11-18T21:39:37.138Z 26489e3b-9702-430b-b1e7-7008601b6195 trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Executed action AWSServerlessApiNET8.Controllers.ValuesController.Test (AWSServerlessApiNET8) in 61.6712ms |
| 1731965977138 | 2024-11-18T21:39:37.138Z 26489e3b-9702-430b-b1e7-7008601b6195 trce [Information] Microsoft.AspNetCore.Routing.EndpointMiddleware: Executed endpoint 'AWSServerlessApiNET8.Controllers.ValuesController.Test (AWSServerlessApiNET8)' |
| 1731965977139 | 2024-11-18T21:39:37.139Z 26489e3b-9702-430b-b1e7-7008601b6195 trce [Information] Microsoft.AspNetCore.Hosting.Diagnostics: Request finished PUT https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test - 202 - - 165.6165ms |
| 1731965977196 | END RequestId: 26489e3b-9702-430b-b1e7-7008601b6195 |
| 1731965977196 | REPORT RequestId: 26489e3b-9702-430b-b1e7-7008601b6195 Duration: 390.33 ms Billed Duration: 391 ms Memory Size: 512 MB Max Memory Used: 89 MB Init Duration: 399.51 ms |
| 1731966059781 | START RequestId: c1b94fd5-639b-487c-88a5-e24827178caf Version: $LATEST |
| 1731966059782 | 2024-11-18T21:40:59.782Z c1b94fd5-639b-487c-88a5-e24827178caf trce [Information] Microsoft.AspNetCore.Hosting.Diagnostics: Request starting PUT https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test - application/json 16 |
| 1731966059782 | 2024-11-18T21:40:59.782Z c1b94fd5-639b-487c-88a5-e24827178caf trce [Information] Microsoft.AspNetCore.Routing.EndpointMiddleware: Executing endpoint 'AWSServerlessApiNET8.Controllers.ValuesController.Test (AWSServerlessApiNET8)' |
| 1731966059783 | 2024-11-18T21:40:59.783Z c1b94fd5-639b-487c-88a5-e24827178caf trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Route matched with {action = "Test", controller = "Values"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult Test(Body) on controller AWSServerlessApiNET8.Controllers.ValuesController (AWSServerlessApiNET8). |
| 1731966059789 | 2024-11-18T21:40:59.789Z c1b94fd5-639b-487c-88a5-e24827178caf trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor: Executing AcceptedResult, writing value of type 'null'. |
| 1731966059789 | 2024-11-18T21:40:59.789Z c1b94fd5-639b-487c-88a5-e24827178caf trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Executed action AWSServerlessApiNET8.Controllers.ValuesController.Test (AWSServerlessApiNET8) in 6.3805ms |
| 1731966059789 | 2024-11-18T21:40:59.789Z c1b94fd5-639b-487c-88a5-e24827178caf trce [Information] Microsoft.AspNetCore.Routing.EndpointMiddleware: Executed endpoint 'AWSServerlessApiNET8.Controllers.ValuesController.Test (AWSServerlessApiNET8)' |
| 1731966059789 | 2024-11-18T21:40:59.789Z c1b94fd5-639b-487c-88a5-e24827178caf trce [Information] Microsoft.AspNetCore.Hosting.Diagnostics: Request finished PUT https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test - 202 - - 6.9651ms |
| 1731966059791 | END RequestId: c1b94fd5-639b-487c-88a5-e24827178caf |
| 1731966059791 | REPORT RequestId: c1b94fd5-639b-487c-88a5-e24827178caf Duration: 9.73 ms Billed Duration: 10 ms Memory Size: 512 MB Max Memory Used: 89 MB |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Please advise if I'm missing anything in reproduction.
Thanks,
Ashish
Good morning,
Thank you for your time. I took another look at it, and you're right - it works with the basic AspNetCoreWebAPI template.
So, I started moving code from my project until it broke, and it seems that the [ApiController] attribute (try decorating ValuesController) is what is required to trigger the issue.
Good morning, Thank you for your time. I took another look at it, and you're right - it works with the basic AspNetCoreWebAPI template. So, I started moving code from my project until it broke, and it seems that the
[ApiController]attribute (try decoratingValuesController) is what is required to trigger the issue.
@MadSciencist Thanks for the input. After decorating ValuesController with [ApiController] attribute:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace AWSServerlessApiNET8.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpPut("test")]
public IActionResult Test([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] Body request = default)
{
return Accepted();
}
public class Body
{
public string Prop { get; set; }
}
}
}-
Issue is not reproducible locally using Kestrel irrespective if we send body, empty body or no body.
-
Issue is reproducible when sending empty body or no body when the project is deployed to API Gateway.
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "$": [ "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0." ] }, "traceId": "00-1ef5b305213ec74a8a63e242898c4a5c-40bd9145a0afe04c-00" }Logs from CloudWatch:
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | timestamp | message | | ---------------| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1732044466909 | 2024-11-19T19:27:46.909Z cafed777-d8e8-4631-b0bd-7035e5b5b95a trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Route matched with {action = "Test", controller = "Values"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult Test(Body) on controller AWSServerlessApiNET8.Controllers.ValuesController (AWSServerlessApiNET8). | | 1732044467007 | 2024-11-19T19:27:47.007Z cafed777-d8e8-4631-b0bd-7035e5b5b95a trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor: Executing BadRequestObjectResult, writing value of type 'Microsoft.AspNetCore.Mvc.ValidationProblemDetails'. | | 1732044467094 | 2024-11-19T19:27:47.093Z cafed777-d8e8-4631-b0bd-7035e5b5b95a trce [Information] Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Executed action AWSServerlessApiNET8.Controllers.ValuesController.Test (AWSServerlessApiNET8) in 160.7038ms | | 1732044467094 | 2024-11-19T19:27:47.094Z cafed777-d8e8-4631-b0bd-7035e5b5b95a trce [Information] Microsoft.AspNetCore.Routing.EndpointMiddleware: Executed endpoint 'AWSServerlessApiNET8.Controllers.ValuesController.Test (AWSServerlessApiNET8)' | | 1732044467109 | 2024-11-19T19:27:47.109Z cafed777-d8e8-4631-b0bd-7035e5b5b95a trce [Information] Microsoft.AspNetCore.Hosting.Diagnostics: Request finished PUT https://<<api-id>>.execute-api.us-east-2.amazonaws.com/Prod/api/values/test - 400 - application/problem+json;+charset=utf-8 318.0821ms | | 1732044467188 | END RequestId: cafed777 - d8e8 - 4631 - b0bd - 7035e5b5b95a | | 1732044701209 | REPORT RequestId: dcbeaf95 - 4a52 - 457e-86a4 - 57002305b6e3 Duration: 3.33 ms Billed Duration: 4 ms Memory Size: 512 MB Max Memory Used: 90 MB | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Just on the side note, [ApiController] attribute:
- Makes attribute routing a requirement, meaning the API methods are inaccessible via conventional routes.
- Also makes model validation errors automatically trigger an HTTP 400 response. Basically, the framework perform following code behind the scene:
if (!ModelState.IsValid) { return BadRequest(ModelState); }
I'm unsure why model validation is not kicked in locally when executing under Kestrel, when using [ApiController] attribute.
Version 9.0.3 of Amazon.Lambda.AspNetCoreServer and 1.7.3 of Amazon.Lambda.AspNetCoreServer.Hosting have been released with this fix. Thanks for letting us know about the issue.
@MadSciencist Closing this issue. Please let us know if you still encounter the issue in the latest versions.
Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.