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
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
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}))
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,
}));
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
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":"..."}
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)