diff --git a/CHANGELOG.md b/CHANGELOG.md index 283dc4a..c5bfbdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +- Infisical API error responses are now parsed to surface the server-side `message`, `error`, and `reqId` fields. The 4xx/5xx exception message includes the human-readable explanation (e.g. "The project is of type secret-manager") instead of an opaque `Infisical API returned 400 (Bad Request)`. The `InfisicalApiException` gains `ApiErrorMessage` and `ApiRequestId` properties; `InfisicalErrorDetails` carries the same fields so PowerShell error records and logger output expose them. + ## 2026.06.04.1920 - Build produced from commit 0f8f44afdb38. -## Unreleased (carried forward) +## Unreleased (carried forward) - `build.ps1` gains a `-CommitArtifacts` switch that, after a successful build, stages and commits only the build outputs (`Module/PSInfisicalAPI/bin/**`, `Module/PSInfisicalAPI/PSInfisicalAPI.psd1`, and the auto-inserted `CHANGELOG.md` build stamp) with a message that references the source commit whose hash is now embedded in `BuildCommitHash`. The switch is mutually exclusive with the older broader `-CommitOnSuccess` (which still uses `git add -A`). README extended with a "Committing source and build artifacts in lockstep" section describing the recommended two-commit workflow. diff --git a/src/PSInfisicalAPI/Errors/InfisicalApiErrorEnvelope.cs b/src/PSInfisicalAPI/Errors/InfisicalApiErrorEnvelope.cs new file mode 100644 index 0000000..831a1ed --- /dev/null +++ b/src/PSInfisicalAPI/Errors/InfisicalApiErrorEnvelope.cs @@ -0,0 +1,119 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace PSInfisicalAPI.Errors +{ + internal static class InfisicalApiErrorEnvelope + { + public static void Enrich(InfisicalApiException exception, string body) + { + if (exception == null || string.IsNullOrEmpty(body)) + { + return; + } + + string trimmed = body.TrimStart(); + if (trimmed.Length == 0 || (trimmed[0] != '{' && trimmed[0] != '[')) + { + return; + } + + JObject obj; + try + { + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return; } + obj = (JObject)token; + } + catch (Exception) + { + return; + } + + string message = ReadString(obj, "message"); + string error = ReadString(obj, "error"); + string reqId = ReadString(obj, "reqId"); + + if (!string.IsNullOrEmpty(message)) { exception.ApiErrorMessage = message; } + if (!string.IsNullOrEmpty(error) && string.IsNullOrEmpty(exception.ApiErrorCode)) { exception.ApiErrorCode = error; } + if (!string.IsNullOrEmpty(reqId)) { exception.ApiRequestId = reqId; } + } + + public static string BuildExceptionMessage(int statusCode, string reasonPhrase, string body) + { + string baseMessage = string.Concat( + "Infisical API returned ", + statusCode.ToString(System.Globalization.CultureInfo.InvariantCulture), + " (", reasonPhrase ?? string.Empty, ")."); + + string apiMessage = null; + string apiError = null; + string reqId = null; + + if (!string.IsNullOrEmpty(body)) + { + string trimmed = body.TrimStart(); + if (trimmed.Length > 0 && trimmed[0] == '{') + { + try + { + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Object) + { + JObject obj = (JObject)token; + apiMessage = ReadString(obj, "message"); + apiError = ReadString(obj, "error"); + reqId = ReadString(obj, "reqId"); + } + } + catch (Exception) + { + } + } + } + + if (string.IsNullOrEmpty(apiMessage) && string.IsNullOrEmpty(apiError) && string.IsNullOrEmpty(reqId)) + { + return baseMessage; + } + + System.Text.StringBuilder builder = new System.Text.StringBuilder(baseMessage); + if (!string.IsNullOrEmpty(apiMessage)) + { + builder.Append(' ').Append(apiMessage); + } + + if (!string.IsNullOrEmpty(apiError) || !string.IsNullOrEmpty(reqId)) + { + builder.Append(" ["); + bool needsSeparator = false; + if (!string.IsNullOrEmpty(apiError)) + { + builder.Append("error=").Append(apiError); + needsSeparator = true; + } + + if (!string.IsNullOrEmpty(reqId)) + { + if (needsSeparator) { builder.Append("; "); } + builder.Append("reqId=").Append(reqId); + } + + builder.Append(']'); + } + + return builder.ToString(); + } + + private static string ReadString(JObject obj, string name) + { + JToken token; + if (obj.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out token) && token != null && token.Type == JTokenType.String) + { + return (string)token; + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Errors/InfisicalErrorDetails.cs b/src/PSInfisicalAPI/Errors/InfisicalErrorDetails.cs index c02a0ae..2cd0aca 100644 --- a/src/PSInfisicalAPI/Errors/InfisicalErrorDetails.cs +++ b/src/PSInfisicalAPI/Errors/InfisicalErrorDetails.cs @@ -10,6 +10,8 @@ namespace PSInfisicalAPI.Errors public int? StatusCode { get; set; } public string ReasonPhrase { get; set; } public string ApiErrorCode { get; set; } + public string ApiErrorMessage { get; set; } + public string ApiRequestId { get; set; } public string SanitizedBody { get; set; } public int? LineNumber { get; set; } public int? LinePosition { get; set; } diff --git a/src/PSInfisicalAPI/Errors/InfisicalErrorHandler.cs b/src/PSInfisicalAPI/Errors/InfisicalErrorHandler.cs index 0da799e..33bc109 100644 --- a/src/PSInfisicalAPI/Errors/InfisicalErrorHandler.cs +++ b/src/PSInfisicalAPI/Errors/InfisicalErrorHandler.cs @@ -26,6 +26,8 @@ namespace PSInfisicalAPI.Errors details.StatusCode = apiException.StatusCode; details.ReasonPhrase = apiException.ReasonPhrase; details.ApiErrorCode = apiException.ApiErrorCode; + details.ApiErrorMessage = apiException.ApiErrorMessage; + details.ApiRequestId = apiException.ApiRequestId; details.SanitizedBody = apiException.SanitizedBody; details.EndpointName = apiException.EndpointName; details.RequestMethod = apiException.RequestMethod; @@ -70,6 +72,16 @@ namespace PSInfisicalAPI.Errors logger.Error(Component, string.Concat("API Error Code: ", details.ApiErrorCode)); } + if (!string.IsNullOrEmpty(details.ApiErrorMessage)) + { + logger.Error(Component, string.Concat("API Error Message: ", details.ApiErrorMessage)); + } + + if (!string.IsNullOrEmpty(details.ApiRequestId)) + { + logger.Error(Component, string.Concat("API Request Id: ", details.ApiRequestId)); + } + if (details.LineNumber.HasValue) { logger.Error(Component, string.Concat("Line: ", details.LineNumber.Value.ToString(CultureInfo.InvariantCulture))); diff --git a/src/PSInfisicalAPI/Errors/InfisicalException.cs b/src/PSInfisicalAPI/Errors/InfisicalException.cs index 88979e9..98f2c1f 100644 --- a/src/PSInfisicalAPI/Errors/InfisicalException.cs +++ b/src/PSInfisicalAPI/Errors/InfisicalException.cs @@ -33,6 +33,8 @@ namespace PSInfisicalAPI.Errors public int StatusCode { get; set; } public string ReasonPhrase { get; set; } public string ApiErrorCode { get; set; } + public string ApiErrorMessage { get; set; } + public string ApiRequestId { get; set; } public string SanitizedBody { get; set; } public string EndpointName { get; set; } public string RequestMethod { get; set; } diff --git a/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs b/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs index ffd988e..a3e2b1a 100644 --- a/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs +++ b/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs @@ -135,15 +135,14 @@ namespace PSInfisicalAPI.Http private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition) { - InfisicalApiException exception = new InfisicalApiException(string.Concat( - "Infisical API returned ", - response.StatusCode.ToString(CultureInfo.InvariantCulture), - " (", response.ReasonPhrase ?? string.Empty, ").")); + string message = InfisicalApiErrorEnvelope.BuildExceptionMessage(response.StatusCode, response.ReasonPhrase, response.Body); + InfisicalApiException exception = new InfisicalApiException(message); exception.StatusCode = response.StatusCode; exception.ReasonPhrase = response.ReasonPhrase; exception.EndpointName = definition.Name; exception.RequestMethod = definition.Method; exception.SanitizedBody = response.Body; + InfisicalApiErrorEnvelope.Enrich(exception, response.Body); return exception; } } diff --git a/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs b/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs index 8e4161b..fb386ac 100644 --- a/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs +++ b/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs @@ -625,15 +625,14 @@ namespace PSInfisicalAPI.Secrets private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition) { - InfisicalApiException exception = new InfisicalApiException(string.Concat( - "Infisical API returned ", - response.StatusCode.ToString(CultureInfo.InvariantCulture), - " (", response.ReasonPhrase ?? string.Empty, ").")); + string message = InfisicalApiErrorEnvelope.BuildExceptionMessage(response.StatusCode, response.ReasonPhrase, response.Body); + InfisicalApiException exception = new InfisicalApiException(message); exception.StatusCode = response.StatusCode; exception.ReasonPhrase = response.ReasonPhrase; exception.EndpointName = definition.Name; exception.RequestMethod = definition.Method; exception.SanitizedBody = response.Body; + InfisicalApiErrorEnvelope.Enrich(exception, response.Body); return exception; }