fix: reduce upload failures on Cloudflare Workers
Optimize zip validation, lower max upload to 25MB, raise CPU limit, and show clear errors when Cloudflare returns HTML error pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { MAX_UPLOAD_BYTES, MAX_UPLOAD_MB } from "../lib/upload-limits";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE ?? "";
|
||||
|
||||
export type BuildStatus =
|
||||
@@ -36,6 +38,12 @@ function apiUrl(path: string): string {
|
||||
return `${API_BASE}${path}`;
|
||||
}
|
||||
|
||||
export function assertUploadSize(file: File): void {
|
||||
if (file.size > MAX_UPLOAD_BYTES) {
|
||||
throw new Error(`zip 不能超过 ${MAX_UPLOAD_MB}MB,请压缩后重试`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBuild(formData: FormData): Promise<{ id: string }> {
|
||||
let response: Response;
|
||||
try {
|
||||
@@ -49,7 +57,7 @@ export async function createBuild(formData: FormData): Promise<{ id: string }> {
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await readJson(response)) as CreateBuildResponse;
|
||||
const data = (await readResponseBody(response)) as CreateBuildResponse;
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error ?? "上传失败");
|
||||
}
|
||||
@@ -58,7 +66,7 @@ export async function createBuild(formData: FormData): Promise<{ id: string }> {
|
||||
|
||||
export async function fetchBuild(id: string): Promise<Build> {
|
||||
const response = await fetch(apiUrl(`/api/builds/${id}`));
|
||||
const data = (await readJson(response)) as Build;
|
||||
const data = (await readResponseBody(response)) as Build;
|
||||
if (!response.ok) {
|
||||
const errorData = data as Build & ApiErrorResponse;
|
||||
throw new Error(errorData.error ?? "Failed to load build");
|
||||
@@ -68,21 +76,56 @@ export async function fetchBuild(id: string): Promise<Build> {
|
||||
|
||||
export async function fetchBuilds(): Promise<Build[]> {
|
||||
const response = await fetch(apiUrl("/api/builds"));
|
||||
const data = (await readJson(response)) as { builds: Build[] };
|
||||
const data = (await readResponseBody(response)) as { builds: Build[] };
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load builds");
|
||||
}
|
||||
return data.builds;
|
||||
}
|
||||
|
||||
async function readJson(response: Response): Promise<unknown> {
|
||||
function parseCloudflareHtmlError(status: number, text: string): string {
|
||||
const codeMatch = text.match(/cloudflare[^0-9]*(\d{4})/i);
|
||||
const cfCode = codeMatch?.[1];
|
||||
|
||||
if (status === 413) {
|
||||
return `上传体积过大(HTTP 413),请压缩 zip 到 ${MAX_UPLOAD_MB}MB 以内`;
|
||||
}
|
||||
if (status === 502 || status === 503 || status === 524) {
|
||||
return `服务器处理超时或暂时不可用(HTTP ${status})。请缩小 zip 体积后重试,或稍后再试`;
|
||||
}
|
||||
if (cfCode === "1102" || text.includes("1102")) {
|
||||
return "Worker CPU 时间超限(Cloudflare 1102)。请缩小 zip 或升级 Workers 付费套餐";
|
||||
}
|
||||
if (cfCode === "1101" || text.includes("1101")) {
|
||||
return "Worker 运行时错误(Cloudflare 1101)。请检查 zip 是否损坏,或查看部署日志";
|
||||
}
|
||||
|
||||
return `服务器返回了错误页面(HTTP ${status}),通常因 zip 过大或 Worker 超时。请压缩到 ${MAX_UPLOAD_MB}MB 以内后重试`;
|
||||
}
|
||||
|
||||
async function readResponseBody(response: Response): Promise<unknown> {
|
||||
const text = await response.text();
|
||||
if (!text) return {};
|
||||
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
try {
|
||||
return JSON.parse(trimmed) as unknown;
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
|
||||
throw new Error(parseCloudflareHtmlError(response.status, text));
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text) as unknown;
|
||||
return JSON.parse(trimmed) as unknown;
|
||||
} catch {
|
||||
throw new Error(`Unexpected response from server: ${text.slice(0, 200)}`);
|
||||
throw new Error(
|
||||
`服务器返回了无法解析的响应(HTTP ${response.status}):${text.slice(0, 120)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
frontend/src/lib/upload-limits.ts
Normal file
7
frontend/src/lib/upload-limits.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/** 与 wrangler.toml MAX_UPLOAD_MB 保持一致 */
|
||||
export const MAX_UPLOAD_MB = 25;
|
||||
export const MAX_UPLOAD_BYTES = MAX_UPLOAD_MB * 1024 * 1024;
|
||||
|
||||
export function formatMaxUploadLabel(): string {
|
||||
return `${MAX_UPLOAD_MB}MB`;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createBuild } from "../api/client";
|
||||
import { assertUploadSize, createBuild } from "../api/client";
|
||||
import { getDefaultAppVersion } from "../lib/version";
|
||||
import { formatMaxUploadLabel } from "../lib/upload-limits";
|
||||
|
||||
export default function Upload() {
|
||||
const navigate = useNavigate();
|
||||
@@ -32,6 +33,13 @@ export default function Upload() {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
assertUploadSize(file);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "文件过大");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
@@ -67,7 +75,8 @@ export default function Upload() {
|
||||
<section className="doc-section">
|
||||
<h2>使用说明</h2>
|
||||
<p className="prose">
|
||||
压缩包需包含 index.html(根目录或单层文件夹内),默认不超过 50MB。
|
||||
压缩包需包含 index.html(根目录或单层文件夹内),默认不超过{" "}
|
||||
{formatMaxUploadLabel()}。
|
||||
可单独上传应用图标(PNG / JPG / ICO,优先于 zip 内图标),版本号默认取当天日期(如
|
||||
2026.5.29)。中文名用于展示,英文名用于安装包与 Bundle ID。
|
||||
</p>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/layout.tsx","./src/lib/version.ts","./src/pages/buildlist.tsx","./src/pages/jobstatus.tsx","./src/pages/upload.tsx"],"version":"5.6.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/layout.tsx","./src/lib/duration.ts","./src/lib/upload-limits.ts","./src/lib/version.ts","./src/pages/buildlist.tsx","./src/pages/jobstatus.tsx","./src/pages/upload.tsx"],"version":"5.6.3"}
|
||||
Reference in New Issue
Block a user