Skip to content

Commit

Permalink
Receive error body from Run Service (actions#3342)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericsciple authored Jun 19, 2024
1 parent 3dab1f1 commit ecb732e
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 19 deletions.
20 changes: 20 additions & 0 deletions src/Sdk/DTWebApi/WebApi/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,26 @@ private TaskOrchestrationJobAlreadyAcquiredException(SerializationInfo info, Str
}
}

[Serializable]
[ExceptionMapping("0.0", "3.0", "TaskOrchestrationJobUnprocessableException", "GitHub.DistributedTask.WebApi.TaskOrchestrationJobUnprocessableException, GitHub.DistributedTask.WebApi, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public sealed class TaskOrchestrationJobUnprocessableException : DistributedTaskException
{
public TaskOrchestrationJobUnprocessableException(String message)
: base(message)
{
}

public TaskOrchestrationJobUnprocessableException(String message, Exception innerException)
: base(message, innerException)
{
}

private TaskOrchestrationJobUnprocessableException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}

[Serializable]
[ExceptionMapping("0.0", "3.0", "TaskOrchestrationPlanSecurityException", "GitHub.DistributedTask.WebApi.TaskOrchestrationPlanSecurityException, GitHub.DistributedTask.WebApi, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public sealed class TaskOrchestrationPlanSecurityException : DistributedTaskException
Expand Down
17 changes: 17 additions & 0 deletions src/Sdk/RSWebApi/Contracts/RunServiceError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Runtime.Serialization;

namespace GitHub.Actions.RunService.WebApi
{
[DataContract]
public class RunServiceError
{
[DataMember(Name = "source", EmitDefaultValue = false)]
public string Source { get; set; }

[DataMember(Name = "statusCode", EmitDefaultValue = false)]
public int Code { get; set; }

[DataMember(Name = "errorMessage", EmitDefaultValue = false)]
public string Message { get; set; }
}
}
110 changes: 99 additions & 11 deletions src/Sdk/RSWebApi/RunServiceHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,29 +86,51 @@ public async Task<AgentJobRequestMessage> GetJobMessageAsync(
httpMethod,
requestUri: requestUri,
content: requestContent,
readErrorBody: true,
cancellationToken: cancellationToken);

if (result.IsSuccess)
{
return result.Value;
}

if (TryParseErrorBody(result.ErrorBody, out RunServiceError error))
{
switch ((HttpStatusCode)error.Code)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job message not found '{messageId}'. {error.Message}");
case HttpStatusCode.Conflict:
throw new TaskOrchestrationJobAlreadyAcquiredException($"Job message already acquired '{messageId}'. {error.Message}");
case HttpStatusCode.UnprocessableEntity:
throw new TaskOrchestrationJobUnprocessableException($"Unprocessable job '{messageId}'. {error.Message}");
}
}

// Temporary back compat
switch (result.StatusCode)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job message not found: {messageId}");
case HttpStatusCode.Conflict:
throw new TaskOrchestrationJobAlreadyAcquiredException($"Job message already acquired: {messageId}");
default:
throw new Exception($"Failed to get job message: {result.Error}");
}

if (!string.IsNullOrEmpty(result.ErrorBody))
{
throw new Exception($"Failed to get job message: {result.Error}. {Truncate(result.ErrorBody)}");
}
else
{
throw new Exception($"Failed to get job message: {result.Error}");
}
}

public async Task CompleteJobAsync(
Uri requestUri,
Guid planId,
Guid jobId,
TaskResult result,
TaskResult conclusion,
Dictionary<String, VariableValue> outputs,
IList<StepResult> stepResults,
IList<Annotation> jobAnnotations,
Expand All @@ -120,7 +142,7 @@ public async Task CompleteJobAsync(
{
PlanID = planId,
JobID = jobId,
Conclusion = result,
Conclusion = conclusion,
Outputs = outputs,
StepResults = stepResults,
Annotations = jobAnnotations,
Expand All @@ -130,22 +152,39 @@ public async Task CompleteJobAsync(
requestUri = new Uri(requestUri, "completejob");

var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
var response = await SendAsync(
var result = await Send2Async(
httpMethod,
requestUri,
content: requestContent,
cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode)
if (result.IsSuccess)
{
return;
}

switch (response.StatusCode)
if (TryParseErrorBody(result.ErrorBody, out RunServiceError error))
{
switch ((HttpStatusCode)error.Code)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}. {error.Message}");
}
}

// Temporary back compat
switch (result.StatusCode)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}");
default:
throw new Exception($"Failed to complete job: {response.ReasonPhrase}");
}

if (!string.IsNullOrEmpty(result.ErrorBody))
{
throw new Exception($"Failed to complete job: {result.Error}. {Truncate(result.ErrorBody)}");
}
else
{
throw new Exception($"Failed to complete job: {result.Error}");
}
}

Expand All @@ -169,19 +208,37 @@ public async Task<RenewJobResponse> RenewJobAsync(
httpMethod,
requestUri,
content: requestContent,
readErrorBody: true,
cancellationToken: cancellationToken);

if (result.IsSuccess)
{
return result.Value;
}

if (TryParseErrorBody(result.ErrorBody, out RunServiceError error))
{
switch ((HttpStatusCode)error.Code)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}. {error.Message}");
}
}

// Temporary back compat
switch (result.StatusCode)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}");
default:
throw new Exception($"Failed to renew job: {result.Error}");
}

if (!string.IsNullOrEmpty(result.ErrorBody))
{
throw new Exception($"Failed to renew job: {result.Error}. {Truncate(result.ErrorBody)}");
}
else
{
throw new Exception($"Failed to renew job: {result.Error}");
}
}

Expand All @@ -190,5 +247,36 @@ public async Task<RenewJobResponse> RenewJobAsync(
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
return JsonConvert.DeserializeObject<T>(json, s_serializerSettings);
}

private static bool TryParseErrorBody(string errorBody, out RunServiceError error)
{
if (!string.IsNullOrEmpty(errorBody))
{
try
{
error = JsonUtility.FromString<RunServiceError>(errorBody);
if (error?.Source == "actions-run-service")
{
return true;
}
}
catch (Exception)
{
}
}

error = null;
return false;
}

private static string Truncate(string errorBody)
{
if (errorBody.Length > 100)
{
return errorBody.Substring(0, 100) + "[truncated]";
}

return errorBody;
}
}
}
61 changes: 58 additions & 3 deletions src/Sdk/WebApi/WebApi/RawHttpClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,55 @@ protected async Task<HttpResponseMessage> SendAsync(
}
}

protected async Task<RawHttpClientResult> Send2Async(
HttpMethod method,
Uri requestUri,
HttpContent content = null,
IEnumerable<KeyValuePair<String, String>> queryParameters = null,
Object userState = null,
CancellationToken cancellationToken = default(CancellationToken))
{
using (var response = await SendAsync(method, requestUri, content, queryParameters, userState, cancellationToken).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
return new RawHttpClientResult(
isSuccess: true,
error: string.Empty,
statusCode: response.StatusCode);
}
else
{
var errorBody = default(string);
try
{
errorBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
errorBody = $"Error reading HTTP response body: {ex.Message}";
}

string errorMessage = $"Error: {response.ReasonPhrase}";
return new RawHttpClientResult(
isSuccess: false,
error: errorMessage,
statusCode: response.StatusCode,
errorBody: errorBody);
}
}
}

protected Task<RawHttpClientResult<T>> SendAsync<T>(
HttpMethod method,
Uri requestUri,
HttpContent content = null,
IEnumerable<KeyValuePair<String, String>> queryParameters = null,
Boolean readErrorBody = false,
Object userState = null,
CancellationToken cancellationToken = default(CancellationToken))
{
return SendAsync<T>(method, null, requestUri, content, queryParameters, userState, cancellationToken);
return SendAsync<T>(method, null, requestUri, content, queryParameters, readErrorBody, userState, cancellationToken);
}

protected async Task<RawHttpClientResult<T>> SendAsync<T>(
Expand All @@ -118,18 +158,20 @@ protected async Task<RawHttpClientResult<T>> SendAsync<T>(
Uri requestUri,
HttpContent content = null,
IEnumerable<KeyValuePair<String, String>> queryParameters = null,
Boolean readErrorBody = false,
Object userState = null,
CancellationToken cancellationToken = default(CancellationToken))
{
using (VssTraceActivity.GetOrCreate().EnterCorrelationScope())
using (HttpRequestMessage requestMessage = CreateRequestMessage(method, additionalHeaders, requestUri, content, queryParameters))
{
return await SendAsync<T>(requestMessage, userState, cancellationToken).ConfigureAwait(false);
return await SendAsync<T>(requestMessage, readErrorBody, userState, cancellationToken).ConfigureAwait(false);
}
}

protected async Task<RawHttpClientResult<T>> SendAsync<T>(
HttpRequestMessage message,
Boolean readErrorBody = false,
Object userState = null,
CancellationToken cancellationToken = default(CancellationToken))
{
Expand All @@ -145,8 +187,21 @@ protected async Task<RawHttpClientResult<T>> SendAsync<T>(
}
else
{
var errorBody = default(string);
if (readErrorBody)
{
try
{
errorBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
errorBody = $"Error reading HTTP response body: {ex.Message}";
}
}

string errorMessage = $"Error: {response.ReasonPhrase}";
return RawHttpClientResult<T>.Fail(errorMessage, response.StatusCode);
return RawHttpClientResult<T>.Fail(errorMessage, response.StatusCode, errorBody);
}
}
}
Expand Down
22 changes: 17 additions & 5 deletions src/Sdk/WebApi/WebApi/RawHttpClientResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,41 @@ namespace Sdk.WebApi.WebApi
public class RawHttpClientResult
{
public bool IsSuccess { get; protected set; }

/// <summary>
/// A description of the HTTP status code, like "Error: Unprocessable Entity"
/// </summary>
public string Error { get; protected set; }

/// <summary>
/// The HTTP response body for unsuccessful HTTP status codes, or an error message when reading the response body fails.
/// </summary>
public string ErrorBody { get; protected set; }

public HttpStatusCode StatusCode { get; protected set; }

public bool IsFailure => !IsSuccess;

protected RawHttpClientResult(bool isSuccess, string error, HttpStatusCode statusCode)
public RawHttpClientResult(bool isSuccess, string error, HttpStatusCode statusCode, string errorBody = null)
{
IsSuccess = isSuccess;
Error = error;
StatusCode = statusCode;
ErrorBody = errorBody;
}
}

public class RawHttpClientResult<T> : RawHttpClientResult
{
public T Value { get; private set; }

protected internal RawHttpClientResult(T value, bool isSuccess, string error, HttpStatusCode statusCode)
: base(isSuccess, error, statusCode)
protected internal RawHttpClientResult(T value, bool isSuccess, string error, HttpStatusCode statusCode, string errorBody)
: base(isSuccess, error, statusCode, errorBody)
{
Value = value;
}

public static RawHttpClientResult<T> Fail(string message, HttpStatusCode statusCode) => new RawHttpClientResult<T>(default(T), false, message, statusCode);
public static RawHttpClientResult<T> Ok(T value) => new RawHttpClientResult<T>(value, true, string.Empty, HttpStatusCode.OK);
public static RawHttpClientResult<T> Fail(string message, HttpStatusCode statusCode, string errorBody) => new RawHttpClientResult<T>(default(T), false, message, statusCode, errorBody);
public static RawHttpClientResult<T> Ok(T value) => new RawHttpClientResult<T>(value, true, string.Empty, HttpStatusCode.OK, null);
}
}

0 comments on commit ecb732e

Please sign in to comment.