Surface Infisical API error body in thrown exceptions

Parse the JSON envelope (message/error/reqId) returned by 4xx/5xx responses and include the human-readable message in the InfisicalApiException message itself, plus new ApiErrorMessage and ApiRequestId properties. InfisicalErrorDetails and the error handler propagate the new fields so PowerShell error records and the logger surface the server-side reason instead of an opaque status line.
This commit is contained in:
GraceSolutions
2026-06-04 16:43:44 -04:00
parent 8e7ab3570a
commit 5e6364f9e0
7 changed files with 144 additions and 9 deletions
+2
View File
@@ -6,6 +6,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased ## 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 ## 2026.06.04.1920
- Build produced from commit 0f8f44afdb38. - Build produced from commit 0f8f44afdb38.
@@ -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;
}
}
}
@@ -10,6 +10,8 @@ namespace PSInfisicalAPI.Errors
public int? StatusCode { get; set; } public int? StatusCode { get; set; }
public string ReasonPhrase { get; set; } public string ReasonPhrase { get; set; }
public string ApiErrorCode { get; set; } public string ApiErrorCode { get; set; }
public string ApiErrorMessage { get; set; }
public string ApiRequestId { get; set; }
public string SanitizedBody { get; set; } public string SanitizedBody { get; set; }
public int? LineNumber { get; set; } public int? LineNumber { get; set; }
public int? LinePosition { get; set; } public int? LinePosition { get; set; }
@@ -26,6 +26,8 @@ namespace PSInfisicalAPI.Errors
details.StatusCode = apiException.StatusCode; details.StatusCode = apiException.StatusCode;
details.ReasonPhrase = apiException.ReasonPhrase; details.ReasonPhrase = apiException.ReasonPhrase;
details.ApiErrorCode = apiException.ApiErrorCode; details.ApiErrorCode = apiException.ApiErrorCode;
details.ApiErrorMessage = apiException.ApiErrorMessage;
details.ApiRequestId = apiException.ApiRequestId;
details.SanitizedBody = apiException.SanitizedBody; details.SanitizedBody = apiException.SanitizedBody;
details.EndpointName = apiException.EndpointName; details.EndpointName = apiException.EndpointName;
details.RequestMethod = apiException.RequestMethod; details.RequestMethod = apiException.RequestMethod;
@@ -70,6 +72,16 @@ namespace PSInfisicalAPI.Errors
logger.Error(Component, string.Concat("API Error Code: ", details.ApiErrorCode)); 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) if (details.LineNumber.HasValue)
{ {
logger.Error(Component, string.Concat("Line: ", details.LineNumber.Value.ToString(CultureInfo.InvariantCulture))); logger.Error(Component, string.Concat("Line: ", details.LineNumber.Value.ToString(CultureInfo.InvariantCulture)));
@@ -33,6 +33,8 @@ namespace PSInfisicalAPI.Errors
public int StatusCode { get; set; } public int StatusCode { get; set; }
public string ReasonPhrase { get; set; } public string ReasonPhrase { get; set; }
public string ApiErrorCode { get; set; } public string ApiErrorCode { get; set; }
public string ApiErrorMessage { get; set; }
public string ApiRequestId { get; set; }
public string SanitizedBody { get; set; } public string SanitizedBody { get; set; }
public string EndpointName { get; set; } public string EndpointName { get; set; }
public string RequestMethod { get; set; } public string RequestMethod { get; set; }
@@ -135,15 +135,14 @@ namespace PSInfisicalAPI.Http
private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition) private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition)
{ {
InfisicalApiException exception = new InfisicalApiException(string.Concat( string message = InfisicalApiErrorEnvelope.BuildExceptionMessage(response.StatusCode, response.ReasonPhrase, response.Body);
"Infisical API returned ", InfisicalApiException exception = new InfisicalApiException(message);
response.StatusCode.ToString(CultureInfo.InvariantCulture),
" (", response.ReasonPhrase ?? string.Empty, ")."));
exception.StatusCode = response.StatusCode; exception.StatusCode = response.StatusCode;
exception.ReasonPhrase = response.ReasonPhrase; exception.ReasonPhrase = response.ReasonPhrase;
exception.EndpointName = definition.Name; exception.EndpointName = definition.Name;
exception.RequestMethod = definition.Method; exception.RequestMethod = definition.Method;
exception.SanitizedBody = response.Body; exception.SanitizedBody = response.Body;
InfisicalApiErrorEnvelope.Enrich(exception, response.Body);
return exception; return exception;
} }
} }
@@ -625,15 +625,14 @@ namespace PSInfisicalAPI.Secrets
private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition) private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition)
{ {
InfisicalApiException exception = new InfisicalApiException(string.Concat( string message = InfisicalApiErrorEnvelope.BuildExceptionMessage(response.StatusCode, response.ReasonPhrase, response.Body);
"Infisical API returned ", InfisicalApiException exception = new InfisicalApiException(message);
response.StatusCode.ToString(CultureInfo.InvariantCulture),
" (", response.ReasonPhrase ?? string.Empty, ")."));
exception.StatusCode = response.StatusCode; exception.StatusCode = response.StatusCode;
exception.ReasonPhrase = response.ReasonPhrase; exception.ReasonPhrase = response.ReasonPhrase;
exception.EndpointName = definition.Name; exception.EndpointName = definition.Name;
exception.RequestMethod = definition.Method; exception.RequestMethod = definition.Method;
exception.SanitizedBody = response.Body; exception.SanitizedBody = response.Body;
InfisicalApiErrorEnvelope.Enrich(exception, response.Body);
return exception; return exception;
} }