Skip to content

feat: disk request body cache - #2780

Merged
Calcium-Ion merged 2 commits into
mainfrom
feat/storage-cache
Jan 29, 2026
Merged

feat: disk request body cache#2780
Calcium-Ion merged 2 commits into
mainfrom
feat/storage-cache

Conversation

@Calcium-Ion

@Calcium-Ion Calcium-Ion commented Jan 29, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Disk caching system with configurable thresholds and automatic file cleanup
    • Performance monitoring dashboard displaying memory, disk, and cache usage statistics
    • Performance settings UI for configuring disk cache behavior
    • API endpoints for retrieving performance data, clearing cache, resetting stats, and triggering garbage collection

✏️ Tip: You can customize this high-level summary in your review settings.

Calcium-Ion and others added 2 commits January 30, 2026 00:36
- 新增 common/body_storage.go 提供 HTTP 请求体存储抽象和文件缓存能力
- 增加 common/disk_cache_config.go 支持全局磁盘缓存配置
- main.go 挂载缓存初始化流程
- 新增和补充 controller/performance.go (及 unix/windows) 用于缓存性能监控接口
- middleware/body_cleanup.go 自动清理缓存文件
- router 挂载相关接口
- 前端 settings 页面新增性能监控设置 PerformanceSetting
- 优化缓存开关状态和模块热插拔能力
- 其他相关文件同步适配缓存扩展
- 修复 diskStorage.Close() 竞态条件,先获取锁再执行 CAS
- 为 memoryStorage 添加互斥锁和 closed 状态检查
- 修复 CreateBodyStorageFromReader 在磁盘存储失败时的回退逻辑
- 添加缓存命中统计调用 (IncrementDiskCacheHits/IncrementMemoryCacheHits)
- 修复 gin.go 中 Seek 错误被忽略的问题
- 在 api-router 添加 BodyStorageCleanup 中间件
- 修复前端 formatBytes 对异常值的处理

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jan 29, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request introduces a pluggable disk-based body storage system with in-memory fallback, disk caching configuration management, performance monitoring APIs, and a corresponding frontend settings interface. The implementation spans core storage abstractions, API endpoints, middleware integration, configuration management, and React UI components.

Changes

Cohort / File(s) Summary
Core Body Storage System
common/body_storage.go, common/disk_cache_config.go, common/gin.go
Implements BodyStorage interface with memory and disk backends. Adds configurable thresholds, cache statistics tracking (increments/decrements), and request body lifecycle management. Modifies GetRequestBody to use new storage cache and adds cleanup helpers.
Performance Monitoring API
controller/performance.go, controller/performance_unix.go, controller/performance_windows.go
Introduces endpoints for performance stats aggregation, disk cache clearing, stats reset, and garbage collection trigger. Platform-specific disk space info gathering via Unix Statfs and Windows GetDiskFreeSpaceExW APIs.
Configuration & Settings
setting/performance_setting/config.go, model/option.go
Adds performance configuration module with DiskCache settings. Extends config update logic to synchronize performance settings to common package when modified.
Middleware & Routing
middleware/body_cleanup.go, middleware/cors.go, router/api-router.go, router/relay-router.go
Introduces body storage cleanup middleware, version header middleware, and new /api/performance routes with authentication. Integrates cleanup middleware into request pipelines.
Frontend UI Components
web/src/components/settings/PerformanceSetting.jsx, web/src/pages/Setting/Performance/SettingsPerformance.jsx, web/src/pages/Setting/index.jsx
Adds React components for performance settings configuration (disk cache toggle, thresholds, paths) and live statistics display (cache hits, memory usage, disk space, GC metrics) with real-time refresh and management actions.
Entry Point & Metadata
main.go, controller/misc.go, web/index.html
Initializes disk cache cleanup on startup, enables version header middleware, adds metadata generator tag, and extends API response with version identifier.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Middleware
    participant Storage as BodyStorage Factory
    participant Backend as Memory/Disk Storage
    participant Context
    participant Cleanup

    Client->>Middleware: HTTP Request with Body
    Middleware->>Storage: CreateBodyStorageFromReader()
    alt Content Length Check
        Storage->>Backend: Use Disk Storage (large)
    else
        Storage->>Backend: Use Memory Storage (small)
    end
    Backend-->>Storage: BodyStorage Instance
    Storage-->>Middleware: BodyStorage
    Middleware->>Context: Store BodyStorage in Context
    Middleware->>Client: Forward to Handler
    Client->>Middleware: Request Processing
    Middleware->>Context: Retrieve BodyStorage
    Context-->>Middleware: BodyStorage
    Middleware->>Backend: Read/Seek Body Data
    Backend-->>Middleware: Body Bytes
    Middleware->>Cleanup: c.Next() Complete
    Cleanup->>Backend: Close & Cleanup
    Backend->>Cleanup: Release Resources
    Cleanup-->>Client: Response
Loading
sequenceDiagram
    participant Frontend as React UI
    participant API as /api/performance
    participant Config as DiskCacheConfig
    participant Stats as DiskCacheStats
    participant Disk as Disk Operations

    Frontend->>API: GET /stats
    API->>Config: GetDiskCacheConfig()
    API->>Stats: GetDiskCacheStats()
    API->>Disk: Gather Disk Space Info
    Stats-->>API: Cache Stats
    Config-->>API: Cache Config
    Disk-->>API: Disk Space Metrics
    API-->>Frontend: Performance Data JSON
    Frontend->>Frontend: Render Stats & Charts
    
    Frontend->>API: DELETE /disk_cache (clear)
    API->>Disk: Remove Cache Directory
    API->>Stats: ResetDiskCacheStats()
    Disk-->>API: Success
    Stats-->>API: Reset Complete
    API-->>Frontend: Clear Confirmation
    Frontend->>Frontend: Update UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024

Poem

🐰 A storage system hops with grace,
Memory and disk in perfect place,
Disk cache dancing, stats in view,
Performance metrics shining through!
With cleanup care, we bound along,
This feature makes the system strong! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.40% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing a pluggable storage system for request body caching with both in-memory and disk-based backends.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@Calcium-Ion Calcium-Ion changed the title feat: storage request body cache feat: disk request body cache Jan 29, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@common/body_storage.go`:
- Around line 231-263: In diskStorage.Bytes() the allocation make([]byte,
d.size) fails because d.size is int64; check that d.size is non-negative and
does not exceed the platform max int (e.g., math.MaxInt) and return an error if
it does, then safely convert with int(size) and use that for the slice
allocation and reading; update references in the method (d.size, Bytes, and the
io.ReadFull call) to use the converted int variable for buffer creation and any
length checks.

In `@common/gin.go`:
- Around line 89-118: GetBodyStorage currently calls GetRequestBody which reads
and caches the full request body (KeyRequestBody) into memory; instead build and
cache a disk-backed BodyStorage directly from c.Request.Body (or its reader) and
store it under KeyBodyStorage without populating KeyRequestBody. Modify
GetBodyStorage to: check for existing KeyBodyStorage and seek to start if
present; if missing, create a BodyStorage from the request reader (wrapping in a
temp file/streaming buffer), store only that under KeyBodyStorage, and return
it; remove the GetRequestBody call so large payloads are not eagerly loaded into
memory. Ensure you still reset the request.Body or replace it with an
io.ReadCloser if handlers need to read it later.

In `@web/src/components/settings/PerformanceSetting.jsx`:
- Around line 38-47: The current success branch builds a newInputs object from
data and calls setInputs(newInputs), which replaces the entire state and drops
any default keys missing from the API response; instead merge the API-derived
values into the existing/default state (inputs) so missing keys are preserved —
build newInputs the same way (using toBoolean for boolean keys) but call
setInputs(prev => ({ ...prev, ...newInputs })) or merge with the initial
defaults so omitted keys are retained; update the code around setInputs,
newInputs, inputs, and toBoolean accordingly.
🧹 Nitpick comments (5)
controller/misc.go (1)

118-118: Consider using a more descriptive key name or adding a comment.

The key _qn is cryptic and its purpose isn't immediately clear. If this is intended as a product/API identifier (possibly "QuantumNous"), consider either:

  • Using a more descriptive key like "_api_identifier" or "_product"
  • Adding an inline comment explaining the purpose

Additionally, this change appears unrelated to the PR's stated objective of "storage request body cache" - is this intentional?

setting/performance_setting/config.go (1)

46-48: Consider returning a copy instead of a pointer to prevent unintended mutations.

GetPerformanceSetting() returns a pointer to the package-level performanceSetting variable. Callers could mutate the struct directly, causing the common package configuration to become out of sync (since syncToCommon() wouldn't be called).

If this is only used for read-only access in admin contexts, the current implementation is acceptable. Otherwise, consider returning a copy:

♻️ Suggested change to return a copy
 // GetPerformanceSetting 获取性能设置
-func GetPerformanceSetting() *PerformanceSetting {
-	return &performanceSetting
+func GetPerformanceSetting() PerformanceSetting {
+	return performanceSetting
 }
web/src/components/settings/PerformanceSetting.jsx (1)

26-33: Prefer const for state variables.

Using let for useState variables is unconventional in React. Since the returned array is never reassigned, const is the idiomatic choice.

♻️ Proposed fix
-  let [inputs, setInputs] = useState({
+  const [inputs, setInputs] = useState({
     'performance_setting.disk_cache_enabled': false,
     'performance_setting.disk_cache_threshold_mb': 10,
     'performance_setting.disk_cache_max_size_mb': 1024,
     'performance_setting.disk_cache_path': '',
   });

-  let [loading, setLoading] = useState(false);
+  const [loading, setLoading] = useState(false);
web/src/pages/Setting/Performance/SettingsPerformance.jsx (2)

82-92: Simplify redundant conditional branches.

Both branches of the if-else perform identical String() conversion. The condition is unnecessary.

♻️ Proposed fix
   const requestQueue = updateArray.map((item) => {
-    let value = '';
-    if (typeof inputs[item.key] === 'boolean') {
-      value = String(inputs[item.key]);
-    } else {
-      value = String(inputs[item.key]);
-    }
+    const value = String(inputs[item.key]);
     return API.put('/api/option/', {
       key: item.key,
       value,
     });
   });

290-377: Add null-safe access for stats properties.

The stats rendering accesses nested properties (e.g., stats.cache_stats.current_disk_usage_bytes) directly. While stats is checked at line 290, malformed API responses could cause runtime errors if nested objects are missing.

Consider optional chaining for defensive access:

🛡️ Example with optional chaining
-  <Text type='tertiary'>
-    {formatBytes(stats.cache_stats.current_disk_usage_bytes)} / {formatBytes(stats.cache_stats.disk_cache_max_bytes)}
-  </Text>
+  <Text type='tertiary'>
+    {formatBytes(stats?.cache_stats?.current_disk_usage_bytes)} / {formatBytes(stats?.cache_stats?.disk_cache_max_bytes)}
+  </Text>

Apply similar changes to other nested accesses like stats.memory_stats.*, stats.disk_cache_info.*, etc.

Comment thread common/body_storage.go
Comment on lines +231 to +263
func (d *diskStorage) Bytes() ([]byte, error) {
d.mu.Lock()
defer d.mu.Unlock()

if atomic.LoadInt32(&d.closed) == 1 {
return nil, ErrStorageClosed
}

// 保存当前位置
currentPos, err := d.file.Seek(0, io.SeekCurrent)
if err != nil {
return nil, err
}

// 移动到开头
if _, err := d.file.Seek(0, io.SeekStart); err != nil {
return nil, err
}

// 读取全部内容
data := make([]byte, d.size)
_, err = io.ReadFull(d.file, data)
if err != nil {
return nil, err
}

// 恢复位置
if _, err := d.file.Seek(currentPos, io.SeekStart); err != nil {
return nil, err
}

return data, nil
}

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

sed -n '1,150p' common/body_storage.go | cat -n

Repository: QuantumNous/new-api

Length of output: 4084


🏁 Script executed:

sed -n '231,263p' common/body_storage.go | cat -n

Repository: QuantumNous/new-api

Length of output: 873


Fix compile error: slice length must be int, not int64.

make([]byte, d.size) won't compile because d.size is int64. Convert safely and guard against overflow before allocation.

Suggested fix
-	// 读取全部内容
-	data := make([]byte, d.size)
+	// 读取全部内容
+	if d.size > int64(^uint(0)>>1) {
+		return nil, fmt.Errorf("body too large to allocate: %d bytes", d.size)
+	}
+	data := make([]byte, int(d.size))
 	_, err = io.ReadFull(d.file, data)
 	if err != nil {
 		return nil, err
 	}
🤖 Prompt for AI Agents
In `@common/body_storage.go` around lines 231 - 263, In diskStorage.Bytes() the
allocation make([]byte, d.size) fails because d.size is int64; check that d.size
is non-negative and does not exceed the platform max int (e.g., math.MaxInt) and
return an error if it does, then safely convert with int(size) and use that for
the slice allocation and reading; update references in the method (d.size,
Bytes, and the io.ReadFull call) to use the converted int variable for buffer
creation and any length checks.

Comment thread common/gin.go
Comment on lines +89 to +118
// GetBodyStorage 获取请求体存储对象(用于需要多次读取的场景)
func GetBodyStorage(c *gin.Context) (BodyStorage, error) {
// 检查是否已有存储
if storage, exists := c.Get(KeyBodyStorage); exists && storage != nil {
if bs, ok := storage.(BodyStorage); ok {
if _, err := bs.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek body storage: %w", err)
}
return bs, nil
}
}

// 如果没有,调用 GetRequestBody 创建存储
_, err := GetRequestBody(c)
if err != nil {
return nil, err
}

// 再次获取存储
if storage, exists := c.Get(KeyBodyStorage); exists && storage != nil {
if bs, ok := storage.(BodyStorage); ok {
if _, err := bs.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek body storage: %w", err)
}
return bs, nil
}
}

return nil, errors.New("failed to get body storage")
}

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.

⚠️ Potential issue | 🟠 Major

Avoid loading the full body into memory in GetBodyStorage.

GetBodyStorage currently calls GetRequestBody, which reads the entire request body and caches KeyRequestBody, negating disk‑backed streaming and risking OOM on large payloads. Build the BodyStorage directly from the reader and cache only the storage.

💡 Suggested fix
 func GetBodyStorage(c *gin.Context) (BodyStorage, error) {
 	// 检查是否已有存储
 	if storage, exists := c.Get(KeyBodyStorage); exists && storage != nil {
 		if bs, ok := storage.(BodyStorage); ok {
 			if _, err := bs.Seek(0, io.SeekStart); err != nil {
 				return nil, fmt.Errorf("failed to seek body storage: %w", err)
 			}
 			return bs, nil
 		}
 	}

-	// 如果没有,调用 GetRequestBody 创建存储
-	_, err := GetRequestBody(c)
-	if err != nil {
-		return nil, err
-	}
-
-	// 再次获取存储
-	if storage, exists := c.Get(KeyBodyStorage); exists && storage != nil {
-		if bs, ok := storage.(BodyStorage); ok {
-			if _, err := bs.Seek(0, io.SeekStart); err != nil {
-				return nil, fmt.Errorf("failed to seek body storage: %w", err)
-			}
-			return bs, nil
-		}
-	}
-
-	return nil, errors.New("failed to get body storage")
+	maxMB := constant.MaxRequestBodyMB
+	if maxMB <= 0 {
+		maxMB = 128 // 默认 128MB
+	}
+	maxBytes := int64(maxMB) << 20
+	contentLength := c.Request.ContentLength
+
+	storage, err := CreateBodyStorageFromReader(c.Request.Body, contentLength, maxBytes)
+	_ = c.Request.Body.Close()
+	if err != nil {
+		if IsRequestBodyTooLargeError(err) {
+			return nil, errors.Wrap(ErrRequestBodyTooLarge, fmt.Sprintf("request body exceeds %d MB", maxMB))
+		}
+		return nil, err
+	}
+
+	c.Set(KeyBodyStorage, storage)
+	return storage, nil
 }
🤖 Prompt for AI Agents
In `@common/gin.go` around lines 89 - 118, GetBodyStorage currently calls
GetRequestBody which reads and caches the full request body (KeyRequestBody)
into memory; instead build and cache a disk-backed BodyStorage directly from
c.Request.Body (or its reader) and store it under KeyBodyStorage without
populating KeyRequestBody. Modify GetBodyStorage to: check for existing
KeyBodyStorage and seek to start if present; if missing, create a BodyStorage
from the request reader (wrapping in a temp file/streaming buffer), store only
that under KeyBodyStorage, and return it; remove the GetRequestBody call so
large payloads are not eagerly loaded into memory. Ensure you still reset the
request.Body or replace it with an io.ReadCloser if handlers need to read it
later.

Comment on lines +38 to +47
if (success) {
let newInputs = {};
data.forEach((item) => {
if (typeof inputs[item.key] === 'boolean') {
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}
});
setInputs(newInputs);

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.

⚠️ Potential issue | 🟡 Minor

State may lose default values if API doesn't return all keys.

The newInputs object only includes keys present in the API response. If the API omits a key, setInputs(newInputs) replaces the entire state, discarding default values for missing keys. Consider merging with the initial defaults.

🛠️ Proposed fix
   if (success) {
-    let newInputs = {};
+    let newInputs = { ...inputs }; // preserve defaults
     data.forEach((item) => {
-      if (typeof inputs[item.key] === 'boolean') {
+      if (item.key in inputs) {
+        if (typeof inputs[item.key] === 'boolean') {
           newInputs[item.key] = toBoolean(item.value);
-      } else {
+        } else {
           newInputs[item.key] = item.value;
+        }
       }
     });
     setInputs(newInputs);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (success) {
let newInputs = {};
data.forEach((item) => {
if (typeof inputs[item.key] === 'boolean') {
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}
});
setInputs(newInputs);
if (success) {
let newInputs = { ...inputs }; // preserve defaults
data.forEach((item) => {
if (item.key in inputs) {
if (typeof inputs[item.key] === 'boolean') {
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}
}
});
setInputs(newInputs);
🤖 Prompt for AI Agents
In `@web/src/components/settings/PerformanceSetting.jsx` around lines 38 - 47, The
current success branch builds a newInputs object from data and calls
setInputs(newInputs), which replaces the entire state and drops any default keys
missing from the API response; instead merge the API-derived values into the
existing/default state (inputs) so missing keys are preserved — build newInputs
the same way (using toBoolean for boolean keys) but call setInputs(prev => ({
...prev, ...newInputs })) or merge with the initial defaults so omitted keys are
retained; update the code around setInputs, newInputs, inputs, and toBoolean
accordingly.

@Calcium-Ion
Calcium-Ion merged commit 1c983a0 into main Jan 29, 2026
1 check passed
@Calcium-Ion
Calcium-Ion deleted the feat/storage-cache branch January 29, 2026 17:10
dreamlx pushed a commit to dreamlx/new-api that referenced this pull request Feb 1, 2026
主要更新:
- feat: 磁盘请求体缓存 (QuantumNous#2780)
- feat: OpenAI Response API /v1/response/compact (QuantumNous#2644)
- feat: 渠道亲和性 (Channel Affinity) (QuantumNous#2669)
- feat: Codex渠道支持 (QuantumNous#2652)
- feat: Claude/Grok refusal reason显示
- feat: 性能监控和GC控制API
- fix: 用户配额获取逻辑 (QuantumNous#2749)
- fix: Gemini多工具调用索引问题
- fix: 错误时仍本地计费

冲突解决:
- README.md: 保留中文版本
- go.mod: 采用上游较新版本

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
ennnnny pushed a commit to ennnnny/new-api that referenced this pull request Mar 17, 2026
* feat: 引入通用 HTTP BodyStorage/DiskCache 缓存配置与管理

- 新增 common/body_storage.go 提供 HTTP 请求体存储抽象和文件缓存能力
- 增加 common/disk_cache_config.go 支持全局磁盘缓存配置
- main.go 挂载缓存初始化流程
- 新增和补充 controller/performance.go (及 unix/windows) 用于缓存性能监控接口
- middleware/body_cleanup.go 自动清理缓存文件
- router 挂载相关接口
- 前端 settings 页面新增性能监控设置 PerformanceSetting
- 优化缓存开关状态和模块热插拔能力
- 其他相关文件同步适配缓存扩展

* fix: 修复 BodyStorage 并发安全和错误处理问题

- 修复 diskStorage.Close() 竞态条件,先获取锁再执行 CAS
- 为 memoryStorage 添加互斥锁和 closed 状态检查
- 修复 CreateBodyStorageFromReader 在磁盘存储失败时的回退逻辑
- 添加缓存命中统计调用 (IncrementDiskCacheHits/IncrementMemoryCacheHits)
- 修复 gin.go 中 Seek 错误被忽略的问题
- 在 api-router 添加 BodyStorageCleanup 中间件
- 修复前端 formatBytes 对异常值的处理

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jun 2, 2026
11 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Jun 11, 2026
11 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Jul 7, 2026
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant