Skip to content

Commit bf9de4e

Browse files
authored
feat: Support Returning Error Resolutions from Providers (open-feature#323)
When provider resolutions with error code set other than `None` are returned, the provider acts as if an error was thrown. Signed-off-by: christian.lutnik <[email protected]>
1 parent 25bc54b commit bf9de4e

File tree

3 files changed

+78
-5
lines changed

3 files changed

+78
-5
lines changed

src/OpenFeature/OpenFeatureClient.cs

+17-1
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,23 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlagAsync<T>(
263263
(await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false))
264264
.ToFlagEvaluationDetails();
265265

266-
await this.TriggerAfterHooksAsync(allHooksReversed, hookContext, evaluation, options, cancellationToken).ConfigureAwait(false);
266+
if (evaluation.ErrorType == ErrorType.None)
267+
{
268+
await this.TriggerAfterHooksAsync(
269+
allHooksReversed,
270+
hookContext,
271+
evaluation,
272+
options,
273+
cancellationToken
274+
).ConfigureAwait(false);
275+
}
276+
else
277+
{
278+
var exception = new FeatureProviderException(evaluation.ErrorType, evaluation.ErrorMessage);
279+
this.FlagEvaluationErrorWithDescription(flagKey, evaluation.ErrorType.GetDescription(), exception);
280+
await this.TriggerErrorHooksAsync(allHooksReversed, hookContext, exception, options, cancellationToken)
281+
.ConfigureAwait(false);
282+
}
267283
}
268284
catch (FeatureProviderException ex)
269285
{

test/OpenFeature.Tests/OpenFeatureClientTests.cs

+36
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,41 @@ public async Task When_Exception_Occurs_During_Evaluation_Should_Return_Error()
433433
_ = featureProviderMock.Received(1).ResolveStructureValueAsync(flagName, defaultValue, Arg.Any<EvaluationContext>());
434434
}
435435

436+
[Fact]
437+
public async Task When_Error_Is_Returned_From_Provider_Should_Not_Run_After_Hook_But_Error_Hook()
438+
{
439+
var fixture = new Fixture();
440+
var domain = fixture.Create<string>();
441+
var clientVersion = fixture.Create<string>();
442+
var flagName = fixture.Create<string>();
443+
var defaultValue = fixture.Create<Value>();
444+
const string testMessage = "Couldn't parse flag data.";
445+
446+
var featureProviderMock = Substitute.For<FeatureProvider>();
447+
featureProviderMock.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any<EvaluationContext>())
448+
.Returns(Task.FromResult(new ResolutionDetails<Value>(flagName, defaultValue, ErrorType.ParseError,
449+
"ERROR", null, testMessage)));
450+
featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create<string>()));
451+
featureProviderMock.GetProviderHooks().Returns(ImmutableList<Hook>.Empty);
452+
453+
await Api.Instance.SetProviderAsync(featureProviderMock);
454+
var client = Api.Instance.GetClient(domain, clientVersion);
455+
var testHook = new TestHook();
456+
client.AddHooks(testHook);
457+
var response = await client.GetObjectDetailsAsync(flagName, defaultValue);
458+
459+
response.ErrorType.Should().Be(ErrorType.ParseError);
460+
response.Reason.Should().Be(Reason.Error);
461+
response.ErrorMessage.Should().Be(testMessage);
462+
_ = featureProviderMock.Received(1)
463+
.ResolveStructureValueAsync(flagName, defaultValue, Arg.Any<EvaluationContext>());
464+
465+
Assert.Equal(1, testHook.BeforeCallCount);
466+
Assert.Equal(0, testHook.AfterCallCount);
467+
Assert.Equal(1, testHook.ErrorCallCount);
468+
Assert.Equal(1, testHook.FinallyCallCount);
469+
}
470+
436471
[Fact]
437472
public async Task Cancellation_Token_Added_Is_Passed_To_Provider()
438473
{
@@ -454,6 +489,7 @@ public async Task Cancellation_Token_Added_Is_Passed_To_Provider()
454489
{
455490
await Task.Delay(10); // artificially delay until cancelled
456491
}
492+
457493
return new ResolutionDetails<string>(flagName, defaultString, ErrorType.None, cancelledReason);
458494
});
459495
featureProviderMock.GetMetadata().Returns(new Metadata(fixture.Create<string>()));

test/OpenFeature.Tests/TestImplementations.cs

+25-4
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,49 @@
88

99
namespace OpenFeature.Tests
1010
{
11-
public class TestHookNoOverride : Hook { }
11+
public class TestHookNoOverride : Hook
12+
{
13+
}
1214

1315
public class TestHook : Hook
1416
{
15-
public override ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
17+
private int _beforeCallCount;
18+
public int BeforeCallCount { get => this._beforeCallCount; }
19+
20+
private int _afterCallCount;
21+
public int AfterCallCount { get => this._afterCallCount; }
22+
23+
private int _errorCallCount;
24+
public int ErrorCallCount { get => this._errorCallCount; }
25+
26+
private int _finallyCallCount;
27+
public int FinallyCallCount { get => this._finallyCallCount; }
28+
29+
public override ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> context,
30+
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
1631
{
32+
Interlocked.Increment(ref this._beforeCallCount);
1733
return new ValueTask<EvaluationContext>(EvaluationContext.Empty);
1834
}
1935

2036
public override ValueTask AfterAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> details,
2137
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
2238
{
39+
Interlocked.Increment(ref this._afterCallCount);
2340
return new ValueTask();
2441
}
2542

26-
public override ValueTask ErrorAsync<T>(HookContext<T> context, Exception error, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
43+
public override ValueTask ErrorAsync<T>(HookContext<T> context, Exception error,
44+
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
2745
{
46+
Interlocked.Increment(ref this._errorCallCount);
2847
return new ValueTask();
2948
}
3049

31-
public override ValueTask FinallyAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
50+
public override ValueTask FinallyAsync<T>(HookContext<T> context,
51+
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
3252
{
53+
Interlocked.Increment(ref this._finallyCallCount);
3354
return new ValueTask();
3455
}
3556
}

0 commit comments

Comments
 (0)