Skip to content

Commit 1ef00a4

Browse files
Replace node-fetch with the Node runtime fetch API
Avoid registration failures on newer Node versions, and require Node 18+.
1 parent 7603542 commit 1ef00a4

8 files changed

Lines changed: 186 additions & 121 deletions

File tree

.github/workflows/javascript-ci.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ jobs:
1818
uses: actions/cache@v3
1919
with:
2020
path: javascript/node_modules
21-
key: 16.x-${{ runner.OS }}-build-${{ hashFiles('javascript/yarn.lock') }}
21+
key: 18.x-${{ runner.OS }}-build-${{ hashFiles('javascript/yarn.lock') }}
2222

2323
- name: Set up Node
2424
uses: actions/setup-node@v3
2525
with:
26-
node-version: '16.x'
26+
node-version: '18.x'
2727
registry-url: 'https://registry.npmjs.org'
2828

2929
- name: Install dependencies
@@ -44,5 +44,5 @@ jobs:
4444

4545
- name: Test
4646
run: |
47-
yarn test
47+
yarn test --run
4848
working-directory: javascript

javascript/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Review Retool's [RPC documentation](https://docs.retool.com/docs/retool-rpc) before installing the JavaScript package.
44

5+
Requires Node.js 18 or later (uses the runtime `fetch` API).
6+
57
## Installation
68

79
You can use `npm`, `yarn`, or `pnpm` to install the package.

javascript/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "retoolrpc",
3-
"version": "0.1.8",
3+
"version": "0.2.0",
44
"description": "TypeScript package for Retool RPC",
55
"keywords": [],
66
"homepage": "https://github.com/tryretool/retoolrpc#readme",
@@ -12,6 +12,9 @@
1212
"url": "git+https://github.com/tryretool/retoolrpc.git"
1313
},
1414
"license": "MIT",
15+
"engines": {
16+
"node": ">=18"
17+
},
1518
"main": "./dist/cjs/index.js",
1619
"module": "./dist/index.mjs",
1720
"types": "./dist/index.d.ts",
@@ -34,21 +37,18 @@
3437
"test:api": "tsc --project tsconfig.json"
3538
},
3639
"dependencies": {
37-
"abort-controller": "^3.0.0",
38-
"node-fetch": "^2.6",
3940
"ts-dedent": "^2.2.0",
4041
"uuid": "^9.0.0"
4142
},
4243
"devDependencies": {
4344
"@rollup/plugin-typescript": "^8.2.0",
4445
"@types/fs-extra": "^11.0.1",
45-
"@types/node": "15.12.1",
46-
"@types/node-fetch": "^2.6.4",
46+
"@types/node": "^18.19.0",
4747
"@types/semver": "^7.5.1",
4848
"@types/uuid": "^9.0.2",
4949
"cross-env": "7.0.3",
5050
"fs-extra": "^11.1.1",
51-
"nock": "^13.3.2",
51+
"nock": "^14.0.17",
5252
"nodemon": "^2.0.15",
5353
"prettier": "2.3.1",
5454
"rollup": "^2.39.0",

javascript/src/rpc.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export class RetoolRPC {
179179
)
180180
}
181181

182-
const { versionHash } = await registerAgentResponse.json()
182+
const { versionHash } = (await registerAgentResponse.json()) as { versionHash: string }
183183
this._versionHash = versionHash
184184
this._logger.info(`Agent registered with versionHash: ${versionHash}`)
185185

@@ -206,7 +206,16 @@ export class RetoolRPC {
206206
throw new Error(`Server error when fetching query: ${pendingQueryFetch.status}. Retrying...`)
207207
}
208208

209-
const { query } = await pendingQueryFetch.json()
209+
const { query } = (await pendingQueryFetch.json()) as {
210+
query: {
211+
queryUuid: string
212+
queryInfo: {
213+
method: string
214+
parameters: Record<string, unknown>
215+
context: RetoolContext
216+
}
217+
} | null
218+
}
210219
if (query) {
211220
this._logger.debug('Executing query', query) // This might contain sensitive information
212221

javascript/src/utils/api.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import http from 'http'
2+
import zlib from 'zlib'
3+
import { afterEach, describe, expect, test, vi } from 'vitest'
4+
5+
import { RetoolAPI } from './api'
6+
7+
describe('RetoolAPI', () => {
8+
afterEach(() => {
9+
vi.restoreAllMocks()
10+
})
11+
12+
test('registerAgent uses globalThis.fetch', async () => {
13+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
14+
new Response(JSON.stringify({ versionHash: 'abc' }), {
15+
status: 200,
16+
headers: { 'Content-Type': 'application/json' },
17+
}),
18+
)
19+
20+
const api = new RetoolAPI({
21+
hostUrl: 'https://example.retool.com',
22+
apiKey: 'token',
23+
pollingTimeoutMs: 1000,
24+
})
25+
26+
const response = await api.registerAgent({
27+
resourceId: 'resource-id',
28+
environmentName: 'production',
29+
version: '0.0.1',
30+
agentUuid: 'agent-uuid',
31+
operations: {},
32+
})
33+
34+
expect(fetchSpy).toHaveBeenCalledTimes(1)
35+
expect(fetchSpy.mock.calls[0][0]).toBe('https://example.retool.com/api/v1/retoolrpc/registerAgent')
36+
expect(response.ok).toBe(true)
37+
await expect(response.json()).resolves.toEqual({ versionHash: 'abc' })
38+
})
39+
40+
// Regression for RE-2830: node-fetch@2 throws FetchError Premature close on some Node 24
41+
// gzip responses. Native fetch must consume a gzip registerAgent body cleanly.
42+
// Node 18 under Vitest can take several seconds for a local fetch round-trip.
43+
test(
44+
'registerAgent consumes a gzip Content-Encoding response body',
45+
async () => {
46+
const payload = JSON.stringify({ versionHash: 'gzip-version-hash' })
47+
const gzipBody = zlib.gzipSync(payload)
48+
49+
const server = http.createServer((req, res) => {
50+
// Drain the request body before responding so fetch does not stall on an unread POST.
51+
req.resume()
52+
req.on('end', () => {
53+
expect(req.method).toBe('POST')
54+
expect(req.url).toBe('/api/v1/retoolrpc/registerAgent')
55+
res.writeHead(200, {
56+
'Content-Type': 'application/json',
57+
'Content-Encoding': 'gzip',
58+
'Content-Length': String(gzipBody.length),
59+
})
60+
res.end(gzipBody)
61+
})
62+
})
63+
64+
await new Promise<void>((resolve) => {
65+
server.listen(0, '127.0.0.1', () => resolve())
66+
})
67+
68+
const address = server.address()
69+
if (!address || typeof address === 'string') {
70+
throw new Error('Expected TCP server address')
71+
}
72+
73+
try {
74+
const api = new RetoolAPI({
75+
hostUrl: `http://127.0.0.1:${address.port}`,
76+
apiKey: 'token',
77+
pollingTimeoutMs: 1000,
78+
})
79+
80+
const response = await api.registerAgent({
81+
resourceId: 'resource-id',
82+
environmentName: 'production',
83+
version: '0.0.1',
84+
agentUuid: 'agent-uuid',
85+
operations: {},
86+
})
87+
88+
expect(response.ok).toBe(true)
89+
await expect(response.json()).resolves.toEqual({ versionHash: 'gzip-version-hash' })
90+
} finally {
91+
await new Promise<void>((resolve, reject) => {
92+
server.close((error) => (error ? reject(error) : resolve()))
93+
})
94+
}
95+
},
96+
20_000,
97+
)
98+
})

javascript/src/utils/api.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import fetch, { RequestInit } from 'node-fetch'
2-
3-
import AbortControllerFallback from 'abort-controller'
4-
5-
// AbortController was added in node v14.17.0 globally, but we need to polyfill it for older versions
6-
const AbortController = globalThis.AbortController || AbortControllerFallback
7-
81
import { AgentServerError } from '../types'
92
import { RetoolRPCVersion } from '../version'
103

4+
// Runtime global fetch. node-fetch@2 fails on some Node 24.x gzip responses with Premature close.
5+
116
type PopQueryRequest = {
127
resourceId: string
138
environmentName: string
@@ -67,9 +62,7 @@ export class RetoolAPI {
6762
'User-Agent': `RetoolRPC/${RetoolRPCVersion} (Javascript)`,
6863
},
6964
body: JSON.stringify(options),
70-
// Had to cast to RequestInit['signal'] because of a bug in the types
71-
// https://github.com/jasonkuhrt/graphql-request/issues/481
72-
signal: abortController.signal as RequestInit['signal'],
65+
signal: abortController.signal,
7366
})
7467
} catch (error: any) {
7568
if (abortController.signal.aborted) {

javascript/src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const RetoolRPCVersion = '0.1.8'
1+
export const RetoolRPCVersion = '0.2.0'

0 commit comments

Comments
 (0)