Skip to content

Commit

Permalink
.Net: Include request data when operation is cancelled (microsoft#7119)
Browse files Browse the repository at this point in the history
### Motivation and Context

Closes microsoft#7118 

When request is cancelled due to configured Timeout on HttpClient, SK
will throw `KernelFunctionCanceledException`. This change sets the Url,
request payload etc. on the `KernelFunctionCanceledException`.

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
  • Loading branch information
markwallace-microsoft authored Jul 8, 2024
1 parent 23f2dce commit 13e3a22
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ private async Task<RestApiOperationResponse> SendAsync(

throw;
}
catch (OperationCanceledException ex)
{
ex.Data.Add(HttpRequestMethod, requestMessage.Method.Method);
ex.Data.Add(UrlFull, requestMessage.RequestUri?.ToString());
ex.Data.Add(HttpRequestBody, payload);

throw;
}
catch (KernelException ex)
{
ex.Data.Add(HttpRequestMethod, requestMessage.Method.Method);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,38 @@ public async Task ItShouldSetHttpRequestMessageOptionsAsync()
Assert.Equal(options.KernelArguments, kernelFunctionContext.Arguments);
}

[Fact]
public async Task ItShouldIncludeRequestDataWhenOperationCanceledExceptionIsThrownAsync()
{
// Arrange
this._httpMessageHandlerStub.ExceptionToThrow = new OperationCanceledException();

var operation = new RestApiOperation(
"fake-id",
new Uri("https://fake-random-test-host"),
"fake-path",
HttpMethod.Post,
"fake-description",
[],
payload: null
);

var arguments = new KernelArguments
{
{ "payload", JsonSerializer.Serialize(new { value = "fake-value" }) },
{ "content-type", "application/json" }
};

var sut = new RestApiOperationRunner(this._httpClient, this._authenticationHandlerMock.Object);

// Act & Assert
var canceledException = await Assert.ThrowsAsync<OperationCanceledException>(() => sut.RunAsync(operation, arguments));
Assert.Equal("The operation was canceled.", canceledException.Message);
Assert.Equal("POST", canceledException.Data["http.request.method"]);
Assert.Equal("https://fake-random-test-host/fake-path", canceledException.Data["url.full"]);
Assert.Equal("{\"value\":\"fake-value\"}", canceledException.Data["http.request.body"]);
}

public class SchemaTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
Expand Down Expand Up @@ -1302,6 +1334,8 @@ private sealed class HttpMessageHandlerStub : DelegatingHandler

public HttpResponseMessage ResponseToReturn { get; set; }

public Exception? ExceptionToThrow { get; set; }

public HttpMessageHandlerStub()
{
this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
Expand All @@ -1312,6 +1346,11 @@ public HttpMessageHandlerStub()

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (this.ExceptionToThrow is not null)
{
throw this.ExceptionToThrow;
}

this.RequestMessage = request;
this.RequestContent = request.Content is null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken);

Expand Down
49 changes: 46 additions & 3 deletions dotnet/src/IntegrationTests/Plugins/OpenApi/RepairServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand All @@ -17,7 +18,7 @@ public async Task ValidateInvokingRepairServicePluginAsync()
{
// Arrange
var kernel = new Kernel();
using var stream = System.IO.File.OpenRead("Plugins/repair-service.json");
using var stream = System.IO.File.OpenRead("Plugins/OpenApi/repair-service.json");
using HttpClient httpClient = new();

var plugin = await kernel.ImportPluginFromOpenApiAsync(
Expand Down Expand Up @@ -73,7 +74,7 @@ public async Task HttpOperationExceptionIncludeRequestInfoAsync()
{
// Arrange
var kernel = new Kernel();
using var stream = System.IO.File.OpenRead("Plugins/repair-service.json");
using var stream = System.IO.File.OpenRead("Plugins/OpenApi/repair-service.json");
using HttpClient httpClient = new();

var plugin = await kernel.ImportPluginFromOpenApiAsync(
Expand Down Expand Up @@ -107,12 +108,54 @@ public async Task HttpOperationExceptionIncludeRequestInfoAsync()
}
}

[Fact(Skip = "This test is for manual verification.")]
public async Task KernelFunctionCanceledExceptionIncludeRequestInfoAsync()
{
// Arrange
var kernel = new Kernel();
using var stream = System.IO.File.OpenRead("Plugins/OpenApi/repair-service.json");
using HttpClient httpClient = new();

var plugin = await kernel.ImportPluginFromOpenApiAsync(
"RepairService",
stream,
new OpenApiFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true, EnableDynamicPayload = false });

var arguments = new KernelArguments
{
["payload"] = """{ "title": "Engine oil change", "description": "Need to drain the old engine oil and replace it with fresh oil.", "assignedTo": "", "date": "", "image": "" }"""
};

var id = 99999;

// Update Repair
arguments = new KernelArguments
{
["payload"] = $"{{ \"id\": {id}, \"assignedTo\": \"Karin Blair\", \"date\": \"2024-04-16\", \"image\": \"https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg\" }}"
};

try
{
httpClient.Timeout = TimeSpan.FromMilliseconds(10); // Force a timeout

await plugin["updateRepair"].InvokeAsync(kernel, arguments);
Assert.Fail("Expected KernelFunctionCanceledException");
}
catch (KernelFunctionCanceledException ex)
{
Assert.Equal("The invocation of function 'updateRepair' was canceled.", ex.Message);
Assert.NotNull(ex.InnerException);
Assert.Equal("Patch", ex.InnerException.Data["http.request.method"]);
Assert.Equal("https://piercerepairsapi.azurewebsites.net/repairs", ex.InnerException.Data["url.full"]);
}
}

[Fact(Skip = "This test is for manual verification.")]
public async Task UseDelegatingHandlerAsync()
{
// Arrange
var kernel = new Kernel();
using var stream = System.IO.File.OpenRead("Plugins/repair-service.json");
using var stream = System.IO.File.OpenRead("Plugins/OpenApi/repair-service.json");

using var httpHandler = new HttpClientHandler();
using var customHandler = new CustomHandler(httpHandler);
Expand Down

0 comments on commit 13e3a22

Please sign in to comment.