Skip to content

Commit 3900b84

Browse files
Deduplicate identical key/value pairs in user agent builder (#1718)
## Summary `useragent.data.With()` appended every key/value pair unconditionally and never deduplicated. When the same user-agent dimension is injected repeatedly onto a reused or long-lived `context.Context` — for example a caller that threads a single context across many operations — the per-context user-agent data accumulates duplicate entries, so `FromContext` produces a `User-Agent` string that grows without bound (e.g. `... sdk/x sdk/x resource/y resource/y ...`). Sufficiently large headers can be rejected or truncated downstream. Note that `With`'s doc comment claimed it "always uses the latest value for a given alphanumeric key", but the implementation always appended, and existing tests rely on multiple **distinct** values per key being preserved (e.g. multiple partners). This change keeps that behavior and only removes exact duplicates. ## Change `data.With()` now skips appending a key/value pair when an identical pair is already present. Distinct values for the same key are still preserved, so no existing behavior or tests change. The doc comment is updated to describe the actual behavior. ## Testing - `go test ./useragent/` — all pass, including `TestMultiplePartners` and `TestFromContext_Custom`. - Added `TestInContext_DeduplicatesIdenticalPairs`: 100 repeated identical injections collapse to one entry each; distinct values still preserved. - `go vet ./useragent/` and `gofmt` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code). --------- Co-authored-by: Renaud Hartert <renaud.hartert@databricks.com>
1 parent ad23528 commit 3900b84

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
### Bug Fixes
1010

11+
* Deduplicate identical key/value pairs in the user agent builder ([useragent](https://pkg.go.dev/github.com/databricks/databricks-sdk-go/useragent)), so repeatedly injecting the same dimension onto a reused `context.Context` no longer grows the `User-Agent` header without bound. Distinct values for the same key are still preserved.
12+
1113
### Documentation
1214

1315
### Internal Changes

useragent/user_agent.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ func (u info) String() string {
111111

112112
type data []info
113113

114-
// With always uses the latest value for a given alphanumeric key.
114+
// With appends the key/value pair, skipping it when an identical key/value
115+
// pair is already present. De-duplicating exact pairs keeps the user agent from
116+
// growing without bound when the same dimension is injected repeatedly onto a
117+
// reused context. Distinct values for the same key are still preserved.
115118
// Panics if key or value don't satisfy alphanumeric or semver format.
116119
func (d data) With(key, value string) data {
117120
if err := matchAlphanum(key); err != nil {
@@ -120,6 +123,11 @@ func (d data) With(key, value string) data {
120123
if err := matchAlphanumOrSemVer(value); err != nil {
121124
panic(err)
122125
}
126+
for _, e := range d {
127+
if e.Key == key && e.Value == value {
128+
return d
129+
}
130+
}
123131
return append(d, info{key, value})
124132
}
125133

useragent/user_agent_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,24 @@ func TestMultiplePartners(t *testing.T) {
8282
assert.Contains(t, userAgent, "partner/partner1")
8383
assert.Contains(t, userAgent, "partner/partner2")
8484
}
85+
86+
func TestInContext_DeduplicatesIdenticalPairs(t *testing.T) {
87+
// Repeatedly injecting the same key/value pair onto a reused context must
88+
// not grow the user agent: this is what happened when a long-lived caller
89+
// threaded one context across many operations.
90+
ctx := context.Background()
91+
for i := 0; i < 100; i++ {
92+
ctx = InContext(ctx, "resource", "cluster")
93+
ctx = InContext(ctx, "sdk", "sdkv2")
94+
}
95+
userAgent := FromContext(ctx)
96+
assert.Equal(t, 1, strings.Count(userAgent, "resource/cluster"))
97+
assert.Equal(t, 1, strings.Count(userAgent, "sdk/sdkv2"))
98+
99+
// Distinct values for the same key are still preserved.
100+
ctx2 := InContext(context.Background(), "resource", "a")
101+
ctx2 = InContext(ctx2, "resource", "b")
102+
userAgent2 := FromContext(ctx2)
103+
assert.Contains(t, userAgent2, "resource/a")
104+
assert.Contains(t, userAgent2, "resource/b")
105+
}

0 commit comments

Comments
 (0)