Azure / azure-functions-dotnet-worker

Azure Functions out-of-process .NET language worker
MIT License
432 stars 184 forks source link

Isolated Http Trigger [FromQuery] bool parameterName cannot serialize Pascal case True or False #2786

Open vtnzDirkMomsen opened 1 month ago

vtnzDirkMomsen commented 1 month ago

Repro steps

  1. Create a function as follows on a default .net 8 isolated function app:

    public class Function1
    {
    public Function1()
    {
    }
    
    [Function("Function1")]
    public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req,
        [FromQuery] bool includeSomething = false)
    {
        return new OkObjectResult($"Did you include something? {includeSomething}");
    }
    }
  2. Run function app
  3. Send get request to function endpoint: http://localhost:7107/api/Function1?includeSomething=False

Expected behavior

Expected response body: Did you include something? False

Actual behavior

Http Error - 500 Internal Server Error occurs Function app console logs are as follows:

[2024-10-17T09:17:56.773Z] Function 'Function1', Invocation id '07a9025c-876f-4f43-a6a1-2421edad600c': An exception was thrown by the invocation.
[2024-10-17T09:17:56.775Z] Result: Function 'Function1', Invocation id '07a9025c-876f-4f43-a6a1-2421edad600c': An exception was thrown by the invocation.
Exception: Microsoft.Azure.Functions.Worker.FunctionInputConverterException: Error converting 1 input parameters for Function 'Function1': Cannot convert input parameter 'includeSomething' to type 'System.Boolean' from type 'System.String'. Error:System.Text.Json.JsonException: 'F' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
[2024-10-17T09:17:56.777Z]  ---> System.Text.Json.JsonReaderException: 'F' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.
[2024-10-17T09:17:56.780Z]    at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
[2024-10-17T09:17:56.783Z]    at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
[2024-10-17T09:17:56.784Z]    at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first)
[2024-10-17T09:17:56.786Z]    at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
[2024-10-17T09:17:56.799Z]    at System.Text.Json.Utf8JsonReader.Read()
[2024-10-17T09:17:56.800Z]    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
[2024-10-17T09:17:56.802Z]    --- End of inner exception stack trace ---
[2024-10-17T09:17:56.805Z]    at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, JsonReaderException ex)
[2024-10-17T09:17:56.807Z]    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
[2024-10-17T09:17:56.813Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.ContinueDeserialize(ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack)
[2024-10-17T09:17:56.817Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
[2024-10-17T09:17:56.820Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken)
[2024-10-17T09:17:56.822Z]    at Microsoft.Azure.Functions.Worker.Converters.JsonPocoConverter.GetConversionResultFromDeserialization(Byte[] bytes, Type type) in D:\a\_work\1\s\src\DotNetWorker.Core\Converters\JsonPocoConverter.cs:line 66
[2024-10-17T09:17:56.824Z]    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 97
[2024-10-17T09:17:56.828Z]    at FromQueryTest.DirectFunctionExecutor.ExecuteAsync(FunctionContext context) in C:\Users\momsend\source\repos\FromQueryTest\FromQueryTest\obj\Debug\net8.0\Microsoft.Azure.Functions.Worker.Sdk.Generators\Microsoft.Azure.Functions.Worker.Sdk.Generators.FunctionExecutorGenerator\GeneratedFunctionExecutor.g.cs:line 31
[2024-10-17T09:17:56.831Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2024-10-17T09:17:56.834Z]    at Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.FunctionsHttpProxyingMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in /mnt/vss/_work/1/s/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs:line 54
[2024-10-17T09:17:56.835Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 91
Stack:    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 97
[2024-10-17T09:17:56.837Z]    at FromQueryTest.DirectFunctionExecutor.ExecuteAsync(FunctionContext context) in C:\Users\momsend\source\repos\FromQueryTest\FromQueryTest\obj\Debug\net8.0\Microsoft.Azure.Functions.Worker.Sdk.Generators\Microsoft.Azure.Functions.Worker.Sdk.Generators.FunctionExecutorGenerator\GeneratedFunctionExecutor.g.cs:line 31
[2024-10-17T09:17:56.840Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2024-10-17T09:17:56.843Z]    at Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.FunctionsHttpProxyingMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in /mnt/vss/_work/1/s/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs:line 54
[2024-10-17T09:17:56.845Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 91.
[2024-10-17T09:17:56.862Z] Executed 'Functions.Function1' (Failed, Id=07a9025c-876f-4f43-a6a1-2421edad600c, Duration=217ms)
[2024-10-17T09:17:56.864Z] System.Private.CoreLib: Exception while executing function: Functions.Function1. System.Private.CoreLib: Result: Failure
Exception: Microsoft.Azure.Functions.Worker.FunctionInputConverterException: Error converting 1 input parameters for Function 'Function1': Cannot convert input parameter 'includeSomething' to type 'System.Boolean' from type 'System.String'. Error:System.Text.Json.JsonException: 'F' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
[2024-10-17T09:17:56.866Z]  ---> System.Text.Json.JsonReaderException: 'F' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.
[2024-10-17T09:17:56.868Z]    at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
[2024-10-17T09:17:56.870Z]    at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
[2024-10-17T09:17:56.873Z]    at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first)
[2024-10-17T09:17:56.875Z]    at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
[2024-10-17T09:17:56.877Z]    at System.Text.Json.Utf8JsonReader.Read()
[2024-10-17T09:17:56.878Z]    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
[2024-10-17T09:17:56.880Z]    --- End of inner exception stack trace ---
[2024-10-17T09:17:56.882Z]    at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, JsonReaderException ex)
[2024-10-17T09:17:56.883Z]    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
[2024-10-17T09:17:56.886Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.ContinueDeserialize(ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack)
[2024-10-17T09:17:56.889Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
[2024-10-17T09:17:56.890Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken)
[2024-10-17T09:17:56.890Z]    at Microsoft.Azure.Functions.Worker.Converters.JsonPocoConverter.GetConversionResultFromDeserialization(Byte[] bytes, Type type) in D:\a\_work\1\s\src\DotNetWorker.Core\Converters\JsonPocoConverter.cs:line 66
[2024-10-17T09:17:56.891Z]    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 97
[2024-10-17T09:17:56.892Z]    at FromQueryTest.DirectFunctionExecutor.ExecuteAsync(FunctionContext context) in C:\Users\momsend\source\repos\FromQueryTest\FromQueryTest\obj\Debug\net8.0\Microsoft.Azure.Functions.Worker.Sdk.Generators\Microsoft.Azure.Functions.Worker.Sdk.Generators.FunctionExecutorGenerator\GeneratedFunctionExecutor.g.cs:line 31
[2024-10-17T09:17:56.893Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2024-10-17T09:17:56.894Z]    at Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.FunctionsHttpProxyingMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in /mnt/vss/_work/1/s/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs:line 54
[2024-10-17T09:17:56.895Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 91
[2024-10-17T09:17:56.897Z]    at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(InvocationRequest request) in D:\a\_work\1\s\src\DotNetWorker.Grpc\Handlers\InvocationHandler.cs:line 88
Stack:    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 97
[2024-10-17T09:17:56.898Z]    at FromQueryTest.DirectFunctionExecutor.ExecuteAsync(FunctionContext context) in C:\Users\momsend\source\repos\FromQueryTest\FromQueryTest\obj\Debug\net8.0\Microsoft.Azure.Functions.Worker.Sdk.Generators\Microsoft.Azure.Functions.Worker.Sdk.Generators.FunctionExecutorGenerator\GeneratedFunctionExecutor.g.cs:line 31
[2024-10-17T09:17:56.899Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2024-10-17T09:17:56.900Z]    at Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.FunctionsHttpProxyingMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in /mnt/vss/_work/1/s/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs:line 54
[2024-10-17T09:17:56.901Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 91
[2024-10-17T09:17:56.902Z]    at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(InvocationRequest request) in D:\a\_work\1\s\src\DotNetWorker.Grpc\Handlers\InvocationHandler.cs:line 88.

Known workarounds

The obvious workaround is the use lowercase first letter true or false e.g. http://localhost:7107/api/Function1?includeSomething=false This will work.

To support pascal case we can use a string input parameter and parse it:

[Function("StringInputParam")]
public IActionResult StringInputParam([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req,
    [FromQuery] string includeSomething)
{
    bool convertedToBool = bool.Parse(includeSomething);

    return new OkObjectResult($"Function2 Did you include something? {convertedToBool}");
}

Related information

The issue is the same whether you run the application locally or deployed in azure.

Can be replicated on a default .net isolated azure function app

Program.cs:

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();

host.json:

{
    "version": "2.0",
    "logging": {
        "applicationInsights": {
            "samplingSettings": {
                "isEnabled": true,
                "excludedTypes": "Request"
            },
            "enableLiveMetricsFilters": true
        }
    }
}

local.settings.json:

{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
    }
}

Azure Logic Apps that use the Http Swagger action or the API Management action will automatically provide a "False" or "True" to query parameters coded as boolean, so I have to support both camel and pascal case when migrating to the isolated worker.

This is mostly just frustrating but there are simple ways to work around it.

vtnzDirkMomsen commented 1 month ago

It should also be noted that when I add services.AddMvc().AddNewtonsoftJson(); to the HostBuilder.ConfigureServices (in Program.cs) nothing changes. It still seems to use System.Text.Json