Skip to content

fix(core): match nested event filters when an ancestor key is missing… - #5022

Closed
Sadikdeveloper wants to merge 1 commit into
triggerdotdev:mainfrom
Sadikdeveloper:arena/0e5c4aef-trigger-dev
Closed

Sadikdeveloper wants to merge 1 commit into
triggerdotdev:mainfrom
Sadikdeveloper:arena/0e5c4aef-trigger-dev

Conversation

@Sadikdeveloper

Copy link
Copy Markdown

… or null

eventFilterMatches returned false for any non-empty filter as soon as the payload was undefined/null, without evaluating the filter itself. Because missing nested keys recurse with an undefined payload, a nested $exists: false filter failed whenever its parent key was missing or null, while the same filter returned true at the top level.

A nullish payload is now matched like an empty object, so a missing payload and a missing key take the same path. Only filters whose leaves already match a missing key change behavior: $exists: false, $isNull: false and $anythingBut now match a missing/null/non-object ancestor too, which is how $exists behaves on nested paths elsewhere (e.g. MongoDB) and how these filters already behaved for a missing key at the top level. Filters that require a value ($exists: true, $startsWith, $gt, value arrays, $includes, $not, ...) are unchanged.

This is the user-facing path behind retry.fetch's bodyFilter, so a filter like { error: { retryAfter: [{ $exists: false }] } } now matches a response body with no error object instead of silently skipping the retry strategy.

  • core: unit tests for missing/null/non-object ancestors, deep nesting, arrays, and guards proving positive filters don't over-match
  • trigger-sdk: end-to-end tests for retry.fetch + bodyFilter against a local server

Closes #

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

[Describe the steps you took to test this change]


Changelog

[Short description of what has changed]


Screenshots

[Screenshots]

💯

… or null

`eventFilterMatches` returned `false` for any non-empty filter as soon as the
payload was `undefined`/`null`, without evaluating the filter itself. Because
missing nested keys recurse with an `undefined` payload, a nested `$exists:
false` filter failed whenever its parent key was missing or `null`, while the
same filter returned `true` at the top level.

A nullish payload is now matched like an empty object, so a missing payload and
a missing key take the same path. Only filters whose leaves already match a
missing key change behavior: `$exists: false`, `$isNull: false` and
`$anythingBut` now match a missing/`null`/non-object ancestor too, which is how
`$exists` behaves on nested paths elsewhere (e.g. MongoDB) and how these filters
already behaved for a missing key at the top level. Filters that require a value
(`$exists: true`, `$startsWith`, `$gt`, value arrays, `$includes`, `$not`, ...)
are unchanged.

This is the user-facing path behind `retry.fetch`'s `bodyFilter`, so a filter
like `{ error: { retryAfter: [{ $exists: false }] } }` now matches a response
body with no `error` object instead of silently skipping the retry strategy.

- core: unit tests for missing/null/non-object ancestors, deep nesting, arrays,
  and guards proving positive filters don't over-match
- trigger-sdk: end-to-end tests for `retry.fetch` + `bodyFilter` against a
  local server
@changeset-bot

changeset-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 71e2fea

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 28 packages
Name Type
@trigger.dev/core Patch
@trigger.dev/build Patch
trigger.dev Patch
@trigger.dev/python Patch
@trigger.dev/redis-worker Patch
@trigger.dev/schema-to-json Patch
@trigger.dev/sdk Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline Patch
@trigger.dev/rbac Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@internal/tracing Patch
@internal/webhook-engine Patch
@internal/webhook-sources Patch
@internal/dashboard-agent Patch
@internal/cache Patch
@trigger.dev/react-hooks Patch
@trigger.dev/rsc Patch
@trigger.dev/billing Patch
@trigger.dev/database Patch
@trigger.dev/otlp-importer Patch
@trigger.dev/sso Patch
@internal/testcontainers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Hi @Sadikdeveloper, thanks for your interest in contributing!

This project requires that pull request authors are vouched, and you are not in the list of vouched users.

This PR will be closed automatically. See https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md for more details.

@github-actions github-actions Bot closed this Oct 12, 2026
@coderabbitai

coderabbitai Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 26a21309-0644-41a2-8132-37c6cbec6876

📥 Commits

Reviewing files that changed from the base of the PR and between 93465d8 and 71e2fea.


📒 Files selected for processing (4)
  • .changeset/nested-event-filter-exists-false.md
  • packages/core/src/eventFilterMatches.ts
  • packages/core/test/eventFilterMatches.test.ts
  • packages/trigger-sdk/src/v3/retry.test.ts

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Devin Review

// give in MongoDB-style filters, and the same result these filters already gave for a missing key
// at the top level. Filters that need a value (`$exists: true`, `$startsWith`, `$gt`, ...) still
// don't match `undefined`.
const payloadObject = payload === undefined || payload === null ? {} : payload;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Missing parents match empty nested filters

When an ancestor is missing, eventFilterMatches now accepts nested {} or [] filters without an absence condition. A retry.fetch body filter like { error: {} } can retry responses without error.

Learn more

An empty nested object or array matches any present value, but the previous nullish guard rejected it when its parent was missing. The new normalization lets those empty filters succeed despite having no leaf that tests absence. This also changes SDK retry selection because getRetryStrategyForResponse uses this matcher for response bodies.

Example: With payload {} and filter { error: {} }, the previous matcher returned false; it now returns true. A retry.fetch strategy using that filter can retry a response with no error field.

Recommended fix: When traversing a missing or null ancestor, distinguish filters that actually match a missing leaf from vacuous matches produced by empty nested objects or arrays. Preserve the previous result for the latter and add tests for both empty forms.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

"@trigger.dev/core": patch
---

Fix `eventFilterMatches` so nested filters that assert the absence of a value match when an ancestor key is missing or `null`, instead of failing before the filter is evaluated. A missing payload is now matched like an empty object: it's how `$exists` behaves on nested paths elsewhere, and how these filters already behaved for a missing key at the top level.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Lead the release note with user behavior

The changeset opens with eventFilterMatches instead of the user-visible change to nested filters. Repository guidance asks release notes to lead with user behavior.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +120 to +124
const server = await startJsonServer((requestIndex) =>
requestIndex === 1
? { status: 429, body: { message: "slow down" } }
: { status: 200, body: { ok: true } }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Null response bypasses retry absence filter

A JSON null response cannot reach the new matching behavior: getRetryStrategyForResponse skips falsy bodies. The tests cover an object without error, not a null body.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant