Skip to content

Commit

Permalink
Merge pull request #14 from Zastai/httputils-adjustments
Browse files Browse the repository at this point in the history
`HttpUtils` adjustments
  • Loading branch information
Zastai authored Dec 12, 2023
2 parents 6c1232d + 3e1e32d commit 16f099b
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 17 deletions.
2 changes: 2 additions & 0 deletions MetaBrainz.Common.sln
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Support Files", "Support Files", "{96AE5FBC-DDA2-423D-8A21-FDC6A97015D4}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
build-package.ps1 = build-package.ps1
Directory.Packages.props = Directory.Packages.props
LICENSE.md = LICENSE.md
Expand Down
69 changes: 65 additions & 4 deletions MetaBrainz.Common/HttpError.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using JetBrains.Annotations;

Expand All @@ -12,7 +16,8 @@ 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>
public HttpError(HttpResponseMessage response) : this(response.StatusCode, response.ReasonPhrase) { }
[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>
Expand All @@ -23,15 +28,71 @@ public HttpError(HttpStatusCode status, string? reason, Exception? cause = null)
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="cause">The exception that caused this one, if any.</param>
public HttpError(HttpStatusCode status, string? reason, Version version, Exception? cause = null) : base(null, 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>
public string? Content { get; private init; }

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

/// <summary>Gets a textual representation of the HTTP error.</summary>
/// <returns>A string of the form <c>HTTP nnn/StatusName 'REASON'</c>.</returns>
public override string Message
=> this.Reason is null ? $"HTTP {(int) this.Status}/{this.Status}" : $"HTTP {(int) this.Status}/{this.Status} '{this.Reason}'";
/// <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>
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>
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>
/// <returns>A new HTTP error containing information taken from the response message.</returns>
public static HttpError FromResponse(HttpResponseMessage response) => AsyncUtils.ResultOf(HttpError.FromResponseAsync(response));

/// <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="cancellationToken">The cancellation token to cancel the operation.</param>
/// <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,
Version = response.Version,
};
}

}
27 changes: 15 additions & 12 deletions MetaBrainz.Common/HttpUtils.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
#define TRACE

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -44,13 +46,7 @@ public static async ValueTask<HttpResponseMessage> EnsureSuccessfulAsync(this Ht
if (response.IsSuccessStatusCode) {
return response;
}
#if DEBUG
// This also prints the contents.
await response.GetStringContentAsync(cancellationToken);
#else
await ValueTask.CompletedTask;
#endif
throw new HttpError(response);
throw await HttpError.FromResponseAsync(response, cancellationToken);
}

/// <summary>Gets the content encoding based on content headers.</summary>
Expand Down Expand Up @@ -83,21 +79,28 @@ public static string GetStringContent(this HttpResponseMessage response)
/// <returns>The content of <paramref name="response"/> as a string.</returns>
public static async Task<string> GetStringContentAsync(this HttpResponseMessage response,
CancellationToken cancellationToken = new()) {
var content = response.Content;
Debug.Print($"[{DateTime.UtcNow}] => RESPONSE ({content.Headers.ContentType}): {content.Headers.ContentLength} bytes");
var headers = response.Content.Headers;
HttpUtils.TraceSource.TraceInformation("RESPONSE ({0}): {1} bytes", headers.ContentType, headers.ContentLength);
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var _ = stream.ConfigureAwait(false);
var characterSet = content.Headers.GetContentEncoding();
var characterSet = headers.GetContentEncoding();
using var sr = new StreamReader(stream, Encoding.GetEncoding(characterSet), false, 1024, true);
#if NET6_0
var text = await sr.ReadToEndAsync().ConfigureAwait(false);
#else
var text = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
#endif
Debug.Print($"[{DateTime.UtcNow}] => RESPONSE TEXT: {TextUtils.FormatMultiLine(text)}");
if (HttpUtils.TraceSource.Switch.ShouldTrace(TraceEventType.Information)) {
HttpUtils.TraceSource.TraceInformation("RESPONSE TEXT: {0}", TextUtils.FormatMultiLine(text));
}
return text;
}

/// <summary>The trace source (named 'MetaBrainz.Common.HttpUtils', with a switch of the same name) used by this class.</summary>
public static readonly TraceSource TraceSource = new("MetaBrainz.Common.HttpUtils") {
Switch = new SourceSwitch("MetaBrainz.Common.HttpUtils", "Off")
};

/// <summary>The name used by <see cref="CreateUserAgentHeader{T}"/> when no assembly name is available.</summary>
public const string UnknownAssemblyName = "*Unknown Assembly*";

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.0.1-pre</Version>
<Version>2.1.0-pre</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
25 changes: 25 additions & 0 deletions public-api/MetaBrainz.Common.net6.0.cs.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public static class AsyncUtils {
```cs
public class HttpError : System.Exception {

string? Content {
public get;
}

System.Net.Http.Headers.HttpContentHeaders? ContentHeaders {
public get;
}

string Message {
public override get;
}
Expand All @@ -38,14 +46,29 @@ public class HttpError : System.Exception {
public get;
}

System.Net.Http.Headers.HttpResponseHeaders? ResponseHeaders {
public get;
}

System.Net.HttpStatusCode Status {
public get;
}

System.Version? Version {
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 static HttpError FromResponse(System.Net.Http.HttpResponseMessage response);

public static System.Threading.Tasks.Task<HttpError> FromResponseAsync(System.Net.Http.HttpResponseMessage response, System.Threading.CancellationToken cancellationToken = default);

}
```

Expand All @@ -54,6 +77,8 @@ public class HttpError : System.Exception {
```cs
public static class HttpUtils {

public static readonly System.Diagnostics.TraceSource TraceSource;

public const string UnknownAssemblyName = "*Unknown Assembly*";

public static System.Net.Http.Headers.ProductInfoHeaderValue CreateUserAgentHeader<T>();
Expand Down
25 changes: 25 additions & 0 deletions public-api/MetaBrainz.Common.net8.0.cs.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public static class AsyncUtils {
```cs
public class HttpError : System.Exception {

string? Content {
public get;
}

System.Net.Http.Headers.HttpContentHeaders? ContentHeaders {
public get;
}

string Message {
public override get;
}
Expand All @@ -38,14 +46,29 @@ public class HttpError : System.Exception {
public get;
}

System.Net.Http.Headers.HttpResponseHeaders? ResponseHeaders {
public get;
}

System.Net.HttpStatusCode Status {
public get;
}

System.Version? Version {
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 static HttpError FromResponse(System.Net.Http.HttpResponseMessage response);

public static System.Threading.Tasks.Task<HttpError> FromResponseAsync(System.Net.Http.HttpResponseMessage response, System.Threading.CancellationToken cancellationToken = default);

}
```

Expand All @@ -54,6 +77,8 @@ public class HttpError : System.Exception {
```cs
public static class HttpUtils {

public static readonly System.Diagnostics.TraceSource TraceSource;

public const string UnknownAssemblyName = "*Unknown Assembly*";

public static System.Net.Http.Headers.ProductInfoHeaderValue CreateUserAgentHeader<T>();
Expand Down

0 comments on commit 16f099b

Please sign in to comment.