Troubleshooting the Data APIs¶
Most first-time failures come down to one of two things: a surface that is still disabled, or a filter written in ANSI SQL instead of AMI expression syntax. This page lists the errors you are likely to see and what each means.
The "it's disabled" traps¶
| Symptom | Cause | Fix |
|---|---|---|
/3forge_rest/query returns 404 with Endpoint not found: /query and a list of the loaded endpoints, but /3forge_rest/stats works |
The QUERY endpoint is not loaded (shipped default *,!QUERY,!ADMIN) |
Add QUERY to ami.rest.plugin.classes and restart. See Configuration. |
| WebSocket connection is refused / port not listening | The WebSocket server never started | Set ami.websocket.port and restart. The log will confirm. |
Log says WebSocket server disabled (no ami.websocket.port configured) |
ami.websocket.port is unset |
Set it in local.properties. |
Response body: Rest not enabled on this port |
The REST server is not bound to the port you called | Check ami.rest.uses.web.port / your standalone REST port. |
REST errors¶
| Message / status | Meaning | Fix |
|---|---|---|
cmd required |
The /query request had no cmd parameter |
Supply cmd=<command>. |
unknown center: <db> (existing centers: …) |
The db parameter names a Center that isn't running |
Use one of the listed names, or omit db for center. |
display must be either: text, pipe, jsonMaps, json, jsonRows |
The display value is wrong. It is case-sensitive: jsonmaps fails, jsonMaps works |
Use one of the five exact spellings. |
HTTP 400 with a plain-text or HTML body and no status field |
A request error (missing cmd, unknown db, bad display): a different shape from statement errors |
Check the HTTP status before calling resp.json(); see REST API → Four kinds of error. |
HTTP 500 with REQUEST TIMEOUT ERROR or UNKNOWN ERROR FROM AMI REST PLUGIN |
The request timed out on the server or failed unexpectedly; the body is plain text, with a stack trace unless ami.rest.show.errors=false |
Check the HTTP status before parsing; for a timeout, narrow the query or check the Center's load. |
error: {message, atLine, atChar} in a JSON response (HTTP 200) |
The command reached the Center but failed; atLine/atChar are 0-based |
Fix the SQL at the reported position. |
rowsAffected:0 on a write you expected to change a row |
rowsAffected counts changed rows, not matched: an upsert over an existing key, and a no-op or zero-match UPDATE, all report 0 |
Use status == "ok" for success, not rowsAffected. |
A table exists but SELECT returns no rows after a restart |
Rows are not persisted without a PersistEngine; the table definition survives a restart, the data does not |
Reload the data, or create the table with a PersistEngine. See Concepts. |
HTTP 401 with WWW-Authenticate: Basic |
Not authenticated | Send Basic credentials (curl -u user:pass) or a valid session cookie. |
HTTP 403 |
Authenticated but disabled, locked, or over a limit | Check the response body text: SERVICE_DISABLED, ACCOUNT_LOCKED, or USER_COUNT_EXCEEDED. |
The limit behind USER_COUNT_EXCEEDED is a licensed cap on distinct (host, username) pairs that have logged in through the Web, JDBC, or the console, not a per-request or per-second rate. The count is cumulative since the process started: a pair is not released when its session ends. REST calls that authenticate with Basic credentials are not counted. Check the current count against the cap with SELECT UniqueUsers, MaxUsers FROM __STATS. The cap comes from the license (USERS= in the license string); an unlicensed instance defaults to 1000. There is no property to raise it; it requires a new license.
WebSocket errors¶
| Symptom | Meaning | Fix |
|---|---|---|
| Connection is refused or times out before it opens | The WebSocket server is not enabled (no ami.websocket.port), or the host/port is wrong. There is no client-facing "is it up" endpoint |
A refused port means the server never started: set ami.websocket.port and restart. This is distinct from a connection that opens then closes with unknown component (server up, wrong path). |
| First connect after a process restart hangs ~2 s, then works | First-connection warm-up | Give the initial connect a generous timeout; don't treat a couple of seconds of silence as failure or reconnect-loop on it. |
Connection closes with unknown component: X (existing: ...) |
The URL path names a Center/Gateway that isn't registered | Use a listed name; confirm that Center is running. |
Subscribe returns an error frame starting bad request: |
The frame itself was rejected: an unknown action, or sql that is not a SELECT |
Send "action":"subscribe" with a SELECT. Writes are not possible here; send them over REST. |
Subscribe returns subscribe failed: authentication required |
The path names a Gateway that authenticates subscribers (the default), and the connection carried no credentials | Connect with credentials in the URL: wss://user:pass@host:3410/<gateway>. A browser sends them only if ami.websocket.auth.plugin.class names an authentication plugin; with DISABLED it never does. |
An error frame whose message starts upstream: |
The upstream Center or Gateway reported an error, such as a Gateway refusing the forwarded login | Read the rest of the message; for a refused login, check the user against the Gateway's authentication source. |
subscribed and snapshot arrive, then no rows ever |
The table does not exist, or is not PUBLIC. An unknown table is acknowledged, not rejected |
Check the name with SHOW TABLES over REST, and confirm the table is PUBLIC. |
| A column is missing from every row you receive | That column does not exist on the table. Unknown columns are dropped silently | Check the column names against the table definition. |
| The filter has no effect: every row arrives, including rows it should exclude, and then no updates ever follow | The WHERE is not a boolean expression, usually because it was written in ANSI SQL with a single =. A WHERE the Center cannot evaluate is not reported: the initial load is unfiltered and the subscription receives no further changes |
Use == and double-quoted strings: SYM == "IBM", escaped \" inside the JSON. |
| A filtered row never disappears after it should leave the filter | Focus filtering is off | Subscribe with oof:true to get delete when a row stops matching. |
Connection closes with status 1009 and reason message too large right after sending a large message |
An inbound message exceeded the size limit: 1 MB for a single frame, or ami.websocket.max.message.bytes (default 1 MB) for a message split across frames |
Send smaller messages. |
wss:// closes immediately with status 1006 from a browser |
The browser doesn't trust the server certificate, or client-cert mTLS is on with no client cert | Import/trust the server cert (or front with a proxy); for browser access leave the authorized-clients file unset. |
| Browser can't authenticate on the handshake | Browsers can't set the Authorization header |
Put credentials in the URL (wss://user:pass@host:3410/center); the browser sends them after the server's 401 challenge. Or run with auth DISABLED behind a proxy. |
A subscription is acknowledged even when it cannot work
The server checks only that action is known and that sql is a SELECT. An unknown table, an unknown column, and a WHERE it cannot evaluate are all accepted, with no error frame. A subscribed acknowledgement means your frame parsed, not that your subscription matches anything.
This matters most where the filter is the condition being watched, as in the limit-breach alert: an ANSI WHERE breached = true loads every limit, breached or not, and then never reports a change, so the monitor raises alerts that are not real and misses the ones that are. Test a filtered subscription against a row it should exclude, not only against one it should include.