将已下载附件的副本保存到 R2,同时将原始响应返回给用户。此脚本捕获最大 25 MiB 的完整文件,并跳过更大的文件。它使用 Content-Disposition 检测附件。
在 Worker 项目中,将 @cloudflare/zt-types ↗ 2026.10.1 或更高版本安装为开发依赖项:
npm i -D @cloudflare/zt-typesyarn add -D @cloudflare/zt-typespnpm add -D @cloudflare/zt-typesbun add -d @cloudflare/zt-types保留 src/index.ts 中 GatewayMiddlewareContext 和 GatewayMiddlewareNext 的 import type 声明。它们描述 Gateway 上下文和延续函数,并会在 TypeScript 编译时被移除。从 cloudflare:workers 导入运行时 WorkerEntrypoint 类。
创建一个名为 gateway-downloads 的私有 R2 存储桶。将此绑定添加到 Worker 配置:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"r2_buckets": [
{
"binding": "CAPTURES",
"bucket_name": "gateway-downloads"
}
]
}[[r2_buckets]]
binding = "CAPTURES"
bucket_name = "gateway-downloads"将此脚本用作 src/index.ts:
import { WorkerEntrypoint } from "cloudflare:workers";
const LIMIT = 25 * 1024 * 1024;
async function readBounded(body) {
const reader = body.getReader();
const chunks = [];
let size = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > LIMIT) {
void reader.cancel().catch(() => {});
return null;
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return bytes.buffer;
}
export class GatewayMiddleware extends WorkerEntrypoint {
async handle(request, context, next) {
const response = await next(request);
const disposition = response.headers.get("content-disposition") ?? "";
if (
request.method === "GET" &&
response.status === 200 &&
!response.headers.has("content-encoding") &&
/^attachment\b/i.test(disposition) &&
response.body
) {
const copy = response.clone();
this.ctx.waitUntil(
this.capture(copy).catch(() =>
console.error("Download capture failed"),
),
);
}
return response;
}
async capture(response) {
const bytes = await readBounded(response.body);
if (bytes === null) {
console.log("Download capture skipped: file exceeds limit");
return;
}
await this.env.CAPTURES.put(crypto.randomUUID(), bytes, {
httpMetadata: { contentType: "application/octet-stream" },
});
}
}import { WorkerEntrypoint } from "cloudflare:workers";
import type {
GatewayMiddlewareContext,
GatewayMiddlewareNext,
} from "@cloudflare/zt-types";
interface Env {
CAPTURES: R2Bucket;
}
const LIMIT = 25 * 1024 * 1024;
async function readBounded(
body: ReadableStream<Uint8Array>,
): Promise<ArrayBuffer | null> {
const reader = body.getReader();
const chunks: Uint8Array[] = [];
let size = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > LIMIT) {
void reader.cancel().catch(() => {});
return null;
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return bytes.buffer;
}
export class GatewayMiddleware extends WorkerEntrypoint<Env> {
async handle(
request: Request,
context: GatewayMiddlewareContext,
next: GatewayMiddlewareNext,
): Promise<Response> {
const response = await next(request);
const disposition = response.headers.get("content-disposition") ?? "";
if (
request.method === "GET" &&
response.status === 200 &&
!response.headers.has("content-encoding") &&
/^attachment\b/i.test(disposition) &&
response.body
) {
const copy = response.clone();
this.ctx.waitUntil(
this.capture(copy).catch(() =>
console.error("Download capture failed"),
),
);
}
return response;
}
async capture(response: Response): Promise<void> {
const bytes = await readBounded(response.body!);
if (bytes === null) {
console.log("Download capture skipped: file exceeds limit");
return;
}
await this.env.CAPTURES.put(crypto.randomUUID(), bytes, {
httpMetadata: { contentType: "application/octet-stream" },
});
}
}读取器统计实际流式传输的字节数,而不是信任 Content-Length。它向 R2 提供大小已知的 ArrayBuffer,而不是任意流。随机对象键避免将不受信任的文件名用作存储路径。
部署 Worker,并为受控的下载主机和路径创建 Forward 策略,HTTP Method 为 GET,并限定到测试用户组。
通过 Gateway 下载小型附件。确认浏览器下载和 R2 对象的内容相同。下载大于 25 MiB 的文件,并确认用户仍能收到该文件,同时 Worker 记录捕获被跳过。
当两个消费者以不同速度运行时,克隆可能会缓冲数据。捕获还会为有界文件分配内存。请保持策略范围较窄,避免大量并行下载,并针对 Workers 内存限制进行测试。缓慢传输或失败的后台写入可能导致没有捕获到对象。当必须保证保留时,请使用专用捕获流水线。
此示例捕获状态为 200 的完整、未压缩附件响应。它跳过范围响应和带有 Content-Encoding 的正文。副本在 Gateway 的最终响应检查之前获取,因此稍后被 Gateway 拦截的文件仍可能存储在 R2 中。保持存储桶为私有,应用保留规则,并将捕获的对象视为不受信任的文件。