Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,26 @@ private async Task<Document> AddAsync(CodeFixContext context, MethodDeclarationS

foreach (var returnStatement in GetReturnStatements(methodDeclarationSyntax))
{
var expressionSyntax = returnStatement.ChildNodes().OfType<ExpressionSyntax>().First()!;
var expressionSyntax = returnStatement.ChildNodes().OfType<ExpressionSyntax>().FirstOrDefault();

// Skip return statements without expressions (e.g., bare "return;")
if (expressionSyntax is null)
{
continue;
}

if (IsTaskFromResult(expressionSyntax, semanticModel)
|| IsAsTaskExtension(expressionSyntax, semanticModel))
{
var firstInnerExpression = expressionSyntax.ChildNodes().OfType<ArgumentListSyntax>().First().Arguments.First().Expression;
var argumentList = expressionSyntax.ChildNodes().OfType<ArgumentListSyntax>().FirstOrDefault();
var firstArgument = argumentList?.Arguments.FirstOrDefault();
var firstInnerExpression = firstArgument?.Expression;

// Skip if we can't find the inner expression to unwrap
if (firstInnerExpression is null)
{
continue;
}

var newReturnStatement = returnStatement.ReplaceNode(expressionSyntax, firstInnerExpression);

Expand Down
2 changes: 1 addition & 1 deletion src/ModularPipelines/Context/Downloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public async Task<HttpResponseMessage> DownloadResponseAsync(DownloadOptions opt
HttpClient = options.HttpClient,
}, cancellationToken).ConfigureAwait(false);

return response.EnsureSuccessStatusCode();
return await response.EnsureSuccessStatusCodeWithContentAsync(cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand Down
90 changes: 90 additions & 0 deletions src/ModularPipelines/Exceptions/HttpResponseException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Net;

namespace ModularPipelines.Exceptions;

/// <summary>
/// Exception thrown when an HTTP request returns a non-success status code.
/// Provides detailed information about the failed response including status code, reason phrase, and response content.
/// </summary>
public class HttpResponseException : PipelineException
{
/// <summary>
/// Gets the HTTP status code of the failed response.
/// </summary>
public HttpStatusCode StatusCode { get; }

/// <summary>
/// Gets the reason phrase from the failed response.
/// </summary>
public string? ReasonPhrase { get; }

/// <summary>
/// Gets the content of the failed response, if available.
/// </summary>
public string? ResponseContent { get; }

/// <summary>
/// Gets the request URI that produced the failed response.
/// </summary>
public Uri? RequestUri { get; }

/// <summary>
/// Initializes a new instance of the <see cref="HttpResponseException"/> class.
/// </summary>
/// <param name="statusCode">The HTTP status code.</param>
/// <param name="reasonPhrase">The reason phrase from the response.</param>
/// <param name="responseContent">The content of the response.</param>
/// <param name="requestUri">The request URI.</param>
public HttpResponseException(HttpStatusCode statusCode, string? reasonPhrase, string? responseContent, Uri? requestUri)
: base(FormatMessage(statusCode, reasonPhrase, responseContent, requestUri))
{
StatusCode = statusCode;
ReasonPhrase = reasonPhrase;
ResponseContent = responseContent;
RequestUri = requestUri;
}

/// <summary>
/// Initializes a new instance of the <see cref="HttpResponseException"/> class with an inner exception.
/// </summary>
/// <param name="statusCode">The HTTP status code.</param>
/// <param name="reasonPhrase">The reason phrase from the response.</param>
/// <param name="responseContent">The content of the response.</param>
/// <param name="requestUri">The request URI.</param>
/// <param name="innerException">The inner exception.</param>
public HttpResponseException(HttpStatusCode statusCode, string? reasonPhrase, string? responseContent, Uri? requestUri, Exception? innerException)
: base(FormatMessage(statusCode, reasonPhrase, responseContent, requestUri), innerException)
{
StatusCode = statusCode;
ReasonPhrase = reasonPhrase;
ResponseContent = responseContent;
RequestUri = requestUri;
}

private static string FormatMessage(HttpStatusCode statusCode, string? reasonPhrase, string? responseContent, Uri? requestUri)
{
var message = $"HTTP request failed with status code {(int)statusCode} ({statusCode})";

if (!string.IsNullOrWhiteSpace(reasonPhrase))
{
message += $": {reasonPhrase}";
}

if (requestUri is not null)
{
message += $"\nRequest URI: {requestUri}";
}

if (!string.IsNullOrWhiteSpace(responseContent))
{
// Truncate very long responses to avoid excessive exception messages
const int maxContentLength = 2000;
var truncatedContent = responseContent.Length > maxContentLength
? responseContent[..maxContentLength] + "... (truncated)"
: responseContent;
message += $"\nResponse content: {truncatedContent}";
}

return message;
}
}
4 changes: 2 additions & 2 deletions src/ModularPipelines/Http/Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public async Task<HttpResponseMessage> SendAsync(HttpOptions httpOptions, Cancel
return response;
}

return response.EnsureSuccessStatusCode();
return await response.EnsureSuccessStatusCodeWithContentAsync(cancellationToken).ConfigureAwait(false);
}

public HttpClient GetLoggingHttpClient(HttpLoggingType loggingType)
Expand Down Expand Up @@ -142,7 +142,7 @@ private async Task<HttpResponseMessage> SendAndWrapLogging(HttpOptions httpOptio
return response;
}

return response.EnsureSuccessStatusCode();
return await response.EnsureSuccessStatusCodeWithContentAsync(cancellationToken).ConfigureAwait(false);
}

private HttpLoggingOptions GetEffectiveLoggingOptions(HttpOptions httpOptions)
Expand Down
44 changes: 44 additions & 0 deletions src/ModularPipelines/Http/HttpResponseExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using ModularPipelines.Exceptions;

namespace ModularPipelines.Http;

/// <summary>
/// Extension methods for <see cref="HttpResponseMessage"/> handling.
/// </summary>
internal static class HttpResponseExtensions
{
/// <summary>
/// Ensures the response has a success status code, throwing a detailed exception if not.
/// Unlike <see cref="HttpResponseMessage.EnsureSuccessStatusCode"/>, this method includes
/// the response content in the exception for easier debugging.
/// </summary>
/// <param name="response">The HTTP response to check.</param>
/// <param name="cancellationToken">Cancellation token for reading response content.</param>
/// <returns>The original response if successful.</returns>
/// <exception cref="HttpResponseException">Thrown when the response status code indicates failure.</exception>
public static async Task<HttpResponseMessage> EnsureSuccessStatusCodeWithContentAsync(
this HttpResponseMessage response,
CancellationToken cancellationToken = default)
{
if (response.IsSuccessStatusCode)
{
return response;
}

string? responseContent = null;
try
{
responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
catch
{
// If we can't read the content, continue with null
}

throw new HttpResponseException(
response.StatusCode,
response.ReasonPhrase,
responseContent,
response.RequestMessage?.RequestUri);
}
}
Loading