What happens
flex-cli assets pull never returns when the Build API answers the pull endpoint with an error. It prints two exceptions and then reprints its download progress line every 100ms indefinitely, holding a core at 100%. It has to be killed; SIGTERM is enough.
Observed output, trimmed:
No matching clause: application/edn
Cannot read properties of null (reading 'on')
Downloaded 0.00MBDownloaded 0.00MBDownloaded 0.00MB… (forever)
Why
Three things line up, all on master at 4480e69:
pull-assets asks for zip: (do-get api-client "/assets/pull" query-params {::api.client/accept "application/zip"}) in src/sharetribe/flex_cli/commands/assets.cljs.
parse-body in src/sharetribe/flex_cli/api/client.cljs dispatches on the response content type with a case that has clauses for "text/plain"/nil, "application/transit+json" and "application/zip", and no default. The Build API answers errors on this endpoint in application/edn, so every error response throws No matching clause: application/edn.
- That failure does not stop
pull-assets from reaching print-progress!, which then receives a null stream: the second message, Cannot read properties of null (reading 'on'), is (.on stream "data" ...) on null. print-progress! creates its setInterval before registering handlers, and clearInterval only ever runs in the "end" handler, which was never registered. The orphaned interval keeps the Node event loop alive and keeps printing.
So the unparseable content type is the trigger, and the leaked interval is what turns a crash into a hang.
Reproduction, no credentials needed
The stub ignores the API key, so any logged-in state works.
// edn-403.mjs
import http from 'node:http';
const body = '{:errors [{:status 403, :code :forbidden, :title "Forbidden"}]}';
http.createServer((req, res) => {
res.writeHead(403, { 'Content-Type': 'application/edn' });
res.end(body);
}).listen(18713, '127.0.0.1');
node edn-403.mjs &
FLEX_API_BASE_URL=http://127.0.0.1:18713/v1/build-api \
flex-cli assets pull -m any-marketplace --path ./assets
It never returns. Verified against flex-cli 1.17.1 installed from npm.
How this shows up in practice
GET /assets/pull currently answers 403 Forbidden, in edn, for our API key on every marketplace we can reach, while POST /assets/push on the same key gets as far as validation and answers 400 validation-invalid-value. So an ordinary permission error is enough to hang the CLI indefinitely. Whatever the right answer is on the permission side, the CLI should report the error and exit.
Suggested fix
Either change alone stops the hang:
- Give
parse-body's case a default clause, so an unexpected content type becomes a reported error instead of No matching clause. Handling "application/edn" next to the transit clause would additionally let the existing error page render the :title from the body.
- In
print-progress!, start the interval only after the handlers are attached, or clear it if attaching fails, so nothing can leave a bare setInterval holding the event loop open.
What happens
flex-cli assets pullnever returns when the Build API answers the pull endpoint with an error. It prints two exceptions and then reprints its download progress line every 100ms indefinitely, holding a core at 100%. It has to be killed; SIGTERM is enough.Observed output, trimmed:
Why
Three things line up, all on master at 4480e69:
pull-assetsasks for zip:(do-get api-client "/assets/pull" query-params {::api.client/accept "application/zip"})insrc/sharetribe/flex_cli/commands/assets.cljs.parse-bodyinsrc/sharetribe/flex_cli/api/client.cljsdispatches on the response content type with acasethat has clauses for"text/plain"/nil,"application/transit+json"and"application/zip", and no default. The Build API answers errors on this endpoint inapplication/edn, so every error response throwsNo matching clause: application/edn.pull-assetsfrom reachingprint-progress!, which then receives a null stream: the second message,Cannot read properties of null (reading 'on'), is(.on stream "data" ...)on null.print-progress!creates itssetIntervalbefore registering handlers, andclearIntervalonly ever runs in the"end"handler, which was never registered. The orphaned interval keeps the Node event loop alive and keeps printing.So the unparseable content type is the trigger, and the leaked interval is what turns a crash into a hang.
Reproduction, no credentials needed
The stub ignores the API key, so any logged-in state works.
node edn-403.mjs & FLEX_API_BASE_URL=http://127.0.0.1:18713/v1/build-api \ flex-cli assets pull -m any-marketplace --path ./assetsIt never returns. Verified against flex-cli 1.17.1 installed from npm.
How this shows up in practice
GET /assets/pullcurrently answers403 Forbidden, in edn, for our API key on every marketplace we can reach, whilePOST /assets/pushon the same key gets as far as validation and answers400 validation-invalid-value. So an ordinary permission error is enough to hang the CLI indefinitely. Whatever the right answer is on the permission side, the CLI should report the error and exit.Suggested fix
Either change alone stops the hang:
parse-body'scasea default clause, so an unexpected content type becomes a reported error instead ofNo matching clause. Handling"application/edn"next to the transit clause would additionally let the existing error page render the:titlefrom the body.print-progress!, start the interval only after the handlers are attached, or clear it if attaching fails, so nothing can leave a baresetIntervalholding the event loop open.