Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions libs/providers/ofrep-web/src/lib/ofrep-web-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,14 @@ export class OFREPWebProvider implements Provider {
};
}

if (resolved.value === undefined || resolved.value === null) {
return {
value: defaultValue,
flagMetadata: resolved.flagMetadata,
reason: resolved.reason || StandardResolutionReasons.DEFAULT,
};
}

if (typeof resolved.value !== type) {
return {
value: defaultValue,
Expand Down
8 changes: 8 additions & 0 deletions libs/providers/ofrep/src/lib/ofrep-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ describe('OFREPProvider should', () => {
);
});

it.each([true, false])('returns default if value is null', async (defaultValue) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test runs with 2 different defaults, and gets the default value both times since we send back a nullish value in OFREP

await expect(provider.resolveBooleanEvaluation('my-null-value-flag', defaultValue, {})).resolves.toStrictEqual({
value: defaultValue,
reason: 'DEFAULT',
flagMetadata: expect.objectContaining(TEST_FLAG_METADATA),
});
});

it('throw OFREPApiUnauthorizedError on HTTP 401', async () => {
await expect(provider.resolveBooleanEvaluation('my-flag', false, { expectedAuthHeader: 'secret' })).rejects.toThrow(
OFREPApiUnauthorizedError,
Expand Down
8 changes: 6 additions & 2 deletions libs/providers/ofrep/src/lib/ofrep-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ export class OFREPProvider implements Provider {
return handleEvaluationError(result, defaultValue);
}

if (typeof result.value.value !== typeof defaultValue) {
if (
result.value.value !== undefined &&
result.value.value !== null &&
typeof result.value.value !== typeof defaultValue
) {
return {
value: defaultValue,
reason: StandardResolutionReasons.ERROR,
Expand All @@ -104,6 +108,6 @@ export class OFREPProvider implements Provider {
};
}

return toResolutionDetails(result.value);
return toResolutionDetails(result.value, defaultValue);
}
}
21 changes: 15 additions & 6 deletions libs/shared/ofrep-core/src/lib/api/ofrep-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,22 @@ export function handleEvaluationError<T>(

export function toResolutionDetails<T extends EvaluationFlagValue>(
result: EvaluationSuccessResponse,
defaultValue: T,
): ResolutionDetails<T> {
return {
value: result.value as T,
variant: result.variant,
reason: result.reason,
flagMetadata: result.metadata && toFlagMetadata(result.metadata),
};
if (result.value === undefined || result.value === null) {
return {
value: defaultValue,
reason: result.reason || StandardResolutionReasons.DEFAULT,
flagMetadata: result.metadata && toFlagMetadata(result.metadata),
};
} else {
return {
value: result.value as T,
variant: result.variant,
reason: result.reason,
flagMetadata: result.metadata && toFlagMetadata(result.metadata),
};
}
}

export function toFlagMetadata(metadata: object): FlagMetadata {
Expand Down
17 changes: 12 additions & 5 deletions libs/shared/ofrep-core/src/test/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,21 @@ export const handlers = [
}

const scopeValue = new URL(info.request.url).searchParams.get('scope');
const key = info.params.key;
const reason = key.includes('null-value')
? StandardResolutionReasons.DEFAULT
: requestBody.context?.targetingKey
? StandardResolutionReasons.TARGETING_MATCH
: StandardResolutionReasons.STATIC;

const value = key.includes('null-value') ? null : true;
Comment on lines +125 to +131
Copy link
Member Author

@toddbaert toddbaert Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send back a value=null, variant=null and reason=DEFAULT, this is my preferred proposal for implementing a "defer to code default" evaluation in flagd. I think this would be non-breaking for other implementations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we need to send a variant at all?

const variant = key.includes('null-value') ? undefined : scopeValue ? 'scoped' : 'default';

return HttpResponse.json<EvaluationResponse>({
key: info.params.key,
reason: requestBody.context?.targetingKey
? StandardResolutionReasons.TARGETING_MATCH
: StandardResolutionReasons.STATIC,
variant: scopeValue ? 'scoped' : 'default',
value: true,
reason,
variant,
value,
metadata: TEST_FLAG_METADATA,
});
},
Expand Down