From 17b18831b7abe6b0226fb110ea0c9a6d680cd43a Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Fri, 22 May 2026 22:15:06 -0500 Subject: [PATCH] Fix: declare trust_probe capability on register + introspection (v1.4.5) Inngest Cloud's /fn/register endpoint started returning 400 "Invalid response returned" on 2026-05-22 for SDKs that don't advertise the trust_probe capability. The TS SDK has always declared this; the .NET SDK was silently relying on legacy fallback behavior that has now been removed. - Out-of-band register payload now includes capabilities.trust_probe=v1 and in_band_sync=v1. - In-band sync inspection adds trust_probe=v1 alongside the existing in_band_sync=v1. - Authenticated GET introspection response includes the same capabilities block (mirrors the TS SDK). - Added regression test that captures the outbound /fn/register POST body and asserts capabilities are present. - Bumped version to 1.4.5. Co-Authored-By: Claude Opus 4.7 (1M context) --- Inngest.Tests/SyncRegistrationTests.cs | 57 ++++++++++++++++++++++++++ Inngest/Inngest.csproj | 2 +- Inngest/InngestClient.cs | 26 +++++++++--- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/Inngest.Tests/SyncRegistrationTests.cs b/Inngest.Tests/SyncRegistrationTests.cs index d6f0908..c631e70 100644 --- a/Inngest.Tests/SyncRegistrationTests.cs +++ b/Inngest.Tests/SyncRegistrationTests.cs @@ -276,6 +276,63 @@ public async Task HandleSync_InBandMode_IncludesInspection() Assert.True(inspection.GetProperty("has_event_key").GetBoolean()); Assert.True(inspection.GetProperty("has_signing_key").GetBoolean()); Assert.Equal("dev", inspection.GetProperty("mode").GetString()); + + // capabilities.trust_probe must be advertised — without it Inngest Cloud + // rejects registrations with 400 "Invalid response returned". + Assert.True(inspection.TryGetProperty("capabilities", out var capabilities)); + Assert.Equal("v1", capabilities.GetProperty("trust_probe").GetString()); + Assert.Equal("v1", capabilities.GetProperty("in_band_sync").GetString()); + } + + [Fact] + public async Task HandleSync_OutOfBand_RegisterPayloadIncludesTrustProbeCapability() + { + // Arrange — out-of-band sync POSTs to {apiOrigin}/fn/register. Inngest + // Cloud rejects this with 400 "Invalid response returned" when the + // payload omits capabilities.trust_probe. Capture the outbound POST + // body and assert capabilities are present. + var options = new InngestOptions + { + AppId = "test-app", + IsDev = false, + EventKey = "test-event-key", + SigningKey = "signkey-prod-abc123" + }; + options.ApplyEnvironmentFallbacks(); + + var registry = new InngestFunctionRegistry(options.AppId!); + registry.RegisterFunction(typeof(SyncTestFunction)); + + var services = new ServiceCollection(); + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + + var handler = new MockHttpMessageHandler(); + handler.QueueResponse(HttpStatusCode.OK, "{\"modified\":true}"); + + var client = new InngestClient( + options, + registry, + serviceProvider, + new HttpClient(handler), + NullLogger.Instance); + + var context = CreateHttpContext("PUT"); + // No X-Inngest-Sync-Kind header => out-of-band path. + + // Act + await client.HandleRequestAsync(context); + + // Assert + var registerRequest = handler.Requests.Single(r => + r.RequestUri != null && r.RequestUri.AbsolutePath.EndsWith("/fn/register")); + var sentBody = await registerRequest.Content!.ReadAsStringAsync(); + var payload = JsonSerializer.Deserialize(sentBody); + + Assert.True(payload.TryGetProperty("capabilities", out var capabilities), + $"register payload missing 'capabilities' field: {sentBody}"); + Assert.Equal("v1", capabilities.GetProperty("trust_probe").GetString()); + Assert.Equal("v1", capabilities.GetProperty("in_band_sync").GetString()); } [Fact] diff --git a/Inngest/Inngest.csproj b/Inngest/Inngest.csproj index 33749b9..e200f28 100644 --- a/Inngest/Inngest.csproj +++ b/Inngest/Inngest.csproj @@ -19,7 +19,7 @@ https://github.com/jmarbutt/InngestDotNet/releases - 1.4.4 + 1.4.5 true diff --git a/Inngest/InngestClient.cs b/Inngest/InngestClient.cs index 50c7275..a3bea12 100644 --- a/Inngest/InngestClient.cs +++ b/Inngest/InngestClient.cs @@ -30,7 +30,7 @@ public class InngestClient : IInngestClient private readonly string _environment; private readonly bool _isDev; private readonly bool _disableCronTriggersInDev; - private readonly string _sdkVersion = "1.4.4"; + private readonly string _sdkVersion = "1.4.5"; private readonly string _appId; private readonly ILogger _logger; private readonly IInngestFunctionRegistry? _registry; @@ -1058,10 +1058,14 @@ private async Task HandleSyncRequest(HttpContext context) { _logger.LogInformation("In-band sync: returning {FunctionCount} functions with URL: {Url}", fnArray.Count, url); - // Build capabilities + // Build capabilities. trust_probe is required by Inngest Cloud as of the + // 2026-05 cloud rollout; without it /fn/register returns 400 "Invalid + // response returned". in_band_sync advertises that we can satisfy a + // signed in-band PUT. var capabilities = new Dictionary { - ["in_band_sync"] = "v1" + ["in_band_sync"] = "v1", + ["trust_probe"] = "v1" }; // Build inspection data @@ -1163,7 +1167,9 @@ private async Task HandleSyncRequest(HttpContext context) } // Out-of-band sync - POST to Inngest API - // Create register payload according to the SDK specification + // Create register payload according to the SDK specification. + // capabilities.trust_probe must be declared; Inngest Cloud rejects + // registrations without it with 400 "Invalid response returned". var registerPayload = new { url = url, @@ -1172,7 +1178,12 @@ private async Task HandleSyncRequest(HttpContext context) sdk = $"cs:v{_sdkVersion}", v = "0.1", framework = "aspnetcore", - functions = fnArray + functions = fnArray, + capabilities = new Dictionary + { + ["trust_probe"] = "v1", + ["in_band_sync"] = "v1" + } }; // Log information about registration for debugging @@ -1617,6 +1628,11 @@ private async Task HandleIntrospectionRequest(HttpContext context) responseObj["api_origin"] = _apiOrigin; responseObj["event_api_origin"] = _eventApiOrigin; responseObj["app_id"] = _appId; + responseObj["capabilities"] = new Dictionary + { + ["trust_probe"] = "v1", + ["in_band_sync"] = "v1" + }; responseObj["env"] = _environment; responseObj["event_key_hash"] = eventKeyHash; responseObj["framework"] = "aspnetcore";