From ecb732eaf45ab06a08a4334a241b0baef3561a57 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 19 Jun 2024 11:38:32 -0500 Subject: [PATCH] Receive error body from Run Service (#3342) --- src/Sdk/DTWebApi/WebApi/Exceptions.cs | 20 ++++ src/Sdk/RSWebApi/Contracts/RunServiceError.cs | 17 +++ src/Sdk/RSWebApi/RunServiceHttpClient.cs | 110 ++++++++++++++++-- src/Sdk/WebApi/WebApi/RawHttpClientBase.cs | 61 +++++++++- src/Sdk/WebApi/WebApi/RawHttpClientResult.cs | 22 +++- 5 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 src/Sdk/RSWebApi/Contracts/RunServiceError.cs diff --git a/src/Sdk/DTWebApi/WebApi/Exceptions.cs b/src/Sdk/DTWebApi/WebApi/Exceptions.cs index 536bf755055..ee47f137063 100644 --- a/src/Sdk/DTWebApi/WebApi/Exceptions.cs +++ b/src/Sdk/DTWebApi/WebApi/Exceptions.cs @@ -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 diff --git a/src/Sdk/RSWebApi/Contracts/RunServiceError.cs b/src/Sdk/RSWebApi/Contracts/RunServiceError.cs new file mode 100644 index 00000000000..009a5914a06 --- /dev/null +++ b/src/Sdk/RSWebApi/Contracts/RunServiceError.cs @@ -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; } + } +} diff --git a/src/Sdk/RSWebApi/RunServiceHttpClient.cs b/src/Sdk/RSWebApi/RunServiceHttpClient.cs index ba176ccf6b6..14bdd2a6379 100644 --- a/src/Sdk/RSWebApi/RunServiceHttpClient.cs +++ b/src/Sdk/RSWebApi/RunServiceHttpClient.cs @@ -86,6 +86,7 @@ public async Task GetJobMessageAsync( httpMethod, requestUri: requestUri, content: requestContent, + readErrorBody: true, cancellationToken: cancellationToken); if (result.IsSuccess) @@ -93,14 +94,35 @@ public async Task GetJobMessageAsync( 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}"); } } @@ -108,7 +130,7 @@ public async Task CompleteJobAsync( Uri requestUri, Guid planId, Guid jobId, - TaskResult result, + TaskResult conclusion, Dictionary outputs, IList stepResults, IList jobAnnotations, @@ -120,7 +142,7 @@ public async Task CompleteJobAsync( { PlanID = planId, JobID = jobId, - Conclusion = result, + Conclusion = conclusion, Outputs = outputs, StepResults = stepResults, Annotations = jobAnnotations, @@ -130,22 +152,39 @@ public async Task CompleteJobAsync( requestUri = new Uri(requestUri, "completejob"); var requestContent = new ObjectContent(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}"); } } @@ -169,6 +208,7 @@ public async Task RenewJobAsync( httpMethod, requestUri, content: requestContent, + readErrorBody: true, cancellationToken: cancellationToken); if (result.IsSuccess) @@ -176,12 +216,29 @@ public async Task RenewJobAsync( 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}"); } } @@ -190,5 +247,36 @@ public async Task RenewJobAsync( var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); return JsonConvert.DeserializeObject(json, s_serializerSettings); } + + private static bool TryParseErrorBody(string errorBody, out RunServiceError error) + { + if (!string.IsNullOrEmpty(errorBody)) + { + try + { + error = JsonUtility.FromString(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; + } } } diff --git a/src/Sdk/WebApi/WebApi/RawHttpClientBase.cs b/src/Sdk/WebApi/WebApi/RawHttpClientBase.cs index de7c3bcb372..23c51472487 100644 --- a/src/Sdk/WebApi/WebApi/RawHttpClientBase.cs +++ b/src/Sdk/WebApi/WebApi/RawHttpClientBase.cs @@ -101,15 +101,55 @@ protected async Task SendAsync( } } + protected async Task Send2Async( + HttpMethod method, + Uri requestUri, + HttpContent content = null, + IEnumerable> 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> SendAsync( HttpMethod method, Uri requestUri, HttpContent content = null, IEnumerable> queryParameters = null, + Boolean readErrorBody = false, Object userState = null, CancellationToken cancellationToken = default(CancellationToken)) { - return SendAsync(method, null, requestUri, content, queryParameters, userState, cancellationToken); + return SendAsync(method, null, requestUri, content, queryParameters, readErrorBody, userState, cancellationToken); } protected async Task> SendAsync( @@ -118,18 +158,20 @@ protected async Task> SendAsync( Uri requestUri, HttpContent content = null, IEnumerable> 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(requestMessage, userState, cancellationToken).ConfigureAwait(false); + return await SendAsync(requestMessage, readErrorBody, userState, cancellationToken).ConfigureAwait(false); } } protected async Task> SendAsync( HttpRequestMessage message, + Boolean readErrorBody = false, Object userState = null, CancellationToken cancellationToken = default(CancellationToken)) { @@ -145,8 +187,21 @@ protected async Task> SendAsync( } 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.Fail(errorMessage, response.StatusCode); + return RawHttpClientResult.Fail(errorMessage, response.StatusCode, errorBody); } } } diff --git a/src/Sdk/WebApi/WebApi/RawHttpClientResult.cs b/src/Sdk/WebApi/WebApi/RawHttpClientResult.cs index 1b2dc5f06cc..113de871fe8 100644 --- a/src/Sdk/WebApi/WebApi/RawHttpClientResult.cs +++ b/src/Sdk/WebApi/WebApi/RawHttpClientResult.cs @@ -5,15 +5,27 @@ namespace Sdk.WebApi.WebApi public class RawHttpClientResult { public bool IsSuccess { get; protected set; } + + /// + /// A description of the HTTP status code, like "Error: Unprocessable Entity" + /// public string Error { get; protected set; } + + /// + /// The HTTP response body for unsuccessful HTTP status codes, or an error message when reading the response body fails. + /// + 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; } } @@ -21,13 +33,13 @@ public class RawHttpClientResult : 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 Fail(string message, HttpStatusCode statusCode) => new RawHttpClientResult(default(T), false, message, statusCode); - public static RawHttpClientResult Ok(T value) => new RawHttpClientResult(value, true, string.Empty, HttpStatusCode.OK); + public static RawHttpClientResult Fail(string message, HttpStatusCode statusCode, string errorBody) => new RawHttpClientResult(default(T), false, message, statusCode, errorBody); + public static RawHttpClientResult Ok(T value) => new RawHttpClientResult(value, true, string.Empty, HttpStatusCode.OK, null); } }