es-client: non-JSON error responses report "Body is unusable" instead of the HTTP status
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 75/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- nodejs, typescript
- Domain
- backend-api-design, cli
Research direction
The bug is in src/lib/es-client.ts lines 125-132. Start by reading the file to understand the request flow. The fix is to read the response body as text first, then try to parse it as JSON, similar to how kibana-client.ts and cloud-client.ts handle it. Write a test to reproduce the error using a mock HTTP server that returns a non-JSON error response, then verify the fix returns the correct status code and body.
Written by the indexing model from the issue text.
Description
Summary
Any non-2xx response whose body is not valid JSON surfaces as Error: Body is unusable: Body has already been read, losing the status code. That is undici's TypeError — the error branch of
EsClient.request reads the body twice. Deterministic, not timing-dependent.
The usual way to hit it is a reverse proxy in front of Elasticsearch: proxy_read_timeout expiring
on a long ES|QL query returns an nginx HTML 504, and the CLI reports Body is unusable instead of
504 — pointing suspicion at the client rather than the proxy.
Root cause
src/lib/es-client.ts#L125-L132:
if (!response.ok) {
let body: unknown
try {
body = await response.json() // consumes the stream
} catch {
body = await response.text() // ← throws: body already consumed
}
throw new EsResponseError(response.status, body)
}
response.json() disturbs the body even when it rejects, so the catch branch can never read it.
Reproduction
cat > elasticrc-local.yml <<'YAML'
current_context: local
contexts:
local:
elasticsearch:
url: http://127.0.0.1:9999
auth: { api_key: ZmFrZTpmYWtl }
YAML
chmod 0600 elasticrc-local.yml
export ELASTIC_CLI_CONFIG_FILE="$PWD/elasticrc-local.yml" NO_PROXY=localhost,127.0.0.1
# stand in for "ES behind a proxy that timed out"
node -e 'require("http").createServer((q,s)=>{s.writeHead(504,{"content-type":"text/html"});s.end("<html><body>504 Gateway Time-out</body></html>")}).listen(9999,"127.0.0.1")' &
elastic es esql query --query 'FROM x | LIMIT 1' --format txt
# Error: Body is unusable: Body has already been read (exit 1)
Same for 502, 503, text/plain and empty bodies. A JSON error body works correctly
(Error: illegal_argument_exception: …). Reproduced on v0.4.0 and v0.5.0, Node v26.8.1,
Linux x86_64, against Elasticsearch 9.5.3.
Suggested fix
Read once, then parse — as kibana-client.ts#L212
and cloud-client.ts#L75
already do in this repo:
const text = await response.text()
let body: unknown
try {
body = JSON.parse(text)
} catch {
body = text
}
throw new EsResponseError(response.status, body)
EsResponseError's message should also carry the status, since a string body currently renders as
bare HTML. Happy to open a PR with tests for an HTML and an empty error body.
Context
Introduced by #281 (native-fetch EsClient); @elastic/transport handled it correctly. Present in
every release since v0.2.0 and on main today. #588 fixed the same blind-JSON.parse mistake on
the success branch of this file — this is the error branch.
- Dominant language
- TypeScript
- Stars
- 43
- Forks
- 24
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 61
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from elastic/cli
-
cloud enhancement
Difficulty 1/5 1-3 hours Newbie friendliness 88/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
vercel-labs/just-bash#464 ·
-
looksLikeSlug() is ASCII-only, so non-Latin entity slugs (e.g. Korean) skip exact match and collapse Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
TanStack/tanstack.com#1293 ·