From e56ec4d60d822c987c6648a39fac84e971d8dfb1 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 21 Sep 2026 17:07:24 +0530 Subject: [PATCH] Fix #3704: return 200 empty payload for cached reads with empty result When entity/runtime caching is enabled and a read returns zero rows, the cached value surfaces as a default(JsonElement) with JsonValueKind.Undefined and no backing document. ParseResultIntoJsonDocument called JsonSerializer.SerializeToUtf8Bytes on it, which throws InvalidOperationException via JsonElement.WriteTo -> CheckValidInstance(), producing HTTP 500. Guard the Undefined case and return null, matching the non-cached path that renders an empty payload. This path is shared by REST and GraphQL. Adds a regression unit test. --- src/Core/Resolvers/SqlQueryEngine.cs | 8 ++++++++ .../UnitTests/SqlQueryEngineHelperTests.cs | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index f567251771..a17f8bc952 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -443,6 +443,14 @@ public object ResolveList(JsonElement array, ObjectField fieldSchema, ref IMetad private static JsonDocument? ParseResultIntoJsonDocument(JsonElement? result) { + // An empty result set surfaces as a default (JsonValueKind.Undefined) JsonElement with no + // backing document; serializing it throws InvalidOperationException. Return null to match the + // non-cached path, which renders an empty payload (e.g. {"value":[]}). + if (result is { ValueKind: JsonValueKind.Undefined }) + { + return null; + } + byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(result); return JsonDocument.Parse(jsonBytes); } diff --git a/src/Service.Tests/UnitTests/SqlQueryEngineHelperTests.cs b/src/Service.Tests/UnitTests/SqlQueryEngineHelperTests.cs index e12b59d6cf..793b6993cc 100644 --- a/src/Service.Tests/UnitTests/SqlQueryEngineHelperTests.cs +++ b/src/Service.Tests/UnitTests/SqlQueryEngineHelperTests.cs @@ -46,6 +46,25 @@ public void ParseResultIntoJsonDocument_HandlesValuesAndNull(string? json, bool Assert.AreEqual(hasObject ? JsonValueKind.Object : JsonValueKind.Null, result.RootElement.ValueKind); } + /// + /// Regression test for https://github.com/Azure/data-api-builder/issues/3704 + /// An empty cached read surfaces as a default (JsonValueKind.Undefined) JsonElement that has no + /// backing document. Serializing it previously threw InvalidOperationException (HTTP 500). + /// ParseResultIntoJsonDocument must instead return null, matching the non-cached empty-result path. + /// + [TestMethod] + public void ParseResultIntoJsonDocument_UndefinedElement_ReturnsNull() + { + JsonElement? undefined = default(JsonElement); + MethodInfo method = typeof(SqlQueryEngine).GetMethod( + "ParseResultIntoJsonDocument", + BindingFlags.Static | BindingFlags.NonPublic)!; + + JsonDocument? result = (JsonDocument?)method.Invoke(null, new object?[] { undefined }); + + Assert.IsNull(result); + } + /// /// Verifies stored-procedure execution returns the first result object and maps empty or absent result arrays to null. ///