Preserve HTTP status codes across target frameworks (#1767)
Copilot-Session: 9a49da98-c8d8-4b90-a6ed-50afe18ac981
This commit is contained in:
parent
f8e9e1d23f
commit
5c305450cd
3 changed files with 53 additions and 10 deletions
|
|
@ -69,10 +69,52 @@ internal static class HttpResponseMessageExtensions
|
||||||
? $"Response status code does not indicate success: {statusCodeInt} ({response.ReasonPhrase})."
|
? $"Response status code does not indicate success: {statusCodeInt} ({response.ReasonPhrase})."
|
||||||
: $"Response status code does not indicate success: {statusCodeInt} ({response.ReasonPhrase}). Response body: {responseBody}";
|
: $"Response status code does not indicate success: {statusCodeInt} ({response.ReasonPhrase}). Response body: {responseBody}";
|
||||||
|
|
||||||
|
return HttpRequestExceptionExtensions.Create(message, innerException: null, response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helpers for preserving HTTP status codes on <see cref="HttpRequestException"/> across all target frameworks.
|
||||||
|
/// </summary>
|
||||||
|
internal static class HttpRequestExceptionExtensions
|
||||||
|
{
|
||||||
|
internal const string StatusCodeDataKey = "ModelContextProtocol.HttpStatusCode";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an <see cref="HttpRequestException"/> and preserves its HTTP status code.
|
||||||
|
/// </summary>
|
||||||
|
public static HttpRequestException Create(string message, Exception? innerException, HttpStatusCode? statusCode)
|
||||||
|
{
|
||||||
#if NET
|
#if NET
|
||||||
return new HttpRequestException(message, inner: null, response.StatusCode);
|
var exception = new HttpRequestException(message, innerException, statusCode);
|
||||||
#else
|
#else
|
||||||
return new HttpRequestException(message);
|
var exception = new HttpRequestException(message, innerException);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (statusCode is not null)
|
||||||
|
{
|
||||||
|
exception.Data[StatusCodeDataKey] = statusCode.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the preserved HTTP status code from an <see cref="HttpRequestException"/>.
|
||||||
|
/// </summary>
|
||||||
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,14 +146,10 @@ internal sealed partial class AutoDetectingClientSessionTransport : ITransport
|
||||||
// keep HttpRequestException as the surfaced type so existing callers can still catch it and read StatusCode.
|
// keep HttpRequestException as the surfaced type so existing callers can still catch it and read StatusCode.
|
||||||
await sseTransport.DisposeAsync().ConfigureAwait(false);
|
await sseTransport.DisposeAsync().ConfigureAwait(false);
|
||||||
LogSseFallbackFailedAfterStreamableHttp(_name, sseError);
|
LogSseFallbackFailedAfterStreamableHttp(_name, sseError);
|
||||||
#if NET
|
throw HttpRequestExceptionExtensions.Create(
|
||||||
throw new HttpRequestException(streamableHttpError.Message, sseError, streamableHttpError.StatusCode);
|
streamableHttpError.Message,
|
||||||
#else
|
sseError,
|
||||||
// net472 has no HttpRequestException overload that carries a status code, so this target
|
streamableHttpError.GetStatusCode());
|
||||||
// preserves the status text in the message but not a programmatic StatusCode. Preserving the
|
|
||||||
// status code is intentionally net5+ only rather than an oversight.
|
|
||||||
throw new HttpRequestException(streamableHttpError.Message, sseError);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,10 @@ public class HttpClientTransportAutoDetectTests(ITestOutputHelper testOutputHelp
|
||||||
Assert.Contains("403", ex.Message);
|
Assert.Contains("403", ex.Message);
|
||||||
Assert.IsType<HttpRequestException>(ex.InnerException);
|
Assert.IsType<HttpRequestException>(ex.InnerException);
|
||||||
Assert.Contains("405", ex.InnerException.Message);
|
Assert.Contains("405", ex.InnerException.Message);
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, ex.Data["ModelContextProtocol.HttpStatusCode"]);
|
||||||
|
#if NET
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -410,6 +414,7 @@ public class HttpClientTransportAutoDetectTests(ITestOutputHelper testOutputHelp
|
||||||
var httpEx = Assert.IsType<HttpRequestException>(ex);
|
var httpEx = Assert.IsType<HttpRequestException>(ex);
|
||||||
Assert.Contains("415", httpEx.Message);
|
Assert.Contains("415", httpEx.Message);
|
||||||
Assert.Contains(streamableHttpBody, httpEx.Message);
|
Assert.Contains(streamableHttpBody, httpEx.Message);
|
||||||
|
Assert.Equal(HttpStatusCode.UnsupportedMediaType, httpEx.Data["ModelContextProtocol.HttpStatusCode"]);
|
||||||
#if NET
|
#if NET
|
||||||
Assert.Equal(HttpStatusCode.UnsupportedMediaType, httpEx.StatusCode);
|
Assert.Equal(HttpStatusCode.UnsupportedMediaType, httpEx.StatusCode);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue