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

Implement Response.Throw() #4

Draft
wants to merge 15 commits into
base: users/annelo/synapse-rbac-llc
Choose a base branch
from
Draft
37 changes: 37 additions & 0 deletions sdk/core/Azure.Core/src/Pipeline/AzureError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Text;

namespace Azure.Core.Pipeline
{
/// <summary>
/// </summary>
public class AzureError
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per arch board, we won't formalize this yet

{
/// <summary>
///
/// </summary>
public AzureError()
{
Data = new Dictionary<object, object?>();
}

/// <summary>
/// Gets or sets the error message.
/// </summary>
public string? Message { get; set; }

/// <summary>
/// Gets or sets the error code.
/// </summary>
public string? ErrorCode { get; set; }

/// <summary>
/// Gets an additional data returned with the error response.
/// </summary>
public IDictionary<object, object?> Data { get; }
}
}
6 changes: 5 additions & 1 deletion sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ public ValueTask SendAsync(HttpMessage message, CancellationToken cancellationTo
{
message.CancellationToken = cancellationToken;
AddHttpMessageProperties(message);
return _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1));
var value = _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1));
message.Response.EvaluateError(message);
return value;
}

/// <summary>
Expand All @@ -83,7 +85,9 @@ public void Send(HttpMessage message, CancellationToken cancellationToken)
message.CancellationToken = cancellationToken;
AddHttpMessageProperties(message);
_pipeline.Span[0].Process(message, _pipeline.Slice(1));
message.Response.EvaluateError(message);
}

/// <summary>
/// Invokes the pipeline asynchronously with the provided request.
/// </summary>
Expand Down
47 changes: 47 additions & 0 deletions sdk/core/Azure.Core/src/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Core.Pipeline;

namespace Azure
{
Expand Down Expand Up @@ -111,6 +113,51 @@ public virtual BinaryData Content
/// <returns>The <see cref="IEnumerable{T}"/> enumerating <see cref="HttpHeader"/> in the response.</returns>
protected internal abstract IEnumerable<HttpHeader> EnumerateHeaders();

internal bool? IsError { get; set; }

internal ResponseClassifier? ResponseClassifier { get; set; }

internal void EvaluateError(HttpMessage message)
{
if (!IsError.HasValue)
{
IsError = message.ResponseClassifier.IsErrorResponse(message);
ResponseClassifier = message.ResponseClassifier;
}
}

/// <summary>
/// If the response is an error response, throw a RequestFailedException.
/// </summary>
public void ThrowIfError()
{
if (!IsError.HasValue)
{
throw new InvalidOperationException("IsError value should have been cached by the pipeline.");
}

if (IsError.Value)
{
throw ResponseClassifier!.CreateRequestFailedException(this);
}
}

/// <summary>
/// If the response is an error response, throw a RequestFailedException.
/// </summary>
public async Task ThrowIfErrorAsync()
{
if (!IsError.HasValue)
{
throw new InvalidOperationException("IsError value should have been cached by the pipeline.");
}

if (IsError.Value)
{
throw await ResponseClassifier!.CreateRequestFailedExceptionAsync(this).ConfigureAwait(false);
}
}

/// <summary>
/// Creates a new instance of <see cref="Response{T}"/> with the provided value and HTTP response.
/// </summary>
Expand Down
217 changes: 217 additions & 0 deletions sdk/core/Azure.Core/src/ResponseClassifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Core.Pipeline;

namespace Azure.Core
{
Expand All @@ -11,6 +18,30 @@ namespace Azure.Core
/// </summary>
public class ResponseClassifier
{
private const string DefaultFailureMessage = "Service request failed.";

private readonly HttpMessageSanitizer _sanitizer;

/// <summary>
/// Initializes a new instance of <see cref="ResponseClassifier"/>.
/// </summary>
public ResponseClassifier() : this(new DefaultClientOptions())
{
}

/// <summary>
/// </summary>

/// <summary>
/// Initializes a new instance of <see cref="ResponseClassifier"/>.
/// </summary>
public ResponseClassifier(ClientOptions options)
{
_sanitizer = new HttpMessageSanitizer(
options?.Diagnostics.LoggedQueryParameters.ToArray() ?? Array.Empty<string>(),
options?.Diagnostics.LoggedHeaderNames.ToArray() ?? Array.Empty<string>());
}

/// <summary>
/// Specifies if the request contained in the <paramref name="message"/> should be retried.
/// </summary>
Expand Down Expand Up @@ -57,5 +88,191 @@ public virtual bool IsErrorResponse(HttpMessage message)
var statusKind = message.Response.Status / 100;
return statusKind == 4 || statusKind == 5;
}

/// <summary>
/// Creates an instance of <see cref="RequestFailedException"/> for the provided failed <see cref="Response"/>.
/// </summary>
/// <param name="response"></param>
/// <param name="error"></param>
/// <param name="innerException"></param>
/// <returns></returns>
public virtual async ValueTask<RequestFailedException> CreateRequestFailedExceptionAsync(
Response response,
AzureError? error = null,
Exception? innerException = null)
{
var content = await ReadContentAsync(response, true).ConfigureAwait(false);
return CreateRequestFailedExceptionWithContent(response, content, error, innerException);
}

/// <summary>
/// Creates an instance of <see cref="RequestFailedException"/> for the provided failed <see cref="Response"/>.
/// </summary>
/// <param name="response"></param>
/// <param name="error"></param>
/// <param name="innerException"></param>
/// <returns></returns>
public virtual RequestFailedException CreateRequestFailedException(
Response response,
AzureError? error = null,
Exception? innerException = null)
{
string? content = ReadContentAsync(response, false).EnsureCompleted();
return CreateRequestFailedExceptionWithContent(response, content, error, innerException);
}

/// <summary>
/// Partial method that can optionally be defined to extract the error
/// message, code, and details in a service specific manner.
/// </summary>
/// <param name="response">The response headers.</param>
/// <param name="textContent">The extracted text content</param>
protected virtual AzureError? ExtractFailureContent(Response response, string? textContent)
{
try
{
// Optimistic check for JSON object we expect
if (textContent == null ||
!textContent.StartsWith("{", StringComparison.OrdinalIgnoreCase))
return null;

var extractFailureContent = new AzureError();

using JsonDocument document = JsonDocument.Parse(textContent);
if (document.RootElement.TryGetProperty("error", out var errorProperty))
{
if (errorProperty.TryGetProperty("code", out var codeProperty))
{
extractFailureContent.ErrorCode = codeProperty.GetString();
}
if (errorProperty.TryGetProperty("message", out var messageProperty))
{
extractFailureContent.Message = messageProperty.GetString();
}
}

return extractFailureContent;
}
catch (Exception)
{
// Ignore any failures - unexpected content will be
// included verbatim in the detailed error message
}

return null;
}

private RequestFailedException CreateRequestFailedExceptionWithContent(
Response response,
string? content,
AzureError? details,
Exception? innerException)
{
var errorInformation = ExtractFailureContent(response, content);

var message = details?.Message ?? errorInformation?.Message ?? DefaultFailureMessage;
var errorCode = details?.ErrorCode ?? errorInformation?.ErrorCode;

IDictionary<object, object?>? data = null;
if (errorInformation?.Data != null)
{
if (details?.Data == null)
{
data = errorInformation.Data;
}
else
{
data = new Dictionary<object, object?>(details.Data);
foreach (var pair in errorInformation.Data)
{
data[pair.Key] = pair.Value;
}
}
}

StringBuilder messageBuilder = new StringBuilder()
.AppendLine(message)
.Append("Status: ")
.Append(response.Status.ToString(CultureInfo.InvariantCulture));

if (!string.IsNullOrEmpty(response.ReasonPhrase))
{
messageBuilder.Append(" (")
.Append(response.ReasonPhrase)
.AppendLine(")");
}
else
{
messageBuilder.AppendLine();
}

if (!string.IsNullOrWhiteSpace(errorCode))
{
messageBuilder.Append("ErrorCode: ")
.Append(errorCode)
.AppendLine();
}

if (data != null && data.Count > 0)
{
messageBuilder
.AppendLine()
.AppendLine("Additional Information:");
foreach (KeyValuePair<object, object?> info in data)
{
messageBuilder
.Append(info.Key)
.Append(": ")
.Append(info.Value)
.AppendLine();
}
}

if (content != null)
{
messageBuilder
.AppendLine()
.AppendLine("Content:")
.AppendLine(content);
}

messageBuilder
.AppendLine()
.AppendLine("Headers:");

foreach (HttpHeader responseHeader in response.Headers)
{
string headerValue = _sanitizer.SanitizeHeader(responseHeader.Name, responseHeader.Value);
messageBuilder.AppendLine($"{responseHeader.Name}: {headerValue}");
}

var exception = new RequestFailedException(response.Status, messageBuilder.ToString(), errorCode, innerException);

if (data != null)
{
foreach (KeyValuePair<object, object?> keyValuePair in data)
{
exception.Data.Add(keyValuePair.Key, keyValuePair.Value);
}
}

return exception;
}

private static async ValueTask<string?> ReadContentAsync(Response response, bool async)
{
string? content = null;

if (response.ContentStream != null &&
ContentTypeUtilities.TryGetTextEncoding(response.Headers.ContentType, out var encoding))
{
using (var streamReader = new StreamReader(response.ContentStream, encoding))
{
content = async ? await streamReader.ReadToEndAsync().ConfigureAwait(false) : streamReader.ReadToEnd();
}
}

return content;
}
}
}
Loading