using System.Net; using System.Net.Http; namespace ModelContextProtocol; /// /// Extension methods for . /// internal static class HttpResponseMessageExtensions { private const int MaxResponseBodyLength = 1024; /// /// Throws an if the property is . /// Unlike , this method includes the response body in the exception message /// to help diagnose issues when the server returns error details in the response body. /// /// The HTTP response message to check. /// The token to monitor for cancellation requests. /// A task that represents the asynchronous operation. /// The response status code does not indicate success. public static async Task EnsureSuccessStatusCodeWithResponseBodyAsync(this HttpResponseMessage response, CancellationToken cancellationToken = default) { if (!response.IsSuccessStatusCode) { throw await CreateHttpRequestExceptionWithBodyAsync(response, cancellationToken).ConfigureAwait(false); } } /// /// Creates an for a non-success response, making a best-effort attempt to /// include the server's response body in the exception message so the diagnostic isn't lost. /// /// The non-success HTTP response message. /// The token to monitor for cancellation requests. /// An with the response status and, when available, its body. public static async Task CreateHttpRequestExceptionWithBodyAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) { string? responseBody = null; try { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(5)); responseBody = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false); if (responseBody.Length > MaxResponseBodyLength) { responseBody = responseBody.Substring(0, MaxResponseBodyLength) + "..."; } } catch { // Ignore all errors reading the response body (e.g., stream closed, timeout, cancellation) - we'll throw without it. } return CreateHttpRequestException(response, responseBody); } /// /// Creates an for a non-success response, including the response body in the message. /// /// The HTTP response message. /// The response body content, if available. /// An with the response details. public static HttpRequestException CreateHttpRequestException(HttpResponseMessage response, string? responseBody) { int statusCodeInt = (int)response.StatusCode; string message = string.IsNullOrEmpty(responseBody) ? $"Response status code does not indicate success: {statusCodeInt} ({response.ReasonPhrase})." : $"Response status code does not indicate success: {statusCodeInt} ({response.ReasonPhrase}). Response body: {responseBody}"; return HttpRequestExceptionExtensions.Create(message, innerException: null, response.StatusCode); } } /// /// Helpers for preserving HTTP status codes on across all target frameworks. /// internal static class HttpRequestExceptionExtensions { internal const string StatusCodeDataKey = "ModelContextProtocol.HttpStatusCode"; /// /// Creates an and preserves its HTTP status code. /// public static HttpRequestException Create(string message, Exception? innerException, HttpStatusCode? statusCode) { #if NET var exception = new HttpRequestException(message, innerException, statusCode); #else var exception = new HttpRequestException(message, innerException); #endif if (statusCode is not null) { exception.Data[StatusCodeDataKey] = statusCode.Value; } return exception; } /// /// Gets the preserved HTTP status code from an . /// public static HttpStatusCode? GetStatusCode(this HttpRequestException exception) { #if NET if (exception.StatusCode is { } statusCode) { return statusCode; } #endif return exception.Data[StatusCodeDataKey] switch { HttpStatusCode storedStatusCode => storedStatusCode, _ => null, }; } }