Skip to content

Commit

Permalink
Merge pull request #10 from Zastai/more-http-utils
Browse files Browse the repository at this point in the history
More HTTP utilities
  • Loading branch information
Zastai authored Dec 5, 2023
2 parents ef04ea3 + 4beb146 commit e8e008d
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 5 deletions.
37 changes: 37 additions & 0 deletions MetaBrainz.Common/HttpError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Net;
using System.Net.Http;

using JetBrains.Annotations;

namespace MetaBrainz.Common;

/// <summary>An error reported by an HTTP response.</summary>
[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>
public HttpError(HttpResponseMessage response) : this(response.StatusCode, response.ReasonPhrase) { }

/// <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>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}'";

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

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

}
36 changes: 31 additions & 5 deletions MetaBrainz.Common/HttpUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,38 @@ public static ProductInfoHeaderValue CreateUserAgentHeader<T>() {
return new ProductInfoHeaderValue(an.Name ?? HttpUtils.UnknownAssemblyName, an.Version?.ToString());
}

/// <summary>Checks a response to ensure it was successful.</summary>
/// <param name="response">The response whose status should be checked.</param>
/// <returns><paramref name="response"/>.</returns>
/// <exception cref="HttpError">When the response did not have a successful status.</exception>
public static HttpResponseMessage EnsureSuccessful(this HttpResponseMessage response)
=> AsyncUtils.ResultOf(response.EnsureSuccessfulAsync());

/// <summary>Checks a response to ensure it was successful.</summary>
/// <param name="response">The response whose status should be checked.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns><paramref name="response"/>.</returns>
/// <exception cref="HttpError">When the response did not have a successful status.</exception>
public static async ValueTask<HttpResponseMessage> EnsureSuccessfulAsync(this HttpResponseMessage response,
CancellationToken cancellationToken = new()) {
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);
}

/// <summary>Gets the content encoding based on content headers.</summary>
/// <param name="contentHeaders">The headers to get the information from.</param>
/// <returns>
/// The content encoding extracted from the headers, or "utf-8" as fallback if no explicit specification was found.
/// </returns>
public static string GetContentEncoding(HttpContentHeaders contentHeaders) {
public static string GetContentEncoding(this HttpContentHeaders contentHeaders) {
var characterSet = contentHeaders.ContentEncoding.FirstOrDefault();
if (string.IsNullOrWhiteSpace(characterSet)) {
// Fall back on the charset portion of the content type.
Expand All @@ -48,20 +74,20 @@ public static string GetContentEncoding(HttpContentHeaders contentHeaders) {
/// <summary>Gets the content of an HTTP response as a string.</summary>
/// <param name="response">The response to process.</param>
/// <returns>The content of <paramref name="response"/> as a string.</returns>
public static string GetStringContent(HttpResponseMessage response)
=> AsyncUtils.ResultOf(HttpUtils.GetStringContentAsync(response));
public static string GetStringContent(this HttpResponseMessage response)
=> AsyncUtils.ResultOf(response.GetStringContentAsync());

/// <summary>Gets the content of an HTTP response as a string.</summary>
/// <param name="response">The response to process.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>The content of <paramref name="response"/> as a string.</returns>
public static async Task<string> GetStringContentAsync(HttpResponseMessage response,
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 stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var _ = stream.ConfigureAwait(false);
var characterSet = HttpUtils.GetContentEncoding(content.Headers);
var characterSet = content.Headers.GetContentEncoding();
using var sr = new StreamReader(stream, Encoding.GetEncoding(characterSet), false, 1024, true);
#if NET6_0
var text = await sr.ReadToEndAsync().ConfigureAwait(false);
Expand Down

0 comments on commit e8e008d

Please sign in to comment.