From d4e10207a91adc1f6db74b97e3095067ed17e4c1 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 2 Dec 2025 17:26:28 +0100 Subject: [PATCH 1/3] Fix StackOverflow on recursive contexts Signed-off-by: christian.lutnik --- .../java/dev/openfeature/sdk/HookSupport.java | 3 ++- .../dev/openfeature/sdk/HookSupportTest.java | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java index 41d36be6a..0254c07fd 100644 --- a/src/main/java/dev/openfeature/sdk/HookSupport.java +++ b/src/main/java/dev/openfeature/sdk/HookSupport.java @@ -63,7 +63,8 @@ public void executeBeforeHooks(HookSupportData data) { .orElse(Optional.empty()); if (returnedEvalContext.isPresent()) { var returnedContext = returnedEvalContext.get(); - if (!returnedContext.isEmpty()) { + // yes, we want to check for reference equality here, this prevents recursive layered contexts + if (returnedContext != hookContext.getCtx() && !returnedContext.isEmpty()) { data.evaluationContext.putHookContext(returnedContext); } } diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java index ef8dcc396..93b65f851 100644 --- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java @@ -1,6 +1,7 @@ package dev.openfeature.sdk; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -113,6 +114,29 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) { assertHookData(testHook2, 2, "before", "after", "finallyAfter", "error"); } + @Test + void hookThatReturnsTheGivenContext_doesNotResultInAStackOverflow() { + var hookSupportData = new HookSupportData(); + var recursiveHook = new Hook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.of(ctx.getCtx()); + } + }; + var layeredEvaluationContext = + new LayeredEvaluationContext(evaluationContextWithValue("key", "value"), null, null, null); + hookSupportData.evaluationContext = layeredEvaluationContext; + hookSupport.setHooks(hookSupportData, List.of(recursiveHook), FlagValueType.STRING); + hookSupport.setHookContexts( + hookSupportData, getBaseHookContextForType(FlagValueType.STRING), layeredEvaluationContext); + + callAllHooks(hookSupportData); + + layeredEvaluationContext.asObjectMap(); + + assertThatNoException(); + } + private static void callAllHooks(HookSupportData hookSupportData) { hookSupport.executeBeforeHooks(hookSupportData); hookSupport.executeAfterHooks( From 088d6d7b9c9aa4519a9488190fd167aa7cd0f1c3 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 2 Dec 2025 17:37:41 +0100 Subject: [PATCH 2/3] Fix StackOverflow on recursive contexts Signed-off-by: christian.lutnik --- src/test/java/dev/openfeature/sdk/HookSupportTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java index 93b65f851..fbea99338 100644 --- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java @@ -132,9 +132,7 @@ public Optional before(HookContext ctx, Map hints) { callAllHooks(hookSupportData); - layeredEvaluationContext.asObjectMap(); - - assertThatNoException(); + assertThatNoException().isThrownBy(layeredEvaluationContext::asObjectMap); } private static void callAllHooks(HookSupportData hookSupportData) { From 3754f75aba04d3d0fbbd861be7ece205878633b4 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 2 Dec 2025 17:45:25 +0100 Subject: [PATCH 3/3] Fix StackOverflow on recursive contexts Signed-off-by: christian.lutnik --- src/test/java/dev/openfeature/sdk/HookSupportTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java index fbea99338..3b21aff84 100644 --- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java @@ -123,10 +123,16 @@ public Optional before(HookContext ctx, Map hints) { return Optional.of(ctx.getCtx()); } }; + var emptyHook = new Hook() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.of(ImmutableContext.EMPTY); + } + }; var layeredEvaluationContext = new LayeredEvaluationContext(evaluationContextWithValue("key", "value"), null, null, null); hookSupportData.evaluationContext = layeredEvaluationContext; - hookSupport.setHooks(hookSupportData, List.of(recursiveHook), FlagValueType.STRING); + hookSupport.setHooks(hookSupportData, List.of(recursiveHook, emptyHook), FlagValueType.STRING); hookSupport.setHookContexts( hookSupportData, getBaseHookContextForType(FlagValueType.STRING), layeredEvaluationContext);