Skip to content

Commit

Permalink
Merge pull request #21 from Zastai/more-http-stuff
Browse files Browse the repository at this point in the history
More HTTP stuff
  • Loading branch information
Zastai committed Dec 18, 2023
2 parents dd92579 + 18f357e commit 9a6389b
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 63 deletions.
78 changes: 37 additions & 41 deletions MetaBrainz.Common/HttpError.cs
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
@@ -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
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
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
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

0 comments on commit 9a6389b

Please sign in to comment.