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

Add Response to RequestFailedException #35716

Merged
merged 20 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions sdk/core/Azure.Core.TestFramework/src/TestEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ private async Task ExtendResourceGroupExpirationAsync()
// unexpected response => throw an exception
if (response.Status != 200)
{
throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(response);
throw new RequestFailedException(response);
}

// parse the response
Expand Down Expand Up @@ -392,7 +392,7 @@ private async Task ExtendResourceGroupExpirationAsync()
response = await pipeline.SendRequestAsync(request, CancellationToken.None);
if (response.Status != 200)
{
throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(response);
throw new RequestFailedException(response);
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions sdk/core/Azure.Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Features Added

- Added the `GetRawResponse` method to `RequestFailedException`.
- Added overloads of `Operation<T>.WaitForCompletion` and `Operation.WaitForCompletionResponse` that take a `DelayStrategy`.

### Breaking Changes

### Bugs Fixed
Expand Down
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net461.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ public RequestFailedException(string message, System.Exception? innerException)
public string? ErrorCode { get { throw null; } }
public int Status { get { throw null; } }
public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { }
public Azure.Response? GetRawResponse() { throw null; }
JoshLove-msft marked this conversation as resolved.
Show resolved Hide resolved
}
public abstract partial class Response : System.IDisposable
{
Expand Down
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net5.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ public RequestFailedException(string message, System.Exception? innerException)
public string? ErrorCode { get { throw null; } }
public int Status { get { throw null; } }
public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { }
public Azure.Response? GetRawResponse() { throw null; }
}
public abstract partial class Response : System.IDisposable
{
Expand Down
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net6.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ public RequestFailedException(string message, System.Exception? innerException)
public string? ErrorCode { get { throw null; } }
public int Status { get { throw null; } }
public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { }
public Azure.Response? GetRawResponse() { throw null; }
}
public abstract partial class Response : System.IDisposable
{
Expand Down
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ public RequestFailedException(string message, System.Exception? innerException)
public string? ErrorCode { get { throw null; } }
public int Status { get { throw null; } }
public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { }
public Azure.Response? GetRawResponse() { throw null; }
}
public abstract partial class Response : System.IDisposable
{
Expand Down
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ public RequestFailedException(string message, System.Exception? innerException)
public string? ErrorCode { get { throw null; } }
public int Status { get { throw null; } }
public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { }
public Azure.Response? GetRawResponse() { throw null; }
}
public abstract partial class Response : System.IDisposable
{
Expand Down
56 changes: 29 additions & 27 deletions sdk/core/Azure.Core/src/RequestFailedException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public class RequestFailedException : Exception, ISerializable
/// </summary>
public string? ErrorCode { get; }

/// <summary>
/// Gets the response, if any, that led to the exception.
JoshLove-msft marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
private readonly Response? _response;

/// <summary>Initializes a new instance of the <see cref="RequestFailedException"></see> class with a specified error message.</summary>
/// <param name="message">The message that describes the error.</param>
public RequestFailedException(string message) : this(0, message)
Expand Down Expand Up @@ -105,6 +110,7 @@ public RequestFailedException(Response response)
public RequestFailedException(Response response, Exception? innerException)
: this(response.Status, GetRequestFailedExceptionContent(response), innerException)
{
_response = response;
heaths marked this conversation as resolved.
Show resolved Hide resolved
}

/// <inheritdoc />
Expand All @@ -126,8 +132,15 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont
base.GetObjectData(info, context);
}

/// <summary>
/// Gets the response, if any, that led to the exception.
/// </summary>
public Response? GetRawResponse() => _response;

internal static (string FormattedError, string? ErrorCode, IDictionary<string, string>? Data) GetRequestFailedExceptionContent(Response response)
{
BufferResponseIfNeeded(response);
JoshLove-msft marked this conversation as resolved.
Show resolved Hide resolved

bool parseSuccess = response.RequestFailedDetailsParser == null ? TryExtractErrorContent(response, out ResponseError? error, out IDictionary<string, string>? data) : response.RequestFailedDetailsParser.TryParse(response, out error, out data);
StringBuilder messageBuilder = new();

Expand Down Expand Up @@ -191,25 +204,23 @@ internal static (string FormattedError, string? ErrorCode, IDictionary<string, s
return (formatMessage, error?.Code, data);
}

/// <summary>
/// This is intentionally sync-only as it will only be called by the ctor.
/// </summary>
/// <param name="response"></param>
/// <returns></returns>
internal static string? ReadContent(Response response)
private static void BufferResponseIfNeeded(Response response)
{
string? content = null;

if (response.ContentStream != null &&
ContentTypeUtilities.TryGetTextEncoding(response.Headers.ContentType, out var encoding))
// Buffer into a memory stream if not already buffered
if (response.ContentStream is null or MemoryStream)
JoshLove-msft marked this conversation as resolved.
Show resolved Hide resolved
JoshLove-msft marked this conversation as resolved.
Show resolved Hide resolved
{
using (var streamReader = new StreamReader(response.ContentStream, encoding))
{
content = streamReader.ReadToEnd();
}
return;
}

return content;
var bufferedStream = new MemoryStream();
response.ContentStream.CopyTo(bufferedStream);

// Dispose the unbuffered stream
response.ContentStream.Dispose();

// Reset the position of the buffered stream and set it on the response
bufferedStream.Position = 0;
response.ContentStream = bufferedStream;
}

internal static bool TryExtractErrorContent(Response response, out ResponseError? error, out IDictionary<string, string>? data)
Expand All @@ -218,18 +229,9 @@ internal static bool TryExtractErrorContent(Response response, out ResponseError
data = null;
try
{
string? content = null;
if (response.ContentStream != null && response.ContentStream.CanSeek)
{
content = response.Content.ToString();
}
else
{
// this path should only happen in exceptional cases such as when
// the RFE ctor was called directly by client or customer code with an un-buffered response.
// Generated code would never do this.
content = ReadContent(response);
JoshLove-msft marked this conversation as resolved.
Show resolved Hide resolved
}
// The response content is buffered at this point.
string? content = response.Content.ToString();

// Optimistic check for JSON object we expect
if (content == null || !content.StartsWith("{", StringComparison.OrdinalIgnoreCase))
{
Expand Down
159 changes: 0 additions & 159 deletions sdk/core/Azure.Core/src/Shared/ClientDiagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ namespace Azure.Core.Pipeline
{
internal class ClientDiagnostics : DiagnosticScopeFactory
{
private const string DefaultMessage = "Service request failed.";

private readonly HttpMessageSanitizer _sanitizer;

/// <summary>
/// Initializes a new instance of the <see cref="ClientDiagnostics"/> class.
/// </summary>
Expand Down Expand Up @@ -55,7 +51,6 @@ public ClientDiagnostics(ClientOptions options, bool? suppressNestedClientActivi
public ClientDiagnostics(string optionsNamespace, string? providerNamespace, DiagnosticsOptions diagnosticsOptions, bool? suppressNestedClientActivities = null)
: base(optionsNamespace, providerNamespace, diagnosticsOptions.IsDistributedTracingEnabled, suppressNestedClientActivities.GetValueOrDefault(false))
{
_sanitizer = CreateMessageSanitizer(diagnosticsOptions);
}

internal static HttpMessageSanitizer CreateMessageSanitizer(DiagnosticsOptions diagnostics)
Expand All @@ -65,154 +60,6 @@ internal static HttpMessageSanitizer CreateMessageSanitizer(DiagnosticsOptions d
diagnostics.LoggedHeaderNames.ToArray());
}

internal static ResponseError? ExtractAzureErrorContent(string? content)
JoshLove-msft marked this conversation as resolved.
Show resolved Hide resolved
{
try
{
// Optimistic check for JSON object we expect
if (content == null ||
!content.StartsWith("{", StringComparison.OrdinalIgnoreCase)) return null;

return JsonSerializer.Deserialize<ErrorResponse>(content)?.Error;
}
catch (Exception)
{
// Ignore any failures - unexpected content will be
// included verbatim in the detailed error message
}

return null;
}

public async ValueTask<RequestFailedException> CreateRequestFailedExceptionAsync(Response response, ResponseError? error = null, IDictionary<string, string>? additionalInfo = null, Exception? innerException = null)
{
if (GetType() == typeof(ClientDiagnostics) && error is null && additionalInfo is null)
{
return new RequestFailedException(response, innerException);
}

var content = await ReadContentAsync(response, true).ConfigureAwait(false);
return CreateRequestFailedExceptionWithContent(response, error, content, additionalInfo, innerException);
}

public RequestFailedException CreateRequestFailedException(Response response, ResponseError? error = null, IDictionary<string, string>? additionalInfo = null, Exception? innerException = null)
{
if (GetType() == typeof(ClientDiagnostics) && error is null && additionalInfo is null)
{
return new RequestFailedException(response, innerException);
}

string? content = ReadContentAsync(response, false).EnsureCompleted();
return CreateRequestFailedExceptionWithContent(response, error, content, additionalInfo, innerException);
}

private RequestFailedException CreateRequestFailedExceptionWithContent(
Response response,
ResponseError? error = null,
string? content = null,
IDictionary<string, string>? additionalInfo = null,
Exception? innerException = null)
{
error ??= ExtractAzureErrorContent(content);
var formatMessage = CreateRequestFailedMessageWithContent(response, error, content, additionalInfo, _sanitizer);
var exception = new RequestFailedException(response.Status, formatMessage, error?.Code, innerException);

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

return exception;
}

public async ValueTask<string> CreateRequestFailedMessageAsync(Response response, ResponseError? error, IDictionary<string, string>? additionalInfo, bool async)
{
var content = await ReadContentAsync(response, async).ConfigureAwait(false);
return CreateRequestFailedMessageWithContent(response, error, content, additionalInfo, _sanitizer);
}

internal static string CreateRequestFailedMessageWithContent(Response response, ResponseError? error, string? content, IDictionary<string, string>? additionalInfo, HttpMessageSanitizer sanitizer)
JoshLove-msft marked this conversation as resolved.
Show resolved Hide resolved
{
StringBuilder messageBuilder = new StringBuilder();

messageBuilder
.AppendLine(error?.Message ?? DefaultMessage)
.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(error?.Code))
{
messageBuilder.Append("ErrorCode: ")
.Append(error?.Code)
.AppendLine();
}

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

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);
string header = $"{responseHeader.Name}: {headerValue}";
messageBuilder.AppendLine(header);
}

return messageBuilder.ToString();
}

internal 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;
}

internal static string? GetResourceProviderNamespace(Assembly assembly)
{
foreach (var customAttribute in assembly.GetCustomAttributes(true))
Expand All @@ -227,11 +74,5 @@ internal static string CreateRequestFailedMessageWithContent(Response response,

return null;
}

private class ErrorResponse
{
[JsonPropertyName("error")]
public ResponseError? Error { get; set; }
}
}
}
4 changes: 0 additions & 4 deletions sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,5 @@ protected DiagnosticScope CreateScope(string scopeName)
scope.Start();
return scope;
}

protected async ValueTask<RequestFailedException> CreateException(bool async, Response response) => async
? await _diagnostics.CreateRequestFailedExceptionAsync(response).ConfigureAwait(false)
: _diagnostics.CreateRequestFailedException(response);
}
}
2 changes: 1 addition & 1 deletion sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ protected override async ValueTask<Response> UpdateStatusAsync(bool async, Cance

if (!state.HasSucceeded && state.OperationFailedException == null)
{
state = OperationState<T>.Failure(state.RawResponse, await CreateException(async, state.RawResponse).ConfigureAwait(false));
state = OperationState<T>.Failure(state.RawResponse, new RequestFailedException(state.RawResponse));
}

asyncLock.SetValue(state);
Expand Down
Loading