Sources hardening (Codex audit): promote-time dedup, postJSON timeout, host-only feed_key
Three follow-ups from Codex's audit of the deep-preview/search/dedup work:
- Promote-time duplicate guard: promote_candidate() now re-checks
find_existing_feed() and raises DuplicateFeedError → 409, so an
old/CLI/direct-DB candidate or a race can't bypass the add-time check and
silently overwrite a live source's settings via upsert. (sources scanned
first, so a real source collision wins over the candidate matching itself.)
- postJSON/putJSON/delJSON gain opt-in {timeout} (AbortController, default
none so other calls are unchanged); deep preview uses 120s and surfaces a
calm "timed out" message instead of pinning the button on "Deep-checking…"
if the LAN model stalls.
- feed_key() now lowercases the host only, not the whole URL — paths/queries
can be case-significant; scheme/www/trailing-slash/host-case still collapse.
Tests: test_candidate_deep_preview_and_dedup extended — promote succeeds once,
then a re-promote of the same candidate is refused 409. 224 pytest + 11 vitest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+34
-18
@@ -10,34 +10,50 @@ export async function getJSON(url, { timeout = 10000 } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function postJSON(url, body) {
|
||||
return sendJSON('POST', url, body);
|
||||
export async function postJSON(url, body, opts) {
|
||||
return sendJSON('POST', url, body, opts);
|
||||
}
|
||||
|
||||
export async function putJSON(url, body) {
|
||||
return sendJSON('PUT', url, body);
|
||||
export async function putJSON(url, body, opts) {
|
||||
return sendJSON('PUT', url, body, opts);
|
||||
}
|
||||
|
||||
export async function delJSON(url) {
|
||||
return sendJSON('DELETE', url);
|
||||
export async function delJSON(url, opts) {
|
||||
return sendJSON('DELETE', url, undefined, opts);
|
||||
}
|
||||
|
||||
async function sendJSON(method, url, body) {
|
||||
// timeout is opt-in (default: none) so normal calls are unchanged, but slow
|
||||
// model-backed actions (e.g. deep preview) can bound the wait and fail calmly
|
||||
// instead of leaving a button stuck if the LAN model hangs.
|
||||
async function sendJSON(method, url, body, { timeout } = {}) {
|
||||
const opts = { method, credentials: 'same-origin' };
|
||||
if (body !== undefined) {
|
||||
opts.headers = { 'Content-Type': 'application/json' };
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const r = await fetch(url, opts);
|
||||
if (!r.ok) {
|
||||
let msg = r.statusText;
|
||||
try {
|
||||
const j = await r.json();
|
||||
if (j && j.detail) msg = j.detail;
|
||||
} catch {
|
||||
/* non-JSON error body */
|
||||
}
|
||||
throw new Error(msg);
|
||||
let timer;
|
||||
if (timeout) {
|
||||
const ctrl = new AbortController();
|
||||
opts.signal = ctrl.signal;
|
||||
timer = setTimeout(() => ctrl.abort(), timeout);
|
||||
}
|
||||
try {
|
||||
const r = await fetch(url, opts);
|
||||
if (!r.ok) {
|
||||
let msg = r.statusText;
|
||||
try {
|
||||
const j = await r.json();
|
||||
if (j && j.detail) msg = j.detail;
|
||||
} catch {
|
||||
/* non-JSON error body */
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return await r.json();
|
||||
} catch (e) {
|
||||
if (e?.name === 'AbortError') throw new Error('Timed out — the server took too long to respond.');
|
||||
throw e;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
return r.json();
|
||||
}
|
||||
|
||||
@@ -286,8 +286,12 @@
|
||||
// model's true acceptance view, not the fast heuristic estimate.
|
||||
async function deepPreview(c) {
|
||||
c._err = ''; c._deep = true;
|
||||
try { Object.assign(c, await postJSON(`/api/admin/candidates/${c.id}/preview?deep=true`), { _deep: false }); }
|
||||
catch (e) { c._err = e?.message || 'Deep preview failed.'; c._deep = false; }
|
||||
try {
|
||||
// ~5-7s/item on the LAN model; bound the wait so a model stall can't pin
|
||||
// the button on "Deep-checking…" forever.
|
||||
const res = await postJSON(`/api/admin/candidates/${c.id}/preview?deep=true`, undefined, { timeout: 120000 });
|
||||
Object.assign(c, res, { _deep: false });
|
||||
} catch (e) { c._err = e?.message || 'Deep preview failed.'; c._deep = false; }
|
||||
}
|
||||
async function promoteCandidate(c) {
|
||||
c._err = '';
|
||||
|
||||
Reference in New Issue
Block a user