mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
fix(admin): improve Cloudflare limit error messages
This commit is contained in:
+40
-14
@@ -1349,11 +1349,16 @@ app.get("/", async (c) => {
|
||||
body: JSON.stringify({ feedIds: batch }),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
let data = {};
|
||||
if (window.parseJsonResponseOrThrow) {
|
||||
data = await window.parseJsonResponseOrThrow(res, { prefix: 'Bulk feed delete failed' });
|
||||
} else {
|
||||
data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const message = data && data.error ? String(data.error) : ('Request failed (' + res.status + ')');
|
||||
const message = data && data.error ? String(data.error) : ('Bulk feed delete failed (HTTP ' + res.status + ')');
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const deletedIds = Array.isArray(data.deletedFeedIds) ? data.deletedFeedIds : batch;
|
||||
const failedIds = Array.isArray(data.failedFeedIds) ? data.failedFeedIds : [];
|
||||
@@ -1376,7 +1381,7 @@ app.get("/", async (c) => {
|
||||
if (failed.length > 0) {
|
||||
if (window.showToast) {
|
||||
window.showToast(
|
||||
'Deleted ' + deletedTotal + ' feed(s). ' + failed.length + ' failed (still visible).',
|
||||
'Deleted ' + deletedTotal + ' feed(s). ' + failed.length + ' failed (still visible). Try again; Cloudflare limits can cause temporary failures.',
|
||||
{ type: 'error', duration: 6500 },
|
||||
);
|
||||
}
|
||||
@@ -1386,7 +1391,7 @@ app.get("/", async (c) => {
|
||||
} catch (error) {
|
||||
if (toast && toast.dismiss) toast.dismiss();
|
||||
if (window.showToast) {
|
||||
window.showToast('Bulk delete failed: ' + (error && error.message ? error.message : 'Unknown error'), { type: 'error', duration: 7000 });
|
||||
window.showToast((error && error.message) ? error.message : 'Bulk feed delete failed.', { type: 'error', duration: 7500 });
|
||||
}
|
||||
} finally {
|
||||
FEED_BULK_DELETE_IN_PROGRESS = false;
|
||||
@@ -1757,13 +1762,12 @@ app.post("/feeds/:feedId/purge", async (c) => {
|
||||
app.post("/feeds/bulk-delete", async (c) => {
|
||||
const env = c.env as unknown as Env;
|
||||
const emailStorage = env.EMAIL_STORAGE;
|
||||
|
||||
try {
|
||||
const contentType = c.req.header("Content-Type") || "";
|
||||
const wantsJson =
|
||||
contentType.includes("application/json") ||
|
||||
(c.req.header("Accept") || "").includes("application/json");
|
||||
|
||||
try {
|
||||
if (wantsJson) {
|
||||
const body = (await c.req.json().catch(() => null)) as {
|
||||
feedIds?: unknown;
|
||||
@@ -1868,7 +1872,16 @@ app.post("/feeds/bulk-delete", async (c) => {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error bulk deleting feeds:", error);
|
||||
return c.text("Error bulk deleting feeds. Please try again.", 400);
|
||||
return wantsJson
|
||||
? c.json(
|
||||
{
|
||||
ok: false,
|
||||
error:
|
||||
"Bulk feed delete failed. This can happen if Cloudflare is rate-limiting requests or if the Worker hit a plan quota. Please try again.",
|
||||
},
|
||||
500,
|
||||
)
|
||||
: c.text("Error bulk deleting feeds. Please try again.", 500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2674,11 +2687,16 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
body: JSON.stringify({ emailKeys: batch }),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
let data = {};
|
||||
if (window.parseJsonResponseOrThrow) {
|
||||
data = await window.parseJsonResponseOrThrow(res, { prefix: 'Bulk email delete failed' });
|
||||
} else {
|
||||
data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const message = data && data.error ? String(data.error) : ('Request failed (' + res.status + ')');
|
||||
const message = data && data.error ? String(data.error) : ('Bulk email delete failed (HTTP ' + res.status + ')');
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
const deletedKeys = Array.isArray(data.deletedEmailKeys) ? data.deletedEmailKeys : batch;
|
||||
const failedKeys = Array.isArray(data.failedEmailKeys) ? data.failedEmailKeys : [];
|
||||
@@ -2700,7 +2718,7 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
if (failed.length > 0) {
|
||||
if (window.showToast) {
|
||||
window.showToast(
|
||||
'Deleted ' + deletedTotal + ' email(s). ' + failed.length + ' failed (still visible).',
|
||||
'Deleted ' + deletedTotal + ' email(s). ' + failed.length + ' failed (still visible). Try again; Cloudflare limits can cause temporary failures.',
|
||||
{ type: 'error', duration: 6500 },
|
||||
);
|
||||
}
|
||||
@@ -2710,7 +2728,7 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
} catch (error) {
|
||||
if (toast && toast.dismiss) toast.dismiss();
|
||||
if (window.showToast) {
|
||||
window.showToast('Bulk delete failed: ' + (error && error.message ? error.message : 'Unknown error'), { type: 'error', duration: 7000 });
|
||||
window.showToast((error && error.message) ? error.message : 'Bulk email delete failed.', { type: 'error', duration: 7500 });
|
||||
}
|
||||
} finally {
|
||||
EMAIL_BULK_DELETE_IN_PROGRESS = false;
|
||||
@@ -3146,13 +3164,12 @@ app.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
|
||||
const env = c.env as unknown as Env;
|
||||
const emailStorage = env.EMAIL_STORAGE;
|
||||
const feedId = c.req.param("feedId");
|
||||
|
||||
try {
|
||||
const contentType = c.req.header("Content-Type") || "";
|
||||
const wantsJson =
|
||||
contentType.includes("application/json") ||
|
||||
(c.req.header("Accept") || "").includes("application/json");
|
||||
|
||||
try {
|
||||
const feedMetadataKey = `feed:${feedId}:metadata`;
|
||||
const feedMetadata = (await emailStorage.get(feedMetadataKey, {
|
||||
type: "json",
|
||||
@@ -3236,7 +3253,16 @@ app.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error bulk deleting emails:", error);
|
||||
return c.text("Error bulk deleting emails. Please try again.", 400);
|
||||
return wantsJson
|
||||
? c.json(
|
||||
{
|
||||
ok: false,
|
||||
error:
|
||||
"Bulk email delete failed. This can happen if Cloudflare is rate-limiting requests or if the Worker hit a plan quota. Please try again.",
|
||||
},
|
||||
500,
|
||||
)
|
||||
: c.text("Error bulk deleting emails. Please try again.", 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// Helpers for turning failed fetch() responses into actionable, user-friendly
|
||||
// error messages (especially for Cloudflare quota / rate-limit pages).
|
||||
|
||||
export const httpErrorScripts = `
|
||||
(function () {
|
||||
function extractRayIdFromText(text) {
|
||||
const match = String(text || '').match(/ray id\\s*[:#]?\\s*([a-z0-9-]+)/i);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
function compact(text, maxLen) {
|
||||
const str = String(text || '');
|
||||
if (str.length <= maxLen) return str;
|
||||
return str.slice(0, Math.max(0, maxLen - 3)) + '...';
|
||||
}
|
||||
|
||||
function classifyCloudflareError(status, text) {
|
||||
const lower = String(text || '').toLowerCase();
|
||||
const looksLikeCloudflare =
|
||||
lower.includes('cloudflare') ||
|
||||
lower.includes('ray id') ||
|
||||
lower.includes('cf-ray');
|
||||
|
||||
if (!looksLikeCloudflare) return null;
|
||||
|
||||
const isRateLimited =
|
||||
status === 429 ||
|
||||
lower.includes('error 1015') ||
|
||||
lower.includes('rate limited') ||
|
||||
lower.includes('you are being rate limited');
|
||||
|
||||
const isQuota =
|
||||
lower.includes('worker') &&
|
||||
(lower.includes('exceeded') ||
|
||||
lower.includes('quota') ||
|
||||
lower.includes('limit') ||
|
||||
lower.includes('requests'));
|
||||
|
||||
const isBlocked =
|
||||
status === 403 &&
|
||||
(lower.includes('access denied') || lower.includes('forbidden'));
|
||||
|
||||
if (isRateLimited) return { kind: 'rate_limit', label: 'Cloudflare rate limit' };
|
||||
if (isQuota) return { kind: 'quota', label: 'Cloudflare plan limit' };
|
||||
if (isBlocked) return { kind: 'blocked', label: 'Cloudflare security block' };
|
||||
|
||||
return { kind: 'cloudflare', label: 'Cloudflare error page' };
|
||||
}
|
||||
|
||||
function buildHelpfulErrorMessage(prefix, status, headers, text, json) {
|
||||
const safePrefix = prefix ? String(prefix) : 'Request failed';
|
||||
const cfRayHeader = headers && headers.get ? (headers.get('cf-ray') || '') : '';
|
||||
const retryAfter = headers && headers.get ? (headers.get('retry-after') || '') : '';
|
||||
|
||||
// Prefer our own API's structured error first.
|
||||
let apiError = '';
|
||||
if (json && typeof json === 'object') {
|
||||
if (typeof json.error === 'string') apiError = json.error;
|
||||
else if (json.error && typeof json.error.message === 'string') apiError = json.error.message;
|
||||
}
|
||||
|
||||
if (apiError) {
|
||||
const parts = [safePrefix + ': ' + apiError, '(HTTP ' + status + ')'];
|
||||
if (cfRayHeader) parts.push('cf-ray ' + cfRayHeader);
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
const cf = classifyCloudflareError(status, text);
|
||||
if (cf) {
|
||||
const ray = cfRayHeader || extractRayIdFromText(text);
|
||||
const base = safePrefix + ': ' + cf.label + ' (HTTP ' + status + ').';
|
||||
let hint = '';
|
||||
if (cf.kind === 'rate_limit') {
|
||||
hint = 'Try again in a bit, or delete smaller batches.';
|
||||
} else if (cf.kind === 'quota') {
|
||||
hint = 'It looks like you hit a plan quota/limit. Try again later or check Cloudflare usage.';
|
||||
} else if (cf.kind === 'blocked') {
|
||||
hint = 'Cloudflare blocked the request. Check WAF/rules and logs.';
|
||||
} else {
|
||||
hint = 'Please try again; if it persists, check Cloudflare logs/usage.';
|
||||
}
|
||||
|
||||
const extras = [];
|
||||
if (retryAfter) extras.push('retry-after ' + retryAfter + 's');
|
||||
if (ray) extras.push('cf-ray ' + ray);
|
||||
const extra = extras.length ? ' (' + extras.join(', ') + ')' : '';
|
||||
return base + ' ' + hint + extra;
|
||||
}
|
||||
|
||||
const snippet = compact(String(text || '').replace(/\\s+/g, ' ').trim(), 140);
|
||||
const fallbackParts = [safePrefix + ' (HTTP ' + status + ')'];
|
||||
if (snippet) fallbackParts.push('- ' + snippet);
|
||||
if (cfRayHeader) fallbackParts.push('(cf-ray ' + cfRayHeader + ')');
|
||||
return fallbackParts.join(' ');
|
||||
}
|
||||
|
||||
async function parseJsonResponseOrThrow(res, opts) {
|
||||
const options = opts || {};
|
||||
const prefix = options.prefix ? String(options.prefix) : 'Request failed';
|
||||
|
||||
const contentType = String(res.headers.get('content-type') || '').toLowerCase();
|
||||
let text = '';
|
||||
try {
|
||||
text = await res.text();
|
||||
} catch (e) {
|
||||
text = '';
|
||||
}
|
||||
|
||||
const trimmed = String(text || '').trim();
|
||||
let json = null;
|
||||
if (
|
||||
trimmed &&
|
||||
(contentType.includes('application/json') ||
|
||||
trimmed.startsWith('{') ||
|
||||
trimmed.startsWith('['))
|
||||
) {
|
||||
try {
|
||||
json = JSON.parse(trimmed);
|
||||
} catch (e) {
|
||||
json = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(buildHelpfulErrorMessage(prefix, res.status, res.headers, text, json));
|
||||
}
|
||||
|
||||
if (json !== null) return json;
|
||||
if (options && options.allowText) return { text: text };
|
||||
throw new Error(prefix + ': Unexpected response format (HTTP ' + res.status + ').');
|
||||
}
|
||||
|
||||
// Expose globally for inline route scripts.
|
||||
window.parseJsonResponseOrThrow = parseJsonResponseOrThrow;
|
||||
})();
|
||||
`;
|
||||
@@ -5,10 +5,12 @@ import { modalScripts, emailViewScripts, initScripts } from "./interactions";
|
||||
import { clipboardScripts } from "./clipboard";
|
||||
import { authHelpers } from "./auth";
|
||||
import { toastScripts } from "./toast";
|
||||
import { httpErrorScripts } from "./httpErrors";
|
||||
|
||||
// Combine all scripts into a single JavaScript string
|
||||
export const interactiveScripts = `
|
||||
${toastScripts}
|
||||
${httpErrorScripts}
|
||||
${modalScripts}
|
||||
${emailViewScripts}
|
||||
${clipboardScripts}
|
||||
@@ -23,4 +25,5 @@ export {
|
||||
clipboardScripts,
|
||||
authHelpers,
|
||||
toastScripts,
|
||||
httpErrorScripts,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user