lwwallet: report a non-Esplora endpoint instead of parsing its HTML - #1145
lwwallet: report a non-Esplora endpoint instead of parsing its HTML#1145litbot-9000 wants to merge 3 commits into
Conversation
Every Esplora error path interpolated the whole response body. That is fine for the short status line a REST API returns, but a base URL that names a web frontend instead of the API root answers with a full HTML document, and the resulting error carried three kilobytes of markup up through the wallet, the daemon and out to the RPC client, where it is neither readable nor useful. Collapse the body onto one line and bound it, so a single-line log record stays a single line. The cut lands on a rune boundary: the body is arbitrary bytes from an endpoint whose shape we have just decided not to trust, and slicing it blindly would emit a half-encoded rune.
A mempool.space instance serves its Esplora-compatible REST API under /api, and serves a single-page web app everywhere else. That app answers every unrouted path with index.html and a 200, so a base URL missing the /api suffix passes the status check and hands markup to a parser. The operator sees the failure as parse tip height: strconv.ParseInt: parsing "<!doctype html>... raised from inside wallet startup, several layers away from the setting that caused it, with the whole document inlined. No Esplora endpoint answers with HTML, so a response that does did not come from the API. Reject it before the body reaches a parser and name the likely fix in the error. The check keys on the content type alone. Body sniffing would be wrong here: /tx/:txid/raw and /block/:hash/raw return arbitrary binary, which can legitimately begin with the same '<' byte an HTML document does. A test pins that case with a version-60 transaction, whose serialization starts with 0x3c. ErrNotEsploraAPI is exported so a caller can tell a misconfiguration apart from a transient failure and decline to retry it.
bd76151 to
bfa3169
Compare
📘 Doc drift advisory —
|
The package doc described the Esplora client's cache-integrity check as the only thing standing between a response and a parser. It now also passes an endpoint-shape guard, and neither that guard nor the new exported ErrNotEsploraAPI sentinel appeared anywhere in the doc. Record both, along with the two decisions a future reader is most likely to want to undo: that the guard keys on Content-Type alone rather than sniffing the body, because the /raw endpoints return binary that can legitimately start with '<', and that a body quoted back in an error is bounded to 256 bytes.
What this fixes
A mainnet beta user hit this while onboarding:
followed by three kilobytes of the mempool.space frontend, inlined into the
gRPC error.
Their
wallet.esploraurlwashttps://mempool.staging.lightningcluster.com,missing the
/apisuffix. A mempool.space instance serves itsEsplora-compatible REST API under
/apiand serves an Angular single-pageapp everywhere else. That app answers every unrouted path with index.html
and a 200, so
GET /blocks/tip/heightreturns markup,EsploraClient.getchecks only the status code, and
strconv.ParseIntgets the document.The setting is one line of config, but nothing in the failure says so. The
error names a parser, is raised from inside wallet startup, and buries the
one useful signal (
<!doctype html>) in its own noise.The change
92e56768— bound the body quoted back in an error. Every Esplora errorpath interpolated the entire response body. Collapse it onto one line and cap
it at 256 bytes. The cut lands on a rune boundary; the body is arbitrary bytes
from an endpoint whose shape we have just decided not to trust, so slicing it
blindly would emit a half-encoded rune into the log.
34b5597e— reject an HTML response as a misconfigured endpoint. NoEsplora endpoint answers with HTML, so a response that does did not come from
the API.
checkNotHTMLruns after the status check on all four request paths(
get,post,TestMempoolAccept,SubmitPackage) and returns a wrappedErrNotEsploraAPInaming the URL and the likely fix. Exported so a caller cantell a misconfiguration apart from a transient failure and decline to retry.
Same daemon, same bad URL, after:
Why content type and not the body
Body sniffing is the obvious implementation and it is wrong here.
/tx/:txid/rawand/block/:hash/rawreturn arbitrary binary, which canlegitimately begin with the same
<byte an HTML document does — aversion-60 transaction serializes to a leading
0x3c.TestEsploraAcceptsBinaryBodyStartingWithAngleBracketpins exactly that caseso a later "just check if it starts with
<" simplification fails a testrather than breaking raw block fetches.
The check is also deliberately narrow. It does not validate that the endpoint
is correct, only that it is not a web page. A wrong-but-JSON endpoint still
fails downstream, as it should.
What this does not change
The defaults are all fine already — every constant in
lwwallet/defaults.gocarries the suffix, as does
chainfees's separate/api/v1/fees/recommendedendpoint. This only affects a hand-set
wallet.esploraurl.Verification
Reproduced and fixed against the real deployment, not just a test server.
wavedat2c3af7d6with-tags "wavewalletrpc swapruntime", ran iton mainnet against
mempool.staging.lightningcluster.comwith no/api,and
wavecli createreproduced the reported error byte for byte./apiadded, a fresh mainnet wallet comes up clean: tip poller syncedat height 962356,
wallet_state: WALLET_STATE_READY,server_connected: true, and the operator correctly refuses the unlisted client(
is not whitelisted), which is the next gate rather than this bug.including
/scripthash/:hash/utxoin the forward byte order the codedocuments (the reversed Electrum order returns
[], as expected). Oneunrelated gap found and reported separately:
POST /api/tx404s there,while it routes on testnet4 and mempool.space — broadcast would fail on that
host.
TestEsploraRejectsHTMLResponsefails onmainwith the originalstrconv.ParseInterror, confirming it pins the regression.go test ./lwwallet/,make lint-changed-local, andmake commitmsg-lint range="origin/main..HEAD"all pass.🤖 Generated with Claude Code