Add fetch timeout and unify logging on stdout

- Abort the Anthropic fire request after FIRE_TIMEOUT_MS instead of
  letting it hang indefinitely with no outcome logged.
- Log info and error levels both to stdout (JSON has a level field
  already) so a single log stream/tail shows the full picture instead
  of splitting across stdout/stderr, which was hiding forward failures
  from anyone only watching one of the two.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 03:08:08 +02:00
parent 6d37579f52
commit 1886e512eb
2 changed files with 16 additions and 3 deletions

View File

@@ -17,3 +17,7 @@ ROUTINE_TOKEN=sk-ant-oat01-changeme
# TTL en milisegundos para el dedup en memoria de entregas de Gitea (X-Gitea-Delivery) # TTL en milisegundos para el dedup en memoria de entregas de Gitea (X-Gitea-Delivery)
DEDUP_TTL_MS=300000 DEDUP_TTL_MS=300000
# Timeout en milisegundos para la llamada a la API de Anthropic (evita que se
# quede colgada indefinidamente si no hay salida a internet)
FIRE_TIMEOUT_MS=10000

View File

@@ -10,6 +10,7 @@ const {
ROUTINE_ID, ROUTINE_ID,
ROUTINE_TOKEN, ROUTINE_TOKEN,
DEDUP_TTL_MS = String(5 * 60 * 1000), DEDUP_TTL_MS = String(5 * 60 * 1000),
FIRE_TIMEOUT_MS = String(10 * 1000),
} = process.env; } = process.env;
for (const [name, value] of Object.entries({ for (const [name, value] of Object.entries({
@@ -29,14 +30,16 @@ const ANTHROPIC_BETA_HEADER = 'experimental-cc-routine-2026-04-01';
const ANTHROPIC_VERSION = '2023-06-01'; const ANTHROPIC_VERSION = '2023-06-01';
const MAX_TEXT_LENGTH = 65536; const MAX_TEXT_LENGTH = 65536;
const dedupTtlMs = Number(DEDUP_TTL_MS); const dedupTtlMs = Number(DEDUP_TTL_MS);
const fireTimeoutMs = Number(FIRE_TIMEOUT_MS);
const app = express(); const app = express();
app.disable('x-powered-by'); app.disable('x-powered-by');
function log(level, fields) { function log(level, fields) {
const line = { level, timestamp: new Date().toISOString(), ...fields }; const line = { level, timestamp: new Date().toISOString(), ...fields };
const out = level === 'error' ? console.error : console.log; // Todo por stdout a propósito: así un solo `tail`/log driver ve info y
out(JSON.stringify(line)); // error juntos, sin depender de que algo separe stdout/stderr aguas abajo.
console.log(JSON.stringify(line));
} }
// Constant-time string comparison. Returns false (without leaking length // Constant-time string comparison. Returns false (without leaking length
@@ -141,6 +144,8 @@ function buildText(event, payload) {
} }
async function fireRoutine(text, deliveryId, event) { async function fireRoutine(text, deliveryId, event) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), fireTimeoutMs);
try { try {
const res = await fetch(ANTHROPIC_FIRE_URL, { const res = await fetch(ANTHROPIC_FIRE_URL, {
method: 'POST', method: 'POST',
@@ -151,6 +156,7 @@ async function fireRoutine(text, deliveryId, event) {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ text }), body: JSON.stringify({ text }),
signal: controller.signal,
}); });
if (res.ok) { if (res.ok) {
@@ -165,7 +171,10 @@ async function fireRoutine(text, deliveryId, event) {
} }
log('error', fields); log('error', fields);
} catch (err) { } catch (err) {
log('error', { deliveryId, event, result: 'fetch_failed', error: err.message }); const reason = err.name === 'AbortError' ? 'timeout' : err.message;
log('error', { deliveryId, event, result: 'fetch_failed', error: reason });
} finally {
clearTimeout(timeout);
} }
} }