diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2d7e74..0c7f754 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,21 +1,75 @@ +on: + push: + branches: + - main + - next + - 'v*' + pull_request: + paths-ignore: + - LICENSE + - '*.md' + name: CI -on: [push, pull_request] jobs: - test: + lint: + permissions: + contents: read + name: Lint runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Install Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: v22.x + cache: 'npm' + cache-dependency-path: package.json + + - name: Install dependencies + run: npm install + - name: Check linting + run: npm run lint:ci + + tests: + permissions: + contents: read + name: Tests strategy: + fail-fast: false matrix: - node: [20.x, 22.x] - name: Node ${{ matrix.node }} + os: [ubuntu-latest, macos-latest, windows-latest] + node-version: [20.x, 22.x, 24.x] + runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v4 - - name: Setup node - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - node-version: ${{ matrix.node }} + persist-credentials: false + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node-version }} cache: 'npm' cache-dependency-path: package.json - - run: npm install - - run: npm run test:ci + - name: Install Dependencies + run: npm install + + - name: Run Tests + run: npm run test:ci + + automerge: + if: > + github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' + needs: + - tests + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Merge Dependabot PR + uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 57b77ff..7be16cc 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,9 @@ type WaitOnOptions = { maxRedirects?: number; followRedirect?: boolean; headers?: Record; - validateStatus?: WaitOnValidateStatusCallback + validateStatus?: WaitOnValidateStatusCallback; + happyEyeballs?: boolean; // default `true` + rejectUnauthorized?: boolean; // default `true` }; socket?: { timeout?: number; diff --git a/index.d.ts b/index.d.ts index ecb24de..6919d88 100644 --- a/index.d.ts +++ b/index.d.ts @@ -46,6 +46,22 @@ type WaitOnResourcesType = | `socket://${string}`; type WaitOnValidateStatusCallback = (status: number) => boolean; +type WaitOnHTTPOptions = { + bodyTimeout?: number; + headersTimeout?: number; + maxRedirects?: number; + followRedirect?: boolean; + headers?: Record; + rejectUnauthorized?: boolean; + happyEyeballs?: boolean; + validateStatus?: WaitOnValidateStatusCallback; +}; +type WaitOnSocketOptions = { + timeout?: number; +}; +type WaitOnTCPOptions = { + timeout?: number; +}; type WaitOnOptions = { resources: WaitOnResourcesType[]; @@ -56,20 +72,9 @@ type WaitOnOptions = { reverse?: boolean; any?: boolean; simultaneous?: number; - http?: { - bodyTimeout?: number; - headersTimeout?: number; - maxRedirects?: number; - followRedirect?: boolean; - headers?: Record; - validateStatus?: WaitOnValidateStatusCallback - }; - socket?: { - timeout?: number; - }; - tcp?: { - timeout?: number; - }; + http?: WaitOnHTTPOptions; + socket?: WaitOnSocketOptions; + tcp?: WaitOnTCPOptions; window?: number; proxy?: WaitOnProxyConfig; events?: { @@ -89,5 +94,8 @@ export { WaitOnResourcesType, WaitOnValidateStatusCallback, WaitOnCallback, + WaitOnHTTPOptions, + WaitOnSocketOptions, + WaitOnTCPOptions, WaitOn, }; diff --git a/lib/http.js b/lib/http.js index 896cc96..2420814 100644 --- a/lib/http.js +++ b/lib/http.js @@ -1,5 +1,5 @@ 'use strict' -const { Agent, ProxyAgent, request } = require('undici') +const { Client, ProxyAgent, interceptors } = require('undici') const HTTP_GET_RE = /^https?-get:/ const HTTP_UNIX_RE = /^http:\/\/unix:([^:]+):([^:]+)$/ @@ -17,7 +17,8 @@ function getHTTPAgent (config, href) { headersTimeout, followRedirect, maxRedirections, - rejectUnauthorized + rejectUnauthorized, + happyEyeballs } = {}, proxy } = config @@ -33,6 +34,7 @@ function getHTTPAgent (config, href) { headersTimeout, connections: 1, // Single connection per resource pipelining: 0, // to disable keep-alive + autoSelectFamily: happyEyeballs != null ? happyEyeballs : true, connect: { timeout, socketPath, @@ -42,7 +44,7 @@ function getHTTPAgent (config, href) { return isProxied ? new ProxyAgent(Object.assign({}, httpOptions, proxy)) - : new Agent(httpOptions) + : new Client(href, httpOptions).compose(interceptors.dump()) } /** @@ -58,44 +60,17 @@ function createHTTPResource (config, resource) { const method = HTTP_GET_RE.test(resource) ? 'GET' : 'HEAD' const href = source.href.replace('-get:', ':') const isStatusValid = httpConfig?.validateStatus - // TODO: this will last as long as happy-eyeballs is not implemented - // within node core - /** @type {{ options: import('undici').Dispatcher.RequestOptions, url: URL }} */ - const primary = { - options: null, - url: null - } - /** @type {{ options?: import('undici').Dispatcher.RequestOptions, url?: URL }} */ - const secondary = { - options: null, - url: null - } - - if (href.includes('localhost')) { - primary.url = new URL(href.replace('localhost', '127.0.0.1')) - - secondary.url = new URL(href.replace('localhost', '[::1]')) - secondary.options = { - path: secondary.url.pathname, - origin: secondary.url.origin, - query: secondary.url.search, + const url = new URL(href) + const handler = { + dispatcher, + options: { + path: url.pathname, + origin: url.origin, + query: url.search, method, - dispatcher, signal: null, headers: httpConfig?.headers } - } else { - primary.url = new URL(href) - } - - primary.options = { - path: primary.url.pathname, - origin: primary.url.origin, - query: primary.url.search, - method, - dispatcher, - signal: null, - headers: httpConfig?.headers } return { @@ -103,12 +78,7 @@ function createHTTPResource (config, resource) { name: resource } - async function exec ( - signal, - handler = primary, - handlerSecondary = secondary, - isSecondary = false - ) { + async function exec (signal) { const start = Date.now() const operation = { successfull: false, @@ -117,35 +87,17 @@ function createHTTPResource (config, resource) { handler.options.signal = signal - if (handlerSecondary.options != null) { - handlerSecondary.options.signal = signal - } - try { - const options = isSecondary ? handlerSecondary.options : handler.options - const { statusCode, body } = await request(options) + const { options, dispatcher } = handler + const { statusCode } = await dispatcher.request(options) const duration = Date.now() - start - // We allow data to flow without worrying about it - body.resume() operation.successfull = isStatusValid != null ? isStatusValid(statusCode) - : statusCode >= 200 && statusCode < 500 - - if ( - !operation.successfull && - !isSecondary && - handlerSecondary.url != null - ) { - return await exec(signal, handler, handlerSecondary, true) - } + : statusCode > 199 && statusCode < 500 operation.reason = `HTTP(s) request for ${method}-${resource} replied with code ${statusCode} - duration ${duration}ms` } catch (e) { - if (!isSecondary && handlerSecondary.url != null) { - return await exec(signal, handler, handlerSecondary, true) - } - operation.reason = `HTTP(s) request for ${method}-${resource} errored: ${ e.message } - duration ${Date.now() - start}` diff --git a/package.json b/package.json index 23063e6..baa625b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ] }, "devDependencies": { - "@types/node": "^22.7.5", + "@types/node": "^24.0.15", "expect-legacy": "^1.20.2", "husky": "^9.0.11", "mkdirp": "^3.0.0", @@ -68,7 +68,7 @@ "minimist": "^1.2.7", "ora": "^8.0.1", "signale": "^1.4.0", - "undici": "^6.14.1" + "undici": "^7.13.0" }, "keywords": [ "wait", diff --git a/test/http.test.js b/test/http.test.js index 3b57c50..d0f037a 100644 --- a/test/http.test.js +++ b/test/http.test.js @@ -148,33 +148,18 @@ test('Wait-On#HTTP', context => { context.test( 'Basic HTTP - fallback to ipv6 if ipv4 not available on localhost', async t => { - let ipv4Called = false let ipv6Called = false - const server4 = createServer((req, res) => { - ipv4Called = true - res.writeHead(500, { 'Content-Type': 'text/plain' }) - res.end('oops!') - }) - const server6 = createServer((req, res) => { ipv6Called = true res.writeHead(200, { 'Content-Type': 'text/plain' }) res.end('Hello World') }) - t.plan(3) + t.plan(2) - t.teardown(server4.close.bind(server4)) t.teardown(server6.close.bind(server6)) - await new Promise((resolve, reject) => { - server4.listen({ host: '127.0.0.1', port: 3006 }, e => { - if (e != null) reject(e) - else resolve() - }) - }) - await new Promise((resolve, reject) => { server6.listen({ host: '::1', port: 3006 }, e => { if (e != null) reject(e) @@ -189,7 +174,6 @@ test('Wait-On#HTTP', context => { }) t.equal(result, true) - t.ok(ipv4Called) t.ok(ipv6Called) } ) @@ -224,7 +208,7 @@ test('Wait-On#HTTP', context => { } ) - context.test('Basic HTTP with timeout', { only: true }, t => { + context.test('Basic HTTP with timeout', t => { t.plan(1) waitOn({