Skip to content

Snapshot stream rejects the 32-character digest issued for multi-document namespaces #6590

Closed

Description

@markphelps

Problem

EvaluationSnapshotNamespaceStream rejects every non-empty digest that isn't 40 characters long (client/server.go:118-120). But when a namespace is built from more than one document, the snapshot digest the server issues is 32 characters. For example:

  • the namespace is split across two files matched by flipt.yml, or
  • one file holds several YAML documents for the same namespace.

In that case snapshot.go:612-622 chains the per-file sha1s and truncates the result to 32 characters. TestEtagWithFewDocs asserts that behavior.

A client that resumes the stream by sending back the digest it received gets InvalidArgument ("must be a 40-character string"). The proto documents the field as the "Last snapshot digest the client has" (evaluationv2.proto:143), so sending it back is the intended use.

No client in this repo sends digest today, so only external clients that use stream resume are affected.

Reproduce

This test fails with invalid field digest: must be a 40-character string for the 32-character digest:

internal/server/evaluation/client/digest_test.go
package client

import (
	"testing"
	"testing/fstest"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"go.flipt.io/flipt/internal/server/environments"
	"go.flipt.io/flipt/internal/server/evaluation"
	storagefs "go.flipt.io/flipt/internal/storage/fs"
	rpcevaluation "go.flipt.io/flipt/rpc/v2/evaluation"
	"go.uber.org/zap/zaptest"
)

func TestEvaluationSnapshotNamespaceStream_MultiDocumentDigest(t *testing.T) {
	logger := zaptest.NewLogger(t)
	src := fstest.MapFS{
		"a/features.yaml": {Data: []byte("namespace:\n  key: default\nflags:\n- key: flag_a\n  name: A\n  type: BOOLEAN_FLAG_TYPE\n  enabled: true\n")},
		"b/features.yaml": {Data: []byte("namespace:\n  key: default\nflags:\n- key: flag_b\n  name: B\n  type: BOOLEAN_FLAG_TYPE\n  enabled: true\n")},
	}

	snap, err := storagefs.SnapshotFromPaths(logger, src, []string{"a/features.yaml", "b/features.yaml"})
	require.NoError(t, err)
	nsSnap, err := snap.EvaluationNamespaceSnapshot(t.Context(), "default")
	require.NoError(t, err)
	require.Len(t, nsSnap.Flags, 2)
	t.Logf("digest=%q len=%d", nsSnap.Digest, len(nsSnap.Digest))
	require.Len(t, nsSnap.Digest, 32)

	mockEnv := environments.NewMockEnvironment(t)
	envStore := evaluation.NewMockEnvironmentStore(t)
	mockEnv.On("Key").Return("env-key").Maybe()
	envStore.On("Get", mock.Anything, "env-key").Return(mockEnv, nil).Maybe()
	mockEnv.On("EvaluationNamespaceSnapshotSubscribe", mock.Anything, "default", mock.Anything).Return(&fakeCloser{}, nil).Run(func(args mock.Arguments) {
		ch := args.Get(2).(chan<- *rpcevaluation.EvaluationNamespaceSnapshot)
		go func() { ch <- nsSnap; close(ch) }()
	}).Maybe()

	stream := &mockStream{ctx: t.Context()}
	stream.On("Send", mock.Anything).Return(nil).Maybe()
	s := NewServer(logger, envStore)
	d := nsSnap.Digest
	err = s.EvaluationSnapshotNamespaceStream(&rpcevaluation.EvaluationNamespaceSnapshotStreamRequest{
		EnvironmentKey: "env-key", Key: "default", Digest: &d,
	}, stream)
	// Expected behavior: resuming with the server-issued digest is accepted and the unchanged snapshot is not resent.
	assert.NoError(t, err)
	assert.Empty(t, stream.sent)
}

Suggested fix

Accept any digest the server can issue: relax the check to 32 or 40 characters, or just a hex string with a sane maximum. The alternative is to stop truncating the chained digest. Add a stream test that uses a digest from a real multi-document snapshot.

Activity

  1. github-actions commented on Sep 24, 2026

    @github-actions
    Contributor

    Issue Health Check: Well scoped

    Type: Bug report

    A precise bug report identifying a validation mismatch where EvaluationSnapshotNamespaceStream rejects 32-character digests that the server itself issues for multi-document namespaces. It includes exact code links, a complete reproduction test, and a suggested fix.

    Thanks — this issue looks well scoped and ready for the maintainers to evaluate.

    Need help with Flipt? Support the project through GitHub Sponsors or learn about Flipt Pro.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    • Status
      Done
    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions