Send DELETE request when closing a Streamable HTTP session on the client (#501)

This commit is contained in:
Stephen Halter 2025-06-19 08:40:11 -07:00 • committed by GitHub
commit a4b4edaa2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 93 additions and 11 deletions

View file

@ -91,7 +91,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
{
Content = content,
};
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders);
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null);
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
@ -152,7 +152,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
{
using var request = new HttpRequestMessage(HttpMethod.Get, _sseEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders);
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(request.Headers, _options.AdditionalHeaders, sessionId: null, protocolVersion: null);
using var response = await _httpClient.SendAsync(
request,

View file

@ -30,6 +30,9 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
private string? _negotiatedProtocolVersion;
private Task? _getReceiveTask;
private readonly SemaphoreSlim _disposeLock = new(1, 1);
private bool _disposed;
public StreamableHttpClientSessionTransport(
string endpointName,
SseClientTransportOptions transportOptions,
@ -138,12 +141,26 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
public override async ValueTask DisposeAsync()
{
using var _ = await _disposeLock.LockAsync().ConfigureAwait(false);
if (_disposed)
{
return;
}
_disposed = true;
try
{
await _connectionCts.CancelAsync().ConfigureAwait(false);
try
{
// Send DELETE request to terminate the session. Only send if we have a session ID, per MCP spec.
if (!string.IsNullOrEmpty(SessionId))
{
await SendDeleteRequest();
}
if (_getReceiveTask != null)
{
await _getReceiveTask.ConfigureAwait(false);
@ -236,6 +253,22 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
return null;
}
private async Task SendDeleteRequest()
{
using var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, _options.Endpoint);
CopyAdditionalHeaders(deleteRequest.Headers, _options.AdditionalHeaders, SessionId, _negotiatedProtocolVersion);
try
{
// Do not validate we get a successful status code, because server support for the DELETE request is optional
(await _httpClient.SendAsync(deleteRequest, CancellationToken.None).ConfigureAwait(false)).Dispose();
}
catch (Exception ex)
{
LogTransportShutdownFailed(Name, ex);
}
}
private void LogJsonException(JsonException ex, string data)
{
if (_logger.IsEnabled(LogLevel.Trace))
@ -251,8 +284,8 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
internal static void CopyAdditionalHeaders(
HttpRequestHeaders headers,
IDictionary<string, string>? additionalHeaders,
string? sessionId = null,
string? protocolVersion = null)
string? sessionId,
string? protocolVersion)
{
if (sessionId is not null)
{