Summary
On every Node.js release that carries the recent http.Agent keep-alive security backport, any clasp command that needs to refresh the OAuth access token fails:
Invalid response body while trying to fetch https://oauth2.googleapis.com/token: Premature close
This is not a clean major-version cut-off. The triggering change was backported across release lines, so it lands on specific patch releases. Empirically bisected:
| Node line |
last good |
first broken |
| 22 LTS |
22.22.0 |
22.23.0 |
| 24 |
24.16.0 |
24.17.0 |
| 25 / 26 |
— |
all tested fail |
| 20 |
20.20.2 (latest, line in maintenance) — no broken release |
|
| 18 |
18.20.8 — works |
|
Once the stored access_token has expired, clasp can no longer refresh it, so push / pull / run / etc. all fail.
Environment
@google/clasp 3.1.3 (global install)
- Reproduced on Node
v22.23.0, v24.17.0, v25.x, v26.3.1; works on v22.22.0, v24.16.0, the entire v20.x line, and v18.x — macOS arm64
~/.clasprc.json with a valid refresh_token but expired access_token
Steps to reproduce
- Have a
~/.clasprc.json whose access_token is expired (so a refresh is required).
- Run
clasp push (any command that authenticates).
- →
Invalid response body while trying to fetch https://oauth2.googleapis.com/token: Premature close
Root cause (isolated)
It is not the token, the network, Google, or Node's native fetch.
1. Native fetch() (undici) in the same Node 26 → the same endpoint → the same refresh_token succeeds (HTTP 200):
await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ client_id, client_secret, refresh_token, grant_type: 'refresh_token' })
}); // → 200, access_token returned
(Also confirmed with Python urllib → 200.)
2. The failure is in clasp's HTTP stack. google-auth-library's refresh throws, stack:
Error: Invalid response body while trying to fetch https://oauth2.googleapis.com/token: Premature close
at Gaxios._request (.../@google/clasp/node_modules/gaxios/build/src/gaxios.js:149:19)
at OAuth2Client.refreshTokenNoCache (.../google-auth-library/build/src/auth/oauth2client.js:212:19)
at OAuth2Client.refreshAccessTokenAsync (.../oauth2client.js:247:19)
at OAuth2Client.getAccessTokenAsync (.../oauth2client.js:276:23)
3. Dependency versions pinned in clasp 3.1.3:
google-auth-library 9.15.1
gaxios 6.7.1 → node-fetch 2.7.0
So the failing path is gaxios 6 → node-fetch 2.x over a keep-alive http.Agent. This is the known interaction between a recent Node security release that changed http.Agent keep-alive socket handling ("fix response queue poisoning in http.Agent") and node-fetch 2.x's latent false-positive ERR_STREAM_PREMATURE_CLOSE on reused sockets — see nodejs/node#63989. node-fetch 2.x is effectively unmaintained.
gaxios 7.x dropped node-fetch in favour of native fetch, which works on Node 26 (point 1 above).
The version table above is consistent with this: the bug appears exactly on the patch releases that carry the http.Agent backport (22.23.0, 24.17.0, 25.x, 26.x) and is absent on the releases just before it (22.22.0, 24.16.0) and on lines that never received it (20.x, 18.x).
Suggested fix
Bump google-auth-library (→ 10.x) / gaxios (→ 7.x, native fetch) so clasp no longer pulls in node-fetch 2.x. This is the same class of breakage as #1106 (Node v25+ SlowBuffer) — "clasp 3.x on modern Node" failures rooted in stale transitive deps.
Workaround
Run clasp under a Node release that predates the http.Agent backport. The Node 20 line is the safest pick — every release works and the line is in maintenance, so no future patch should regress it:
# with fnm / nvm
fnm install 20 && fnm use 20
npm i -g @google/clasp@3 # global installs are per Node version
clasp push
⚠️ brew install node@22 does NOT work — it currently installs 22.23.0, which is already broken. If you must stay on Node 22, pin ≤ 22.22.0. Node 18 also works but is EOL.
Summary
On every Node.js release that carries the recent
http.Agentkeep-alive security backport, any clasp command that needs to refresh the OAuth access token fails:This is not a clean major-version cut-off. The triggering change was backported across release lines, so it lands on specific patch releases. Empirically bisected:
Once the stored
access_tokenhas expired, clasp can no longer refresh it, sopush/pull/run/ etc. all fail.Environment
@google/clasp3.1.3 (global install)v22.23.0,v24.17.0,v25.x,v26.3.1; works onv22.22.0,v24.16.0, the entirev20.xline, andv18.x— macOS arm64~/.clasprc.jsonwith a validrefresh_tokenbut expiredaccess_tokenSteps to reproduce
~/.clasprc.jsonwhoseaccess_tokenis expired (so a refresh is required).clasp push(any command that authenticates).Invalid response body while trying to fetch https://oauth2.googleapis.com/token: Premature closeRoot cause (isolated)
It is not the token, the network, Google, or Node's native
fetch.1. Native
fetch()(undici) in the same Node 26 → the same endpoint → the samerefresh_tokensucceeds (HTTP 200):(Also confirmed with Python
urllib→ 200.)2. The failure is in clasp's HTTP stack.
google-auth-library's refresh throws, stack:3. Dependency versions pinned in clasp 3.1.3:
google-auth-library9.15.1gaxios6.7.1 →node-fetch2.7.0So the failing path is gaxios 6 → node-fetch 2.x over a keep-alive
http.Agent. This is the known interaction between a recent Node security release that changedhttp.Agentkeep-alive socket handling ("fix response queue poisoning in http.Agent") and node-fetch 2.x's latent false-positiveERR_STREAM_PREMATURE_CLOSEon reused sockets — see nodejs/node#63989. node-fetch 2.x is effectively unmaintained.gaxios7.x dropped node-fetch in favour of nativefetch, which works on Node 26 (point 1 above).The version table above is consistent with this: the bug appears exactly on the patch releases that carry the
http.Agentbackport (22.23.0, 24.17.0, 25.x, 26.x) and is absent on the releases just before it (22.22.0, 24.16.0) and on lines that never received it (20.x, 18.x).Suggested fix
Bump
google-auth-library(→ 10.x) /gaxios(→ 7.x, native fetch) so clasp no longer pulls innode-fetch2.x. This is the same class of breakage as #1106 (Node v25+SlowBuffer) — "clasp 3.x on modern Node" failures rooted in stale transitive deps.Workaround
Run clasp under a Node release that predates the
http.Agentbackport. The Node 20 line is the safest pick — every release works and the line is in maintenance, so no future patch should regress it:brew install node@22does NOT work — it currently installs 22.23.0, which is already broken. If you must stay on Node 22, pin ≤ 22.22.0. Node 18 also works but is EOL.