Mintplex-Labs/anything-llm · error · Error

${res.status} - ${res.statusText}

Error message

${res.status} - ${res.statusText}

What it means

After POSTing the search query to the Keenable endpoint, the plugin checks res.ok; any non-2xx HTTP response causes it to throw an Error formatted as '<status> - <statusText>' (e.g. '401 - Unauthorized'). The thrown message is caught locally and returned to the agent as 'There was an error searching for content. <status> - <statusText>'. It is a thin wrapper that surfaces the upstream Keenable API's rejection of the request.

Solutions

  1. Check the numeric status in the message: 401/403 means fix AGENT_KEENABLE_API_KEY (correct, non-expired key for the /v1/search endpoint); 404 means AGENT_KEENABLE_API_URL points to a host that doesn't serve the Keenable search routes; 429 means back off/retry later; 5xx means the upstream service is down.
  2. If you don't need the keyed endpoint, unset AGENT_KEENABLE_API_KEY so the plugin falls back to the keyless /v1/search/public route.
  3. Curl the endpoint directly to reproduce: POST {baseUrl}/v1/search (or /v1/search/public) with the same headers and body {"query":"test","mode":"pro"} and inspect the response body for details the wrapper discards.
  4. If running a self-hosted proxy, verify it proxies POST /v1/search and /v1/search/public to Keenable unchanged, including the X-API-Key header.
  5. Retry the search after addressing the cause — the plugin already catches the throw and degrades to an error string, so no crash occurs.

Example fix

// before (diagnose the raw status)
throw new Error(`${res.status} - ${res.statusText}`);
// after (surface upstream detail)
const body = await res.text();
throw new Error(`${res.status} - ${res.statusText}: ${body.slice(0, 200)}`);
Defensive patterns

Strategy: retry

Validate before calling

async function keenableReachable(baseUrl, apiKey) {
  const path = apiKey ? "/v1/search" : "/v1/search/public";
  const headers = { "Content-Type": "application/json" };
  if (apiKey) headers["X-API-Key"] = apiKey;
  const res = await fetch(`${baseUrl}${path}`, {
    method: "POST",
    headers,
    body: JSON.stringify({ query: "ping", mode: "pro" }),
  });
  if (!res.ok) throw new Error(`Keenable endpoint unhealthy: ${res.status} - ${res.statusText}`);
  return true;
}

Type guard

function isOkResponse(res) {
  return res instanceof Response && res.ok && res.status >= 200 && res.status < 300;
}

Try / catch

const MAX_RETRIES = 3;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
  try {
    return await agent._keenableSearch(query);
  } catch (e) {
    const status = parseInt(e.message, 10);
    const retryable = status === 429 || status >= 500;
    if (!retryable || attempt === MAX_RETRIES) {
      console.error(`Keenable search failed: ${e.message}`);
      return null;
    }
    await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
  }
}

Prevention

When it happens

Trigger: The fetch to `${baseUrl}${path}` (either /v1/search with X-API-Key when AGENT_KEENABLE_API_KEY is set, or /v1/search/public) returns a non-2xx status: 401/403 for an invalid or missing API key on the keyed endpoint, 404 when AGENT_KEENABLE_API_URL points to a host without the search route, 429 on rate limiting, or 5xx from an outage/misconfigured proxy.

Common situations: Expired or wrong AGENT_KEENABLE_API_KEY, self-hosted proxy at AGENT_KEENABLE_API_URL that doesn't implement /v1/search or /v1/search/public, rate-limit throttling under heavy agent traffic, or Keenable service downtime returning 502/503.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of Mintplex-Labs/anything-llm@a145d4d87d (2026-09-15). Data as JSON: /api/errors/c7e9c704901d9372. Report an issue: GitHub.

Appendix: source

Thrown at server/utils/agents/aibitat/plugins/web-browsing.js:1402

            const headers = {
              "Content-Type": "application/json",
              "User-Agent": "keenable-anythingllm",
              "X-Keenable-Title": getAnythingLLMUserAgent(),
            };

            // Keyless public endpoint by default; keyed endpoint + X-API-Key
            // when a key is configured.
            const path = apiKey ? "/v1/search" : "/v1/search/public";
            if (apiKey) headers["X-API-Key"] = apiKey;

            const { response, error } = await fetch(`${baseUrl}${path}`, {
              method: "POST",
              headers,
              body: JSON.stringify({ query: String(query), mode: "pro" }),
            })
              .then((res) => {
                if (res.ok) return res.json();
                throw new Error(`${res.status} - ${res.statusText}`);
              })
              .then((data) => {
                return { response: data, error: null };
              })
              .catch((e) => {
                this.super.handlerProps.log(
                  `Keenable Search Error: ${e.message}`
                );
                return { response: null, error: e.message };
              });
            if (error)
              return `There was an error searching for content. ${error}`;

            const data = [];
            response.results?.forEach((searchResult) => {
              const { title, url, description, snippet } = searchResult;
              // Keenable returns both fields: `snippet` carries the page text and
              // `description` is the page's meta description, which is empty for

View on GitHub (pinned to a145d4d87d)