Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More HTTP stuff #21

Merged
merged 4 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 37 additions & 41 deletions MetaBrainz.Common/HttpError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,68 +14,48 @@ namespace MetaBrainz.Common;
[PublicAPI]
public class HttpError : Exception {

/// <summary>Creates a new HTTP error.</summary>
/// <param name="response">The response to take the status code and reason from.</param>
[Obsolete($"Use {nameof(HttpError.FromResponse)} or {nameof(HttpError.FromResponseAsync)} instead.")]
public HttpError(HttpResponseMessage response) : this(response.StatusCode, response.ReasonPhrase, response.Version) { }

/// <summary>Creates a new HTTP error.</summary>
/// <param name="status">The status code for the error.</param>
/// <param name="reason">The reason phrase associated with the error.</param>
/// <param name="cause">The exception that caused this one, if any.</param>
public HttpError(HttpStatusCode status, string? reason, Exception? cause = null) : base(null, cause) {
this.Reason = reason;
this.Status = status;
}

/// <summary>Creates a new HTTP error.</summary>
/// <param name="status">The status code for the error.</param>
/// <param name="reason">The reason phrase associated with the error.</param>
/// <param name="version">The HTTP message version.</param>
/// <param name="message">
/// The message to use; if this is not specified or <see langword="null"/>, a message will be constructed based on
/// <paramref name="status"/>, <paramref name="reason"/> and <paramref name="version"/>.
/// </param>
/// <param name="cause">The exception that caused this one, if any.</param>
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;
}

/// <summary>The content (assumed to be text) of the response that triggered the error, if available.</summary>
/// <summary>The content (assumed to be text) of the error response, if available.</summary>
public string? Content { get; private init; }

/// <summary>The content headers of the response that triggered the error, if available.</summary>
/// <summary>The content headers of the error response, if available.</summary>
public HttpContentHeaders? ContentHeaders { get; private init; }

/// <summary>Gets a textual representation of the HTTP error.</summary>
/// <returns>A textual representation of the HTTP error.</returns>
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();
}
}

/// <summary>The reason phrase associated with the error.</summary>
public string? Reason { get; }

/// <summary>The headers of the response that triggered the error, if available.</summary>
/// <summary>The headers of the request that provoked the error response, if available.</summary>
public HttpRequestHeaders? RequestHeaders { get; private init; }

/// <summary>The URI for the request that provoked the error response, if available.</summary>
public Uri? RequestUri { get; private init; }

/// <summary>The headers of the error response, if available.</summary>
public HttpResponseHeaders? ResponseHeaders { get; private init; }

/// <summary>The status code for the error.</summary>
public HttpStatusCode Status { get; }

/// <summary>The HTTP message version from the response that triggered the error, if available.</summary>
/// <summary>The HTTP message version from the error response, if available.</summary>
public Version? Version { get; private init; }

/// <summary>Creates a new HTTP error based on an response message.</summary>
/// <param name="response">The response message that triggered the error.</param>
/// <param name="response">The response.</param>
/// <returns>A new HTTP error containing information taken from the response message.</returns>
public static HttpError FromResponse(HttpResponseMessage response) => AsyncUtils.ResultOf(HttpError.FromResponseAsync(response));

Expand All @@ -85,14 +65,30 @@ public class HttpError : Exception {
/// <returns>A new HTTP error containing information taken from the response message.</returns>
public static async Task<HttpError> 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();
}

}
53 changes: 50 additions & 3 deletions MetaBrainz.Common/HttpUtils.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,6 +17,54 @@ namespace MetaBrainz.Common;
[PublicAPI]
public static class HttpUtils {

/// <summary>Creates a copy of a set of HTTP content headers.</summary>
/// <param name="headers">The headers to copy.</param>
/// <returns>A new set of HTTP content headers, with the same contents as the set provided.</returns>
[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<byte>());
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);
}
}

/// <summary>Creates a copy of a set of HTTP request headers.</summary>
/// <param name="headers">The headers to copy.</param>
/// <returns>A new set of HTTP request headers, with the same contents as the set provided.</returns>
[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;
}

/// <summary>Creates a copy of a set of HTTP response headers.</summary>
/// <param name="headers">The headers to copy.</param>
/// <returns>A new set of HTTP response headers, with the same contents as the set provided.</returns>
[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;
}

/// <summary>Create a user agent header containing the name and version of the assembly containing a particular type.</summary>
/// <typeparam name="T">The type to use to determine the assembly name and version.</typeparam>
/// <returns>
Expand Down
2 changes: 1 addition & 1 deletion MetaBrainz.Common/MetaBrainz.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<PackageCopyrightYears>2022, 2023</PackageCopyrightYears>
<PackageRepositoryName>MetaBrainz.Common</PackageRepositoryName>
<PackageTags>MetaBrainz</PackageTags>
<Version>2.1.1-pre</Version>
<Version>3.0.0-pre</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
26 changes: 17 additions & 9 deletions public-api/MetaBrainz.Common.net6.0.cs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);

Expand All @@ -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<T>();

public static System.Net.Http.HttpResponseMessage EnsureSuccessful(this System.Net.Http.HttpResponseMessage response);
Expand Down
26 changes: 17 additions & 9 deletions public-api/MetaBrainz.Common.net8.0.cs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);

Expand All @@ -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<T>();

public static System.Net.Http.HttpResponseMessage EnsureSuccessful(this System.Net.Http.HttpResponseMessage response);
Expand Down