What it is
The three older taste clients bound their request with a timeout but not
their response body. once() aborts on a timer, then clears that timer in
its finally the moment fetch() resolves — and fetch() resolves on the
response headers. The await response.json() that follows runs with no
timeout and no abort signal, so a service that sends headers and then stalls
mid-body is waited on forever.
The seams, all the same shape:
src/music/sources/netease.ts:255 — timer cleared in once(); body read in call() / plain().
src/music/sources/bilibili.ts:207 — same.
src/music/sources/qishui.ts:279 — same.
Measured on the identical shape in qqmusic.ts during the closing review of
PR #253: with timeoutMs set to 20 ms and a response whose body never
completes, the call still sat for ~153 ms and then returned, instead of
aborting at 20 ms. qqmusic.ts was fixed there (body read inside the same
timeout and the same one-retry, regression test
test/sources-qqmusic.test.ts "times out a response whose body never
arrives, not just its headers"); these three were out of that PR's scope.
Impact: a /sources mount is in the foreground with the listener waiting on
it, and TasteRefresher runs these in the background — neither can end. Not
observed against the live services; the defect is in the code path, and the
stall is the remote end's to cause.
Spec
specs/spec14/14-listening-taste.md §2.8 — "Every client: timeouts, one retry
on network error, no retry on auth error". The timeout does not currently
cover the body, so the promise is not met.
Done when
Each of netease.ts, bilibili.ts and qishui.ts reads its response body
inside the same timeout and the same single retry as its request (the
qqmusic.ts shape: once() returns the parsed body, not the Response),
and each has a regression test that a response whose body never arrives
rejects rather than hanging — the same test test/sources-qqmusic.test.ts
already carries.
What it is
The three older taste clients bound their request with a timeout but not
their response body.
once()aborts on a timer, then clears that timer inits
finallythe momentfetch()resolves — andfetch()resolves on theresponse headers. The
await response.json()that follows runs with notimeout and no abort signal, so a service that sends headers and then stalls
mid-body is waited on forever.
The seams, all the same shape:
src/music/sources/netease.ts:255— timer cleared inonce(); body read incall()/plain().src/music/sources/bilibili.ts:207— same.src/music/sources/qishui.ts:279— same.Measured on the identical shape in
qqmusic.tsduring the closing review ofPR #253: with
timeoutMsset to 20 ms and a response whose body nevercompletes, the call still sat for ~153 ms and then returned, instead of
aborting at 20 ms.
qqmusic.tswas fixed there (body read inside the sametimeout and the same one-retry, regression test
test/sources-qqmusic.test.ts"times out a response whose body neverarrives, not just its headers"); these three were out of that PR's scope.
Impact: a
/sourcesmount is in the foreground with the listener waiting onit, and
TasteRefresherruns these in the background — neither can end. Notobserved against the live services; the defect is in the code path, and the
stall is the remote end's to cause.
Spec
specs/spec14/14-listening-taste.md§2.8 — "Every client: timeouts, one retryon network error, no retry on auth error". The timeout does not currently
cover the body, so the promise is not met.
Done when
Each of
netease.ts,bilibili.tsandqishui.tsreads its response bodyinside the same timeout and the same single retry as its request (the
qqmusic.tsshape:once()returns the parsed body, not theResponse),and each has a regression test that a response whose body never arrives
rejects rather than hanging — the same test
test/sources-qqmusic.test.tsalready carries.