perf(cache): add strategy-based cache policy

This commit is contained in:
xixu-me committed 2026-05-05 15:15:58 +08:00
1 parent 79e6069c37
commit 931463ed63
14 files changed
+407 -49

No files matched your search

+3 -3
View File
@@ -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)
+7 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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`
+14
View File
@@ -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
View File
@@ -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 : ['*'],
+22 -13
View File
@@ -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
});
}
+4 -5
View File
@@ -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
};
+219
View File
@@ -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)
};
}
+3
View File
@@ -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 ||
+15 -8
View File
@@ -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,
+1 -1
View File
@@ -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 () => {
+21
View File
@@ -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');
});
+85
View File
@@ -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);
}
});
});