perf(cache): add strategy-based cache policy
This commit is contained in:
1 parent
79e6069c37
commit
931463ed63
14 files changed
+407
-49
No files matched your search
@@ -96,7 +96,7 @@ npm run commitlint # Validate the latest commit message
|
||||
- **`index.js`**: Runtime configuration with environment variable overrides
|
||||
- `TIMEOUT_SECONDS`: Request timeout (default: 30s)
|
||||
- `MAX_RETRIES`: Retry attempts (default: 3)
|
||||
- `CACHE_DURATION`: Cache TTL (default: 1800s = 30 minutes)
|
||||
- `CACHE_DURATION`: Fallback mutable cache TTL (default: 300s = 5 minutes)
|
||||
- `SECURITY.ALLOWED_METHODS`: HTTP methods (default: GET, HEAD)
|
||||
|
||||
- **`platform-catalog.js`**: Platform base URL definitions
|
||||
@@ -143,7 +143,7 @@ npm run commitlint # Validate the latest commit message
|
||||
### Caching Strategy
|
||||
|
||||
- Uses Cloudflare Cache API for GET requests (200 OK only)
|
||||
- Cache TTL controlled by `CACHE_DURATION` config
|
||||
- Fallback mutable cache TTL controlled by `CACHE_DURATION` config
|
||||
- Skips cache for: Git operations, Docker operations, AI inference requests
|
||||
- Range requests: First checks for range-specific cache, falls back to full
|
||||
content cache
|
||||
@@ -315,7 +315,7 @@ Configure in Cloudflare Workers dashboard or via `wrangler.toml`:
|
||||
|
||||
- `TIMEOUT_SECONDS`: Override default timeout
|
||||
- `MAX_RETRIES`: Override retry count
|
||||
- `CACHE_DURATION`: Override cache TTL
|
||||
- `CACHE_DURATION`: Override fallback mutable cache TTL
|
||||
- `ALLOWED_METHODS`: Override allowed HTTP methods (comma-separated)
|
||||
- `ALLOWED_ORIGINS`: Override CORS origins (comma-separated)
|
||||
|
||||
|
||||
@@ -156,10 +156,11 @@ supported platform URL to Xget's acceleration format with one click
|
||||
- Automatic error recovery, improved download success rate
|
||||
- Timeout detection and interruption handling
|
||||
- **Efficient Caching Strategy**:
|
||||
- 1800 seconds (30 minutes) default cache duration, significantly reduces
|
||||
origin server pressure
|
||||
- Strategy-based cache durations keep mutable metadata fresh while caching
|
||||
immutable artifacts longer
|
||||
- Git operations skip caching to ensure real-time data
|
||||
- Edge caching based on Cloudflare Cache API
|
||||
- Edge caching based on Cloudflare Cache API and Cloudflare fetch cache
|
||||
controls
|
||||
- **Performance Monitoring System**:
|
||||
- Built-in `PerformanceMonitor` class for real-time tracking of request stage
|
||||
durations
|
||||
@@ -2892,7 +2893,7 @@ export const CONFIG = {
|
||||
TIMEOUT_SECONDS: 30, // Request timeout (seconds)
|
||||
MAX_RETRIES: 3, // Maximum retry count
|
||||
RETRY_DELAY_MS: 1000, // Retry delay (milliseconds)
|
||||
CACHE_DURATION: 1800, // Cache duration (1800 seconds = 30 minutes)
|
||||
CACHE_DURATION: 300, // Fallback mutable cache duration (300 seconds = 5 minutes)
|
||||
SECURITY: {
|
||||
ALLOWED_METHODS: ['GET', 'HEAD'], // Base allowlist for regular requests; protocol traffic has broader built-in allowances
|
||||
ALLOWED_ORIGINS: ['*'], // Allowed CORS origins
|
||||
@@ -2903,8 +2904,8 @@ export const CONFIG = {
|
||||
|
||||
### Performance Tuning Recommendations
|
||||
|
||||
- **Cache Optimization**: Adjust `CACHE_DURATION` based on usage patterns,
|
||||
reduce appropriately for frequently updated repositories
|
||||
- **Cache Optimization**: Adjust the `CACHE_DURATION` fallback value based on
|
||||
usage patterns; metadata and immutable artifacts use built-in policy TTLs
|
||||
- **Timeout Settings**: Increase `TIMEOUT_SECONDS` appropriately for poor
|
||||
network conditions
|
||||
- **Retry Strategy**: Increase `MAX_RETRIES` and `RETRY_DELAY_MS` in
|
||||
|
||||
+4
-4
@@ -128,9 +128,9 @@
|
||||
- 自动错误恢复,提高下载成功率
|
||||
- 超时检测和中断处理
|
||||
- **高效缓存策略**:
|
||||
- 1800 秒(30 分钟)默认缓存时长,显著减少源站压力
|
||||
- 基于策略的缓存时长,让可变元数据保持新鲜,同时对不可变制品使用更长缓存
|
||||
- Git 操作跳过缓存,确保实时性
|
||||
- 基于 Cloudflare Cache API 的边缘缓存
|
||||
- 基于 Cloudflare Cache API 和 Cloudflare fetch 缓存控制的边缘缓存
|
||||
- **性能监控系统**:
|
||||
- 内置 `PerformanceMonitor` 类,实时追踪请求各阶段耗时
|
||||
- 通过 `X-Performance-Metrics` 响应头提供详细性能数据
|
||||
@@ -2808,7 +2808,7 @@ export const CONFIG = {
|
||||
TIMEOUT_SECONDS: 30, // 请求超时时间(秒)
|
||||
MAX_RETRIES: 3, // 最大重试次数
|
||||
RETRY_DELAY_MS: 1000, // 重试延迟时间(毫秒)
|
||||
CACHE_DURATION: 1800, // 缓存持续时间(1800秒 = 30分钟)
|
||||
CACHE_DURATION: 300, // 可变资源兜底缓存时长(300秒 = 5分钟)
|
||||
SECURITY: {
|
||||
ALLOWED_METHODS: ['GET', 'HEAD'], // 常规请求的基础允许列表;协议流量内置了更宽的允许范围
|
||||
ALLOWED_ORIGINS: ['*'], // 允许的 CORS 源
|
||||
@@ -2819,7 +2819,7 @@ export const CONFIG = {
|
||||
|
||||
### 性能调优建议
|
||||
|
||||
- **缓存优化**:根据使用模式调整 `CACHE_DURATION`,频繁更新的存储库可适当降低
|
||||
- **缓存优化**:根据使用模式调整 `CACHE_DURATION` 兜底值;元数据和不可变制品会使用内置策略化 TTL
|
||||
- **超时设置**:网络条件较差时可适当增加 `TIMEOUT_SECONDS`
|
||||
- **重试策略**:高延迟环境下可增加 `MAX_RETRIES` 和 `RETRY_DELAY_MS`
|
||||
|
||||
|
||||
+4
-4
@@ -129,9 +129,9 @@
|
||||
- 自動錯誤恢復,提高下載成功率
|
||||
- 逾時檢測和中斷處理
|
||||
- **高效快取策略**:
|
||||
- 1800 秒(30 分鐘)預設快取時長,顯著減少原始伺服器壓力
|
||||
- 基於策略的快取時長,讓可變中繼資料保持新鮮,同時對不可變製品使用更長快取
|
||||
- Git 操作跳過快取,確保即時性
|
||||
- 基於 Cloudflare Cache API 的邊緣快取
|
||||
- 基於 Cloudflare Cache API 和 Cloudflare fetch 快取控制的邊緣快取
|
||||
- **效能監控系統**:
|
||||
- 內建 `PerformanceMonitor` 類別,即時追蹤請求各階段耗時
|
||||
- 透過 `X-Performance-Metrics` 回應標頭提供詳細效能數據
|
||||
@@ -2808,7 +2808,7 @@ export const CONFIG = {
|
||||
TIMEOUT_SECONDS: 30, // 請求逾時時間(秒)
|
||||
MAX_RETRIES: 3, // 最大重試次數
|
||||
RETRY_DELAY_MS: 1000, // 重試延遲時間(毫秒)
|
||||
CACHE_DURATION: 1800, // 快取持續時間(1800秒 = 30分鐘)
|
||||
CACHE_DURATION: 300, // 可變資源兜底快取時長(300秒 = 5分鐘)
|
||||
SECURITY: {
|
||||
ALLOWED_METHODS: ['GET', 'HEAD'], // 常規請求的基礎允許清單;協定流量內建了更寬的允許範圍
|
||||
ALLOWED_ORIGINS: ['*'], // 允許的 CORS 來源
|
||||
@@ -2819,7 +2819,7 @@ export const CONFIG = {
|
||||
|
||||
### 效能調優建議
|
||||
|
||||
- **快取最佳化**:根據使用模式調整 `CACHE_DURATION`,頻繁更新的儲存庫可適當降低
|
||||
- **快取最佳化**:根據使用模式調整 `CACHE_DURATION` 兜底值;中繼資料和不可變製品會使用內建策略化 TTL
|
||||
- **逾時設定**:網路條件較差時可適當增加 `TIMEOUT_SECONDS`
|
||||
- **重試策略**:高延遲環境下可增加 `MAX_RETRIES` 和 `RETRY_DELAY_MS`
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
resolveTarget
|
||||
} from '../routing/resolve-target.js';
|
||||
import { getDefaultCache, tryReadCachedResponse } from '../upstream/cache.js';
|
||||
import { resolveCachePolicy } from '../upstream/cache-policy.js';
|
||||
import { fetchUpstreamResponse } from '../upstream/fetch-upstream.js';
|
||||
import { PerformanceMonitor, addPerformanceHeaders } from '../utils/performance.js';
|
||||
import { addCorsHeaders, addSecurityHeaders, createErrorResponse } from '../utils/security.js';
|
||||
@@ -113,11 +114,22 @@ export async function handleRequest(request, env, ctx) {
|
||||
);
|
||||
const canUseCache = request.method === 'GET' || request.method === 'HEAD';
|
||||
const shouldPassthroughRequest = isProtocolRequest(requestContext) || !canUseCache;
|
||||
const cachePolicy = resolveCachePolicy({
|
||||
canUseCache,
|
||||
config,
|
||||
effectivePath,
|
||||
hasSensitiveHeaders,
|
||||
platform,
|
||||
request,
|
||||
requestContext,
|
||||
targetUrl
|
||||
});
|
||||
const cache = getDefaultCache();
|
||||
|
||||
response = await tryReadCachedResponse({
|
||||
cache,
|
||||
cacheTargetUrl,
|
||||
cachePolicy,
|
||||
canUseCache,
|
||||
hasSensitiveHeaders,
|
||||
monitor,
|
||||
@@ -131,6 +143,7 @@ export async function handleRequest(request, env, ctx) {
|
||||
responseGeneratedLocally: upstreamResponseGeneratedLocally
|
||||
} = await fetchUpstreamResponse({
|
||||
authorization,
|
||||
cachePolicy,
|
||||
canUseCache,
|
||||
config,
|
||||
effectivePath,
|
||||
@@ -155,6 +168,7 @@ export async function handleRequest(request, env, ctx) {
|
||||
requestContext,
|
||||
response: upstreamResponse,
|
||||
responseGeneratedLocally: upstreamResponseGeneratedLocally,
|
||||
targetUrl,
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
+5
-5
@@ -50,7 +50,7 @@ import { PLATFORMS } from './platform-catalog.js';
|
||||
* @property {number} TIMEOUT_SECONDS - Request timeout in seconds (default: 30)
|
||||
* @property {number} MAX_RETRIES - Maximum number of retry attempts for failed requests (default: 3)
|
||||
* @property {number} RETRY_DELAY_MS - Delay between retry attempts in milliseconds (default: 1000)
|
||||
* @property {number} CACHE_DURATION - Cache duration in seconds for successful responses (default: 1800)
|
||||
* @property {number} CACHE_DURATION - Fallback cache duration in seconds for mutable successful responses (default: 300)
|
||||
* @property {SecurityConfig} SECURITY - Security-related configurations
|
||||
* @property {{ [key: string]: string }} PLATFORMS - Platform-specific base URL mappings
|
||||
* @example
|
||||
@@ -59,7 +59,7 @@ import { PLATFORMS } from './platform-catalog.js';
|
||||
* TIMEOUT_SECONDS: 30,
|
||||
* MAX_RETRIES: 3,
|
||||
* RETRY_DELAY_MS: 1000,
|
||||
* CACHE_DURATION: 1800,
|
||||
* CACHE_DURATION: 300,
|
||||
* SECURITY: {
|
||||
* ALLOWED_METHODS: ['GET', 'HEAD'],
|
||||
* ALLOWED_ORIGINS: ['*'],
|
||||
@@ -89,7 +89,7 @@ import { PLATFORMS } from './platform-catalog.js';
|
||||
* - `TIMEOUT_SECONDS` - Override default timeout (default: 30)
|
||||
* - `MAX_RETRIES` - Override max retry attempts (default: 3)
|
||||
* - `RETRY_DELAY_MS` - Override retry delay (default: 1000)
|
||||
* - `CACHE_DURATION` - Override cache TTL (default: 1800 = 30 minutes)
|
||||
* - `CACHE_DURATION` - Override fallback mutable cache TTL (default: 300 = 5 minutes)
|
||||
* - `ALLOWED_METHODS` - Comma-separated HTTP methods (default: 'GET,HEAD')
|
||||
* - `ALLOWED_ORIGINS` - Comma-separated CORS origins (default: '*')
|
||||
* - `MAX_PATH_LENGTH` - Override max path length (default: 2048)
|
||||
@@ -99,7 +99,7 @@ import { PLATFORMS } from './platform-catalog.js';
|
||||
* // Create config with defaults (no environment variables)
|
||||
* const config = createConfig();
|
||||
* console.log(config.TIMEOUT_SECONDS); // 30
|
||||
* console.log(config.CACHE_DURATION); // 1800
|
||||
* console.log(config.CACHE_DURATION); // 300
|
||||
* @example
|
||||
* // Create config with environment overrides
|
||||
* const env = {
|
||||
@@ -149,7 +149,7 @@ export function createConfig(env = {}) {
|
||||
TIMEOUT_SECONDS: parseInt(String(env.TIMEOUT_SECONDS), 10) || 30,
|
||||
MAX_RETRIES: parseInt(String(env.MAX_RETRIES), 10) || 3,
|
||||
RETRY_DELAY_MS: parseInt(String(env.RETRY_DELAY_MS), 10) || 1000,
|
||||
CACHE_DURATION: parseInt(String(env.CACHE_DURATION), 10) || 1800, // 30 minutes
|
||||
CACHE_DURATION: parseInt(String(env.CACHE_DURATION), 10) || 300, // 5 minutes
|
||||
SECURITY: {
|
||||
ALLOWED_METHODS: allowedMethods.length ? allowedMethods : ['GET', 'HEAD'],
|
||||
ALLOWED_ORIGINS: allowedOrigins.length ? allowedOrigins : ['*'],
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
rewriteTextResponse,
|
||||
shouldRewriteTextResponse
|
||||
} from '../utils/rewrite.js';
|
||||
import { resolveCachePolicy } from '../upstream/cache-policy.js';
|
||||
import { addSecurityHeaders, createErrorResponse } from '../utils/security.js';
|
||||
|
||||
/**
|
||||
@@ -95,6 +96,7 @@ async function finalizeErrorResponse({ requestContext, response, responseGenerat
|
||||
* isHF: boolean
|
||||
* },
|
||||
* response: Response,
|
||||
* targetUrl?: string,
|
||||
* url: URL
|
||||
* }} options
|
||||
* @returns {Promise<Response>} Final proxied response.
|
||||
@@ -112,6 +114,7 @@ async function finalizeSuccessfulResponse({
|
||||
request,
|
||||
requestContext,
|
||||
response,
|
||||
targetUrl = cacheTargetUrl,
|
||||
url
|
||||
}) {
|
||||
const { isAI, isDocker, isGit, isGitLFS, isHF } = requestContext;
|
||||
@@ -140,18 +143,27 @@ async function finalizeSuccessfulResponse({
|
||||
headers.set('Content-Length', String(rewrittenContentLength));
|
||||
}
|
||||
|
||||
const cachePolicy = resolveCachePolicy({
|
||||
canUseCache,
|
||||
config,
|
||||
effectivePath,
|
||||
hasOriginBoundRewrite,
|
||||
hasSensitiveHeaders,
|
||||
platform,
|
||||
request,
|
||||
requestContext,
|
||||
targetUrl
|
||||
});
|
||||
|
||||
if (!isGit && !isGitLFS && !isDocker && !isAI && !isHF) {
|
||||
if (!canUseCache || hasOriginBoundRewrite) {
|
||||
headers.set('Cache-Control', 'no-store');
|
||||
} else if (hasSensitiveHeaders) {
|
||||
headers.set('Cache-Control', 'private, no-store');
|
||||
headers.set('Cache-Control', cachePolicy.cacheControl);
|
||||
|
||||
if (cachePolicy.mode === 'private') {
|
||||
const existingVary = headers.get('Vary');
|
||||
headers.set(
|
||||
'Vary',
|
||||
existingVary ? `${existingVary}, Authorization, Cookie` : 'Authorization, Cookie'
|
||||
);
|
||||
} else {
|
||||
headers.set('Cache-Control', `public, max-age=${config.CACHE_DURATION}`);
|
||||
}
|
||||
|
||||
headers.set('X-Content-Type-Options', 'nosniff');
|
||||
@@ -178,13 +190,7 @@ async function finalizeSuccessfulResponse({
|
||||
|
||||
if (
|
||||
cache &&
|
||||
!isGit &&
|
||||
!isGitLFS &&
|
||||
!isDocker &&
|
||||
!isAI &&
|
||||
!isHF &&
|
||||
!hasOriginBoundRewrite &&
|
||||
!hasSensitiveHeaders &&
|
||||
cachePolicy.allowCacheApi &&
|
||||
request.method === 'GET' &&
|
||||
finalizedResponse.ok &&
|
||||
finalizedResponse.status === 200
|
||||
@@ -250,6 +256,7 @@ async function finalizeSuccessfulResponse({
|
||||
* },
|
||||
* response: Response,
|
||||
* responseGeneratedLocally: boolean,
|
||||
* targetUrl?: string,
|
||||
* url: URL
|
||||
* }} options
|
||||
* @returns {Promise<Response>} Final response returned to the client.
|
||||
@@ -268,6 +275,7 @@ export async function finalizeResponse({
|
||||
requestContext,
|
||||
response,
|
||||
responseGeneratedLocally,
|
||||
targetUrl = cacheTargetUrl,
|
||||
url
|
||||
}) {
|
||||
const errorResponse = await finalizeErrorResponse({
|
||||
@@ -297,6 +305,7 @@ export async function finalizeResponse({
|
||||
request,
|
||||
requestContext,
|
||||
response: errorResponse,
|
||||
targetUrl,
|
||||
url
|
||||
});
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
import { SORTED_PLATFORMS } from './platform-index.js';
|
||||
import { transformPath } from './platform-transformers.js';
|
||||
import { normalizeRegistryApiPath } from '../protocols/docker.js';
|
||||
import { isFlatpakReferenceFilePath } from '../utils/rewrite.js';
|
||||
import { shouldVaryCacheByOrigin } from '../upstream/cache-policy.js';
|
||||
import { createErrorResponse } from '../utils/security.js';
|
||||
|
||||
export const HOME_PAGE_URL = 'https://github.com/xixu-me/Xget';
|
||||
@@ -98,16 +98,15 @@ export function resolveTarget(url, effectivePath, platforms) {
|
||||
? normalizeRegistryApiPath(platform, transformedPath)
|
||||
: transformedPath;
|
||||
const targetUrl = `${platforms[platform]}${targetPath}${url.search}`;
|
||||
const shouldVaryCacheByOrigin =
|
||||
platform === 'flathub' && isFlatpakReferenceFilePath(effectivePath);
|
||||
const cacheTargetUrl = shouldVaryCacheByOrigin
|
||||
const varyCacheByOrigin = shouldVaryCacheByOrigin(platform, effectivePath);
|
||||
const cacheTargetUrl = varyCacheByOrigin
|
||||
? `${targetUrl}${targetUrl.includes('?') ? '&' : '?'}__xget_origin=${encodeURIComponent(url.origin)}`
|
||||
: targetUrl;
|
||||
|
||||
return {
|
||||
cacheTargetUrl,
|
||||
platform,
|
||||
shouldVaryCacheByOrigin,
|
||||
shouldVaryCacheByOrigin: varyCacheByOrigin,
|
||||
targetPath,
|
||||
targetUrl
|
||||
};
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Xget - High-performance acceleration engine for developer resources
|
||||
* Copyright (C) Xi Xu
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isFlatpakReferenceFilePath } from '../utils/rewrite.js';
|
||||
|
||||
const METADATA_EDGE_TTL_SECONDS = 60;
|
||||
const MUTABLE_EDGE_TTL_SECONDS = 300;
|
||||
const IMMUTABLE_EDGE_TTL_SECONDS = 86400;
|
||||
const IMMUTABLE_BROWSER_TTL_SECONDS = 3600;
|
||||
|
||||
const IMMUTABLE_ARTIFACT_PATTERN =
|
||||
/\.(?:tgz|whl|jar|zip|gem|crate|deb|rpm|nupkg|tar\.gz|tar\.bz2|tar\.xz)(?:$|[?#])/i;
|
||||
|
||||
/**
|
||||
* Checks whether a request path points to versioned or content-addressed package artifacts.
|
||||
* @param {string} value Request path or target URL.
|
||||
* @returns {boolean} True when the resource can use long-lived immutable caching.
|
||||
*/
|
||||
export function isImmutableArtifactPath(value) {
|
||||
return IMMUTABLE_ARTIFACT_PATTERN.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an npm path points to rewritten package metadata instead of tarball content.
|
||||
* @param {string} effectivePath Normalized request path.
|
||||
* @returns {boolean} True when npm response rewriting can bind content to request origin.
|
||||
*/
|
||||
function isNpmMetadataPath(effectivePath) {
|
||||
return effectivePath.startsWith('/npm/') && !isImmutableArtifactPath(effectivePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the cache key must include the request origin because response rewriting embeds it.
|
||||
* @param {string} platform Platform key.
|
||||
* @param {string} effectivePath Normalized request path.
|
||||
* @returns {boolean} True when the generated response varies by request origin.
|
||||
*/
|
||||
export function shouldVaryCacheByOrigin(platform, effectivePath) {
|
||||
return (
|
||||
(platform === 'flathub' && isFlatpakReferenceFilePath(effectivePath)) ||
|
||||
(platform === 'npm' && isNpmMetadataPath(effectivePath))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a request targets mutable metadata or package index resources.
|
||||
* @param {string} platform Platform key.
|
||||
* @param {string} effectivePath Normalized request path.
|
||||
* @returns {boolean} True when freshness should be preferred over hit ratio.
|
||||
*/
|
||||
function isMetadataOrIndexPath(platform, effectivePath) {
|
||||
if (platform === 'npm') {
|
||||
return isNpmMetadataPath(effectivePath);
|
||||
}
|
||||
|
||||
if (platform === 'pypi') {
|
||||
return effectivePath.startsWith('/pypi/simple/') || effectivePath === '/pypi/simple';
|
||||
}
|
||||
|
||||
if (platform === 'maven') {
|
||||
return effectivePath.endsWith('/maven-metadata.xml');
|
||||
}
|
||||
|
||||
if (platform === 'flathub') {
|
||||
return (
|
||||
effectivePath === '/flathub/repo/summary' || effectivePath === '/flathub/repo/summary.sig'
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a shared-cache Cache-Control value.
|
||||
* @param {number} browserTtl Browser max-age in seconds.
|
||||
* @param {number} edgeTtl Shared cache max-age in seconds.
|
||||
* @param {boolean} immutable Whether the response is immutable.
|
||||
* @returns {string} Cache-Control header value.
|
||||
*/
|
||||
function buildPublicCacheControl(browserTtl, edgeTtl, immutable) {
|
||||
const directives = ['public', `max-age=${browserTtl}`, `s-maxage=${edgeTtl}`];
|
||||
|
||||
if (immutable) {
|
||||
directives.push('immutable');
|
||||
} else {
|
||||
directives.push('must-revalidate');
|
||||
}
|
||||
|
||||
return directives.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves cache behavior for a proxied request/response.
|
||||
* @param {{
|
||||
* canUseCache: boolean,
|
||||
* config: import('../config/index.js').ApplicationConfig,
|
||||
* effectivePath: string,
|
||||
* hasOriginBoundRewrite?: boolean,
|
||||
* hasSensitiveHeaders: boolean,
|
||||
* platform: string,
|
||||
* request: Request,
|
||||
* requestContext: {
|
||||
* isAI: boolean,
|
||||
* isDocker: boolean,
|
||||
* isGit: boolean,
|
||||
* isGitLFS: boolean,
|
||||
* isHF: boolean
|
||||
* },
|
||||
* targetUrl: string
|
||||
* }} options
|
||||
* @returns {{
|
||||
* allowCacheApi: boolean,
|
||||
* allowFetchCache: boolean,
|
||||
* browserTtl: number,
|
||||
* cacheControl: string,
|
||||
* edgeTtl: number,
|
||||
* mode: 'bypass' | 'edge' | 'private',
|
||||
* varyByOrigin: boolean
|
||||
* }} Cache policy.
|
||||
*/
|
||||
export function resolveCachePolicy({
|
||||
canUseCache,
|
||||
config,
|
||||
effectivePath,
|
||||
hasOriginBoundRewrite = false,
|
||||
hasSensitiveHeaders,
|
||||
platform,
|
||||
request,
|
||||
requestContext,
|
||||
targetUrl
|
||||
}) {
|
||||
const isProtocolRequest =
|
||||
requestContext.isGit ||
|
||||
requestContext.isGitLFS ||
|
||||
requestContext.isDocker ||
|
||||
requestContext.isAI ||
|
||||
requestContext.isHF;
|
||||
|
||||
if (hasSensitiveHeaders) {
|
||||
return {
|
||||
allowCacheApi: false,
|
||||
allowFetchCache: false,
|
||||
browserTtl: 0,
|
||||
cacheControl: 'private, no-store',
|
||||
edgeTtl: 0,
|
||||
mode: 'private',
|
||||
varyByOrigin: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!canUseCache || isProtocolRequest || hasOriginBoundRewrite) {
|
||||
return {
|
||||
allowCacheApi: false,
|
||||
allowFetchCache: false,
|
||||
browserTtl: 0,
|
||||
cacheControl: 'no-store',
|
||||
edgeTtl: 0,
|
||||
mode: 'bypass',
|
||||
varyByOrigin: false
|
||||
};
|
||||
}
|
||||
|
||||
if (isImmutableArtifactPath(effectivePath) || isImmutableArtifactPath(targetUrl)) {
|
||||
return {
|
||||
allowCacheApi: request.method === 'GET',
|
||||
allowFetchCache: true,
|
||||
browserTtl: IMMUTABLE_BROWSER_TTL_SECONDS,
|
||||
cacheControl: buildPublicCacheControl(
|
||||
IMMUTABLE_BROWSER_TTL_SECONDS,
|
||||
IMMUTABLE_EDGE_TTL_SECONDS,
|
||||
true
|
||||
),
|
||||
edgeTtl: IMMUTABLE_EDGE_TTL_SECONDS,
|
||||
mode: 'edge',
|
||||
varyByOrigin: shouldVaryCacheByOrigin(platform, effectivePath)
|
||||
};
|
||||
}
|
||||
|
||||
if (isMetadataOrIndexPath(platform, effectivePath)) {
|
||||
return {
|
||||
allowCacheApi: request.method === 'GET',
|
||||
allowFetchCache: true,
|
||||
browserTtl: 0,
|
||||
cacheControl: buildPublicCacheControl(0, METADATA_EDGE_TTL_SECONDS, false),
|
||||
edgeTtl: METADATA_EDGE_TTL_SECONDS,
|
||||
mode: 'edge',
|
||||
varyByOrigin: shouldVaryCacheByOrigin(platform, effectivePath)
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackEdgeTtl = Number.isFinite(config.CACHE_DURATION)
|
||||
? config.CACHE_DURATION
|
||||
: MUTABLE_EDGE_TTL_SECONDS;
|
||||
|
||||
return {
|
||||
allowCacheApi: request.method === 'GET',
|
||||
allowFetchCache: true,
|
||||
browserTtl: 0,
|
||||
cacheControl: buildPublicCacheControl(0, fallbackEdgeTtl, false),
|
||||
edgeTtl: fallbackEdgeTtl,
|
||||
mode: 'edge',
|
||||
varyByOrigin: shouldVaryCacheByOrigin(platform, effectivePath)
|
||||
};
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export function getDefaultCache() {
|
||||
* @param {{
|
||||
* cache: Cache | null,
|
||||
* cacheTargetUrl: string,
|
||||
* cachePolicy?: { allowCacheApi: boolean },
|
||||
* canUseCache: boolean,
|
||||
* hasSensitiveHeaders: boolean,
|
||||
* monitor: import('../utils/performance.js').PerformanceMonitor,
|
||||
@@ -54,6 +55,7 @@ export function getDefaultCache() {
|
||||
export async function tryReadCachedResponse({
|
||||
cache,
|
||||
cacheTargetUrl,
|
||||
cachePolicy,
|
||||
canUseCache,
|
||||
hasSensitiveHeaders,
|
||||
monitor,
|
||||
@@ -64,6 +66,7 @@ export async function tryReadCachedResponse({
|
||||
|
||||
if (
|
||||
!cache ||
|
||||
(cachePolicy && !cachePolicy.allowCacheApi) ||
|
||||
!canUseCache ||
|
||||
isGit ||
|
||||
isGitLFS ||
|
||||
|
||||
@@ -35,6 +35,7 @@ const MEDIA_FILE_PATTERN =
|
||||
* Creates upstream fetch options for the current request.
|
||||
* @param {{
|
||||
* authorization: string | null,
|
||||
* cachePolicy?: { allowFetchCache: boolean, edgeTtl: number },
|
||||
* canUseCache: boolean,
|
||||
* config: import('../config/index.js').ApplicationConfig,
|
||||
* request: Request,
|
||||
@@ -53,6 +54,7 @@ const MEDIA_FILE_PATTERN =
|
||||
*/
|
||||
function createFetchOptions({
|
||||
authorization,
|
||||
cachePolicy,
|
||||
canUseCache,
|
||||
config,
|
||||
request,
|
||||
@@ -97,14 +99,16 @@ function createFetchOptions({
|
||||
return { fetchOptions, requestHeaders };
|
||||
}
|
||||
|
||||
Object.assign(fetchOptions, {
|
||||
cf: {
|
||||
http3: true,
|
||||
cacheTtl: config.CACHE_DURATION,
|
||||
cacheEverything: true,
|
||||
preconnect: true
|
||||
}
|
||||
});
|
||||
if (!cachePolicy || cachePolicy.allowFetchCache) {
|
||||
Object.assign(fetchOptions, {
|
||||
cf: {
|
||||
http3: true,
|
||||
cacheTtl: cachePolicy ? cachePolicy.edgeTtl : config.CACHE_DURATION,
|
||||
cacheEverything: true,
|
||||
preconnect: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
requestHeaders.set('Accept-Encoding', 'gzip, deflate, br');
|
||||
requestHeaders.set('Connection', 'keep-alive');
|
||||
@@ -307,6 +311,7 @@ async function retryDockerWithAnonymousToken({
|
||||
* Fetches an upstream resource with retries and protocol-specific handling.
|
||||
* @param {{
|
||||
* authorization: string | null,
|
||||
* cachePolicy?: { allowFetchCache: boolean, edgeTtl: number },
|
||||
* canUseCache: boolean,
|
||||
* config: import('../config/index.js').ApplicationConfig,
|
||||
* effectivePath: string,
|
||||
@@ -328,6 +333,7 @@ async function retryDockerWithAnonymousToken({
|
||||
*/
|
||||
export async function fetchUpstreamResponse({
|
||||
authorization,
|
||||
cachePolicy,
|
||||
canUseCache,
|
||||
config,
|
||||
effectivePath,
|
||||
@@ -342,6 +348,7 @@ export async function fetchUpstreamResponse({
|
||||
let responseGeneratedLocally = false;
|
||||
const { fetchOptions, requestHeaders } = createFetchOptions({
|
||||
authorization,
|
||||
cachePolicy,
|
||||
canUseCache,
|
||||
config,
|
||||
request,
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('Authentication Header Forwarding', () => {
|
||||
expect(new Headers(fetchSpy.mock.calls[0][1]?.headers).get('Authorization')).toBe(authToken);
|
||||
expect(cacheDefault.match).not.toHaveBeenCalled();
|
||||
expect(cacheDefault.put).not.toHaveBeenCalled();
|
||||
expect(response.headers.get('Cache-Control')).toBe('no-store');
|
||||
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
|
||||
});
|
||||
|
||||
it('forwards Authorization for gated Hugging Face model downloads', async () => {
|
||||
|
||||
@@ -47,6 +47,27 @@ describe('Cache Privacy', () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(cacheDefault.match).not.toHaveBeenCalled();
|
||||
expect(cacheDefault.put).not.toHaveBeenCalled();
|
||||
expect(fetchStub).toHaveBeenCalled();
|
||||
expect(fetchStub.mock.calls[0][1]?.cf).toBeUndefined();
|
||||
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
|
||||
});
|
||||
|
||||
it('should not enable Cloudflare fetch caching for requests with Cookie', async () => {
|
||||
const request = new Request('https://example.com/gh/test/repo/file.txt', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: 'session=secret'
|
||||
}
|
||||
});
|
||||
|
||||
const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };
|
||||
const response = await worker.fetch(request, {}, ctx);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(cacheDefault.match).not.toHaveBeenCalled();
|
||||
expect(cacheDefault.put).not.toHaveBeenCalled();
|
||||
expect(fetchStub).toHaveBeenCalled();
|
||||
expect(fetchStub.mock.calls[0][1]?.cf).toBeUndefined();
|
||||
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createRequestContext } from '../../src/app/request-context.js';
|
||||
import { CONFIG } from '../../src/config/index.js';
|
||||
import { finalizeResponse } from '../../src/response/finalize-response.js';
|
||||
import { resolveTarget } from '../../src/routing/resolve-target.js';
|
||||
import { tryReadCachedResponse } from '../../src/upstream/cache.js';
|
||||
import { fetchUpstreamResponse } from '../../src/upstream/fetch-upstream.js';
|
||||
import { PerformanceMonitor } from '../../src/utils/performance.js';
|
||||
@@ -112,5 +113,89 @@ describe('Pipeline modules', () => {
|
||||
expect(response.headers.get('Content-Length')).toBe(
|
||||
String(new TextEncoder().encode(body).byteLength)
|
||||
);
|
||||
expect(response.headers.get('Cache-Control')).toBe(
|
||||
'public, max-age=0, s-maxage=60, must-revalidate'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses long-lived caching for immutable package artifacts', async () => {
|
||||
const artifactCases = [
|
||||
{
|
||||
cacheTargetUrl: 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz',
|
||||
effectivePath: '/npm/pkg/-/pkg-1.0.0.tgz',
|
||||
platform: 'npm',
|
||||
requestUrl: 'https://example.com/npm/pkg/-/pkg-1.0.0.tgz'
|
||||
},
|
||||
{
|
||||
cacheTargetUrl:
|
||||
'https://files.pythonhosted.org/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl',
|
||||
effectivePath: '/pypi/files/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl',
|
||||
platform: 'pypi-files',
|
||||
requestUrl:
|
||||
'https://example.com/pypi/files/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl'
|
||||
},
|
||||
{
|
||||
cacheTargetUrl: 'https://repo1.maven.org/maven2/org/example/demo/1.0.0/demo-1.0.0.jar',
|
||||
effectivePath: '/maven/maven2/org/example/demo/1.0.0/demo-1.0.0.jar',
|
||||
platform: 'maven',
|
||||
requestUrl: 'https://example.com/maven/maven2/org/example/demo/1.0.0/demo-1.0.0.jar'
|
||||
}
|
||||
];
|
||||
|
||||
for (const artifactCase of artifactCases) {
|
||||
const request = new Request(artifactCase.requestUrl);
|
||||
const requestContext = createRequestContext(request, {});
|
||||
|
||||
const response = await finalizeResponse({
|
||||
cache: null,
|
||||
cacheTargetUrl: artifactCase.cacheTargetUrl,
|
||||
canUseCache: true,
|
||||
config: CONFIG,
|
||||
ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }),
|
||||
effectivePath: artifactCase.effectivePath,
|
||||
hasSensitiveHeaders: false,
|
||||
monitor: new PerformanceMonitor(),
|
||||
platform: artifactCase.platform,
|
||||
request,
|
||||
requestContext,
|
||||
response: new Response('artifact-data', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': '13'
|
||||
}
|
||||
}),
|
||||
responseGeneratedLocally: false,
|
||||
url: new URL(request.url)
|
||||
});
|
||||
|
||||
expect(response.headers.get('Cache-Control')).toBe(
|
||||
'public, max-age=3600, s-maxage=86400, immutable'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('varies npm metadata cache keys by request origin after rewriting', () => {
|
||||
const targetA = resolveTarget(
|
||||
new URL('https://mirror-a.example/npm/pkg'),
|
||||
'/npm/pkg',
|
||||
CONFIG.PLATFORMS
|
||||
);
|
||||
const targetB = resolveTarget(
|
||||
new URL('https://mirror-b.example/npm/pkg'),
|
||||
'/npm/pkg',
|
||||
CONFIG.PLATFORMS
|
||||
);
|
||||
|
||||
expect('cacheTargetUrl' in targetA && targetA.cacheTargetUrl).toContain(
|
||||
'__xget_origin=https%3A%2F%2Fmirror-a.example'
|
||||
);
|
||||
expect('cacheTargetUrl' in targetB && targetB.cacheTargetUrl).toContain(
|
||||
'__xget_origin=https%3A%2F%2Fmirror-b.example'
|
||||
);
|
||||
expect('cacheTargetUrl' in targetA && 'cacheTargetUrl' in targetB).toBe(true);
|
||||
if ('cacheTargetUrl' in targetA && 'cacheTargetUrl' in targetB) {
|
||||
expect(targetA.cacheTargetUrl).not.toBe(targetB.cacheTargetUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user