Skip to content

clasp 3.1.3 fails on Node releases carrying the http.Agent keep-alive security backport (22.23.0, 24.17+, 25.x, 26.x): OAuth token refresh throws 'Premature close' (gaxios 6 → node-fetch 2.x) #1158

Description

@Slavic-Sensei

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

  1. Have a ~/.clasprc.json whose access_token is expired (so a refresh is required).
  2. Run clasp push (any command that authenticates).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions