From caf416f9be066480ef4f5534d733fda85cf201b2 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:50:14 +0000 Subject: [PATCH] .Net: Handle media types with parameters (#10000) ### Motivation, Context and Description This PR updates the RestApiOperationRunner class to be resilient to media types specified with parameters. It's a continuation of the first PR - https://github.com/microsoft/semantic-kernel/pull/9959, where the OpenApiDocumentParser was updated to allow media types with parameters. --- .../RestApiOperationRunner.cs | 10 ++++- .../OpenApi/RestApiOperationRunnerTests.cs | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs index 7c0ada6d830c..a6427e58061c 100644 --- a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs +++ b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs @@ -352,7 +352,15 @@ private async Task ReadContentAndCreateOperationRespon mediaType = mediaTypeFallback; } - if (!this._payloadFactoryByMediaType.TryGetValue(mediaType!, out var payloadFactory)) + // Remove media type parameters, such as x-api-version, from the "text/plain; x-api-version=2.0" media type string. + mediaType = mediaType!.Split(';').First(); + + // Normalize the media type to lowercase and remove trailing whitespaces. +#pragma warning disable CA1308 // Normalize strings to uppercase + mediaType = mediaType!.ToLowerInvariant().Trim(); +#pragma warning restore CA1308 // Normalize strings to uppercase + + if (!this._payloadFactoryByMediaType.TryGetValue(mediaType, out var payloadFactory)) { throw new KernelException($"The media type {mediaType} of the {operation.Id} operation is not supported by {nameof(RestApiOperationRunner)}."); } diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs index fd48bbcb6a50..764e5e2b4f07 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs @@ -1705,6 +1705,44 @@ public async Task ItShouldReturnExpectedSchemaAsync(string expectedStatusCode, p Assert.Equal(JsonSerializer.Serialize(expected), JsonSerializer.Serialize(result.ExpectedSchema)); } + [Theory] + [InlineData("application/json;x-api-version=2.0", "application/json")] + [InlineData("application/json ; x-api-version=2.0", "application/json")] + [InlineData(" application/JSON; x-api-version=2.0", "application/json")] + [InlineData(" TEXT/PLAIN ; x-api-version=2.0", "text/plain")] + public async Task ItShouldNormalizeContentTypeArgumentAsync(string actualContentType, string normalizedContentType) + { + // Arrange + this._httpMessageHandlerStub.ResponseToReturn.Content = new StringContent("fake-content", Encoding.UTF8, MediaTypeNames.Text.Plain); + + var operation = new RestApiOperation( + id: "fake-id", + servers: [new RestApiServer("https://fake-random-test-host")], + path: "fake-path", + method: HttpMethod.Post, + description: "fake-description", + parameters: [], + responses: new Dictionary(), + securityRequirements: [], + payload: null + ); + + var arguments = new KernelArguments + { + { "payload", "fake-input-value" }, + { "content-type", actualContentType }, + }; + + var sut = new RestApiOperationRunner(this._httpClient, this._authenticationHandlerMock.Object, enableDynamicPayload: false); + + // Act + var result = await sut.RunAsync(operation, arguments); + + // Assert + Assert.NotNull(this._httpMessageHandlerStub.ContentHeaders); + Assert.Contains(this._httpMessageHandlerStub.ContentHeaders, h => h.Key == "Content-Type" && h.Value.Any(h => h.StartsWith(normalizedContentType, StringComparison.InvariantCulture))); + } + /// /// Disposes resources used by this class. ///