From e33a11cc630b625e2e2bdd4f966877e431d41bf9 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:19:38 +0000 Subject: [PATCH] Add request timeout and explicit error state to WhatsApp HTTP helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared HTTP helpers (lib/http.js, lib/request.mjs metaProxyRequest) used by every flow/template/data-endpoint script issued a bare fetch with no timeout, so a slow or hung Meta/Platform call left the script spinning forever with no feedback — the agent-tooling analog of the "Sync from Meta" hang. Bound each request with an AbortController (default 30s, override via KAPSO_HTTP_TIMEOUT_MS). On timeout the CJS helper throws a RequestError with status 408 and { timedOut: true }, and the ESM helper returns { ok: false, status: 408, timedOut: true } with a clear message, so callers surface an actionable error instead of hanging. Also sharpen the integrate-whatsapp SKILL troubleshooting: blank/preview-URL flow previews map to a missing data endpoint, scoped listings return empty on a mismatched phone_number_id/WABA, and document the new timeout behavior. paths: plugins/kapso/skills/integrate-whatsapp/scripts/lib/http.js plugins/kapso/skills/integrate-whatsapp/scripts/lib/request.mjs plugins/kapso/skills/integrate-whatsapp/SKILL.md Generated-By: PostHog Code Task-Id: a2dfd3bf-2074-49b4-8ae8-150079840501 --- .../kapso/skills/integrate-whatsapp/SKILL.md | 5 ++- .../integrate-whatsapp/scripts/lib/http.js | 41 ++++++++++++++--- .../scripts/lib/request.mjs | 45 ++++++++++++++++--- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/plugins/kapso/skills/integrate-whatsapp/SKILL.md b/plugins/kapso/skills/integrate-whatsapp/SKILL.md index d38dd09..34874d7 100644 --- a/plugins/kapso/skills/integrate-whatsapp/SKILL.md +++ b/plugins/kapso/skills/integrate-whatsapp/SKILL.md @@ -16,6 +16,7 @@ Env vars: - `KAPSO_API_BASE_URL` (host only, no `/platform/v1`) - `KAPSO_API_KEY` - `META_GRAPH_VERSION` (optional, default `v24.0`) +- `KAPSO_HTTP_TIMEOUT_MS` (optional, default `30000`) - aborts a slow/hung Meta or Platform call with a clear timeout error instead of spinning forever. Increase it for large template/flow syncs over slow links. Auth header (direct API calls): ``` @@ -281,9 +282,11 @@ async function handler(request, env) { ### Troubleshooting -- Preview shows `"flow_token is missing"`: flow is dynamic without a data endpoint. Attach one and refresh. +- Preview shows `"flow_token is missing"` or renders blank with a preview-URL warning: flow is dynamic (`data_api_version` set / `routing_model` present) but has no working data endpoint. Attach and register one (see "Attach a data endpoint"), then refresh. Confirm it is live with `node scripts/get-data-endpoint.js --flow-id `. - Encryption setup errors: enable encryption in Settings for the phone number/WABA. - OAuthException 139000 (Integrity): WABA must be verified in Meta security center. +- `list-flows.js` / `list-templates.mjs` returns nothing where you expect data: confirm you are scoping by the same `phone_number_id` / `business_account_id` (WABA) the flow or template was created under — listings are scoped, so a mismatched ID returns an empty list rather than an error. +- A flow/template sync or listing hangs: requests now abort after `KAPSO_HTTP_TIMEOUT_MS` (default 30s) and return a `408` timeout error (`timedOut: true`) instead of spinning forever. Retry, or raise the timeout for large syncs. ## Scripts diff --git a/plugins/kapso/skills/integrate-whatsapp/scripts/lib/http.js b/plugins/kapso/skills/integrate-whatsapp/scripts/lib/http.js index 9b50ab9..b61c6db 100644 --- a/plugins/kapso/skills/integrate-whatsapp/scripts/lib/http.js +++ b/plugins/kapso/skills/integrate-whatsapp/scripts/lib/http.js @@ -1,5 +1,7 @@ const { getConfig } = require('./env'); +const DEFAULT_TIMEOUT_MS = 30000; + class RequestError extends Error { constructor(message, status, body) { super(message); @@ -8,6 +10,16 @@ class RequestError extends Error { } } +// Resolve the per-request timeout. Without a bound, a slow or hung Meta/Platform +// call leaves the script spinning forever with no feedback. Override with +// KAPSO_HTTP_TIMEOUT_MS (milliseconds); 0 or invalid falls back to the default. +function getTimeoutMs() { + const raw = process.env.KAPSO_HTTP_TIMEOUT_MS; + if (!raw) return DEFAULT_TIMEOUT_MS; + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS; +} + function buildUrl(baseUrl, path, query) { const trimmed = baseUrl.replace(/\/+$/, ''); const safePath = path.startsWith('/') ? path : `/${path}`; @@ -56,11 +68,30 @@ async function request({ baseUrl, path, method, query, body, headers }) { } } - const response = await fetch(url, { - method, - headers: finalHeaders, - body: finalBody - }); + const timeoutMs = getTimeoutMs(); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + let response; + try { + response = await fetch(url, { + method, + headers: finalHeaders, + body: finalBody, + signal: controller.signal + }); + } catch (error) { + if (error && error.name === 'AbortError') { + throw new RequestError( + `Request timed out after ${timeoutMs}ms (set KAPSO_HTTP_TIMEOUT_MS to adjust)`, + 408, + { timedOut: true, url } + ); + } + throw error; + } finally { + clearTimeout(timer); + } const contentType = response.headers.get('content-type') || ''; const text = await response.text(); diff --git a/plugins/kapso/skills/integrate-whatsapp/scripts/lib/request.mjs b/plugins/kapso/skills/integrate-whatsapp/scripts/lib/request.mjs index c52b852..49f9114 100644 --- a/plugins/kapso/skills/integrate-whatsapp/scripts/lib/request.mjs +++ b/plugins/kapso/skills/integrate-whatsapp/scripts/lib/request.mjs @@ -1,5 +1,17 @@ import { metaProxyConfig } from './env.mjs'; +const DEFAULT_TIMEOUT_MS = 30000; + +// Resolve the per-request timeout. Without a bound, a slow or hung Meta call +// leaves the script spinning forever with no feedback. Override with +// KAPSO_HTTP_TIMEOUT_MS (milliseconds); 0 or invalid falls back to the default. +function getTimeoutMs() { + const raw = process.env.KAPSO_HTTP_TIMEOUT_MS; + if (!raw) return DEFAULT_TIMEOUT_MS; + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS; +} + function buildUrl(path, query) { const { baseUrl, graphVersion } = metaProxyConfig(); const cleanedPath = path.replace(/^\/+/, ''); @@ -30,11 +42,34 @@ export async function metaProxyRequest({ method, path, query, headers, body }) { finalHeaders.set('Content-Type', 'application/json'); } - const response = await fetch(url, { - method, - headers: finalHeaders, - body - }); + const timeoutMs = getTimeoutMs(); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + + let response; + try { + response = await fetch(url, { + method, + headers: finalHeaders, + body, + signal: controller.signal + }); + } catch (error) { + if (error && error.name === 'AbortError') { + return { + ok: false, + status: 408, + url: url.toString(), + timedOut: true, + data: { + error: `Request timed out after ${timeoutMs}ms (set KAPSO_HTTP_TIMEOUT_MS to adjust)` + } + }; + } + throw error; + } finally { + clearTimeout(timer); + } const contentType = response.headers.get('content-type') || ''; const text = await response.text();