diff --git a/MetaBrainz.Common/HttpError.cs b/MetaBrainz.Common/HttpError.cs index df4281e..586a2b3 100644 --- a/MetaBrainz.Common/HttpError.cs +++ b/MetaBrainz.Common/HttpError.cs @@ -14,68 +14,48 @@ namespace MetaBrainz.Common; [PublicAPI] public class HttpError : Exception { - /// Creates a new HTTP error. - /// The response to take the status code and reason from. - [Obsolete($"Use {nameof(HttpError.FromResponse)} or {nameof(HttpError.FromResponseAsync)} instead.")] - public HttpError(HttpResponseMessage response) : this(response.StatusCode, response.ReasonPhrase, response.Version) { } - - /// Creates a new HTTP error. - /// The status code for the error. - /// The reason phrase associated with the error. - /// The exception that caused this one, if any. - public HttpError(HttpStatusCode status, string? reason, Exception? cause = null) : base(null, cause) { - this.Reason = reason; - this.Status = status; - } - /// Creates a new HTTP error. /// The status code for the error. /// The reason phrase associated with the error. /// The HTTP message version. + /// + /// The message to use; if this is not specified or , a message will be constructed based on + /// , and . + /// /// The exception that caused this one, if any. - public HttpError(HttpStatusCode status, string? reason, Version version, Exception? cause = null) : base(null, cause) { + public HttpError(HttpStatusCode status, string? reason = null, Version? version = null, string? message = null, + Exception? cause = null) : base(HttpError.MessageFor(status, reason, version, message), cause) { this.Reason = reason; this.Status = status; this.Version = version; } - /// The content (assumed to be text) of the response that triggered the error, if available. + /// The content (assumed to be text) of the error response, if available. public string? Content { get; private init; } - /// The content headers of the response that triggered the error, if available. + /// The content headers of the error response, if available. public HttpContentHeaders? ContentHeaders { get; private init; } - /// Gets a textual representation of the HTTP error. - /// A textual representation of the HTTP error. - public override string Message { - get { - var sb = new StringBuilder(); - sb.Append("HTTP"); - if (this.Version is not null) { - sb.Append('/').Append(this.Version); - } - sb.Append(' ').Append((int) this.Status).Append(" (").Append(this.Status).Append(')'); - if (this.Reason is not null) { - sb.Append(" '").Append(this.Reason).Append('\''); - } - return sb.ToString(); - } - } - /// The reason phrase associated with the error. public string? Reason { get; } - /// The headers of the response that triggered the error, if available. + /// The headers of the request that provoked the error response, if available. + public HttpRequestHeaders? RequestHeaders { get; private init; } + + /// The URI for the request that provoked the error response, if available. + public Uri? RequestUri { get; private init; } + + /// The headers of the error response, if available. public HttpResponseHeaders? ResponseHeaders { get; private init; } /// The status code for the error. public HttpStatusCode Status { get; } - /// The HTTP message version from the response that triggered the error, if available. + /// The HTTP message version from the error response, if available. public Version? Version { get; private init; } /// Creates a new HTTP error based on an response message. - /// The response message that triggered the error. + /// The response. /// A new HTTP error containing information taken from the response message. public static HttpError FromResponse(HttpResponseMessage response) => AsyncUtils.ResultOf(HttpError.FromResponseAsync(response)); @@ -85,14 +65,30 @@ public class HttpError : Exception { /// A new HTTP error containing information taken from the response message. public static async Task FromResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) { - // It's unfortunate that the headers are not easily copied (the classes do not have public constructors), so any changes to the - // response after this method is called will be reflected in the error's properties. return new HttpError(response.StatusCode, response.ReasonPhrase) { Content = await response.GetStringContentAsync(cancellationToken), - ContentHeaders = response.Content.Headers, - ResponseHeaders = response.Headers, + ContentHeaders = HttpUtils.Copy(response.Content.Headers), + RequestHeaders = HttpUtils.Copy(response.RequestMessage?.Headers), + RequestUri = response.RequestMessage?.RequestUri, + ResponseHeaders = HttpUtils.Copy(response.Headers), Version = response.Version, }; } + private static string MessageFor(HttpStatusCode status, string? reason, Version? version, string? message) { + if (message is not null) { + return message; + } + var sb = new StringBuilder(); + sb.Append("HTTP"); + if (version is not null) { + sb.Append('/').Append(version); + } + sb.Append(' ').Append((int) status).Append(" (").Append(status).Append(')'); + if (reason is not null) { + sb.Append(" '").Append(reason).Append('\''); + } + return sb.ToString(); + } + } diff --git a/MetaBrainz.Common/HttpUtils.cs b/MetaBrainz.Common/HttpUtils.cs index d706418..cb08216 100644 --- a/MetaBrainz.Common/HttpUtils.cs +++ b/MetaBrainz.Common/HttpUtils.cs @@ -1,7 +1,6 @@ -#define TRACE - -using System; +using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; @@ -18,6 +17,54 @@ namespace MetaBrainz.Common; [PublicAPI] public static class HttpUtils { + /// Creates a copy of a set of HTTP content headers. + /// The headers to copy. + /// A new set of HTTP content headers, with the same contents as the set provided. + [return: NotNullIfNotNull(nameof(headers))] + public static HttpContentHeaders? Copy(HttpContentHeaders? headers) { + if (headers is null) { + return null; + } + // There is no way to construct a copy of HTTP headers directly at the moment (see dotnet/runtime#95912). + using var dummy = new ByteArrayContent(Array.Empty()); + HttpUtils.Copy(headers, dummy.Headers); + return dummy.Headers; + } + + private static void Copy(HttpHeaders from, HttpHeaders to) { + foreach (var (name, values) in from.NonValidated) { + to.TryAddWithoutValidation(name, values); + } + } + + /// Creates a copy of a set of HTTP request headers. + /// The headers to copy. + /// A new set of HTTP request headers, with the same contents as the set provided. + [return: NotNullIfNotNull(nameof(headers))] + public static HttpRequestHeaders? Copy(HttpRequestHeaders? headers) { + if (headers is null) { + return null; + } + // There is no way to construct a copy of HTTP headers directly at the moment (see dotnet/runtime#95912). + using var dummy = new HttpRequestMessage(); + HttpUtils.Copy(headers, dummy.Headers); + return dummy.Headers; + } + + /// Creates a copy of a set of HTTP response headers. + /// The headers to copy. + /// A new set of HTTP response headers, with the same contents as the set provided. + [return: NotNullIfNotNull(nameof(headers))] + public static HttpResponseHeaders? Copy(HttpResponseHeaders? headers) { + if (headers is null) { + return null; + } + // There is no way to construct a copy of HTTP headers directly at the moment (see dotnet/runtime#95912). + using var dummy = new HttpResponseMessage(); + HttpUtils.Copy(headers, dummy.Headers); + return dummy.Headers; + } + /// Create a user agent header containing the name and version of the assembly containing a particular type. /// The type to use to determine the assembly name and version. /// diff --git a/MetaBrainz.Common/MetaBrainz.Common.csproj b/MetaBrainz.Common/MetaBrainz.Common.csproj index adc7a4e..5994183 100644 --- a/MetaBrainz.Common/MetaBrainz.Common.csproj +++ b/MetaBrainz.Common/MetaBrainz.Common.csproj @@ -11,7 +11,7 @@ 2022, 2023 MetaBrainz.Common MetaBrainz - 2.1.1-pre + 3.0.0-pre diff --git a/public-api/MetaBrainz.Common.net6.0.cs.md b/public-api/MetaBrainz.Common.net6.0.cs.md index b505a78..3496b84 100644 --- a/public-api/MetaBrainz.Common.net6.0.cs.md +++ b/public-api/MetaBrainz.Common.net6.0.cs.md @@ -38,11 +38,15 @@ public class HttpError : System.Exception { public get; } - string Message { - public override get; + string? Reason { + public get; } - string? Reason { + System.Net.Http.Headers.HttpRequestHeaders? RequestHeaders { + public get; + } + + System.Uri? RequestUri { public get; } @@ -58,12 +62,7 @@ public class HttpError : System.Exception { public get; } - [System.ObsoleteAttribute("Use FromResponse or FromResponseAsync instead.")] - public HttpError(System.Net.Http.HttpResponseMessage response); - - public HttpError(System.Net.HttpStatusCode status, string? reason, System.Exception? cause = null); - - public HttpError(System.Net.HttpStatusCode status, string? reason, System.Version version, System.Exception? cause = null); + public HttpError(System.Net.HttpStatusCode status, string? reason = null, System.Version? version = null, string? message = null, System.Exception? cause = null); public static HttpError FromResponse(System.Net.Http.HttpResponseMessage response); @@ -81,6 +80,15 @@ public static class HttpUtils { public const string UnknownAssemblyName = "*Unknown Assembly*"; + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("headers")] + public static System.Net.Http.Headers.HttpContentHeaders? Copy(System.Net.Http.Headers.HttpContentHeaders? headers); + + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("headers")] + public static System.Net.Http.Headers.HttpRequestHeaders? Copy(System.Net.Http.Headers.HttpRequestHeaders? headers); + + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("headers")] + public static System.Net.Http.Headers.HttpResponseHeaders? Copy(System.Net.Http.Headers.HttpResponseHeaders? headers); + public static System.Net.Http.Headers.ProductInfoHeaderValue CreateUserAgentHeader(); public static System.Net.Http.HttpResponseMessage EnsureSuccessful(this System.Net.Http.HttpResponseMessage response); diff --git a/public-api/MetaBrainz.Common.net8.0.cs.md b/public-api/MetaBrainz.Common.net8.0.cs.md index a76b53e..87a532b 100644 --- a/public-api/MetaBrainz.Common.net8.0.cs.md +++ b/public-api/MetaBrainz.Common.net8.0.cs.md @@ -38,11 +38,15 @@ public class HttpError : System.Exception { public get; } - string Message { - public override get; + string? Reason { + public get; } - string? Reason { + System.Net.Http.Headers.HttpRequestHeaders? RequestHeaders { + public get; + } + + System.Uri? RequestUri { public get; } @@ -58,12 +62,7 @@ public class HttpError : System.Exception { public get; } - [System.ObsoleteAttribute("Use FromResponse or FromResponseAsync instead.")] - public HttpError(System.Net.Http.HttpResponseMessage response); - - public HttpError(System.Net.HttpStatusCode status, string? reason, System.Exception? cause = null); - - public HttpError(System.Net.HttpStatusCode status, string? reason, System.Version version, System.Exception? cause = null); + public HttpError(System.Net.HttpStatusCode status, string? reason = null, System.Version? version = null, string? message = null, System.Exception? cause = null); public static HttpError FromResponse(System.Net.Http.HttpResponseMessage response); @@ -81,6 +80,15 @@ public static class HttpUtils { public const string UnknownAssemblyName = "*Unknown Assembly*"; + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("headers")] + public static System.Net.Http.Headers.HttpContentHeaders? Copy(System.Net.Http.Headers.HttpContentHeaders? headers); + + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("headers")] + public static System.Net.Http.Headers.HttpRequestHeaders? Copy(System.Net.Http.Headers.HttpRequestHeaders? headers); + + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("headers")] + public static System.Net.Http.Headers.HttpResponseHeaders? Copy(System.Net.Http.Headers.HttpResponseHeaders? headers); + public static System.Net.Http.Headers.ProductInfoHeaderValue CreateUserAgentHeader(); public static System.Net.Http.HttpResponseMessage EnsureSuccessful(this System.Net.Http.HttpResponseMessage response);