Skip to content

fix: replace the association on the same event type when attaching - #38

Open
hideokamoto wants to merge 3 commits into
mainfrom
claude/nifty-planck-0bzpm8
Open

hideokamoto wants to merge 3 commits into
mainfrom
claude/nifty-planck-0bzpm8

Conversation

@hideokamoto

@hideokamoto hideokamoto commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

attatchEdgeFunction only removes an association whose ARN matches exactly before it appends the new one. When another version of the same function (or another function) is already on the target event type, the config sent to UpdateDistribution ends up with two associations for that event type. CloudFront allows only one function per event type.

This PR makes attach replace whatever is associated with the target event type of the default cache behavior. Detach is unchanged.

Cause

At 1d57cc8 (master; libs/configUpdator.ts is unchanged since 999a8e8):

  • libs/configUpdator.ts:136-153 attatchEdgeFunction calls detatchEdgeFunction (L137) and then pushes the new association (L145-149).
  • libs/configUpdator.ts:115-121 detatchEdgeFunction drops an item only when item.EventType === this.eventType and isTargetLambdaArn(item.LambdaFunctionARN) is true.
  • libs/configUpdator.ts:96-99 isTargetLambdaArn is a strict === comparison, so ...:function:edge-fn:2 is not treated as the target when the controller holds ...:function:edge-fn:3.

Result: starting from viewer-request -> edge-fn:2 and attaching edge-fn:3 gives two viewer-request items and Quantity: 2 (see the Red output below).

The existing tests use the fixture 'edge-lambda-arn', which has no ARN shape and no version, and only re-attach the same ARN. That is why they did not cover this case.

The CloudFront rule

From node_modules/@aws-sdk/client-cloudfront/dist-types/models/models_0.d.ts:717 (@aws-sdk/client-cloudfront 3.879.0, the version in pnpm-lock.yaml), the doc comment of LambdaFunctionAssociations, quoted verbatim:

 * <p>A complex type that specifies a list of Lambda@Edge functions associations for a cache behavior.</p> <p>If you want to invoke one or more Lambda@Edge functions triggered by requests that match the <code>PathPattern</code> of the cache behavior, specify the applicable values for <code>Quantity</code> and <code>Items</code>. Note that there can be up to 4 <code>LambdaFunctionAssociation</code> items in this list (one for each possible value of <code>EventType</code>) and each <code>EventType</code> can be associated with only one function.</p> <p>If you don't want to invoke any Lambda@Edge functions for the requests that match <code>PathPattern</code>, specify <code>0</code> for <code>Quantity</code> and omit <code>Items</code>.</p>

Unconfirmed: whether real CloudFront actually rejects an UpdateDistribution request with two associations for one event type. I did not test against real AWS. An earlier local run against an AWS API emulator produced the duplicated viewer-request association with this library, and the emulator accepted the update, so the emulator does not tell us either way. I did not re-run that emulator check for this PR.

Design choice

Two options were considered:

  • (A) chosen: on attach, replace any association on the same event type.
  • (B) rejected: on attach, replace only other versions of the same function (same unversioned ARN), and throw if a different function is on that event type.

Why (A):

  1. The README lists "A/B Testing: Quickly switch between different Lambda@Edge functions for testing" and "Disaster Recovery: Quickly rollback to previous edge function versions" as use cases (README.md:21, README.md:26). A controller is built with one ARN and one event type, and attachEdgeFunction is the only way to put a function on an event type. With (B), switching to a different function would throw. The caller would first have to build a second controller with the old function's exact versioned ARN and detach it. That is two UpdateDistribution calls (two deployments) with a window where nothing runs on that event type.
  2. Per the rule above, an event type holds one function. After "attach X to viewer-request" the only valid state is "X on viewer-request", and (A) produces exactly that.
  3. (A) needs no ARN parsing. (B) would have to derive the unversioned ARN. This library does not validate ARNs, so callers can pass unversioned, $LATEST or alias ARNs, and (B) would need extra rules for those.

What (B) would have given us: protection against replacing a function that was put there by someone else by mistake. If you prefer that safety, the change is small and I can rework the PR.

Behaviour change

The output changes only when the target event type of the default cache behavior already has an association with a different ARN:

Situation Before After
Same event type has another version of the same function Two associations on that event type, Quantity +1 Replaced, Quantity unchanged
Same event type has a different function Two associations on that event type, Quantity +1 Replaced, Quantity unchanged
Same event type has the same ARN One association (unchanged) One association (unchanged)
Event type is empty Appended Appended
  • If CloudFront rejects the old output (as the SDK docs suggest, but unconfirmed), attach used to fail in the first two rows and now succeeds. Calls that succeeded before produce the same result.
  • The visible change: attach can now remove a different function from the same event type. Code that relied on attach failing in that situation would now overwrite the other function.
  • I used fix: without a BREAKING CHANGE footer, so under the README's Release Process (semantic-release, fix: -> patch) this would ship as a patch. If you consider the second row a breaking change, the fix commit needs fix!: or a BREAKING CHANGE: footer to get a major release; tell me and I will reword it.
  • Unchanged: detach still removes only the association on the controller's event type whose ARN matches exactly. Only the default cache behavior is updated; CacheBehaviors are not touched (same as before). The new association is created without IncludeBody, as before. When it replaces an association that had IncludeBody: true on the same event type, that flag is not carried over (re-attaching the same ARN already behaved this way).

README.md and README_ja.md now describe this for attachEdgeFunction and detachEdgeFunction.

Tests (Red -> Green)

New tests use versioned, ARN-shaped fixtures (arn:aws:lambda:us-east-1:123456789012:function:edge-fn:3, the AWS documentation placeholder account also used in the README):

  • configUpdator.test.ts, attach: replaces another version of the same function; replaces a different function; keeps origin-request (with IncludeBody) and viewer-response associations, at most one association per event type, and Quantity equals Items.length; leaves CacheBehaviors unchanged; re-attaching the same version keeps one association.
  • configUpdator.test.ts, detach: removes only the exact version.
  • controller.test.ts: attachEdgeFunction with a stubbed CloudFrontClient sends one viewer-request association in UpdateDistribution.

Red

Commit 3123eb5 (tests only, on top of master 1d57cc8). pnpm run test --ci exited 1: Tests: 5 failed, 14 passed, 19 total.

Full output
> lambda-edge-controller@2.0.0 test /dev/shm/lec-wt3/red
> jest --ci

FAIL __tests__/configUpdator.test.ts
  ● libs/configUpdator.ts › with versioned Lambda function ARNs › #attatchEdgeFunction() › should replace another version of the same function on the same event type

    expect(received).toEqual(expected) // deep equality

    - Expected  - 1
    + Received  + 5

      Object {
        "Items": Array [
          Object {
            "EventType": "viewer-request",
    +       "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:2",
    +     },
    +     Object {
    +       "EventType": "viewer-request",
            "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:3",
          },
        ],
    -   "Quantity": 1,
    +   "Quantity": 2,
      }

      278 |         ])
      279 |         const result = updator.attatchEdgeFunction(config)
    > 280 |         expect(result.DefaultCacheBehavior?.LambdaFunctionAssociations).toEqual({
          |                                                                         ^
      281 |           Quantity: 1,
      282 |           Items: [
      283 |             { EventType: 'viewer-request', LambdaFunctionARN: functionArn('edge-fn', 3) }

      at Object.<anonymous> (__tests__/configUpdator.test.ts:280:73)

  ● libs/configUpdator.ts › with versioned Lambda function ARNs › #attatchEdgeFunction() › should replace a different function on the same event type

    expect(received).toEqual(expected) // deep equality

    - Expected  - 1
    + Received  + 5

      Object {
        "Items": Array [
          Object {
            "EventType": "viewer-request",
    +       "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:other-fn:1",
    +     },
    +     Object {
    +       "EventType": "viewer-request",
            "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:3",
          },
        ],
    -   "Quantity": 1,
    +   "Quantity": 2,
      }

      290 |         ])
      291 |         const result = updator.attatchEdgeFunction(config)
    > 292 |         expect(result.DefaultCacheBehavior?.LambdaFunctionAssociations).toEqual({
          |                                                                         ^
      293 |           Quantity: 1,
      294 |           Items: [
      295 |             { EventType: 'viewer-request', LambdaFunctionARN: functionArn('edge-fn', 3) }

      at Object.<anonymous> (__tests__/configUpdator.test.ts:292:73)

  ● libs/configUpdator.ts › with versioned Lambda function ARNs › #attatchEdgeFunction() › should keep associations for other event types and keep Quantity equal to the number of Items

    expect(received).toHaveLength(expected)

    Expected length: 3
    Received length: 4
    Received array:  [{"EventType": "viewer-request", "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:2"}, {"EventType": "origin-request", "IncludeBody": true, "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:origin-fn:5"}, {"EventType": "viewer-response", "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:response-fn:1"}, {"EventType": "viewer-request", "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:3"}]

      315 |         const associations = result.DefaultCacheBehavior?.LambdaFunctionAssociations
      316 |         const items = associations?.Items || []
    > 317 |         expect(items).toHaveLength(3)
          |                       ^
      318 |         expect(associations?.Quantity).toEqual(items.length)
      319 |         expect(items).toEqual(expect.arrayContaining([
      320 |           { EventType: 'viewer-request', LambdaFunctionARN: functionArn('edge-fn', 3) },

      at Object.<anonymous> (__tests__/configUpdator.test.ts:317:23)

  ● libs/configUpdator.ts › with versioned Lambda function ARNs › #attatchEdgeFunction() › should not change the other cache behaviors

    expect(received).toEqual(expected) // deep equality

    - Expected  - 1
    + Received  + 5

      Object {
        "Items": Array [
          Object {
            "EventType": "viewer-request",
    +       "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:2",
    +     },
    +     Object {
    +       "EventType": "viewer-request",
            "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:3",
          },
        ],
    -   "Quantity": 1,
    +   "Quantity": 2,
      }

      349 |         const result = updator.attatchEdgeFunction(config)
      350 |         expect(result.CacheBehaviors).toEqual(expectedCacheBehaviors)
    > 351 |         expect(result.DefaultCacheBehavior?.LambdaFunctionAssociations).toEqual({
          |                                                                         ^
      352 |           Quantity: 1,
      353 |           Items: [
      354 |             { EventType: 'viewer-request', LambdaFunctionARN: functionArn('edge-fn', 3) }

      at Object.<anonymous> (__tests__/configUpdator.test.ts:351:73)

FAIL __tests__/controller.test.ts
  ● libs/contorller.ts › #attachEdgeFunction() › should send a single viewer-request association when another version is already attached

    expect(received).toEqual(expected) // deep equality

    - Expected  - 1
    + Received  + 5

      Object {
        "Items": Array [
          Object {
            "EventType": "viewer-request",
    +       "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:2",
    +     },
    +     Object {
    +       "EventType": "viewer-request",
            "LambdaFunctionARN": "arn:aws:lambda:us-east-1:123456789012:function:edge-fn:3",
          },
        ],
    -   "Quantity": 1,
    +   "Quantity": 2,
      }

      50 |       expect(updateInputs[0].Id).toEqual('EDISTRIBUTIONID')
      51 |       expect(updateInputs[0].IfMatch).toEqual('etag')
    > 52 |       expect(updateInputs[0].DistributionConfig?.DefaultCacheBehavior?.LambdaFunctionAssociations).toEqual({
         |                                                                                                    ^
      53 |         Quantity: 1,
      54 |         Items: [
      55 |           { EventType: 'viewer-request', LambdaFunctionARN: functionArn('edge-fn', 3) }

      at Object.<anonymous> (__tests__/controller.test.ts:52:100)

Test Suites: 2 failed, 2 total
Tests:       5 failed, 14 passed, 19 total
Snapshots:   0 total
Time:        3.223 s
Ran all test suites.
 ELIFECYCLE  Test failed. See above for more details.

Green

Commit 060a363 (the fix). pnpm run test --ci --verbose exited 0: Tests: 19 passed, 19 total.

Full output
> lambda-edge-controller@2.0.0 test /dev/shm/lec-wt3/fix
> jest --ci --verbose

PASS __tests__/configUpdator.test.ts
  libs/configUpdator.ts
    constructor
      ✓ should set valid arn (6 ms)
    methods
      ✓ return new param that removed edge Lambda when given associated edge lambda and more (1 ms)
      #getLambdaArn()
        ✓ should return lambda arn (1 ms)
      #updateEventType()
        ✓ should return default event type (1 ms)
        ✓ should update event type (1 ms)
      #createUpdateDistributionParam()
        ✓ should return valid update params
      detatchEdgeFunction
        ✓ return same param when given no associated edge lambda (1 ms)
        ✓ return same param when given no associated edge lambda
        ✓ return new param that removed edge Lambda when given associated edge lambda
      #attatchEdgeFunction()
        ✓ should attach edge lambda arn when given no associated lambda@edge
        ✓ should attach edge lambda arn when given no associated lambda@edge
    with versioned Lambda function ARNs
      #attatchEdgeFunction()
        ✓ should replace another version of the same function on the same event type
        ✓ should replace a different function on the same event type
        ✓ should keep associations for other event types and keep Quantity equal to the number of Items (2 ms)
        ✓ should not change the other cache behaviors
        ✓ should keep a single association when the same version is attached again
      #detatchEdgeFunction()
        ✓ should remove only the exact function version

PASS __tests__/controller.test.ts
  libs/contorller.ts
    constructor
      ✓ should not be throw error (8 ms)
    #attachEdgeFunction()
      ✓ should send a single viewer-request association when another version is already attached (2 ms)

Test Suites: 2 passed, 2 total
Tests:       19 passed, 19 total
Snapshots:   0 total
Time:        3.014 s
Ran all test suites.

Other checks (head of this branch 41bfc74, Node 22.23.4, pnpm install --frozen-lockfile)

The same steps as the GitHub Actions workflow:

  • pnpm run lint: exit 0
  • pnpm run typecheck: exit 0
  • pnpm run test --ci: 19 passed
  • pnpm run build: exit 0

This branch was rebased onto #37 (yarn -> pnpm) after the PR was opened. The Red and Green output above is from the rebased commits (each run used a git archive export of that commit, which is why the paths in the output are not a repository checkout). Before the rebase, the same tests gave the same Red result (5 failed, 14 passed) and the same Green result (19 passed) with yarn.

Notes on existing tooling (not changed in this PR):

  • pnpm run lint only covers libs/*.ts. __tests__/.eslintrc.yml is not valid YAML (bad indentation of a mapping entry (2:46)), so ESLint cannot run on the tests directory as is. Running ESLint on the tests with the root config reports nothing on the new lines.
  • commitlint.config.js fails to load because package.json has "type": "module". I checked the three commit messages with the same rules (@commitlint/config-conventional) from a .cjs config outside the repo: no problems.
  • .releaserc.json lists main as the release branch, while this repository's default branch (and the base of this PR) is master.

Not in this PR

  • Integration tests against an emulator (separate task).
  • Real AWS verification of the one-function-per-event-type rule (unconfirmed, see above).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bmzxkp9hit6dRSrgRbVgx4

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Summary

Summary by CodeRabbit

  • 修正
    • CloudFrontのデフォルトキャッシュ動作で、同じイベント種別のLambda@Edge関連付けを新しい関数またはバージョンに置き換えるようになりました。
    • その他のイベント種別やキャッシュ動作の関連付けは維持されます。
    • 関数の切り離しでは、イベント種別とバージョンを含むARNが一致する関連付けのみが対象となることを明記しました。

attachEdgeFunctionは、Default Cache Behaviorで同じイベントタイプの既存関連付けを除外し、新しい関連付けを追加するよう変更されました。APIリファレンスとテストも更新されました。

Changes: Lambda@Edge関連付け

Layer / File(s) Summary
関連付けの置換とAPIリファレンス
libs/configUpdator.ts, README.md, README_ja.md
attachEdgeFunctionは、同じイベントタイプの既存関連付けを除外して新しい項目を追加します。APIリファレンスはDefault Cache Behaviorを対象とすること、attachでは同じイベントタイプの関連付けを置き換えること、detachではイベントタイプとバージョンを含むARNが一致する関連付けを対象とすることを説明します。
関連付け更新のテスト
__tests__/configUpdator.test.ts, __tests__/controller.test.ts
設定更新のテストは、別バージョンや別関数の置換、他イベントタイプの関連付けとIncludeBodyの保持、Quantityの整合性、他キャッシュビヘイビアの保持、同一バージョンの重複防止、および不一致ARNの保持を検証します。コントローラーテストは、バージョン3を指定した更新要求が1回送信され、Distribution IDとETagが設定されることを検証します。

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested reviewers: hidetaka-dc

Merge Risk: Minimal · up to 41bfc

Attaching a function now replaces any existing association on the same event type of the default cache behavior, which fixes the duplicate associations produced before. A manually set IncludeBody on the replaced association is not carried over, as was already the case when re-attaching the same ARN. This is a design decision, not a blocker, and the change looks safe to merge.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed タイトル「fix: replace the association on the same event type when attaching」は、attach時に同じイベントタイプの既存関連付けを置き換えるという、このPRの主な変更を具体的に示しています。変更内容(libs/configUpdator.tsのattatchEdgeFunctionの修正、テスト追加、README更新)と一致します…
Description check Passed 説明文は、attatchEdgeFunctionが同じイベントタイプの既存関連付けを残す問題、修正内容(同じイベントタイプの関連付けを置き換える)、原因の箇所、テスト結果、設計判断、動作変更、README更新を具体的に記述しています。変更内容と明確に関連しています。
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

タイトル「fix: replace the association on the same event type when attaching」は、attach時に同じイベントタイプの既存関連付けを置き換えるという、このPRの主な変更を具体的に示しています。変更内容(libs/configUpdator.tsのattatchEdgeFunctionの修正、テスト追加、README更新)と一致します。

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

__tests__/configUpdator.test.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

YAMLException: Cannot read config file: /tests/.eslintrc.yml
Error: bad indentation of a mapping entry (2:46)

1 | ...
2 | ... -eslint/no-explicit-any": "off",
-----------------------------------------^
3 | ... ff"
at generateError (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:197:10)
at throwError (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:201:9)
at readBlockMapping (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:1232:7)
at composeNode (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:1520:53)
at readBlockMapping (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:1214:11)
at composeNode (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:1520:53)
at readDocument (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:1710:3)
at loadDocuments (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:1768:5)
at Object.load (/.eslint-tmp/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/lib/loader.js:1792:21)
at loadYAMLConfigFile (/.eslint-tmp/node_modules/.pnpm/@eslint+eslintrc@2.1.4_supports-color@7.2.0/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2519:21)

__tests__/controller.test.ts

ESLint skipped: the matched ESLint configuration already failed (config-incompatibility).

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: c8a37060-6308-4d58-9d83-8c03caa84e02
Commits

Reviewing files that changed from the base of the PR and between 1d57cc8 and 41bfc74.

Files selected for processing: 5

  • README.md
  • README_ja.md
  • __tests__/configUpdator.test.ts
  • __tests__/controller.test.ts
  • libs/configUpdator.ts

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.

…ersion

attatchEdgeFunction only removes an association whose ARN matches exactly
before appending, so attaching edge-fn:3 while edge-fn:2 is on
viewer-request leaves two viewer-request associations (Quantity 2).
CloudFront allows only one function per event type.

The existing fixtures ('edge-lambda-arn') have no version, so they did not
cover this case. These tests use versioned ARN fixtures and fail on the
current code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bmzxkp9hit6dRSrgRbVgx4
CloudFront allows only one function per event type in a cache behavior.
attatchEdgeFunction now drops every association whose EventType equals the
target event type of the default cache behavior before appending the new
one, instead of dropping only an exact ARN match. Attaching edge-fn:3 while
edge-fn:2 (or another function) is on viewer-request now leaves a single
viewer-request association.

Associations for other event types and the other cache behaviors are left
unchanged. detatchEdgeFunction still removes only the exact ARN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bmzxkp9hit6dRSrgRbVgx4
@hideokamoto
hideokamoto force-pushed the claude/nifty-planck-0bzpm8 branch from b7c7591 to 41bfc74 Compare November 12, 2026 07:49
@hideokamoto
hideokamoto marked this pull request as ready for review November 12, 2026 07:57

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants