DEV Community

vectronodeAPI
vectronodeAPI

Posted on

Your API Error Is HTML: Keep the Real Failure Behind an Agent's First Call

An AI Agent can fail before a model sees the prompt. A proxy, wrong route, expired session, or upstream gateway may return HTML or plain text instead of the JSON shape your OpenAI-compatible client expects.

If the code immediately calls response.json(), the useful failure becomes JSONDecodeError, Unexpected token <, or ConvertFrom-Json failed. This article fixes one verifiable problem: preserve the HTTP failure when the response is not JSON.

Disclosure: VectorNode is the gateway used for the configuration example. The response guard is provider-independent.

1. The developer problem

This common pattern hides the evidence:

POST request -> 404 HTML response -> parse as JSON -> parser exception
Enter fullscreen mode Exit fullscreen mode

The parser exception does not tell you whether the route was wrong, authentication failed, or a proxy intercepted the request. The first Agent call should record four safe fields before parsing:

  • HTTP status
  • Content-Type
  • request ID, when present
  • a short, redacted body preview for non-JSON errors

2. Why the response is not JSON

OpenAI compatibility describes an API contract, but network components outside that contract can still produce their own error pages. A missing /v1, an incorrect path, a reverse proxy, or a web application firewall may answer before the API handler runs.

Use the API documentation only to confirm the current base URL, route, model parameter, and request fields. Do not infer those values from a dashboard URL.

The safe order is: read status and headers, read the body once, parse only when appropriate, then validate the expected response shape.

3. Configure the probe

Create an account at https://www.vectronode.com, create a key, and copy a currently available model ID. Keep the values outside source code:

API_KEY=replace-locally
BASE_URL=replace-with-the-documented-v1-base-url
MODEL_DRAFT=replace-with-a-current-model-id
MODEL_REVIEW=replace-with-another-current-model-id
AGENT_ROLE=draft
PROBE_BAD_PATH=0
Enter fullscreen mode Exit fullscreen mode

Set AGENT_ROLE to draft or review to switch models without editing code. Set PROBE_BAD_PATH=1 only for the negative test.

4. Minimal runnable examples

All three examples send the marker AGENT_PROBE_OK. They print structured diagnostics and never print the API key.

Python

Install requests, then run:

import json
import os

import requests

models = {
    "draft": os.environ["MODEL_DRAFT"],
    "review": os.environ["MODEL_REVIEW"],
}
role = os.getenv("AGENT_ROLE", "draft")
if role not in models:
    raise SystemExit("AGENT_ROLE must be draft or review")

base = os.environ["BASE_URL"].rstrip("/")
route = "not-a-real-endpoint" if os.getenv("PROBE_BAD_PATH") == "1" else "chat/completions"
response = requests.post(
    f"{base}/{route}",
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    json={
        "model": models[role],
        "messages": [{"role": "user", "content": "Reply exactly AGENT_PROBE_OK"}],
    },
    timeout=30,
)

content_type = response.headers.get("content-type", "")
request_id = response.headers.get("x-request-id", "unavailable")
try:
    data = response.json() if "json" in content_type.lower() else None
except ValueError:
    data = None

if not response.ok:
    error = {
        "ok": False,
        "status": response.status_code,
        "content_type": content_type or "missing",
        "request_id": request_id,
        "api_error": data.get("error") if isinstance(data, dict) else None,
        "body_preview": None if data else response.text[:160],
    }
    raise SystemExit(json.dumps(error, ensure_ascii=False))

if not isinstance(data, dict):
    raise SystemExit("Successful response was not valid JSON")

text = data["choices"][0]["message"]["content"].strip()
print(json.dumps({"ok": text == "AGENT_PROBE_OK", "role": role,
                  "requested_model": models[role], "request_id": request_id}))
Enter fullscreen mode Exit fullscreen mode

JavaScript

Node.js 18 or newer includes fetch:

const models = {
  draft: process.env.MODEL_DRAFT,
  review: process.env.MODEL_REVIEW,
};
const role = process.env.AGENT_ROLE || "draft";
if (!models[role]) throw new Error("AGENT_ROLE must be draft or review");

const base = process.env.BASE_URL.replace(/\/$/, "");
const route = process.env.PROBE_BAD_PATH === "1"
  ? "not-a-real-endpoint"
  : "chat/completions";

const response = await fetch(`${base}/${route}`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: models[role],
    messages: [{ role: "user", content: "Reply exactly AGENT_PROBE_OK" }],
  }),
});

const contentType = response.headers.get("content-type") || "";
const requestId = response.headers.get("x-request-id") || "unavailable";
const raw = await response.text();
let data = null;
if (contentType.toLowerCase().includes("json")) {
  try { data = JSON.parse(raw); } catch { data = null; }
}

if (!response.ok) {
  console.error(JSON.stringify({
    ok: false,
    status: response.status,
    content_type: contentType || "missing",
    request_id: requestId,
    api_error: data?.error || null,
    body_preview: data ? null : raw.slice(0, 160),
  }));
  process.exit(1);
}
if (!data) throw new Error("Successful response was not valid JSON");

const text = data.choices?.[0]?.message?.content?.trim();
console.log(JSON.stringify({
  ok: text === "AGENT_PROBE_OK",
  role,
  requested_model: models[role],
  request_id: requestId,
}));
Enter fullscreen mode Exit fullscreen mode

PowerShell 7

$models = @{
    draft  = $env:MODEL_DRAFT
    review = $env:MODEL_REVIEW
}
$role = if ($env:AGENT_ROLE) { $env:AGENT_ROLE } else { "draft" }
if (-not $models.ContainsKey($role) -or -not $models[$role]) {
    throw "AGENT_ROLE must be draft or review"
}

$base = $env:BASE_URL.TrimEnd('/')
$route = if ($env:PROBE_BAD_PATH -eq "1") {
    "not-a-real-endpoint"
} else {
    "chat/completions"
}
$body = @{
    model = $models[$role]
    messages = @(@{ role = "user"; content = "Reply exactly AGENT_PROBE_OK" })
} | ConvertTo-Json -Depth 5

$response = Invoke-WebRequest `
    -Uri "$base/$route" `
    -Method Post `
    -Headers @{ Authorization = "Bearer $env:API_KEY" } `
    -ContentType "application/json" `
    -Body $body `
    -TimeoutSec 30 `
    -SkipHttpErrorCheck

$contentType = [string]$response.Headers["Content-Type"]
$requestId = [string]$response.Headers["x-request-id"]
$data = $null
if ($contentType -match "json") {
    try { $data = $response.Content | ConvertFrom-Json -ErrorAction Stop } catch {}
}

if ([int]$response.StatusCode -ge 400) {
    [pscustomobject]@{
        ok = $false
        status = [int]$response.StatusCode
        content_type = $contentType
        request_id = $requestId
        api_error = if ($data) { $data.error } else { $null }
        body_preview = if ($data) { $null } else { $response.Content.Substring(0, [Math]::Min(160, $response.Content.Length)) }
    } | ConvertTo-Json -Depth 5
    exit 1
}
if (-not $data) { throw "Successful response was not valid JSON" }

$text = ([string]$data.choices[0].message.content).Trim()
[pscustomobject]@{
    ok = ($text -eq "AGENT_PROBE_OK")
    role = $role
    requested_model = $models[$role]
    request_id = $requestId
} | ConvertTo-Json
Enter fullscreen mode Exit fullscreen mode

5. Verify both paths

First run with PROBE_BAD_PATH=0. Success should contain:

{"ok":true,"role":"draft","requested_model":"your-model-id","request_id":"..."}
Enter fullscreen mode Exit fullscreen mode

Then set PROBE_BAD_PATH=1. The command should exit unsuccessfully but print a structured record containing status, content_type, and request_id. A JSON API error may populate api_error; an HTML or text response should populate only body_preview.

The negative test passes when no JSON parser stack trace replaces the HTTP evidence.

6. Common errors

Symptom Check
401 or 403 Key validity and authorization header; do not print the key
404 Base URL, /v1, and route spelling
HTML with 200 Proxy or login page intercepted the API route
JSON content type but invalid JSON Truncated proxy response or upstream fault
Valid JSON without choices Wrong endpoint or a different response schema
One model works, one fails Model availability for the account, not the parser

Do not retry every failure. A wrong route, invalid key, or unavailable model requires configuration changes, not repeated traffic.

7. Security and scope

Store keys in environment variables or a secret manager. Redact authorization headers and limit body previews because an error page may reflect request data. Keep request IDs: they are useful for support without exposing credentials.

This guard improves diagnostics at the HTTP boundary. It does not validate model quality, tool-call safety, rate-limit policy, or the semantic equivalence of different models.

Try the example

Use the tested Python, JavaScript, and PowerShell examples in the VectorNode API Quickstart:

https://github.com/Vector-Compute-Engine/api-quickstart

Never publish your complete API key in source code, screenshots, videos, or public issues.

Top comments (0)