From 5538cb43dc4b3fec9e1458630a7b7f37f4b0067b Mon Sep 17 00:00:00 2001 From: Valera <50830352+ValeraFinebits@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:03:38 +0300 Subject: [PATCH 1/2] fix: Handle run-test-payment failures safely --- .../UIPayJoinControllerTests.cs | 10 ++++- .../Controllers/UIPayJoinController.cs | 39 ++++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/BTCPayServer.Plugins.Payjoin.Tests/UIPayJoinControllerTests.cs b/BTCPayServer.Plugins.Payjoin.Tests/UIPayJoinControllerTests.cs index fe7cd16..2ca967e 100644 --- a/BTCPayServer.Plugins.Payjoin.Tests/UIPayJoinControllerTests.cs +++ b/BTCPayServer.Plugins.Payjoin.Tests/UIPayJoinControllerTests.cs @@ -81,11 +81,17 @@ public void RunTestPaymentUsesCheatModeRoute() } [Fact] - public async Task RunTestPaymentThrowsWhenRequestIsNull() + public async Task RunTestPaymentReturnsBadRequestWhenRequestIsNull() { using var controller = CreateController(); - await Assert.ThrowsAsync(() => controller.RunTestPayment(null!, TestContext.Current.CancellationToken)); + var result = await controller.RunTestPayment(null!, TestContext.Current.CancellationToken); + + var badRequest = Assert.IsType(result.Result); + var response = Assert.IsType(badRequest.Value); + Assert.False(response.Succeeded); + Assert.Contains("invoiceId", response.Message, StringComparison.Ordinal); + Assert.Null(response.TransactionId); } [Fact] diff --git a/BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs b/BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs index 5dacc65..fc75673 100644 --- a/BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs +++ b/BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs @@ -12,6 +12,7 @@ using NBitcoin; using Payjoin; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using PayjoinUri = Payjoin.Uri; @@ -26,6 +27,10 @@ public class UIPayJoinController : Controller LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(LogPayjoinSenderBroadcasted)), "Payjoin sender broadcasted payjoin transaction {TransactionId} for {InvoiceId}"); + private static readonly Action LogRunTestPaymentFailed = + LoggerMessage.Define(LogLevel.Error, new EventId(2, nameof(LogRunTestPaymentFailed)), + "Payjoin test payment for {InvoiceId} failed with an unexpected exception"); + private readonly BTCPayServerEnvironment _env; private readonly InvoiceRepository _invoiceRepository; private readonly StoreRepository _storeRepository; @@ -93,11 +98,12 @@ private static GetCheckoutBip21Response ToCheckoutResponse(GetBip21Response paym [AllowAnonymous] [IgnoreAntiforgeryToken] [HttpPost("run-test-payment")] + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Any exception escaping a plugin controller makes BTCPay disable the plugin and stop the host process.")] public async Task> RunTestPayment([FromBody] RunTestPaymentRequest request, CancellationToken cancellationToken) { if (request is null) { - throw new ArgumentNullException(nameof(request)); + return BadRequest(RunTestPaymentResponse.Failure("A JSON body containing an invoiceId is required.")); } if (string.IsNullOrWhiteSpace(request.InvoiceId)) @@ -105,10 +111,31 @@ public async Task> RunTestPayment([FromBody return RunTestPaymentFailure("invoiceId is required"); } - var invoicePaymentUrl = await _paymentUrlService.GetInvoicePaymentUrlAsync(request.InvoiceId, cancellationToken).ConfigureAwait(false); + try + { + return await RunTestPaymentCoreAsync(request.InvoiceId, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + if (_logger is not null) + { + LogRunTestPaymentFailed(_logger, request.InvoiceId, ex); + } + + return RunTestPaymentFailure($"The test payment for invoice {request.InvoiceId} failed unexpectedly: {ex.Message}"); + } + } + + private async Task> RunTestPaymentCoreAsync(string invoiceId, CancellationToken cancellationToken) + { + var invoicePaymentUrl = await _paymentUrlService.GetInvoicePaymentUrlAsync(invoiceId, cancellationToken).ConfigureAwait(false); if (invoicePaymentUrl is null) { - return RunTestPaymentFailure("paymentUrl not available for invoice"); + return RunTestPaymentFailure($"No payjoin payment URL is available for invoice {invoiceId}. The invoice is not payable or has no Bitcoin payment method."); } if (invoicePaymentUrl.Status != PayjoinAvailabilityStatus.Active) @@ -121,7 +148,7 @@ public async Task> RunTestPayment([FromBody return RunTestPaymentFailure("invoice paymentUrl invalid"); } - var invoice = await _invoiceRepository.GetInvoice(request.InvoiceId).ConfigureAwait(false); + var invoice = await _invoiceRepository.GetInvoice(invoiceId).ConfigureAwait(false); if (invoice is null) { return RunTestPaymentFailure("invoice not found"); @@ -185,7 +212,7 @@ public async Task> RunTestPayment([FromBody } var runTestPaymentContext = new RunTestPaymentContext( - request.InvoiceId, + invoiceId, canonicalPaymentUrl, ohttpRelayUrls, paymentAddressValue, @@ -197,7 +224,7 @@ public async Task> RunTestPayment([FromBody var txid = await _runTestPaymentService.ExecuteAsync(runTestPaymentContext, cancellationToken).ConfigureAwait(false); if (_logger is not null) { - LogPayjoinSenderBroadcasted(_logger, txid, request.InvoiceId, null); + LogPayjoinSenderBroadcasted(_logger, txid, invoiceId, null); } return RunTestPaymentSuccess($"Payjoin transaction broadcasted: {txid}", txid); From 10f57d5fc824db35a665ea985cfbfcce1f319572 Mon Sep 17 00:00:00 2001 From: Valera <50830352+ValeraFinebits@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:12:28 +0300 Subject: [PATCH 2/2] fix: Fix error message for unavailable payment URL --- BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs b/BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs index fc75673..3ed32a6 100644 --- a/BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs +++ b/BTCPayServer.Plugins.Payjoin/Controllers/UIPayJoinController.cs @@ -135,7 +135,7 @@ private async Task> RunTestPaymentCoreAsync var invoicePaymentUrl = await _paymentUrlService.GetInvoicePaymentUrlAsync(invoiceId, cancellationToken).ConfigureAwait(false); if (invoicePaymentUrl is null) { - return RunTestPaymentFailure($"No payjoin payment URL is available for invoice {invoiceId}. The invoice is not payable or has no Bitcoin payment method."); + return RunTestPaymentFailure("paymentUrl not available for invoice"); } if (invoicePaymentUrl.Status != PayjoinAvailabilityStatus.Active)