Skip to content

feat(request-filters): advanced mode operations and execution engine#912

Merged
ding113 merged 5 commits into
devfrom
feat/request-filter-advanced-mode
Mar 13, 2026
Merged

feat(request-filters): advanced mode operations and execution engine#912
ding113 merged 5 commits into
devfrom
feat/request-filter-advanced-mode

Conversation

@ding113

@ding113 ding113 commented Mar 13, 2026

Copy link
Copy Markdown
Owner

Summary

Add advanced mode to request filters with a typed Operation DSL (set, remove, merge, insert) enabling expressive header/body manipulation beyond simple scope/action/target/replacement. Implement final execution phase in proxy forwarder - advanced-mode filters are now applied to outbound requests before forwarding to upstream providers. Harden request filter engine against prototype pollution (__proto__, constructor, prototype path traversal) and ReDoS bypass (regex complexity limits). Address i18n and a11y code review findings across all 5 supported languages.

Problem

The existing request filter engine supports only simple operations (single scope/action/target/replacement pattern). This limits expressiveness for complex header/body manipulation scenarios such as:

  • Setting headers with conditional write modes (overwrite vs. if-missing)
  • Removing array elements by matcher patterns
  • Deep merging objects with null-delete semantics
  • Inserting elements with position/anchor/deduplication

Related Issues: None (new feature)

Solution

Introduce a two-mode system for request filters:

  1. Simple mode (default): Preserves backward compatibility with existing filter definitions
  2. Advanced mode: Enables typed Operation DSL for expressive manipulation

Key design decisions:

  • Operation executor supporting 4 operation types with configurable behaviors
  • FilterMatcher abstraction for exact/contains/regex matching strategies
  • Security hardening: prototype pollution guards and ReDoS complexity limits
  • Execution phase differentiation: guard phase (pre-validation) vs final phase (pre-forward)

Changes

Core Engine (src/lib/request-filter-engine.ts)

  • New operation executor supporting set (with overwrite/if_missing write modes), remove (with matcher-based array element removal), merge (deep recursive with null-delete semantics), and insert (with position/anchor/dedupe)
  • FilterMatcher supporting exact, contains, and regex match strategies
  • Prototype pollution guard on all dot-path traversal
  • Regex complexity limit to prevent ReDoS

Types (src/lib/request-filter-types.ts)

  • New Operation DSL type definitions: SetOp, RemoveOp, MergeOp, InsertOp, FilterMatcher

Schema & Data Layer

  • DB migration adding rule_mode, execution_phase, operations JSONB column to request filters table
  • New index idx_request_filters_phase for efficient phase-based queries
  • Repository layer support for advanced mode filter queries

Proxy Integration (src/app/v1/_lib/proxy/forwarder.ts)

  • Advanced-mode filter execution integrated into the proxy forwarding pipeline
  • Final phase filters applied before upstream request

UI (filter-dialog.tsx, filter-table.tsx)

  • Advanced mode toggle in filter creation/edit dialog
  • JSON editor for operation arrays with validation
  • Filter table displays mode badge (simple/advanced)
  • i18n keys added for all 5 languages (en, ja, ru, zh-CN, zh-TW)

Security (src/actions/request-filters.ts)

  • Server-side validation for operation arrays (path safety, regex complexity)
  • Input sanitization for advanced mode payloads

Breaking Changes

None. This is a backward-compatible addition:

  • Existing filters default to rule_mode: 'simple' and execution_phase: 'guard'
  • New columns have safe defaults
  • Simple mode behavior unchanged

Testing

Automated Tests

  • 985 lines of unit tests covering all 4 operation types (tests/unit/request-filter-advanced.test.ts)
  • Prototype pollution path rejection tests
  • ReDoS regex complexity limit tests
  • Matcher strategies (exact, contains, regex) tests
  • Insert dedup and anchor positioning tests
  • Deep merge with null-delete semantics tests

Manual Testing

  • Create advanced-mode filter via UI, verify JSON editor validation
  • Verify advanced filter applies correctly to proxied requests

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • i18n updated for all 5 languages
  • Manual testing completed

Description enhanced by Claude AI

Greptile Summary

This PR introduces advanced mode for request filters — a typed Operation DSL (set, remove, merge, insert) with a two-phase execution model (guard for pre-validation, final for pre-forward). The existing simple mode is fully backward-compatible. The engine implementation is solid: prototype pollution guards in parsePath and deepMerge, ReDoS protection via safe-regex, and transport header blacklisting. The DB migration, repository layer, forwarder integration, UI, and i18n are all clean additions.

Two server-side validation gaps need addressing before the feature ships:

  • dedupe.byFields field names are not validated against VALIDATION_UNSAFE_KEYS (src/actions/request-filters.ts:116). An admin submitting byFields: ["__proto__"] causes executeInsertOp to silently skip every insertion because deepEqual(Object.prototype, Object.prototype) always short-circuits to true, making every element appear to already exist as a duplicate.
  • validateMatcher.field is not checked for prototype-polluting names (src/actions/request-filters.ts:38). A matcher.field = "__proto__" combined with matchType: "contains" and value: "object" matches every plain object (since String(Object.prototype) is "[object Object]"), allowing a remove operation to silently wipe an entire array.

Both issues are admin-only exploitable but represent correctness and defense-in-depth gaps that should be closed before the feature is enabled.

Confidence Score: 2/5

  • Not safe to merge — two server-side validation gaps allow admin-crafted payloads to silently break advanced filter behaviour
  • The overall architecture (two-phase execution, prototype pollution guards in the engine, ReDoS protection, DB migration with safe defaults) is well thought-out and correctly implemented. However, two specific validation gaps in validateOperations and validateMatcher allow __proto__-class field names to reach the engine via dedupe.byFields and matcher.field, producing silent correctness failures rather than caught errors. These are in addition to the three issues already flagged in prior review threads (transport-header blacklist reachability, merge value key scanning, and matchElement field traversal). With five open defects across the same security/correctness surface, the feature needs another pass before merging.
  • src/actions/request-filters.ts — two validation functions (validateMatcher and the dedupe.byFields path in validateOperations) need VALIDATION_UNSAFE_KEYS coverage added; src/lib/request-filter-engine.ts — three issues flagged in prior review threads remain open

Important Files Changed

Filename Overview
src/lib/request-filter-engine.ts Core engine for advanced filter execution. Adds applyFinal, advanced op executors (set/remove/merge/insert), and FilterMatcher. Prototype pollution guards in parsePath and deepMerge are correct; however matchElement does not guard matcher.field against unsafe key traversal (flagged in prior thread). dedupe.byFields in executeInsertOp also accesses fields directly without UNSAFE_KEYS check, causing silent no-op inserts when byFields: ["__proto__"] is used.
src/actions/request-filters.ts Server actions with comprehensive validation for advanced mode. validateOperations correctly guards op.path for prototype pollution but misses two gaps: (1) dedupe.byFields field names are not checked against VALIDATION_UNSAFE_KEYS, enabling a silent insert no-op; (2) validateMatcher.field is never validated, allowing dangerous key traversal at the engine layer.
src/app/v1/_lib/proxy/forwarder.ts Integrates applyFinal into the proxy forwarding pipeline for both Gemini and Anthropic paths. Gemini correctly uses structuredClone before passing body to applyFinal to avoid mutating session.request.message. The Anthropic path safely avoids mutation by working on the copy returned by filterPrivateParameters. Both bypassRequestFilters guards are in place.
src/lib/request-filter-types.ts Clean TypeScript DSL type definitions for the four operation types. InsertOp.dedupe.byFields is typed as string[] with no runtime safety constraints — the validation gap in actions means dangerous values can reach the engine.
src/repository/request-filters.ts Database layer correctly wires the three new columns (rule_mode, execution_phase, operations) through all CRUD operations, with safe defaults and proper type casting.
drizzle/0081_strange_zarek.sql Additive migration adding three columns with safe defaults and a new btree index on (is_enabled, execution_phase). Backward compatible; no data loss risk.
tests/unit/request-filter-advanced.test.ts Comprehensive 985-line test suite covering all four operation types, prototype pollution paths, ReDoS limits, matcher strategies, insert dedupe, anchor positioning, deep merge, and phase integration. Does not cover the byFields: ["__proto__"] silent no-op edge case.
src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx UI dialog with advanced mode toggle, JSON editor for operations, and client-side validation. i18n keys wired correctly via useTranslations.
src/app/[locale]/settings/request-filters/_components/filter-table.tsx Table correctly displays mode and phase badges for the new columns. Toggle, delete, and refresh interactions remain consistent with existing patterns.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming Request] --> B[Guard Phase]
    B --> C{Filter\nbindingType?}
    C -->|global| D[applyGlobal\nmodifies session.headers\n& session.request.message]
    C -->|providers/groups| E[applyForProvider\nmodifies session]
    D --> F[Provider Selection\n& Auth]
    E --> F
    F --> G{Provider\nType?}
    G -->|Anthropic/Claude| H[filterPrivateParameters\nreturns fresh copy]
    G -->|Gemini| I[structuredClone\nbodyToSerialize]
    H --> J[applyFinal\nAnthropomorphic Path\nline 2250]
    I --> K[applyFinal\nGemini Path\nline 1915]
    J --> L{Filter ruleMode?}
    K --> L
    L -->|simple| M[applySimpleFilterDirect\nheaders / body]
    L -->|advanced| N[executeAdvancedOps]
    N --> O{op type}
    O -->|set| P[executeSetOp\noverwrite / if_missing]
    O -->|remove| Q[executeRemoveOp\nmatcher-based array filter]
    O -->|merge| R[executeMergeOp\ndeepMerge null-delete]
    O -->|insert| S[executeInsertOp\nposition / anchor / dedupe]
    M --> T[Transport Header\nBlacklist Enforcement]
    P --> T
    Q --> T
    R --> T
    S --> T
    T --> U[Serialize & Forward\nto Upstream]
Loading

Comments Outside Diff (2)

  1. src/actions/request-filters.ts, line 277-358 (link)

    Up to 3 sequential DB round-trips for the same record in a single update

    updateRequestFilterAction calls getRequestFilterById(id) independently in three separate conditional blocks (lines 278, 311, 334). In the worst case — when ruleMode, target, and bindingType all change in the same request — three sequential DB queries hit the same row before the actual updateRequestFilter write.

    Fetch the record once unconditionally at the top of the function, then reuse existing throughout all three validation blocks:

    const existing = await getRequestFilterById(id);
    if (!existing) return { ok: false, error: "记录不存在" };
    // then use `existing` in all three validation blocks below
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/actions/request-filters.ts
    Line: 277-358
    
    Comment:
    **Up to 3 sequential DB round-trips for the same record in a single update**
    
    `updateRequestFilterAction` calls `getRequestFilterById(id)` independently in three separate conditional blocks (lines 278, 311, 334). In the worst case — when `ruleMode`, `target`, and `bindingType` all change in the same request — three sequential DB queries hit the same row before the actual `updateRequestFilter` write.
    
    Fetch the record once unconditionally at the top of the function, then reuse `existing` throughout all three validation blocks:
    
    ```typescript
    const existing = await getRequestFilterById(id);
    if (!existing) return { ok: false, error: "记录不存在" };
    // then use `existing` in all three validation blocks below
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/lib/request-filter-engine.ts, line 351-364 (link)

    reload() early-return resolves callers before actual reload completes

    When isLoading is true a second concurrent call to reload() returns an already-resolved Promise<void> (line 352). Any awaiting caller — notably refreshRequestFiltersCache which does await requestFilterEngine.reload(); const stats = requestFilterEngine.getStats(); — will receive stale stats.count data because the in-flight reload hasn't finished yet.

    Consider storing the in-flight promise and returning it for subsequent callers, similar to the pattern used in ensureInitialized:

    private reloadPromise: Promise<void> | null = null;
    
    async reload(): Promise<void> {
      if (this.reloadPromise) return this.reloadPromise;
      this.reloadPromise = this._doReload().finally(() => {
        this.reloadPromise = null;
      });
      return this.reloadPromise;
    }
    
    private async _doReload(): Promise<void> {
      this.isLoading = true;
      try {
        const { getActiveRequestFilters } = await import("@/repository/request-filters");
        const filters = await getActiveRequestFilters();
        this.loadFilters(filters);
      } catch (error) {
        logger.error("[RequestFilterEngine] Failed to reload filters", { error });
      } finally {
        this.isLoading = false;
      }
    }
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/request-filter-engine.ts
    Line: 351-364
    
    Comment:
    **`reload()` early-return resolves callers before actual reload completes**
    
    When `isLoading` is `true` a second concurrent call to `reload()` returns an already-resolved `Promise<void>` (line 352). Any awaiting caller — notably `refreshRequestFiltersCache` which does `await requestFilterEngine.reload(); const stats = requestFilterEngine.getStats();` — will receive stale `stats.count` data because the in-flight reload hasn't finished yet.
    
    Consider storing the in-flight promise and returning it for subsequent callers, similar to the pattern used in `ensureInitialized`:
    
    ```typescript
    private reloadPromise: Promise<void> | null = null;
    
    async reload(): Promise<void> {
      if (this.reloadPromise) return this.reloadPromise;
      this.reloadPromise = this._doReload().finally(() => {
        this.reloadPromise = null;
      });
      return this.reloadPromise;
    }
    
    private async _doReload(): Promise<void> {
      this.isLoading = true;
      try {
        const { getActiveRequestFilters } = await import("@/repository/request-filters");
        const filters = await getActiveRequestFilters();
        this.loadFilters(filters);
      } catch (error) {
        logger.error("[RequestFilterEngine] Failed to reload filters", { error });
      } finally {
        this.isLoading = false;
      }
    }
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/actions/request-filters.ts
Line: 116-118

Comment:
**`dedupe.byFields` entries not validated for prototype-polluting names**

The current check only verifies that `dedupe.byFields` is an array, but never inspects the individual field name strings. If an admin submits `{ "dedupe": { "byFields": ["__proto__"] } }`, it passes server-side validation, is stored in the DB, and reaches `executeInsertOp`. There, at line 769 of the engine:

```ts
return byFields.every((field) => deepEqual(existingObj[field], valueObj[field]));
```

For any plain object, `existingObj["__proto__"]` returns `Object.prototype`. The `deepEqual` check immediately hits the `a === b` short-circuit (both sides are the same `Object.prototype` reference) and returns `true`. As a result, the dedupe check always fires, and **no element is ever inserted** — the operation is silently swallowed with no error.

Fix by adding a check similar to the existing `op.path` guard:

```typescript
if (insertOp.dedupe?.byFields && !Array.isArray(insertOp.dedupe.byFields)) {
  return `${prefix}: dedupe.byFields must be an array`;
}
// ADD: validate each byField entry
if (insertOp.dedupe?.byFields) {
  for (const field of insertOp.dedupe.byFields) {
    if (VALIDATION_UNSAFE_KEYS.test(field)) {
      return `${prefix}: dedupe.byFields contains a forbidden property name`;
    }
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/actions/request-filters.ts
Line: 38-44

Comment:
**`validateMatcher` does not check `matcher.field` for prototype-polluting names**

`validateMatcher` only validates the regex safety of `matcher.value`. It never inspects `matcher.field`, even though `matcher.field` is used directly as a dot-path key during element traversal in `matchElement`. An admin can submit `{ "field": "__proto__", "matchType": "contains", "value": "object" }` — this passes server-side validation, gets stored, and at runtime `matchElement` evaluates `element["__proto__"]` which returns `Object.prototype` for any plain JS object. `String(Object.prototype)` is `"[object Object]"`, so the `contains: "object"` check matches **every** array element, causing a remove operation to wipe the entire array.

This is a distinct layer from the engine-level concern already noted in thread #3: the server-side validation should reject dangerous `field` values before they ever reach the engine.

Add a `VALIDATION_UNSAFE_KEYS` check on `matcher.field`:

```typescript
function validateMatcher(matcher: FilterMatcher, context: string): string | null {
  if (matcher.field && VALIDATION_UNSAFE_KEYS.test(matcher.field)) {
    return `${context}: matcher.field contains a forbidden property name`;
  }
  if (matcher.matchType === "regex" && typeof matcher.value === "string") {
    if (!safeRegex(matcher.value)) {
      return `${context}: regex matcher has ReDoS risk`;
    }
  }
  return null;
}
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 875c0b5

Greptile also left 2 inline comments on this PR.

ding113 and others added 4 commits March 13, 2026 01:54
…on phase

Introduce two new axes for request filters: rule mode (simple/advanced)
and execution phase (guard/final). Advanced mode supports a JSON-defined
operation DSL with set, remove, merge, and insert operations. The final
phase executes after all provider overrides, enabling post-processing
use cases like system message injection with dedup and cache_control
manipulation.

Key changes:
- Schema: add rule_mode, execution_phase, operations columns + index
- Types: new FilterOperation DSL (SetOp, RemoveOp, MergeOp, InsertOp)
- Engine: 4-bucket cache split, applyFinal() method, operation executors
  with deep merge (null-as-delete), deep equal dedup, anchor matching
- Forwarder: integrate applyFinal in standard and Gemini branches
- Validation: validateOperations with ReDoS prevention via safe-regex
- UI: rule mode toggle, execution phase selector, JSON operations editor
- i18n: new keys for all 5 locales (en, zh-CN, zh-TW, ja, ru)
- Tests: 29 new tests covering all operation types and edge cases
…on and ReDoS bypass

- Block __proto__/constructor/prototype traversal in parsePath, deepMerge, and validation
- Add runtime safe-regex check in matchElement for regex matchers
- Fix ReDoS bypass in updateRequestFilterAction by checking effective target/matchType/action
- Add operations array length limit (max 50) and value validation for set/merge/insert ops
- Fix UI default executionPhase to match server default (guard, not final)
- Fix parsePath regex corruption from edit tool (double-escaped backslashes)
- Remove console.error from filter-dialog.tsx (error already shown via toast)
- Fix ja table.actions translation to "操作"
- Fix ru table.createdAt to "Дата создания"
- Fix zh-CN confirmDelete halfwidth ? to fullwidth ?
- Show operations count in target column for advanced mode filters
- Add aria-label to filter toggle Switch for accessibility
@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

添加高级请求过滤器支持:引入规则模式(simple/advanced)、执行阶段(guard/final)与JSON操作序列;扩展数据库模式、类型定义、验证与引擎执行路径;在代理转发中加入最终阶段过滤;新增 UI 和多语言文案及全面单元测试覆盖。

Changes

Cohort / File(s) Summary
数据库迁移与模式
drizzle/0081_strange_zarek.sql, src/drizzle/schema.ts, drizzle/meta/_journal.json
request_filters 添加 rule_modeexecution_phaseoperations 字段,并创建基于 is_enabledexecution_phase 的索引;新增迁移 journal 条目。
类型与持久化
src/lib/request-filter-types.ts, src/repository/request-filters.ts
新增 FilterOperation DSL(Set/Remove/Merge/Insert 等类型);添加 RequestFilterRuleModeRequestFilterExecutionPhase 类型;扩展 RequestFilter、创建/更新输入以包含新字段并持久化。
动作与验证
src/actions/request-filters.ts
增加操作验证(validateOperations、validateMatcher)、不安全键检查与常量(MAX_OPERATIONS、VALIDATION_UNSAFE_KEYS);在创建/更新流程中接收并验证 ruleModeexecutionPhaseoperations
核心过滤引擎
src/lib/request-filter-engine.ts, tests/unit/request-filter-advanced.test.ts
实现最终阶段(final)过滤流程、操作执行引擎(set/remove/merge/insert)、路径/匹配/合并工具、传输头黑名单与原型污染防护;新增大量单元测试覆盖高级操作与最终阶段行为。
代理转发器调整
src/app/v1/_lib/proxy/forwarder.ts
在 Gemini 与标准请求路径中引入/重排最终阶段请求过滤点,改进 Gemini 认证/headers 构建与流检测,确保在适当时机应用 final 过滤器。
前端组件与本地化
src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx, src/app/[locale]/settings/request-filters/_components/filter-table.tsx, messages/*/settings/requestFilters.json
在对话框与表格中加入 ruleModeexecutionPhase 控件与显示;支持高级模式的 JSON 操作输入与验证;为多语言(en/ja/ru/zh-CN/zh-TW)添加相应文案与验证消息键。

估计代码审查工作量

🎯 4 (复杂) | ⏱️ ~75 分钟

可能相关的PR

  • PR #520:也修改了 src/app/v1/_lib/proxy/forwarder.ts 中 Gemini header 构建与请求流位置,可能与本次对 forwarder 的改动冲突或重叠。
  • PR #729:对同一 forwarder 流程进行调整(请求处理顺序/注入点),与本次在转发器中加入 final 过滤的改动相关。
  • PR #536:在代理转发管道中注入 provider 特定转换,触及与本 PR 相同的请求处理逻辑,可能存在交叉影响。
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了PR的核心改动:为请求过滤器添加高级模式操作引擎和执行阶段支持。
Description check ✅ Passed 描述详细阐述了问题、解决方案、关键设计决策、安全加固措施和测试覆盖范围,与变更集完全相关。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/request-filter-advanced-mode
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

CodeRabbit can suggest fixes for GitHub Check annotations.

Configure the reviews.tools.github-checks setting to adjust the time to wait for GitHub Checks to complete.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the request filtering capabilities by introducing an 'advanced mode' that allows for highly granular and programmatic manipulation of request headers and bodies. This new mode operates through a defined DSL, enabling complex operations like deep merging, conditional insertions, and targeted removals. Crucially, these advanced filters are now applied in a dedicated 'final execution phase' within the proxy, ensuring they take effect after all other provider-specific overrides. The changes also include critical security measures to prevent common vulnerabilities like prototype pollution and ReDoS, alongside comprehensive UI updates for managing these new filter types and full internationalization support.

Highlights

  • Advanced Request Filter Mode: Introduced an 'advanced mode' for request filters, allowing for more complex header and body manipulations using a typed Operation DSL (set, remove, merge, insert).
  • Final Execution Phase: Implemented a new 'final execution phase' in the proxy forwarder, ensuring advanced-mode filters are applied to outbound requests just before they are sent to upstream providers.
  • Security Hardening: Enhanced the request filter engine with robust protections against prototype pollution (blocking 'proto', 'constructor', 'prototype' path traversal) and ReDoS (Regular Expression Denial of Service) attacks through complexity limits.
  • Database & API Updates: Added new database columns (rule_mode, execution_phase, operations) to support advanced filters and updated the API to handle the new filter types and their server-side validation.
  • User Interface Enhancements: Updated the UI with an advanced mode toggle, a JSON editor for operations with validation, and badges in the filter table to display the rule mode and execution phase. Also addressed i18n and a11y findings across all supported languages.
  • Comprehensive Unit Testing: Added extensive unit tests (985 lines) covering all four new operation types, prototype pollution path rejection, ReDoS regex complexity limits, matcher strategies, and insert deduplication/anchoring.
Changelog
  • drizzle/0081_strange_zarek.sql
    • Added rule_mode column to request_filters table with default 'simple' and 'advanced' options.
    • Added execution_phase column to request_filters table with default 'guard' and 'final' options.
    • Added operations JSONB column to request_filters table for advanced mode operations.
    • Created a new index idx_request_filters_phase on is_enabled and execution_phase for optimized querying.
  • drizzle/meta/_journal.json
    • Updated the Drizzle migration journal to include the new 0081_strange_zarek migration.
  • messages/en/settings/requestFilters.json
    • Added new internationalization keys for 'Execution Phase', 'Final' and 'Guard' descriptions.
    • Added new internationalization keys for 'Operations (JSON)', 'Define advanced operations as a JSON array', and 'Rule Mode'.
    • Added new validation messages for advanced mode operations, including anchor requirements, body scope restrictions for merge/insert, invalid JSON format, and unsafe regex patterns.
    • Added new table headers for 'Mode', 'Phase', and 'Advanced Ops' count.
  • messages/ja/settings/requestFilters.json
    • Added Japanese internationalization keys for new execution phase, rule mode, operations, and validation messages.
  • messages/ru/settings/requestFilters.json
    • Added Russian internationalization keys for new execution phase, rule mode, operations, and validation messages.
  • messages/zh-CN/settings/requestFilters.json
    • Added Simplified Chinese internationalization keys for new execution phase, rule mode, operations, and validation messages.
  • messages/zh-TW/settings/requestFilters.json
    • Added Traditional Chinese internationalization keys for new execution phase, rule mode, operations, and validation messages.
  • src/actions/request-filters.ts
    • Imported new types FilterMatcher, FilterOperation, InsertOp for advanced mode validation.
    • Introduced VALIDATION_UNSAFE_KEYS and MAX_OPERATIONS constants for security and limits.
    • Implemented validateMatcher function to check regex safety within matchers.
    • Implemented validateOperations function to perform comprehensive validation of advanced mode operation arrays, including type checks, scope restrictions, path safety (prototype pollution), value requirements, and op-specific logic.
    • Modified validatePayload to conditionally apply validation based on ruleMode (simple or advanced).
    • Updated createRequestFilterAction and updateRequestFilterAction to accept and persist new ruleMode, executionPhase, and operations fields.
    • Enhanced ReDoS validation in updateRequestFilterAction to consider effective target, matchType, and action from both updates and existing filter state.
  • src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx
    • Imported new types FilterOperation, RequestFilterExecutionPhase, RequestFilterRuleMode.
    • Added state variables for ruleMode, executionPhase, and operationsJson to manage advanced filter properties.
    • Updated useEffect hook to synchronize new filter properties when editing an existing filter.
    • Modified handleSubmit to include validation for advanced mode operations (JSON parsing, array check, required operations).
    • Adjusted payload construction in handleSubmit to conditionally include simple mode fields or advanced mode operations based on ruleMode.
    • Refactored UI to include Rule Mode and Execution Phase selectors.
    • Conditionally rendered simple mode fields (scope, action, match type, target, replacement) or an Operations (JSON) editor based on the selected ruleMode.
  • src/app/[locale]/settings/request-filters/_components/filter-table.tsx
    • Added new table headers for 'Mode' and 'Phase' to display filter rule mode and execution phase.
    • Implemented rendering logic for 'Mode' and 'Phase' badges, with distinct styling for advanced/final modes.
    • Modified the 'Target' column to display 'X operation(s)' for advanced mode filters instead of the target path.
    • Added aria-label to the Switch component for improved accessibility.
  • src/app/v1/_lib/proxy/forwarder.ts
    • Added isApiKey variable initialization.
    • Integrated requestFilterEngine.applyFinal call into the Gemini handling logic, applying final-phase filters after headers are built and before body serialization.
    • Integrated requestFilterEngine.applyFinal call into the general proxy forwarding logic, applying final-phase filters after all provider overrides and before body serialization.
  • src/drizzle/schema.ts
    • Added rule_mode column definition to requestFilters table with varchar(20), default 'simple', and type inference.
    • Added execution_phase column definition to requestFilters table with varchar(20), default 'guard', and type inference.
    • Added operations column definition to requestFilters table as jsonb.
    • Added requestFiltersPhaseIdx index on is_enabled and execution_phase.
  • src/lib/request-filter-engine.ts
    • Imported new types for FilterMatcher, FilterOperation, InsertOp, MergeOp, RemoveOp, SetOp.
    • Defined TRANSPORT_HEADER_BLACKLIST for headers that should never be user-controlled.
    • Defined UNSAFE_KEYS to prevent prototype pollution during path traversal.
    • Enhanced parsePath to include prototype pollution prevention for keys.
    • Removed redundant comments regarding optimizations in setValueByPath and replaceText.
    • Added getValueByPath function for safe read-only traversal of objects.
    • Added deleteByPath function for safely deleting properties or array elements by path.
    • Implemented deepEqual for recursive structural equality checks.
    • Implemented deepMerge for recursive object merging with null-as-delete semantics, including prototype pollution prevention.
    • Implemented matchElement function to check if an element matches a FilterMatcher (exact, contains, regex).
    • Refactored RequestFilterEngine to separate filters into globalGuardFilters, providerGuardFilters, globalFinalFilters, and providerFinalFilters based on executionPhase.
    • Added hasGroupBasedFinalFilters property.
    • Modified loadFilters to categorize and sort filters by executionPhase and ruleMode.
    • Updated getStats to reflect the counts of all new filter buckets.
    • Introduced applyFinal method to apply final-phase filters directly to provided body and headers, including transport header blacklisting.
    • Implemented collectFinalFilters to gather relevant final-phase filters for a given session.
    • Implemented applySimpleFilterDirect to apply simple-mode filters on raw body/headers within the final phase.
    • Implemented executeAdvancedOps to iterate and execute advanced operations.
    • Implemented executeSetOp for setting values with overwrite or if_missing modes.
    • Implemented executeRemoveOp for removing properties or array elements (with optional matcher).
    • Implemented executeMergeOp for deep merging objects.
    • Implemented executeInsertOp for inserting elements into arrays with position, anchor, and deduplication logic.
    • Updated setFiltersForTest to use the new loadFilters logic.
  • src/lib/request-filter-types.ts
    • Added new file to define TypeScript types for the advanced request filter mode.
    • Defined FilterMatcher interface for matching elements in arrays, including field, value, and matchType.
    • Defined BaseOp interface with scope.
    • Defined SetOp interface for setting values, including op, path, value, and writeMode.
    • Defined RemoveOp interface for removing values, including op, path, and optional matcher.
    • Defined MergeOp interface for deep merging objects, including op, path, and value.
    • Defined InsertOp interface for inserting elements into arrays, including op, path, value, position, optional anchor, onAnchorMissing fallback, and dedupe options.
    • Defined FilterOperation as a union type of all specific operation interfaces.
  • src/repository/request-filters.ts
    • Imported FilterOperation type.
    • Added new type definitions: RequestFilterRuleMode ('simple' | 'advanced') and RequestFilterExecutionPhase ('guard' | 'final').
    • Extended RequestFilter interface with ruleMode, executionPhase, and operations properties.
    • Updated mapRow function to correctly map new columns from database rows.
    • Updated CreateRequestFilterInput and UpdateRequestFilterInput interfaces to include ruleMode, executionPhase, and operations.
  • tests/unit/request-filter-advanced.test.ts
    • Added new file for comprehensive unit tests for the advanced request filter mode.
    • Implemented helper functions createFilter and createAdvancedFilter for test setup.
    • Added tests for 'Insert Operations', covering default end insertion, start insertion, before/after anchor positioning, onAnchorMissing fallbacks ('start', 'end', 'skip'), and deduplication logic (deep equal, byFields, disabled).
    • Added tests for 'Remove Operations', covering path deletion and array element removal by matcher.
    • Added tests for 'Set Operations', covering overwrite and 'if_missing' write modes, and intermediate object creation.
    • Added tests for 'Merge Operations', covering deep merging new fields, overwriting existing fields, null-as-delete semantics, nested object merging, and target object creation if missing.
    • Added tests for 'Matcher' functionality, covering 'contains' and 'regex' match types.
    • Added tests for 'Final Phase Integration', verifying that final filters execute on provided body/headers, transport header blacklist enforcement, provider binding in the final phase, and simple mode filters working in the final phase.
Activity
  • The author, ding113, has introduced a significant new feature: advanced request filters with a dedicated execution engine.
  • The pull request includes extensive unit tests (985 lines) to ensure the correctness and robustness of the new advanced mode operations.
  • Security concerns regarding prototype pollution and ReDoS have been proactively addressed and tested.
  • Internationalization (i18n) and accessibility (a11y) considerations have been integrated into the UI changes across five languages.
  • The database schema has been updated to support the new filter properties, and the repository layer has been adapted accordingly.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions Bot added enhancement New feature or request area:i18n area:UI size/XL Extra Large PR (> 1000 lines) labels Mar 13, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces an "advanced mode" for request filters, enabling more granular manipulation of request bodies and headers through a new Domain Specific Language (DSL) of operations (set, remove, merge, insert). Key changes include database schema updates to store rule_mode, execution_phase, and operations, new UI components for configuring these advanced filters, and extensive backend logic for validating and executing the operations, with built-in protections against ReDoS and prototype pollution. The filter engine was refactored to support two execution phases: "guard" (before provider selection) and "final" (after provider overrides), with advanced operations primarily targeting the "final" phase. However, the current implementation has two critical issues: the applyFinal filter logic is not being applied to Gemini requests that lack a body, preventing header modifications, and the general applyFinal logic is incorrectly placed inside a hasBody check, meaning it won't execute for any requests without a body, thus failing to apply header-modifying filters.

Comment on lines +1939 to +1945
processedHeaders = ProxyForwarder.buildGeminiHeaders(
session,
provider,
effectiveBaseUrl,
accessToken,
isApiKey
);

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.

high

Final-phase filters are not being applied to Gemini requests that don't have a body (e.g., GET requests). These filters might need to modify headers, so applyFinal should be called here in the else block as well, passing an empty object for the body.

        processedHeaders = ProxyForwarder.buildGeminiHeaders(
          session,
          provider,
          effectiveBaseUrl,
          accessToken,
          isApiKey
        );

        // Final-phase request filter for Gemini (no body)
        if (!session.getEndpointPolicy().bypassRequestFilters) {
          const { requestFilterEngine } = await import("@/lib/request-filter-engine");
          await requestFilterEngine.applyFinal(session, {}, processedHeaders);
        }

Comment on lines +2234 to +2238
// Final-phase request filter: after all provider overrides, before serialization
if (!session.getEndpointPolicy().bypassRequestFilters) {
const { requestFilterEngine } = await import("@/lib/request-filter-engine");
await requestFilterEngine.applyFinal(session, messageToSend, processedHeaders);
}

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.

high

Final-phase filters are not applied to requests without a body (e.g., GET requests), as this logic is inside an if (hasBody) check. This logic should be moved outside the if (hasBody) block to ensure filters can operate on headers for all request types. For requests without a body, an empty object {} can be passed to applyFinal.

Comment on lines +540 to +563
const allFinal = this.collectFinalFilters(session);
if (allFinal.length === 0) return;

for (const filter of allFinal) {
try {
if (filter.ruleMode === "advanced" && filter.operations) {
this.executeAdvancedOps(filter.operations, body, headers);
} else {
// simple mode: reuse existing logic but on body/headers directly
this.applySimpleFilterDirect(filter, body, headers);
}
} catch (error) {
logger.error("[RequestFilterEngine] Failed to apply final filter", {
filterId: filter.id,
ruleMode: filter.ruleMode,
error,
});
}
}

// Transport header blacklist enforcement
for (const h of TRANSPORT_HEADER_BLACKLIST) {
headers.delete(h);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Transport header blacklist silently skipped when no final-phase filters match

The early-return on line 541 exits the function before the transport header blacklist loop at lines 560–563 runs. This means if the current session has zero matching final-phase filters, the blacklist (content-length, connection, transfer-encoding) is never enforced on processedHeaders.

Although guard-phase filters operate on session.headers (a different object), advanced-mode operations in other branches of the same request, or future forwarder changes, could inject transport headers into processedHeaders. The intent of adding the blacklist here was to provide an absolute safety net, but the current structure makes it conditional on filter presence.

The blacklist enforcement should be moved before the early return so it always runs:

  async applyFinal(
    session: ProxySession,
    body: Record<string, unknown>,
    headers: Headers
  ): Promise<void> {
    await this.ensureInitialized();

    // Always enforce transport header blacklist regardless of filter count
    for (const h of TRANSPORT_HEADER_BLACKLIST) {
      headers.delete(h);
    }

    const allFinal = this.collectFinalFilters(session);
    if (allFinal.length === 0) return;

    for (const filter of allFinal) {
      // ...
    }
  }
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/request-filter-engine.ts
Line: 540-563

Comment:
**Transport header blacklist silently skipped when no final-phase filters match**

The early-return on line 541 exits the function before the transport header blacklist loop at lines 560–563 runs. This means if the current session has **zero** matching final-phase filters, the blacklist (`content-length`, `connection`, `transfer-encoding`) is never enforced on `processedHeaders`.

Although guard-phase filters operate on `session.headers` (a different object), advanced-mode operations in other branches of the same request, or future forwarder changes, could inject transport headers into `processedHeaders`. The intent of adding the blacklist here was to provide an absolute safety net, but the current structure makes it conditional on filter presence.

The blacklist enforcement should be moved **before** the early return so it always runs:

```
  async applyFinal(
    session: ProxySession,
    body: Record<string, unknown>,
    headers: Headers
  ): Promise<void> {
    await this.ensureInitialized();

    // Always enforce transport header blacklist regardless of filter count
    for (const h of TRANSPORT_HEADER_BLACKLIST) {
      headers.delete(h);
    }

    const allFinal = this.collectFinalFilters(session);
    if (allFinal.length === 0) return;

    for (const filter of allFinal) {
      // ...
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +92 to +101
if (op.op === "merge") {
if (
!("value" in raw) ||
raw.value === null ||
typeof raw.value !== "object" ||
Array.isArray(raw.value)
) {
return `${prefix}: merge value must be a plain object`;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Merge op.value not scanned for prototype-polluting keys

validateOperations guards against prototype pollution in op.path (line 84), but the recursive value payload of a merge operation is only checked to be a plain object — its keys are never examined. An admin could submit:

{ "op": "merge", "scope": "body", "path": "meta", "value": { "__proto__": { "isAdmin": true } } }

This passes server-side validation (the value is a non-null, non-array object), is stored in the DB, and reaches the engine. The engine's deepMerge correctly blocks the __proto__ key, but the defense should exist in both layers. Server-side validation should recursively scan merge values for keys matching UNSAFE_KEYS and reject the payload proactively.

function hasPollutingKeys(obj: Record<string, unknown>): boolean {
  for (const key of Object.keys(obj)) {
    if (UNSAFE_KEYS.has(key)) return true; // UNSAFE_KEYS is a Set
    const val = obj[key];
    if (val && typeof val === "object" && !Array.isArray(val)) {
      if (hasPollutingKeys(val as Record<string, unknown>)) return true;
    }
  }
  return false;
}

Where UNSAFE_KEYS is exported or duplicated from the engine, and called after the plain-object check for merge ops.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/request-filters.ts
Line: 92-101

Comment:
**Merge `op.value` not scanned for prototype-polluting keys**

`validateOperations` guards against prototype pollution in `op.path` (line 84), but the recursive `value` payload of a `merge` operation is only checked to be a plain object — its **keys** are never examined. An admin could submit:

```json
{ "op": "merge", "scope": "body", "path": "meta", "value": { "__proto__": { "isAdmin": true } } }
```

This passes server-side validation (the value is a non-null, non-array object), is stored in the DB, and reaches the engine. The engine's `deepMerge` correctly blocks the `__proto__` key, but the defense should exist in both layers. Server-side validation should recursively scan merge values for keys matching `UNSAFE_KEYS` and reject the payload proactively.

```typescript
function hasPollutingKeys(obj: Record<string, unknown>): boolean {
  for (const key of Object.keys(obj)) {
    if (UNSAFE_KEYS.has(key)) return true; // UNSAFE_KEYS is a Set
    const val = obj[key];
    if (val && typeof val === "object" && !Array.isArray(val)) {
      if (hasPollutingKeys(val as Record<string, unknown>)) return true;
    }
  }
  return false;
}
```

Where `UNSAFE_KEYS` is exported or duplicated from the engine, and called after the plain-object check for `merge` ops.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +178 to +190
if (matcher.field) {
if (element === null || element === undefined || typeof element !== "object") return false;
// Support dot-path extraction
const parts = matcher.field.split(".");
let cur: unknown = element;
for (const part of parts) {
if (cur === null || cur === undefined || typeof cur !== "object") return false;
cur = (cur as Record<string, unknown>)[part];
}
fieldValue = cur;
} else {
fieldValue = element;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

matchElement field traversal does not check UNSAFE_KEYS

Every other path-traversal helper in this file (parsePath, deepMerge) checks against UNSAFE_KEYS. However, the dot-path traversal inside matchElement at lines 181–186 splits matcher.field on "." and indexes directly into the element without any guard:

const parts = matcher.field.split(".");
let cur: unknown = element;
for (const part of parts) {
  cur = (cur as Record<string, unknown>)[part]; // no UNSAFE_KEYS check
}

A FilterMatcher with field: "__proto__" or field: "constructor.prototype" will follow the prototype chain of the element object. While this is a read-only path and cannot mutate state, it breaks the principle of least surprise, reads internal JavaScript properties rather than own-enumerable data, and may return misleading truthy values that affect filter decisions (e.g., constructor always exists on any object).

Add the guard:

for (const part of parts) {
  if (UNSAFE_KEYS.has(part)) return false;
  if (cur === null || cur === undefined || typeof cur !== "object") return false;
  cur = (cur as Record<string, unknown>)[part];
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/request-filter-engine.ts
Line: 178-190

Comment:
**`matchElement` field traversal does not check `UNSAFE_KEYS`**

Every other path-traversal helper in this file (`parsePath`, `deepMerge`) checks against `UNSAFE_KEYS`. However, the dot-path traversal inside `matchElement` at lines 181–186 splits `matcher.field` on `"."` and indexes directly into the element without any guard:

```typescript
const parts = matcher.field.split(".");
let cur: unknown = element;
for (const part of parts) {
  cur = (cur as Record<string, unknown>)[part]; // no UNSAFE_KEYS check
}
```

A `FilterMatcher` with `field: "__proto__"` or `field: "constructor.prototype"` will follow the prototype chain of the element object. While this is a read-only path and cannot mutate state, it breaks the principle of least surprise, reads internal JavaScript properties rather than own-enumerable data, and may return misleading truthy values that affect filter decisions (e.g., `constructor` always exists on any object).

Add the guard:
```typescript
for (const part of parts) {
  if (UNSAFE_KEYS.has(part)) return false;
  if (cur === null || cur === undefined || typeof cur !== "object") return false;
  cur = (cur as Record<string, unknown>)[part];
}
```

How can I resolve this? If you propose a fix, please make it concise.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/v1/_lib/proxy/forwarder.ts (1)

1839-1848: ⚠️ Potential issue | 🟠 Major

Gemini final-phase 会把 DSL 改动写回 session.request.message

这里把 session.request.message 的同一对象引用直接传给了 applyFinal(),而 applyFinal() 会原地修改 body。一旦当前请求重试或切换供应商,insert / text_replace 这类操作会在已经改过的 payload 上再次执行,结果会漂移。建议像标准分支一样,在 provider override 之后先克隆,再把克隆体交给 final-phase。

Also applies to: 1910-1914

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/forwarder.ts` around lines 1839 - 1848, The code passes
the original session.request.message reference into final-phase where
applyFinal() mutates the body in-place, causing repeated mutations on
retries/provider switches; after applying provider overrides
(applyGeminiGoogleSearchOverrideWithAudit) clone the overriddenBody (e.g., deep
clone bodyToSerialize) and assign the clone to bodyToSerialize and
session.request.message before calling applyFinal(), so applyFinal() mutates the
clone not the original; update the same pattern in the other occurrence
mentioned (around the applyFinal() call at the 1910-1914 section) and ensure
session.addSpecialSetting(googleSearchAudit) still receives the audit without
changing the original message object.
src/lib/request-filter-engine.ts (1)

35-47: ⚠️ Potential issue | 🟠 Major

不安全路径不应该被静默改写。

这里命中 UNSAFE_KEYS 时选择 continue,会把 a.__proto__.b 解析成 a.b,把本该拒绝的操作重定向到另一条合法路径。这样虽然挡住了 prototype pollution,但仍然会误删/误写正常字段。

建议把整条 path 判为无效
 function parsePath(path: string): Array<string | number> {
   const parts: Array<string | number> = [];
   const regex = /([^.[\]]+)|(\[(\d+)\])/g;
   let match: RegExpExecArray | null;
   while ((match = regex.exec(path)) !== null) {
     if (match[1]) {
-      if (UNSAFE_KEYS.has(match[1])) continue; // block prototype pollution
+      if (UNSAFE_KEYS.has(match[1])) return [];
       parts.push(match[1]);
     } else if (match[3]) {
       parts.push(Number(match[3]));
     }
   }
   return parts;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/request-filter-engine.ts` around lines 35 - 47, The parser silently
skips unsafe segments (UNSAFE_KEYS) so "a.__proto__.b" becomes "a.b" and wrongly
targets a valid property; change parsePath to treat the entire path as invalid
when any unsafe key is encountered: in parsePath (the function using UNSAFE_KEYS
and regex/match logic) replace the continue with immediate failure (e.g., throw
a clear Error like "Invalid path: contains unsafe key" or return a null/Result
indicating invalid path) and update callers to handle that failure instead of
proceeding with a sanitized path.
🧹 Nitpick comments (3)
src/drizzle/schema.ts (1)

640-640: operations 补齐 JSONB 默认值与显式类型,保持迁移一致性。

Line 640 目前没有 .default(null),后续生成迁移时会与仓库既有 JSONB 默认约定不一致,且类型推断也偏弱。

建议修改
-  operations: jsonb('operations'),
+  operations: jsonb('operations').$type<Array<Record<string, unknown>> | null>().default(null),
Based on learnings: Maintain the existing JSONB default pattern `jsonb DEFAULT 'null'::jsonb` in drizzle migrations and keep consistency across migration files.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/drizzle/schema.ts` at line 640, The operations column currently lacks the
JSONB default and explicit type which causes inconsistent migrations and weak
type inference; update the operations column definition (symbol: operations in
schema.ts) to include the JSONB default pattern used across the repo by adding
the Drizzle SQL default (e.g. .default(sql`'null'::jsonb`)) and ensure the
column is declared with jsonb('operations') so migrations emit "jsonb DEFAULT
'null'::jsonb" and type inference remains strong.
src/app/[locale]/settings/request-filters/_components/filter-table.tsx (1)

284-284: 建议优化 Switch 的 aria-label 语义。

Line 284 目前使用启用/禁用状态词作为 label,读屏体验可再提升为“该开关的用途描述”,状态由控件自身 checked 传达即可。

可选修改示例
-                    aria-label={filter.isEnabled ? t("enable") : t("disable")}
+                    aria-label={`${filter.name} ${t("table.status")}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/request-filters/_components/filter-table.tsx at
line 284, 当前 Switch 的 aria-label 使用启用/禁用词(aria-label={filter.isEnabled ?
t("enable") : t("disable")),应改为描述开关用途的语义文本,状态由控件的 checked/aria-checked 属性传达即可。请在
Switch 组件(使用 aria-label 的那一处)改为返回描述性标签,例如基于 filter.name 或 filter.label 构建的文本(例如
t("toggleFilter", { name: filter.name }) 或 t("filterToggleDescription", { label:
filter.label })),同时保留 checked={filter.isEnabled} 不变,以便读屏器能读出用途和当前状态。
src/actions/request-filters.ts (1)

276-291: 可优化:多次数据库查询同一记录。

updateRequestFilterAction 中可能对同一 id 调用多达 3 次 getRequestFilterById(分别在 Line 278、311、334)。建议在函数开头统一获取一次,减少数据库往返。

♻️ 建议优化
 ): Promise<ActionResult<RequestFilter>> {
   const session = await getSession();
   if (!isAdmin(session)) return { ok: false, error: "权限不足" };

+  // Fetch existing filter once for all validations
+  const needsExisting =
+    updates.ruleMode !== undefined ||
+    updates.operations !== undefined ||
+    updates.target !== undefined ||
+    updates.matchType !== undefined ||
+    updates.action !== undefined ||
+    updates.bindingType !== undefined ||
+    updates.providerIds !== undefined ||
+    updates.groupTags !== undefined;
+
+  let existing: RequestFilter | null = null;
+  if (needsExisting) {
+    existing = await getRequestFilterById(id);
+    if (!existing) {
+      return { ok: false, error: "记录不存在" };
+    }
+  }

   // Validate operations when ruleMode or operations change
   if (updates.ruleMode !== undefined || updates.operations !== undefined) {
-    const existing = await getRequestFilterById(id);
-    if (!existing) {
-      return { ok: false, error: "记录不存在" };
-    }
-
     const effectiveRuleMode = updates.ruleMode ?? existing.ruleMode;

后续使用 existing 的地方同理修改,移除重复的 getRequestFilterById 调用。

Also applies to: 306-317, 334-344

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/request-filters.ts` around lines 276 - 291, The PR makes multiple
redundant calls to getRequestFilterById inside updateRequestFilterAction; fetch
the record once at the start (e.g., const existing = await
getRequestFilterById(id)) and reuse that variable for all subsequent validations
and updates (including where effectiveRuleMode/effectiveOperations are computed
and later checks around Lines ~306-317 and ~334-344), remove the additional
getRequestFilterById calls and rely on the single existing object to perform
validateOperations, compare fields, and apply updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 1910-1914: The GET/HEAD and other no-body code paths currently
skip the final-phase request filter because applyFinal(...) is only invoked
inside the body branch; update the forwarder to call
requestFilterEngine.applyFinal(session, bodyToSerialize, processedHeaders) after
processedHeaders is constructed in the no-body branches as well (respecting
session.getEndpointPolicy().bypassRequestFilters), and ensure the same change is
applied where similar logic appears (around the blocks you noted: lines near
1919-1945 and 2234-2238); reference requestFilterEngine, applyFinal,
session.getEndpointPolicy().bypassRequestFilters, bodyToSerialize, and
processedHeaders so the final-phase rules run for header-only requests like
GET/HEAD and /v1/models.

In `@src/lib/request-filter-engine.ts`:
- Around line 367-419: loadFilters currently buckets filters with
(executionPhase ?? "guard") === "guard" but the guard code path still only uses
the old applyHeaderFilter()/applyBodyFilter() logic and never runs
filter.operations, so advanced guard rules become no-ops; update the guard phase
to either (A) execute the DSL operations for guard filters by invoking the
existing DSL executor on the cached.filter.operations (reuse whatever function
runs advanced rules for final phase—call it from the guard path) for filters
loaded by loadFilters(), ensuring compiledRegex/provider/group set lookups still
apply, or (B) if you prefer prohibition, add validation that rejects filters
with executionPhase === "guard" when they contain advanced filter.operations;
modify the code paths that currently call applyHeaderFilter/applyBodyFilter to
also handle filter.operations (or prevent creation via validation) so
advanced+guard combos are not silently ineffective.

In `@src/lib/request-filter-types.ts`:
- Around line 26-28: The BaseOp's scope is too broad for ops that only support
body paths; narrow the scope for the specific operation interfaces so that the
merge and insert operation types (e.g., the MergeOp / InsertOp interfaces or
type aliases that currently extend BaseOp) require scope: "body" instead of
inheriting "header" | "body" from BaseOp; update those interfaces (and any
similar types around the MergeOp/InsertOp declarations and the other affected op
types referenced in the file) to explicitly set scope: "body" so callers and
editors cannot supply "header" for body-only ops.

---

Outside diff comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 1839-1848: The code passes the original session.request.message
reference into final-phase where applyFinal() mutates the body in-place, causing
repeated mutations on retries/provider switches; after applying provider
overrides (applyGeminiGoogleSearchOverrideWithAudit) clone the overriddenBody
(e.g., deep clone bodyToSerialize) and assign the clone to bodyToSerialize and
session.request.message before calling applyFinal(), so applyFinal() mutates the
clone not the original; update the same pattern in the other occurrence
mentioned (around the applyFinal() call at the 1910-1914 section) and ensure
session.addSpecialSetting(googleSearchAudit) still receives the audit without
changing the original message object.

In `@src/lib/request-filter-engine.ts`:
- Around line 35-47: The parser silently skips unsafe segments (UNSAFE_KEYS) so
"a.__proto__.b" becomes "a.b" and wrongly targets a valid property; change
parsePath to treat the entire path as invalid when any unsafe key is
encountered: in parsePath (the function using UNSAFE_KEYS and regex/match logic)
replace the continue with immediate failure (e.g., throw a clear Error like
"Invalid path: contains unsafe key" or return a null/Result indicating invalid
path) and update callers to handle that failure instead of proceeding with a
sanitized path.

---

Nitpick comments:
In `@src/actions/request-filters.ts`:
- Around line 276-291: The PR makes multiple redundant calls to
getRequestFilterById inside updateRequestFilterAction; fetch the record once at
the start (e.g., const existing = await getRequestFilterById(id)) and reuse that
variable for all subsequent validations and updates (including where
effectiveRuleMode/effectiveOperations are computed and later checks around Lines
~306-317 and ~334-344), remove the additional getRequestFilterById calls and
rely on the single existing object to perform validateOperations, compare
fields, and apply updates.

In `@src/app/`[locale]/settings/request-filters/_components/filter-table.tsx:
- Line 284: 当前 Switch 的 aria-label 使用启用/禁用词(aria-label={filter.isEnabled ?
t("enable") : t("disable")),应改为描述开关用途的语义文本,状态由控件的 checked/aria-checked 属性传达即可。请在
Switch 组件(使用 aria-label 的那一处)改为返回描述性标签,例如基于 filter.name 或 filter.label 构建的文本(例如
t("toggleFilter", { name: filter.name }) 或 t("filterToggleDescription", { label:
filter.label })),同时保留 checked={filter.isEnabled} 不变,以便读屏器能读出用途和当前状态。

In `@src/drizzle/schema.ts`:
- Line 640: The operations column currently lacks the JSONB default and explicit
type which causes inconsistent migrations and weak type inference; update the
operations column definition (symbol: operations in schema.ts) to include the
JSONB default pattern used across the repo by adding the Drizzle SQL default
(e.g. .default(sql`'null'::jsonb`)) and ensure the column is declared with
jsonb('operations') so migrations emit "jsonb DEFAULT 'null'::jsonb" and type
inference remains strong.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c941bdb3-52fd-449e-9638-335d80a85c51

📥 Commits

Reviewing files that changed from the base of the PR and between 8a30a43 and 900561b.

📒 Files selected for processing (17)
  • drizzle/0081_strange_zarek.sql
  • drizzle/meta/0081_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/settings/requestFilters.json
  • messages/ja/settings/requestFilters.json
  • messages/ru/settings/requestFilters.json
  • messages/zh-CN/settings/requestFilters.json
  • messages/zh-TW/settings/requestFilters.json
  • src/actions/request-filters.ts
  • src/app/[locale]/settings/request-filters/_components/filter-dialog.tsx
  • src/app/[locale]/settings/request-filters/_components/filter-table.tsx
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/drizzle/schema.ts
  • src/lib/request-filter-engine.ts
  • src/lib/request-filter-types.ts
  • src/repository/request-filters.ts
  • tests/unit/request-filter-advanced.test.ts

Comment thread src/app/v1/_lib/proxy/forwarder.ts
Comment on lines +367 to +419
private loadFilters(filters: RequestFilter[]): void {
const cachedFilters = filters.map((f) => {
const cached: CachedRequestFilter = { ...f };

// Optimization #2: Pre-compile regex for text_replace (with ReDoS validation)
if (f.matchType === "regex" && f.action === "text_replace") {
if (!safeRegex(f.target)) {
logger.warn("[RequestFilterEngine] Skip unsafe regex at load", {
if (f.matchType === "regex" && f.action === "text_replace") {
if (!safeRegex(f.target)) {
logger.warn("[RequestFilterEngine] Skip unsafe regex at load", {
filterId: f.id,
target: f.target,
});
} else {
try {
cached.compiledRegex = new RegExp(f.target, "g");
} catch (error) {
logger.warn("[RequestFilterEngine] Failed to compile regex at load", {
filterId: f.id,
target: f.target,
error,
});
} else {
try {
cached.compiledRegex = new RegExp(f.target, "g");
} catch (error) {
logger.warn("[RequestFilterEngine] Failed to compile regex at load", {
filterId: f.id,
target: f.target,
error,
});
}
}
}
}

// Optimization #3: Create Set caches for faster lookups
if (f.bindingType === "providers" && f.providerIds) {
cached.providerIdsSet = new Set(f.providerIds);
}
if (f.bindingType === "groups" && f.groupTags) {
cached.groupTagsSet = new Set(f.groupTags);
}

return cached;
});

// Split filters by binding type
this.globalFilters = cachedFilters
.filter((f) => f.bindingType === "global" || !f.bindingType)
.sort((a, b) => a.priority - b.priority || a.id - b.id);
if (f.bindingType === "providers" && f.providerIds) {
cached.providerIdsSet = new Set(f.providerIds);
}
if (f.bindingType === "groups" && f.groupTags) {
cached.groupTagsSet = new Set(f.groupTags);
}

this.providerFilters = cachedFilters
.filter((f) => f.bindingType === "providers" || f.bindingType === "groups")
.sort((a, b) => a.priority - b.priority || a.id - b.id);
return cached;
});

// Optimization #5: Cache whether we have group-based filters
this.hasGroupBasedFilters = this.providerFilters.some((f) => f.bindingType === "groups");
const isGlobal = (f: CachedRequestFilter) => f.bindingType === "global" || !f.bindingType;
const isProvider = (f: CachedRequestFilter) =>
f.bindingType === "providers" || f.bindingType === "groups";
const isGuard = (f: CachedRequestFilter) => (f.executionPhase ?? "guard") === "guard";
const isFinal = (f: CachedRequestFilter) => f.executionPhase === "final";
const byPriority = (a: CachedRequestFilter, b: CachedRequestFilter) =>
a.priority - b.priority || a.id - b.id;

this.globalGuardFilters = cachedFilters
.filter((f) => isGlobal(f) && isGuard(f))
.sort(byPriority);
this.providerGuardFilters = cachedFilters
.filter((f) => isProvider(f) && isGuard(f))
.sort(byPriority);
this.globalFinalFilters = cachedFilters
.filter((f) => isGlobal(f) && isFinal(f))
.sort(byPriority);
this.providerFinalFilters = cachedFilters
.filter((f) => isProvider(f) && isFinal(f))
.sort(byPriority);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

executionPhase = "guard" 的 advanced 规则当前基本不会生效。

loadFilters() 已经把这类规则放进了 guard buckets,但这里仍然只走 applyHeaderFilter() / applyBodyFilter() 的旧字段逻辑,从不读取 filter.operations。现在 UI/API 已经能创建 advanced + guard 组合,这会生成一条“保存成功但几乎是空操作”的规则。要么在 guard phase 复用 DSL 执行器,要么在校验层直接禁止这个组合。

Also applies to: 451-518

🧰 Tools
🪛 ast-grep (0.41.1)

[warning] 378-378: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(f.target, "g")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/request-filter-engine.ts` around lines 367 - 419, loadFilters
currently buckets filters with (executionPhase ?? "guard") === "guard" but the
guard code path still only uses the old applyHeaderFilter()/applyBodyFilter()
logic and never runs filter.operations, so advanced guard rules become no-ops;
update the guard phase to either (A) execute the DSL operations for guard
filters by invoking the existing DSL executor on the cached.filter.operations
(reuse whatever function runs advanced rules for final phase—call it from the
guard path) for filters loaded by loadFilters(), ensuring
compiledRegex/provider/group set lookups still apply, or (B) if you prefer
prohibition, add validation that rejects filters with executionPhase === "guard"
when they contain advanced filter.operations; modify the code paths that
currently call applyHeaderFilter/applyBodyFilter to also handle
filter.operations (or prevent creation via validation) so advanced+guard combos
are not silently ineffective.

Comment thread src/lib/request-filter-types.ts

@github-actions github-actions 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.

Code Review Summary

This PR introduces a well-architected advanced mode for request filters with a typed Operation DSL, final execution phase support, and comprehensive security hardening. The implementation is solid with good test coverage.

PR Size: XL

  • Lines changed: 6,496 (6,223 additions + 273 deletions)
  • Files changed: 17

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Security Highlights (Positive)

  • ReDoS Prevention: All regex patterns validated via safe-regex package
  • Prototype Pollution Guards: UNSAFE_KEYS set blocks __proto__, constructor, prototype in path traversal and deep merge
  • Transport Header Blacklist: Prevents user-controlled content-length, connection, transfer-encoding headers

Architecture Observations

The codebase follows established patterns:

  • 4-bucket filter caching (globalGuard, providerGuard, globalFinal, providerFinal)
  • Defense-in-depth with validation at both action layer and engine layer
  • Clean separation between simple mode (backward compatible) and advanced mode

Test Coverage

  • 985 lines of unit tests in tests/unit/request-filter-advanced.test.ts
  • Covers all 4 operation types (set, remove, merge, insert)
  • Tests for dedup, anchor positioning, matcher strategies
  • Prototype pollution rejection tests
  • ReDoS regex complexity limit tests

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (hardened)
  • Error handling - Clean (all errors logged)
  • Type safety - Clean (well-typed DSL)
  • Documentation accuracy - Clean
  • Test coverage - Excellent
  • Code clarity - Good

Automated review by Claude AI

}
try {
return new RegExp(pattern).test(String(fieldValue));
} catch {

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.

[HIGH] [ERROR-SILENT] Swallowed regex compilation error in matcher

Location: src/lib/request-filter-engine.ts:209

Evidence: The matcher regex path silently ignores invalid patterns:

try {
  return new RegExp(pattern).test(String(fieldValue));
} catch {
  return false;
}

Why this is a problem: An invalid pattern makes the filter behave like it "doesn't match" with no log signal, which is hard to debug and violates the project's no-silent-failure expectation.

Suggested fix:

try {
  return new RegExp(pattern).test(String(fieldValue));
} catch (error) {
  logger.warn("[RequestFilterEngine] Invalid regex matcher pattern", { pattern, error });
  return false;
}

if (this.globalGuardFilters.length === 0) return;

for (const filter of this.globalFilters) {
for (const filter of this.globalGuardFilters) {

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.

[HIGH] [LOGIC-BUG] ruleMode: "advanced" filters are accepted for guard phase but never executed

Location: src/lib/request-filter-engine.ts:457

Evidence: Guard-phase application only uses scope/action/target/replacement and never branches on filter.ruleMode / filter.operations:

for (const filter of this.globalGuardFilters) {
  if (filter.scope === "header") {
    this.applyHeaderFilter(session, filter);
  } else if (filter.scope === "body") {
    this.applyBodyFilter(session, filter);
  }
}

Why this is a problem: The UI and server actions allow ruleMode: "advanced" with executionPhase: "guard", but the engine will silently ignore the operation DSL in guard phase (filters become no-ops unless the legacy fields are populated). This is user-visible misconfiguration with no warning.

Suggested fix (support advanced ops in guard phase; do the same in applyForProvider):

for (const filter of this.globalGuardFilters) {
  try {
    if (filter.ruleMode === "advanced" && filter.operations) {
      const message = session.request.message as Record<string, unknown>;
      this.executeAdvancedOps(filter.operations, message, session.headers);
      continue;
    }

    if (filter.scope === "header") {
      this.applyHeaderFilter(session, filter);
    } else if (filter.scope === "body") {
      this.applyBodyFilter(session, filter);
    }
  } catch (error) {
    logger.error("[RequestFilterEngine] Failed to apply global filter", {
      filterId: filter.id,
      scope: filter.scope,
      action: filter.action,
      error,
    });
  }
}

Comment thread src/lib/request-filter-types.ts Outdated
// Merge (body only)
// ---------------------------------------------------------------------------

export interface MergeOp extends BaseOp {

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.

[MEDIUM] [TYPE-WEAK-INVARIANT] Merge/Insert ops are documented as body-only but types allow scope: "header"

Location: src/lib/request-filter-types.ts:59

Evidence: The file labels these as body-only, but both extend BaseOp (scope: "header" | "body").

// Merge (body only)
export interface MergeOp extends BaseOp {
  op: "merge";
  // ...
}

Why this is a problem: The type system currently allows invalid states (e.g., { op: "merge", scope: "header", ... }), pushing correctness to runtime validation and making it easier to construct impossible operations in code.

Suggested fix (encode the invariant in types):

export interface MergeOp {
  op: "merge";
  scope: "body";
  path: string;
  value: Record<string, unknown>;
}

export interface InsertOp {
  op: "insert";
  scope: "body";
  path: string;
  value: unknown;
  // ...
}


function validateOperations(operations: unknown): string | null {
if (!Array.isArray(operations) || operations.length === 0) {
return "Advanced mode requires at least one operation";

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.

[MEDIUM] [STANDARD-VIOLATION] Advanced-mode validation returns hardcoded English strings (non-i18n)

Location: src/actions/request-filters.ts:49

Evidence: New advanced-mode validation returns raw English messages:

return "Advanced mode requires at least one operation";

These error strings are returned via ActionResult.error and are surfaced directly in the UI (toast.error(result.error)), so users in non-English locales will see unlocalized messages.

Why this is a problem: Project standard requires i18n for user-facing strings. Returning raw strings here also makes it hard to keep parity across the 5 supported locales.

Suggested fix (return an errorCode and translate via next-intl on the server):

import { getTranslations } from "next-intl/server";

type ValidationError = { code: string; params?: Record<string, string | number> };

function validateOperations(operations: unknown): ValidationError | null {
  if (!Array.isArray(operations) || operations.length === 0) {
    return { code: "REQUEST_FILTER_OPS_REQUIRED" };
  }
  if (operations.length > MAX_OPERATIONS) {
    return { code: "REQUEST_FILTER_OPS_TOO_MANY", params: { max: MAX_OPERATIONS } };
  }
  return null;
}

// inside the action
const tError = await getTranslations("errors");
const err = validateOperations(data.operations);
if (err) {
  return {
    ok: false,
    error: tError(err.code, err.params),
    errorCode: err.code,
    errorParams: err.params,
  };
}

// Final-phase request filter: after all provider overrides, before serialization
if (!session.getEndpointPolicy().bypassRequestFilters) {
const { requestFilterEngine } = await import("@/lib/request-filter-engine");
await requestFilterEngine.applyFinal(session, messageToSend, processedHeaders);

@github-actions github-actions Bot Mar 13, 2026

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.

[HIGH] [LOGIC-BUG] Final-phase filters can override enforced host header and are skipped for GET/HEAD

Location: src/app/v1/_lib/proxy/forwarder.ts:2237

Evidence: Final filters run on processedHeaders:

await requestFilterEngine.applyFinal(session, messageToSend, processedHeaders);

Why this is a problem:

  1. processedHeaders has a required host override for undici TLS validation earlier in this method. Because applyFinal() can run header set/remove operations, it can overwrite host after it was enforced, causing upstream request failures.
  2. applyFinal() is only invoked inside the hasBody JSON-serialization path. For GET/HEAD (e.g., /v1/models) and raw passthrough bodies, final-phase header filters never run.

Suggested fix (apply final filters for all methods, and re-enforce host after filters):

const applyFinalFilters = async (body: Record<string, unknown>) => {
  if (session.getEndpointPolicy().bypassRequestFilters) return;
  const { requestFilterEngine } = await import("@/lib/request-filter-engine");
  await requestFilterEngine.applyFinal(session, body, processedHeaders);
  processedHeaders.set("host", actualHost);
};

// JSON body path
await applyFinalFilters(messageToSend);

// GET/HEAD + raw passthrough paths
await applyFinalFilters({});

Comment thread src/app/v1/_lib/proxy/forwarder.ts Outdated
// Final-phase request filter for Gemini: after headers built, before body serialization
if (!session.getEndpointPolicy().bypassRequestFilters) {
const { requestFilterEngine } = await import("@/lib/request-filter-engine");
await requestFilterEngine.applyFinal(session, bodyToSerialize, processedHeaders);

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.

[HIGH] [LOGIC-BUG] Gemini final-phase filters can override required headers and are skipped for no-body requests

Location: src/app/v1/_lib/proxy/forwarder.ts:1913

Evidence: Gemini path applies final filters directly on processedHeaders:

await requestFilterEngine.applyFinal(session, bodyToSerialize, processedHeaders);

Why this is a problem:

  • applyFinal() can mutate headers after buildGeminiHeaders() has enforced critical overrides (host, auth headers, transport-header blacklist). A final filter can reintroduce invalid values (especially host) and break upstream requests.
  • The else branch (no body) never calls applyFinal(), so final-phase header filters won't run for GET/HEAD Gemini requests.

Suggested fix (reuse a helper and re-enforce host after final filters in both branches):

const applyGeminiFinalFilters = async (body: Record<string, unknown>) => {
  if (session.getEndpointPolicy().bypassRequestFilters) return;
  const { requestFilterEngine } = await import("@/lib/request-filter-engine");
  await requestFilterEngine.applyFinal(session, body, processedHeaders);
  processedHeaders.set("host", HeaderProcessor.extractHost(proxyUrl));
};

await applyGeminiFinalFilters(hasBody ? bodyToSerialize : {});

@github-actions github-actions 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.

Code Review Summary

This PR adds an advanced request-filter operation DSL, integrates a new final execution phase into the proxy forwarder, and updates schema/UI/i18n accordingly. There are several high-priority correctness and safety gaps in the new execution flow (final-phase header safety, GET/HEAD coverage) and in the engine/action layering (advanced guard-phase behavior, non-i18n validation errors).

PR Size: XL

  • Lines changed: 6496
  • Files changed: 17
  • Split suggestion (required for XL): Split into 3–4 PRs: (1) DB/schema + repository types + migration, (2) request-filter engine + DSL types + unit tests, (3) forwarder integration, (4) UI + i18n message updates.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 1 0 0
Security 0 2 0 0
Error Handling 0 1 0 0
Types 0 0 1 0
Comments/Docs 0 0 1 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

  • (Confidence 98) Final-phase filters can override enforced host header and are skipped for no-body requests in forwarder paths (src/app/v1/_lib/proxy/forwarder.ts), risking upstream request failures.

High Priority Issues (Should Fix)

  • (Confidence 90) ruleMode: "advanced" filters are accepted for guard phase but never execute operations (engine ignores operations outside applyFinal).
  • (Confidence 90) Swallowed regex compilation errors in matcher logic hide misconfiguration and make debugging difficult.
  • (Confidence 85) Advanced-mode validation in server actions returns hardcoded English strings; these surface directly in UI and bypass i18n.
  • (Confidence 85) Operation DSL types allow invalid states (Merge/Insert documented as body-only but scope type still permits "header").

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

@github-actions github-actions 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.

  • Identified and reviewed PR #912 (feat/request-filters: advanced mode operations and execution engine).
  • Applied PR size label size/XL (6223 additions, 273 deletions, 17 files).
  • Posted 6 inline review comments (diff-only) focused on:
    • src/app/v1/_lib/proxy/forwarder.ts (final-phase filters: host safety + GET/HEAD/no-body coverage; Gemini path too)
    • src/lib/request-filter-engine.ts (advanced ops not executing in guard phase; swallowed regex errors in matcher)
    • src/actions/request-filters.ts (advanced-mode validation errors are hardcoded/non-i18n)
    • src/lib/request-filter-types.ts (Merge/Insert “body-only” invariant not enforced by types)
  • Submitted the required “Code Review Summary” via gh pr review --comment.

Security:
- parsePath now rejects entire path on unsafe key instead of silently
  dropping segments (e.g. `a.__proto__.b` no longer becomes `a.b`)
- getValueByPath returns undefined on rejected paths instead of root object
- MergeOp/InsertOp types narrowed to scope:"body" with runtime validation

Correctness:
- GET/HEAD requests now run final-phase filters for header-only operations
  (both Gemini and non-Gemini branches)
- Gemini branch clones body via structuredClone before applyFinal to prevent
  in-place mutation of session.request.message on retries
- Reject advanced+guard combo at validation layer (create + update paths)

Quality:
- Consolidate 3 redundant getRequestFilterById calls into 1 per update
- Fix Switch aria-label to describe toggle purpose with filter name
- Add $type<FilterOperation[]|null> annotation to operations schema column
- Add toggleStatus i18n key across all 5 locales
@ding113
ding113 merged commit 23ee26b into dev Mar 13, 2026
7 of 10 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Mar 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/lib/request-filter-engine.ts (1)

208-211: ⚠️ Potential issue | 🟠 Major

正则匹配构造失败被静默吞掉,排障成本高

catch 分支直接返回 false,会把配置错误伪装成“不匹配”。建议至少记录 warn 日志,保留 pattern 与异常信息。

建议修改
-      } catch {
-        return false;
+      } catch (error) {
+        logger.warn("[RequestFilterEngine] Invalid regex matcher pattern", {
+          pattern,
+          error,
+        });
+        return false;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/request-filter-engine.ts` around lines 208 - 211, The catch branch
that swallows RegExp construction errors should log a warning with the failing
pattern and the exception before returning false; locate the try/catch that uses
new RegExp(pattern).test(String(fieldValue)) and in the catch block call the
module's logger (e.g., logger.warn or console.warn if no logger exists) with a
message including the pattern and the caught error/stack, then return false as
before.
src/app/v1/_lib/proxy/forwarder.ts (1)

1910-1917: ⚠️ Potential issue | 🟠 Major

applyFinal 后未重新强制 host,可能破坏上游请求

applyFinal 可修改 headers;当前多个分支在执行后都没有回写 host。这会让 final 规则有机会覆盖关键 host,影响 undici 的目标主机校验与上游连通性。建议每次 applyFinal 后统一重新设置 host

建议修改
+const enforceHostHeader = () => {
+  processedHeaders.set("host", HeaderProcessor.extractHost(proxyUrl));
+};

 // after applyFinal(...)
 await requestFilterEngine.applyFinal(session, bodyForFinal, processedHeaders);
+enforceHostHeader();

在 Gemini 与标准路径(含 no-body 分支)都执行同样处理。

Also applies to: 1951-1957, 2247-2251, 2279-2285

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/forwarder.ts` around lines 1910 - 1917, The final
request filter (requestFilterEngine.applyFinal) can mutate headers and may
overwrite Host, breaking upstream routing; after every call to applyFinal (e.g.,
the Gemini path and the standard/no-body branches) re-assign
processedHeaders["host"] (or processedHeaders.get/set as appropriate) back to
the original host value used for the request (the value computed earlier for
undici target) so Host is never lost by filters; update the code paths that call
requestFilterEngine.applyFinal (references: session.getEndpointPolicy(),
requestFilterEngine.applyFinal, processedHeaders, bodyToSerialize) to restore
the Host header immediately after each applyFinal invocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/actions/request-filters.ts`:
- Around line 145-153: validatePayload is being reused without passing the
ruleMode/operations context so advanced rules fall back to simple validation and
can incorrectly reject updates (e.g., empty data.target when only binding
changed). Fix by ensuring calls to validatePayload include the ruleMode and
operations from the incoming payload (use data.ruleMode and data.operations),
and keep the existing logic in request-filters: when ruleMode === "advanced"
call validateOperations(data.operations) and skip the simple target check;
otherwise perform the simple target validation. Update both places where
validatePayload is invoked (the block around ruleMode/validateOperations and the
similar block at lines ~356-364) so advanced-mode validations run correctly.

In `@src/lib/request-filter-engine.ts`:
- Around line 182-187: The manual dot-split traversal for matcher.field bypasses
the existing UNSAFE_KEYS checks; replace the custom loop with the shared safe
path-reading utilities (use parsePath and/or getValueByPath) so matcher.field is
resolved through the same safety logic (UNSAFE_KEYS) used elsewhere.
Specifically, in request-filter-engine.ts where matcher.field is walked, call
the common getValueByPath(parsePath(...)) (or the project’s single
getValueByPath(matcher.field, element) entrypoint) and treat an undefined/null
result as a non-match (return false), preserving the original
null/undefined/type checks but relying on the shared implementation to enforce
UNSAFE_KEYS.

---

Duplicate comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 1910-1917: The final request filter
(requestFilterEngine.applyFinal) can mutate headers and may overwrite Host,
breaking upstream routing; after every call to applyFinal (e.g., the Gemini path
and the standard/no-body branches) re-assign processedHeaders["host"] (or
processedHeaders.get/set as appropriate) back to the original host value used
for the request (the value computed earlier for undici target) so Host is never
lost by filters; update the code paths that call requestFilterEngine.applyFinal
(references: session.getEndpointPolicy(), requestFilterEngine.applyFinal,
processedHeaders, bodyToSerialize) to restore the Host header immediately after
each applyFinal invocation.

In `@src/lib/request-filter-engine.ts`:
- Around line 208-211: The catch branch that swallows RegExp construction errors
should log a warning with the failing pattern and the exception before returning
false; locate the try/catch that uses new
RegExp(pattern).test(String(fieldValue)) and in the catch block call the
module's logger (e.g., logger.warn or console.warn if no logger exists) with a
message including the pattern and the caught error/stack, then return false as
before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc9dae2d-0e1b-42d0-b9cc-6494cebfbb3d

📥 Commits

Reviewing files that changed from the base of the PR and between 900561b and 875c0b5.

📒 Files selected for processing (11)
  • messages/en/settings/requestFilters.json
  • messages/ja/settings/requestFilters.json
  • messages/ru/settings/requestFilters.json
  • messages/zh-CN/settings/requestFilters.json
  • messages/zh-TW/settings/requestFilters.json
  • src/actions/request-filters.ts
  • src/app/[locale]/settings/request-filters/_components/filter-table.tsx
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/drizzle/schema.ts
  • src/lib/request-filter-engine.ts
  • src/lib/request-filter-types.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/drizzle/schema.ts
  • messages/en/settings/requestFilters.json
  • src/lib/request-filter-types.ts
  • messages/zh-CN/settings/requestFilters.json
  • messages/ru/settings/requestFilters.json

Comment on lines +145 to +153
const ruleMode = data.ruleMode ?? "simple";

if (ruleMode === "advanced") {
// Advanced mode: validate operations, skip simple fields
const opsError = validateOperations(data.operations);
if (opsError) return opsError;
} else {
// Simple mode: existing validation
if (!data.target?.trim()) return "目标字段不能为空";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

binding 更新校验缺少 ruleMode 上下文,advanced 规则会被误拦截

这里复用 validatePayload 时没有传 ruleMode/operations,会退回 simple 校验路径。对于 advanced 规则(尤其 target 为空的场景),仅修改 binding 也可能被错误拒绝。

建议修改
     const validationError = validatePayload({
       name: existing!.name,
       scope: existing!.scope,
       action: existing!.action,
       target: existing!.target,
+      ruleMode: updates.ruleMode ?? existing!.ruleMode,
+      operations: updates.operations !== undefined ? updates.operations : existing!.operations,
       bindingType: effectiveBindingType,
       providerIds: effectiveProviderIds,
       groupTags: effectiveGroupTags,
     });

Also applies to: 356-364

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/request-filters.ts` around lines 145 - 153, validatePayload is
being reused without passing the ruleMode/operations context so advanced rules
fall back to simple validation and can incorrectly reject updates (e.g., empty
data.target when only binding changed). Fix by ensuring calls to validatePayload
include the ruleMode and operations from the incoming payload (use data.ruleMode
and data.operations), and keep the existing logic in request-filters: when
ruleMode === "advanced" call validateOperations(data.operations) and skip the
simple target check; otherwise perform the simple target validation. Update both
places where validatePayload is invoked (the block around
ruleMode/validateOperations and the similar block at lines ~356-364) so
advanced-mode validations run correctly.

Comment on lines +182 to +187
const parts = matcher.field.split(".");
let cur: unknown = element;
for (const part of parts) {
if (cur === null || cur === undefined || typeof cur !== "object") return false;
cur = (cur as Record<string, unknown>)[part];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

matcher.field 路径遍历绕过了现有的安全路径防护

这里手动按 . 分割遍历,没有复用 parsePath/getValueByPathUNSAFE_KEYS 防护,导致 matcher 路径与其余路径处理策略不一致。建议统一走同一套安全路径读取逻辑。

建议修改
-    // Support dot-path extraction
-    const parts = matcher.field.split(".");
-    let cur: unknown = element;
-    for (const part of parts) {
-      if (cur === null || cur === undefined || typeof cur !== "object") return false;
-      cur = (cur as Record<string, unknown>)[part];
-    }
-    fieldValue = cur;
+    fieldValue = getValueByPath(element as Record<string, unknown>, matcher.field);
+    if (fieldValue === undefined) return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/request-filter-engine.ts` around lines 182 - 187, The manual
dot-split traversal for matcher.field bypasses the existing UNSAFE_KEYS checks;
replace the custom loop with the shared safe path-reading utilities (use
parsePath and/or getValueByPath) so matcher.field is resolved through the same
safety logic (UNSAFE_KEYS) used elsewhere. Specifically, in
request-filter-engine.ts where matcher.field is walked, call the common
getValueByPath(parsePath(...)) (or the project’s single
getValueByPath(matcher.field, element) entrypoint) and treat an undefined/null
result as a non-match (return false), preserving the original
null/undefined/type checks but relying on the shared implementation to enforce
UNSAFE_KEYS.

Comment on lines +116 to +118
if (insertOp.dedupe?.byFields && !Array.isArray(insertOp.dedupe.byFields)) {
return `${prefix}: dedupe.byFields must be an array`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

dedupe.byFields entries not validated for prototype-polluting names

The current check only verifies that dedupe.byFields is an array, but never inspects the individual field name strings. If an admin submits { "dedupe": { "byFields": ["__proto__"] } }, it passes server-side validation, is stored in the DB, and reaches executeInsertOp. There, at line 769 of the engine:

return byFields.every((field) => deepEqual(existingObj[field], valueObj[field]));

For any plain object, existingObj["__proto__"] returns Object.prototype. The deepEqual check immediately hits the a === b short-circuit (both sides are the same Object.prototype reference) and returns true. As a result, the dedupe check always fires, and no element is ever inserted — the operation is silently swallowed with no error.

Fix by adding a check similar to the existing op.path guard:

if (insertOp.dedupe?.byFields && !Array.isArray(insertOp.dedupe.byFields)) {
  return `${prefix}: dedupe.byFields must be an array`;
}
// ADD: validate each byField entry
if (insertOp.dedupe?.byFields) {
  for (const field of insertOp.dedupe.byFields) {
    if (VALIDATION_UNSAFE_KEYS.test(field)) {
      return `${prefix}: dedupe.byFields contains a forbidden property name`;
    }
  }
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/request-filters.ts
Line: 116-118

Comment:
**`dedupe.byFields` entries not validated for prototype-polluting names**

The current check only verifies that `dedupe.byFields` is an array, but never inspects the individual field name strings. If an admin submits `{ "dedupe": { "byFields": ["__proto__"] } }`, it passes server-side validation, is stored in the DB, and reaches `executeInsertOp`. There, at line 769 of the engine:

```ts
return byFields.every((field) => deepEqual(existingObj[field], valueObj[field]));
```

For any plain object, `existingObj["__proto__"]` returns `Object.prototype`. The `deepEqual` check immediately hits the `a === b` short-circuit (both sides are the same `Object.prototype` reference) and returns `true`. As a result, the dedupe check always fires, and **no element is ever inserted** — the operation is silently swallowed with no error.

Fix by adding a check similar to the existing `op.path` guard:

```typescript
if (insertOp.dedupe?.byFields && !Array.isArray(insertOp.dedupe.byFields)) {
  return `${prefix}: dedupe.byFields must be an array`;
}
// ADD: validate each byField entry
if (insertOp.dedupe?.byFields) {
  for (const field of insertOp.dedupe.byFields) {
    if (VALIDATION_UNSAFE_KEYS.test(field)) {
      return `${prefix}: dedupe.byFields contains a forbidden property name`;
    }
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +38 to +44
function validateMatcher(matcher: FilterMatcher, context: string): string | null {
if (matcher.matchType === "regex" && typeof matcher.value === "string") {
if (!safeRegex(matcher.value)) {
return `${context}: regex matcher has ReDoS risk`;
}
}
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

validateMatcher does not check matcher.field for prototype-polluting names

validateMatcher only validates the regex safety of matcher.value. It never inspects matcher.field, even though matcher.field is used directly as a dot-path key during element traversal in matchElement. An admin can submit { "field": "__proto__", "matchType": "contains", "value": "object" } — this passes server-side validation, gets stored, and at runtime matchElement evaluates element["__proto__"] which returns Object.prototype for any plain JS object. String(Object.prototype) is "[object Object]", so the contains: "object" check matches every array element, causing a remove operation to wipe the entire array.

This is a distinct layer from the engine-level concern already noted in thread #3: the server-side validation should reject dangerous field values before they ever reach the engine.

Add a VALIDATION_UNSAFE_KEYS check on matcher.field:

function validateMatcher(matcher: FilterMatcher, context: string): string | null {
  if (matcher.field && VALIDATION_UNSAFE_KEYS.test(matcher.field)) {
    return `${context}: matcher.field contains a forbidden property name`;
  }
  if (matcher.matchType === "regex" && typeof matcher.value === "string") {
    if (!safeRegex(matcher.value)) {
      return `${context}: regex matcher has ReDoS risk`;
    }
  }
  return null;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/request-filters.ts
Line: 38-44

Comment:
**`validateMatcher` does not check `matcher.field` for prototype-polluting names**

`validateMatcher` only validates the regex safety of `matcher.value`. It never inspects `matcher.field`, even though `matcher.field` is used directly as a dot-path key during element traversal in `matchElement`. An admin can submit `{ "field": "__proto__", "matchType": "contains", "value": "object" }` — this passes server-side validation, gets stored, and at runtime `matchElement` evaluates `element["__proto__"]` which returns `Object.prototype` for any plain JS object. `String(Object.prototype)` is `"[object Object]"`, so the `contains: "object"` check matches **every** array element, causing a remove operation to wipe the entire array.

This is a distinct layer from the engine-level concern already noted in thread #3: the server-side validation should reject dangerous `field` values before they ever reach the engine.

Add a `VALIDATION_UNSAFE_KEYS` check on `matcher.field`:

```typescript
function validateMatcher(matcher: FilterMatcher, context: string): string | null {
  if (matcher.field && VALIDATION_UNSAFE_KEYS.test(matcher.field)) {
    return `${context}: matcher.field contains a forbidden property name`;
  }
  if (matcher.matchType === "regex" && typeof matcher.value === "string") {
    if (!safeRegex(matcher.value)) {
      return `${context}: regex matcher has ReDoS risk`;
    }
  }
  return null;
}
```

How can I resolve this? If you propose a fix, please make it concise.

@github-actions github-actions Bot mentioned this pull request Mar 14, 2026
10 tasks
ding113 added a commit that referenced this pull request Mar 14, 2026
* fix(proxy): prevent TypeError in NON_RETRYABLE_CLIENT_ERROR handler with transport errors

Fixes a regression where native transport errors (SocketError, UND_ERR_SOCKET)
could be misclassified as NON_RETRYABLE_CLIENT_ERROR by error rule matching,
causing the error handler to crash with "TypeError: getDetailedErrorMessage is
not a function" instead of properly rethrowing the original error.

Changes:
1. errors.ts: Add transport-error guard in categorizeErrorAsync() at priority 1.5
   - Detects SocketError, undici error codes (UND_ERR_SOCKET, ECONNREFUSED, etc.)
   - Returns SYSTEM_ERROR before error rule matching to prevent misclassification
   - Preserves correct failover behavior for network-level failures

2. forwarder.ts: Bifurcate NON_RETRYABLE_CLIENT_ERROR branch on instanceof check
   - ProxyError path: uses full ProxyError fields (statusCode, upstreamError, etc.)
   - Plain Error path: uses only base Error fields, avoids calling non-existent methods
   - Both paths preserve original error rethrow and provider chain recording

3. Add regression tests:
   - Test A: Verifies plain SocketError + forced NON_RETRYABLE_CLIENT_ERROR doesn't throw TypeError
   - Test B: Verifies categorizeErrorAsync returns SYSTEM_ERROR for transport errors

All 657 proxy unit tests pass. Type check and lint pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: format code (dev-a3fd666)

* fix: assorted UI/UX and logic small fixes

* feat(request-filters): advanced mode operations and execution engine (#912)

* feat(request-filters): add advanced mode operations and final execution phase

Introduce two new axes for request filters: rule mode (simple/advanced)
and execution phase (guard/final). Advanced mode supports a JSON-defined
operation DSL with set, remove, merge, and insert operations. The final
phase executes after all provider overrides, enabling post-processing
use cases like system message injection with dedup and cache_control
manipulation.

Key changes:
- Schema: add rule_mode, execution_phase, operations columns + index
- Types: new FilterOperation DSL (SetOp, RemoveOp, MergeOp, InsertOp)
- Engine: 4-bucket cache split, applyFinal() method, operation executors
  with deep merge (null-as-delete), deep equal dedup, anchor matching
- Forwarder: integrate applyFinal in standard and Gemini branches
- Validation: validateOperations with ReDoS prevention via safe-regex
- UI: rule mode toggle, execution phase selector, JSON operations editor
- i18n: new keys for all 5 locales (en, zh-CN, zh-TW, ja, ru)
- Tests: 29 new tests covering all operation types and edge cases

* chore: format code (feat-request-filter-advanced-mode-ccee157)

* fix(security): harden request filter engine against prototype pollution and ReDoS bypass

- Block __proto__/constructor/prototype traversal in parsePath, deepMerge, and validation
- Add runtime safe-regex check in matchElement for regex matchers
- Fix ReDoS bypass in updateRequestFilterAction by checking effective target/matchType/action
- Add operations array length limit (max 50) and value validation for set/merge/insert ops
- Fix UI default executionPhase to match server default (guard, not final)
- Fix parsePath regex corruption from edit tool (double-escaped backslashes)

* fix(i18n,a11y): address code review findings for request filters

- Remove console.error from filter-dialog.tsx (error already shown via toast)
- Fix ja table.actions translation to "操作"
- Fix ru table.createdAt to "Дата создания"
- Fix zh-CN confirmDelete halfwidth ? to fullwidth ?
- Show operations count in target column for advanced mode filters
- Add aria-label to filter toggle Switch for accessibility

* fix(request-filters): address CodeRabbit review findings across 8 issues

Security:
- parsePath now rejects entire path on unsafe key instead of silently
  dropping segments (e.g. `a.__proto__.b` no longer becomes `a.b`)
- getValueByPath returns undefined on rejected paths instead of root object
- MergeOp/InsertOp types narrowed to scope:"body" with runtime validation

Correctness:
- GET/HEAD requests now run final-phase filters for header-only operations
  (both Gemini and non-Gemini branches)
- Gemini branch clones body via structuredClone before applyFinal to prevent
  in-place mutation of session.request.message on retries
- Reject advanced+guard combo at validation layer (create + update paths)

Quality:
- Consolidate 3 redundant getRequestFilterById calls into 1 per update
- Fix Switch aria-label to describe toggle purpose with filter name
- Add $type<FilterOperation[]|null> annotation to operations schema column
- Add toggleStatus i18n key across all 5 locales

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(proxy): distinguish local client abort 499 from upstream HTTP 499

Adds isLocalAbort flag to ProxyError to differentiate CCH-synthesized 499
errors (client disconnection) from upstream providers returning HTTP 499.

Previously, isClientAbortError() matched any ProxyError with statusCode 499,
causing upstream 499 responses to bypass circuit-breaker and fallback logic.
Now only ProxyError instances explicitly marked with isLocalAbort=true are
treated as client aborts.

Also removes the broad "aborted" substring match from the message whitelist
to avoid false positives on upstream error messages containing that word.

* chore: format code (dev-bcdc6b7)

* fix: speed up users last usage lookup

* fix(db): add covering index for getUsers LATERAL join and fix CASE type mismatch in cache-hit-rate-alert

1. Add idx_usage_ledger_key_created_at_desc_cover index on (key, created_at
   DESC NULLS LAST, final_provider_id) with partial filter WHERE blocked_by IS
   NULL. This enables index-only scans for the LATERAL last-usage-per-key
   subquery in getUsers, reducing query time from ~28s to ~6.4ms.

2. Fix "CASE types integer and text cannot be matched" error in
   cache-hit-rate-alert by adding explicit ::integer casts to the
   ttlFallbackSecondsExpr CASE branches. PostgreSQL inferred parameterized
   values as text, which conflicted with integer literals (3600, 300) in the
   outer ttlSecondsExpr CASE.

3. Add regression test verifying ::integer casts are present in the generated
   SQL for cache-hit-rate-alert queries.

* chore: format code (dev-63c4c9c)

* feat(proxy): support codex desktop alias matching

* feat(billing): remove 1M context premium and add Codex 272k badge

Anthropic has GA'd the 1M context window for Opus 4.6 and Sonnet 4.6,
eliminating the long-context surcharge. The beta header
`context-1m-2025-08-07` is no longer needed.

Backend:
- Remove CONTEXT_1M_SUPPORTED_MODEL_PREFIXES, CONTEXT_1M_BETA_HEADER,
  Context1mPreference type, isContext1mSupportedModel, shouldApplyContext1m
- Remove AnthropicContext1mHeaderOverrideSpecialSetting type and derived logic
- Simplify forwarder 1M decision: just record client header, no injection
- Remove provider-selector Step 2.5 context1mPreference filter
- Add Codex 272k badge: set context1mApplied when input > 272k tokens

Frontend:
- Remove 1M Context Window setting from provider forms (options-section,
  provider-form-context, provider-form-types, batch-edit, legacy form)
- Remove premium pricing tooltips from usage-logs-table

i18n:
- Remove context1m provider config keys (5 languages)
- Remove context1mPricing premium text from dashboard (5 languages)

Tests:
- Remove anthropic_context_1m_header_override test cases
- Remove context1mPreference UI and patch draft tests

* chore: format code (dev-5a7db40)

* feat(usage): add anthropic effort tracking and display. #900 (#901)

* feat(usage): add anthropic effort tracking and display. #900

* chore: format code (effort-tag-558c8fd)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* feat(deploy): support incremental updates to preserve secrets on re-deploy

Previously, re-running deploy.sh/deploy.ps1 would regenerate DB_PASSWORD,
ADMIN_TOKEN, and SUFFIX, causing database connection failures because the
PostgreSQL data volume still held the old password.

Now the scripts auto-detect existing deployments (.env + docker-compose.yaml)
and enter update mode: preserving DB_PASSWORD, ADMIN_TOKEN, SUFFIX, APP_PORT,
and any user-added custom env vars (e.g. LANGFUSE_*, FETCH_*). A --force-new
/ -ForceNew flag allows forcing a fresh install when needed.

* feat: support long-context pricing metadata

* feat: support long-context pricing metadata

* fix: return billing metadata when pricing is missing

* refactor: extract utilities, i18n fixes, and codex threshold constant

- Extract getUserFacingProviderTypes() to filter internal provider types
- Extract CODEX_1M_CONTEXT_TOKEN_THRESHOLD constant and maybeSetCodexContext1m helper
- Refactor limit-rule-picker to use t() shorthand for getTranslation
- i18n: add templateClaudeApi/GeminiApi/OpenaiApi keys to errorRules (5 locales)
- i18n: replace hardcoded API names in override-section with t() calls
- Add codex desktop alias matching tests for detectClientFull
- Improve error logging in edit-key-form provider group fetch
- Remove default parameter value from data-table actions column title

* refactor: move effort badge from table to request detail dialog

Move the Anthropic effort badge display from the usage logs table
(both admin and my-usage) into the SummaryTab's Session Info section
within the ErrorDetailsDialog. This declutters the table view and
provides richer context by showing override information (original vs
overridden effort) when a provider parameter override changes the
effort value.

- Add extractAnthropicEffortInfo utility combining anthropic_effort
  and provider_parameter_override special settings
- Display effort badge(s) in SummaryTab with override arrow notation
- Remove effort badge from ModelDisplayWithRedirect, both usage tables
- Add i18n keys (effort.label, effort.overridden) for all 5 languages
- Add 11 table-driven unit tests covering all branches

* fix: return proper object type in updateRequestCostFromUsage (#914)

Fixed TypeScript error where bare 'return;' returned undefined instead
of the expected object type with costUsd, resolvedPricing,
longContextPricing, and longContextPricingApplied properties.

Error: src/app/v1/_lib/proxy/response-handler.ts(3111,7): error TS2322:
Type 'undefined' is not assignable to type '{ costUsd: string | null;
resolvedPricing: ResolvedPricing | null; longContextPricing:
ResolvedLongContextPricing | null; longContextPricingApplied: boolean; }'.

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/23082652215

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: return proper type in calculateAndUpdateRequestCost early exit (#915)

The early return when no price data is found was returning undefined
instead of the expected object type. Fixed to return consistent object
structure matching other early returns in the function.

Fixed:
- Type 'undefined' is not assignable to type return object in response-handler.ts:3111

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/23082651541

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: miraserver <20286838+miraserver@users.noreply.github.com>
Co-authored-by: Night <lzy200816@gmail.com>
Co-authored-by: Hwwwww-dev <47653238+Hwwwww-dev@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
@ding113
ding113 deleted the feat/request-filter-advanced-mode branch May 13, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:UI enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant