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..3b21aff84 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,33 @@ 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 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, emptyHook), FlagValueType.STRING); + hookSupport.setHookContexts( + hookSupportData, getBaseHookContextForType(FlagValueType.STRING), layeredEvaluationContext); + + callAllHooks(hookSupportData); + + assertThatNoException().isThrownBy(layeredEvaluationContext::asObjectMap); + } + private static void callAllHooks(HookSupportData hookSupportData) { hookSupport.executeBeforeHooks(hookSupportData); hookSupport.executeAfterHooks(