Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
using ApiException = Refit.ApiException;

namespace BotSharp.Plugin.Membase.Controllers;

Expand Down Expand Up @@ -91,9 +92,7 @@ public async Task<IActionResult> 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.");
}
}

Expand Down Expand Up @@ -594,4 +593,48 @@ public async Task<IActionResult> ValidatePgtDefinition(string graphId, string de
new { message = "An error occurred while validating the PGT definition.", error = ex.Message });
}
}

/// <summary>
/// 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).
/// </summary>
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;
}
}
}
41 changes: 40 additions & 1 deletion src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -155,7 +158,7 @@ private AsyncPolicy BuildRetryPolicy()
.Handle<HttpRequestException>()
.Or<TaskCanceledException>()
.Or<TimeoutRejectedException>()
.Or<ApiException>(ex => ex.StatusCode == HttpStatusCode.ServiceUnavailable || ex.StatusCode == HttpStatusCode.InternalServerError)
.Or<ApiException>(IsTransientApiException)
.WaitAndRetryAsync(
retryCount: RETRY_COUNT,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
Expand All @@ -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
}
Loading