Skip to content
DezfilDocsv0.1.0

Errors

Read the error answer a failed API call returns, and know what to do about the codes a script or an agent meets most often.

A failed request answers with an HTTP error status and a JSON body that names what went wrong. Try it with a query on a dataset that does not exist:

curl

Terminal
curl -s -w "\n%{http_code}\n" http://localhost:3000/api/query \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT * FROM no_such_dataset"}'
  • -w "\n%{http_code}\n" prints the status after the body.
  • <YOUR_TOKEN> is your API token. The API quickstart shows how to create one.
Output
{"error":{"code":"sql_error","message":"The query refers to an unknown table.","position":{"line":1,"column":15}}}
400

Every error answer an operation gives holds one error object. These two fields are always in it:

FieldWhat it holds
codeA stable name for what went wrong, such as sql_error.
messageA sentence for a person. It often names the detail of this request, such as a dataset, a limit or the setting that changes it.

Branch on code, not on the message or the status. One status covers many codes: 400 answers both sql_error and invalid_request.

Some codes add a field beside the two:

FieldComes withWhat it holds
positionsql_error, when the engine reports whereThe line and column of the problem in your SQL, counted from 1.
retryAfterrate_limited from sign-in or a password check, and authentication_busyThe seconds to wait before you try again. The Retry-After header carries the same number.
retryAtrate_limited from sign-in or a password checkThe time you may try again.
rulerule_refusedThe instance rule that refused you: tokens.create, publishing.publish, connections.create or imports.links.
settingbasemap_not_offeredThe basemap switch that refused it, such as basemaps.satellite.
issuesgeometry_choice_invalid, and invalid_request for a tool’s arguments over MCPEach reason the request fails: a code and a message for a geometry choice, a message and the argument’s path for a tool call.

Find the code in these tables, then do what its row says. The error catalogue lists every other code with its meaning, and Limits gives the code each limit answers with.

Agents can read the same list from the API description: x-error-codes maps each code to its meaning.

CodeStatusWhat happenedWhat to do
authentication_required401The request has no token, or one Dezfil doesn’t accept: mistyped, revoked, or its account deactivated.Send the header Authorization: Bearer <YOUR_TOKEN>. If it still fails, create a new token.
token_expired401The token reached the expiry it was created with.Create a new token and use it instead.
forbidden403The operation is for admins, you can view the item but not change it, or a request signed in with the session cookie lacks the instance’s Origin. Nothing changed.Use an admin’s token for admin operations. For an item you can’t change, see Who may do what; for the Origin, Sign in.
rule_refused403An instance rule leaves you out, such as Who may publish. The rule field names it. Nothing changed.Ask an admin to change the rule in Settings.
not_found404, or 410 for an expired query downloadThe item does not exist, or your account cannot reach it. Both answer the same.Check the id. A list call, such as GET /api/maps, shows what you reach.
CodeStatusWhat happenedWhat to do
invalid_request400, in a few cases 404, 409, 410 or 422The request is malformed, or a field fails validation. A change to a map or workflow that sends no edit revision, or a malformed one, answers this too.Fix what the message names. Read a map or workflow before you change it, and send its editRevision in the X-Edit-Revision header, or over MCP as the expected_revision argument.
method_not_allowed405The path exists, but not with this method.Use a method from the Allow header.
stale_revision409Someone changed the item after you read it. Your change was not applied.Read the item again, then redo your change on what you find.
name_taken409, or 400 when you save a workflow stepA dataset of that name exists already.Choose another name.
run_in_flight409The workflow is already running, and a workflow runs one run at a time.Wait for the run to finish, or cancel it, then start again.
refresh_in_flight409A refresh of this dataset is already running.Retry when it finishes.
request_body_too_large413The body is over the operation’s byte limit.Send a smaller body. The operation reference gives each operation’s limit.
upload_too_large413The file is over the instance’s largest upload, or a picture is over its fixed 10 MB.Send a smaller file, or ask an admin to raise the limit. A picture’s limit is fixed. See Limits.
unsupported_image415The picture is not a still JPEG, PNG or WebP image: an SVG, a GIF, an animated image, or one damaged or incomplete. Nothing changed.Choose a still JPEG, PNG or WebP image.
image_too_large413The picture has more than 64 million pixels, a fixed limit. Nothing changed.Choose a smaller image.
rate_limited429Too many sign-in attempts or wrong current passwords, too many requests to a published map, or the OpenStreetMap service refused an import.Wait, then retry. A sign-in or password refusal gives the wait in retryAfter and the Retry-After header.
CodeStatusWhat happenedWhat to do
sql_rejected400The SQL is not one read-only SELECT, or it reads a file directly.Send one SELECT over your datasets.
sql_error400The SQL failed to parse or run, or it names a dataset you cannot reach.Fix the SQL. position points at the problem when the engine gives one. Another person’s dataset is <sql name>.<dataset>: see Who may do what.
query_timeout408The query ran past the instance’s query time limit.Narrow the query, with a WHERE or fewer columns. The message names the setting that raises the limit. See Limits.
query_memory_exceeded400The query needed more memory than the engine allows SQL.Narrow the query, or ask the operator to raise DEZFIL_QUERY_MEMORY_MB.
query_result_too_large422The answer, or one value in it, is over a byte limit, so no rows come back. A query answers this, and so do a dataset’s page, a geometry or PostGIS preview, a layer’s filter values and a category fill.Narrow the query, or keep every row: Answers too large to return says what to do at each read.
CodeStatusWhat happenedWhat to do
operation_queue_full503Too many operations are already waiting to run.Retry once some finish.
authentication_busy503Too many sign-ins or password checks are waiting their turn.Retry after the seconds in Retry-After.
internal500The server failed in a way that has no code of its own.Retry once. If it fails again, the operator finds the detail in the server log.

Limits gives the two byte limits behind query_result_too_large, and how geometry counts. No setting raises them, so what to do depends on the read that answers it:

The readWhat to do
A query, on the SQL page, with POST /api/query or run_sqlNarrow it, as Query with SQL shows. To keep every row, download it or save it as a dataset. In the app, a SQL step saves it, and the dataset’s export in Data downloads it.
A dataset’s page, or its Set geometry panelKeep a copy without the large column, as A dataset’s page does not open shows.
A layer’s Filters or Color by, or a Category widget’s Group byChoose another column: one with a value that long holds no categories.
The PostGIS five-row previewImport the table all the same: an import is not held to these limits.

An export runs the same query and streams every row of its result as a file, with no byte limit:

curl

Terminal
curl -s http://localhost:3000/api/query/export \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT name, country, population FROM world_cities ORDER BY population DESC LIMIT 3", "format": "csv", "name": "largest_cities"}'
Output
name,country,population
Tokyo,Japan,35676000
New York,United States of America,19040000
Mexico City,Mexico,19028000
  • sql is your query, here over the world_cities dataset of the API quickstart.
  • format is csv, geoparquet or geojson. CSV writes geometry as WKT text. GeoJSON needs a geometry column and uses the first one.
  • name names the file, here largest_cities.csv. Without it the file is query_result.csv.
  • curl prints the file. To save it instead, add -o largest_cities.csv -w "%{http_code}\n": 200 means the file holds the rows, and any other status that it holds the error answer.

The export still keeps to the Query time limit and Engine memory, so it can answer query_timeout or query_memory_exceeded. The operation reference gives every field.

Over MCP, the tool export_sql_result hands the file back to the agent, up to Tool attachment size. A larger file fails with attachment_too_large: download it over HTTP, or save the rows as a dataset with run_operation, as the next section shows.

A run of the operation sql_step saves every row of a query as a new dataset, with no byte limit:

curl

Terminal
curl -s http://localhost:3000/api/operations/run \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"operation": "sql_step", "parameters": {"sql": "SELECT name, country, population, geometry FROM world_cities WHERE population > 10000000", "name": "big_cities"}}'
Output
{"run":{"id":"<RUN_ID>","operation":"sql_step","state":"queued",...}}
  • parameters holds the query as sql and the new dataset’s name. A name already taken answers 409 name_taken, and nothing runs.
  • The answer comes at once, with the run’s id, and the run writes the dataset afterwards.

Read the run to see how it ended:

curl

Terminal
curl -s http://localhost:3000/api/operations/runs/<RUN_ID> \
-H "Authorization: Bearer <YOUR_TOKEN>"
Output
{"run":{"id":"<RUN_ID>","operation":"sql_step","state":"succeeded",...,"finishedAt":"2026-10-20T09:00:01.503Z","output":{"id":"<DATASET_ID>","name":"big_cities"}}}
  • state is queued, then running. Read the run again every few seconds until it is succeeded, failed or cancelled.
  • A succeeded run names its dataset under output. A failed one holds an error with a code and a message, such as sql_error, or operation_timeout past Time limit per tool run.
  • Over MCP, run_operation takes the same operation and parameters, and get_operation_run reads the run.

The operation reference gives every field of a run.

This helper returns the answer of a successful call and throws on an error, with its status and code. It retries the three codes that mean “wait and try again”:

dezfil.mjs
const base = "http://localhost:3000";
const token = "<YOUR_TOKEN>";
const waitAndRetry = new Set(["rate_limited", "operation_queue_full", "authentication_busy"]);
export async function request(method, path, { body, headers = {} } = {}) {
for (let attempt = 1; ; attempt++) {
const response = await fetch(base + path, {
method,
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...headers },
body: body === undefined ? undefined : JSON.stringify(body),
});
const answer = await response.json().catch(() => null);
if (response.ok) return answer;
const error = answer?.error ?? { code: `http_${response.status}`, message: response.statusText };
if (!waitAndRetry.has(error.code) || attempt === 3) {
throw Object.assign(new Error(`${response.status} ${error.code}: ${error.message}`), { status: response.status, error });
}
const seconds = Number(response.headers.get("Retry-After")) || 5;
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
}
}
  • A successful call returns its JSON answer, or null when the answer is not JSON or has no body.
  • rate_limited, operation_queue_full and authentication_busy wait for the seconds in Retry-After, or 5 without it, and try again, three attempts in all.
  • Every other error throws. failure.error keeps the whole error object, with any extra fields.
  • An answer without an error object gets the code http_<status>, such as http_404. See Troubleshooting.

Call it the way you call the API:

query.mjs
import { request } from "./dezfil.mjs";
try {
await request("POST", "/api/query", { body: { sql: "SELECT * FROM no_such_dataset" } });
} catch (failure) {
console.log(failure.message);
}

Run it with node query.mjs, with both files in one folder.

Output
400 sql_error: The query refers to an unknown table.

Leave stale_revision out of the retry list: the same change with the same revision fails again. Read the item, then send your change with the new editRevision. How the API works shows the pattern.

The path does not exist, so Dezfil answers with its not-found page. Check the path and its spelling against the operation reference.

Check that you send the revision from the latest answer, not from your first read. Edit with revisions shows the chain.

A dataset whose first 50 rows are too large to return keeps its page and Set geometry closed, and its inspection answers query_result_too_large. Text there shows only its first 512 characters, so the large value is of another kind, such as JSON, a list, a binary value or a second geometry column.

To find its column, run this on the SQL page, or with POST /api/query or run_sql:

SQL
SELECT max(length(CAST(COLUMNS(*) AS VARCHAR))) FROM <DATASET>;

It answers one row: the length of each column’s longest value, in characters, under the column’s name. The column to leave out shows a number in the millions. The dataset’s own geometry column does not count.

Keep a copy without that column: on the SQL page, run SELECT * EXCLUDE (<COLUMN>) FROM <DATASET> and click Save as dataset. If that query answers query_result_too_large too, save it as a SQL step in a workflow, or as Save every row as a dataset shows. The copy’s page opens.

The catalogue gives each code’s general meaning. An answer’s message adds the detail of this request, such as the dataset, the limit or the setting that changes it.

Dezfil · Apache-2.0docs from 7439201b