From 82fca79d80601e2f960bc68e0cb53a9aca70aa50 Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Wed, 15 Apr 2026 16:38:46 -0400 Subject: [PATCH] Fix signature + response handling for DELETE requests Two bugs prevented `WebhooksService.DeleteAsync` from working against the real API: 1. DELETE fell through to the "body payload" branch in `MakeRequestAsync` and signed an empty string with no `sig_payload` query param, so the server returned 401. Extend the GET/POST-no-body signing path to also cover DELETE so the resource id is attached as `sig_payload`. 2. `MakeRequestAsync` always tried to deserialize the response body, which for a 204/empty DELETE response threw JsonException. Return `default(T)` when the body is empty. Adds a regression test asserting DELETE requests include `sig_payload=`. --- AccessGridTest/ConsoleServiceTests.cs | 15 +++++++++++++++ src/AccessGrid/AccessGridClient.cs | 13 ++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/AccessGridTest/ConsoleServiceTests.cs b/AccessGridTest/ConsoleServiceTests.cs index cd792e8..b729f2e 100644 --- a/AccessGridTest/ConsoleServiceTests.cs +++ b/AccessGridTest/ConsoleServiceTests.cs @@ -865,6 +865,21 @@ public async Task WebhooksDeleteAsync_ShouldDeleteWebhook() )), Times.Once); } + [Test] + public async Task WebhooksDeleteAsync_IncludesSigPayloadQueryParam() + { + // DELETE requests have no body, so the server looks for `sig_payload` in + // the query string and verifies the signature against that. + StubHttpResponse("{}"); + + await _client.Console.Webhooks.DeleteAsync("wh_123"); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Delete && + req.RequestUri!.ToString().Contains("sig_payload=") + )), Times.Once); + } + #endregion #region HIDOrgsService diff --git a/src/AccessGrid/AccessGridClient.cs b/src/AccessGrid/AccessGridClient.cs index 8c4923e..af34b10 100644 --- a/src/AccessGrid/AccessGridClient.cs +++ b/src/AccessGrid/AccessGridClient.cs @@ -163,7 +163,7 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob { // Extract resource ID from the endpoint if needed for signature string resourceId = null; - if (method == HttpMethod.Get || (method == HttpMethod.Post && data == null)) + if (method == HttpMethod.Get || method == HttpMethod.Delete || (method == HttpMethod.Post && data == null)) { // Extract the ID from the endpoint - patterns like /resource/{id} or /resource/{id}/action var parts = endpoint.Trim('/').Split('/'); @@ -186,8 +186,9 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob // Special handling for requests with no payload: // 1. POST requests with empty body (like unlink/suspend/resume) // 2. GET requests + // 3. DELETE requests (always have no body) string payload; - if ((method == HttpMethod.Post && data == null) || method == HttpMethod.Get) + if ((method == HttpMethod.Post && data == null) || method == HttpMethod.Get || method == HttpMethod.Delete) { // For listing cards endpoint (like what the Python list.py script does) if (method == HttpMethod.Get && endpoint == "/v1/key-cards") @@ -228,7 +229,7 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob // For GET requests or POST requests with empty bodies that need the sig_payload parameter // Note: We've already added sig_payload for /v1/key-cards endpoint above - if ((method == HttpMethod.Get || (method == HttpMethod.Post && data == null)) && !finalQueryParams.ContainsKey("sig_payload")) + if ((method == HttpMethod.Get || method == HttpMethod.Delete || (method == HttpMethod.Post && data == null)) && !finalQueryParams.ContainsKey("sig_payload")) { if (!string.IsNullOrEmpty(resourceId) && resourceId != "key-cards" && !resourceId.Contains("templates")) { @@ -298,6 +299,12 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob return (T)(object)responseContent; } + // Empty response bodies (e.g. DELETE 204) have nothing to deserialize. + if (string.IsNullOrWhiteSpace(responseContent)) + { + return default; + } + // Deserialize the response try {