fix: don't mutate caller's fmtOptions when hiddenFields is passed - #474
fix: don't mutate caller's fmtOptions when hiddenFields is passed#474CedricConday wants to merge 2 commits into
Conversation
When both `fmtOptions` and `hiddenFields` are supplied, the request
builders assigned the caller's `fmtOptions` object to `queryParams.fmt_options`
by reference and then wrote `hidden_fields` onto it. This mutated the
caller's own object as a side effect — reusing the same `fmtOptions`
across calls silently accumulated a `hidden_fields` key.
Shallow-copy `fmtOptions` on assignment (`{ ...fmtOptions }`) so the
serialized request is unchanged while the caller's input is left intact.
Affects autocomplete, search, browse, recommendations and quizzes.
(agent.js assigns by reference too but never mutates, so it is unaffected.)
Adds an offline regression spec (stubbed fetch, no API key) asserting the
caller's `fmtOptions` is not mutated across all five modules.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbUHs855FyBt2J7cG2Qmhh
There was a problem hiding this comment.
Pull request overview
Fixes an API-footgun where passing both fmtOptions and hiddenFields caused request builders to mutate the caller’s fmtOptions object, by shallow-copying fmtOptions before adding hidden_fields. Adds a regression spec to ensure fmtOptions remains immutable across the affected modules.
Changes:
- Shallow-copy
fmtOptionsintoqueryParams.fmt_optionsto prevent caller object mutation whenhiddenFields(and related fmt mutations) are applied. - Apply the fix across autocomplete, search, browse, recommendations, and quizzes modules.
- Add an offline regression spec covering all five modules.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/modules/search.js | Copy fmtOptions before adding hidden_fields / fmt mutations to avoid mutating caller input. |
| src/modules/recommendations.js | Copy fmtOptions before adding hidden_fields. |
| src/modules/quizzes.js | Copy fmtOptions before adding hidden_fields. |
| src/modules/browse.js | Copy fmtOptions before adding hidden_fields / fmt mutations. |
| src/modules/autocomplete.js | Copy fmtOptions before adding hidden_fields. |
| spec/src/modules/fmtOptions-immutability.js | Adds regression coverage ensuring fmtOptions is not mutated by calls across modules. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| beforeEach(() => { | ||
| fetchStub = sinon.stub(global, 'fetch').resolves({ | ||
| ok: true, | ||
| status: 200, | ||
| json: () => Promise.resolve({}), | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| fetchStub.restore(); | ||
| }); | ||
|
|
||
| const client = () => new ConstructorIO({ | ||
| apiKey: 'key_immutability_test', | ||
| sessionId: 1, | ||
| clientId: 'immutability-client-id', | ||
| }); |
Address review note on Constructor-io#474: stub fetch through the ConstructorIO `fetch` option (as the other module specs do, e.g. spec/src/modules/search.js) instead of stubbing `global.fetch`, avoiding global side effects. Same assertions.
|
Checking back in — still current against
One thing worth flagging: no CI has ever run on this PR, so the checks list is empty rather than green. I think that needs a maintainer to approve the workflow for a fork. Happy to rebase if that helps kick it off. |
Problem
When a request is made with both
fmtOptionsandhiddenFields, the request builders assign the caller'sfmtOptionsobject toqueryParams.fmt_optionsby reference and then writehidden_fieldsonto it:So calling e.g.
getAutocompleteResultsmutates the object the caller passed in. Reusing the samefmtOptionsobject across calls silently accumulates ahidden_fieldskey.Reproduction
Fix
Shallow-copy
fmtOptionson assignment so the serialized request is identical while the caller's input is left intact:Applied to the five modules that exhibit the mutation: autocomplete, search, browse, recommendations, quizzes.
agent.jsassigns by reference too but never writes to it, so it is unaffected and left unchanged.Tests
Adds
spec/src/modules/fmtOptions-immutability.js— an offline regression spec (stubbedfetch, no API key/network) asserting the caller'sfmtOptionsis not mutated across all five modules. Verified it fails on the current code and passes with the fix.eslintclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01DbUHs855FyBt2J7cG2Qmhh