From ffdfcf0dc058c67ccf4068c62fd152a515d1f52d Mon Sep 17 00:00:00 2001 From: Yanan Wang <103942@smsassist.com> Date: Tue, 8 Sep 2026 11:46:46 -0500 Subject: [PATCH] Surface Membase error details and skip retries on Membase - query execution errors --- .../Controllers/MembaseController.cs | 49 +++++++++++++++++-- .../GraphDb/MembaseGraphDb.cs | 41 +++++++++++++++- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs b/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs index 8e9cf1b22..67dcd1337 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using ApiException = Refit.ApiException; namespace BotSharp.Plugin.Membase.Controllers; @@ -91,9 +92,7 @@ public async Task ExecuteGraphQuery(string graphId, [FromBody] Cy } catch (Exception ex) { - return StatusCode( - StatusCodes.Status500InternalServerError, - new { message = "An error occurred while executing the query.", error = ex.Message }); + return ErrorResult(ex, "An error occurred while executing the query."); } } @@ -594,4 +593,48 @@ public async Task ValidatePgtDefinition(string graphId, string de new { message = "An error occurred while validating the PGT definition.", error = ex.Message }); } } + + /// + /// Build a 500 response. "message" always carries the generic per-endpoint description; + /// "error" carries the concrete cause — for a Refit ApiException that is the "detail" + /// field of the upstream Membase problem-details body (falling back to the exception + /// message when the upstream body has no usable detail). + /// + private ObjectResult ErrorResult(Exception ex, string message) + { + var error = ex.Message; + + if (ex is ApiException apiEx) + { + var detail = GetUpstreamDetail(apiEx.Content); + if (!string.IsNullOrWhiteSpace(detail)) + { + error = detail; + } + } + + return StatusCode(StatusCodes.Status500InternalServerError, new { message, error }); + } + + private static string? GetUpstreamDetail(string? content) + { + if (string.IsNullOrWhiteSpace(content)) + { + return null; + } + + try + { + using var doc = JsonDocument.Parse(content); + return doc.RootElement.ValueKind == JsonValueKind.Object + && doc.RootElement.TryGetProperty("detail", out var detail) + && detail.ValueKind == JsonValueKind.String + ? detail.GetString() + : null; + } + catch (JsonException) + { + return null; + } + } } diff --git a/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs b/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs index a1516062f..81b443c21 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs @@ -13,6 +13,9 @@ public partial class MembaseGraphDb : IGraphDb private readonly IMembaseApi _membaseApi; private const int RETRY_COUNT = 3; + // Membase reports Cypher compile/runtime failures as HTTP 500 problem details with this title. + // Those failures are deterministic, so retrying them only delays the error. + private const string QUERY_EXECUTION_ERROR_TITLE = "Query Execution Error"; public MembaseGraphDb( IServiceProvider services, @@ -155,7 +158,7 @@ private AsyncPolicy BuildRetryPolicy() .Handle() .Or() .Or() - .Or(ex => ex.StatusCode == HttpStatusCode.ServiceUnavailable || ex.StatusCode == HttpStatusCode.InternalServerError) + .Or(IsTransientApiException) .WaitAndRetryAsync( retryCount: RETRY_COUNT, sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), @@ -168,5 +171,41 @@ private AsyncPolicy BuildRetryPolicy() return Policy.WrapAsync(retryPolicy, timeoutPolicy); } + + private static bool IsTransientApiException(ApiException ex) + { + switch (ex.StatusCode) + { + case HttpStatusCode.BadGateway: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.GatewayTimeout: + return true; + case HttpStatusCode.InternalServerError: + return !IsQueryExecutionError(ex.Content); + default: + return false; + } + } + + private static bool IsQueryExecutionError(string? content) + { + if (string.IsNullOrWhiteSpace(content)) + { + return false; + } + + try + { + using var doc = JsonDocument.Parse(content); + return doc.RootElement.ValueKind == JsonValueKind.Object + && doc.RootElement.TryGetProperty("title", out var title) + && title.ValueKind == JsonValueKind.String + && string.Equals(title.GetString(), QUERY_EXECUTION_ERROR_TITLE, StringComparison.OrdinalIgnoreCase); + } + catch (JsonException) + { + return false; + } + } #endregion }