Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

missing commits #3040

Merged
merged 13 commits into from Apr 2, 2024
38 changes: 26 additions & 12 deletions .github/workflows/nightly.yml
Expand Up @@ -5,26 +5,40 @@ on:
schedule:
- cron: "0 10 * * *"

permissions:
contents: read

jobs:
test:
test-linux:
if: github.repository == 'nodejs/undici'
uses: ./.github/workflows/test.yml
with:
node-version: 22-nightly
runs-on: ubuntu-latest
secrets: inherit

test-windows:
if: github.repository == 'nodejs/undici'
uses: ./.github/workflows/test.yml
with:
node-version: 22-nightly
runs-on: windows-latest
secrets: inherit

test-macos:
if: github.repository == 'nodejs/undici'
strategy:
fail-fast: false
max-parallel: 0
matrix:
runs-on:
- ubuntu-latest
- windows-latest
- macos-latest
uses: ./.github/workflows/test.yml
with:
node-version: 22-nightly
runs-on: ${{ matrix.runs-on }}
runs-on: macos-latest
secrets: inherit

report-failure:
if: failure()
needs: test
if: ${{ always() && (needs.test-linux.result == 'failure' && needs.test-windows.result == 'failure' && needs.test-macos.result == 'failure') }}
needs:
- test-linux
- test-windows
- test-macos
runs-on: ubuntu-latest
permissions:
issues: write
Expand Down
73 changes: 72 additions & 1 deletion .github/workflows/nodejs.yml
Expand Up @@ -69,6 +69,77 @@ jobs:
runs-on: ${{ matrix.runs-on }}
secrets: inherit

test-without-intl:
name: Test with Node.js ${{ matrix.version }} compiled --without-intl
strategy:
fail-fast: false
max-parallel: 0
matrix:
version: [20, 21]
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false

# Setup node, install deps, and build undici prior to building icu-less node and testing
- name: Setup Node.js@${{ inputs.version }}
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ inputs.version }}

- name: Install dependencies
run: npm install

- name: Build undici
run: npm run build:node

- name: Determine latest release
id: release
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
result-encoding: string
script: |
const req = await fetch('https://nodejs.org/download/release/index.json')
const releases = await req.json()

const latest = releases.find((r) => r.version.startsWith('v${{ matrix.version }}'))
return latest.version

- name: Download and extract source
run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ -

- name: Install ninja
run: sudo apt-get install ninja-build

- name: ccache
uses: hendrikmuhs/ccache-action@faf867a11c028c0b483fb2ae72b6fc8f7d842714 #v1.2.12
with:
key: node${{ matrix.version }}

- name: Build node
working-directory: ./node-${{ steps.release.outputs.result }}
run: |
export CC="ccache gcc"
export CXX="ccache g++"
./configure --without-intl --ninja --prefix=./final
make
make install
echo "$(pwd)/final/bin" >> $GITHUB_PATH

- name: Print version information
run: |
echo OS: $(node -p "os.version()")
echo Node.js: $(node --version)
echo npm: $(npm --version)
echo git: $(git --version)
echo icu config: $(node -e "console.log(process.config)" | grep icu)

- name: Run tests
run: npm run test:javascript:withoutintl

test-types:
name: Test TypeScript types
timeout-minutes: 15
Expand Down Expand Up @@ -97,12 +168,12 @@ jobs:
- dependency-review
- test
- test-types
- test-without-intl
- lint
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
actions: write
steps:
- name: Merge Dependabot PR
uses: fastify/github-action-merge-dependabot@9e7bfb249c69139d7bdcd8d984f9665edd49020b # v3.10.1
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Expand Up @@ -10,6 +10,9 @@ on:
required: true
type: string

permissions:
contents: read

jobs:
test:
name: Test with Node.js ${{ inputs.node-version }} on ${{ inputs.runs-on }}
Expand Down
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -7,8 +7,12 @@ An HTTP/1.1 client, written from scratch for Node.js.
> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
It is also a Stranger Things reference.

## How to get involved

Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel.

Looking to contribute? Start by reading the [contributing guide](./CONTRIBUTING.md)

## Install

```
Expand Down
12 changes: 12 additions & 0 deletions lib/dispatcher/client-h2.js
Expand Up @@ -405,6 +405,18 @@ function writeH2 (client, request) {
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
request.onResponseStarted()

// Due to the stream nature, it is possible we face a race condition
// where the stream has been assigned, but the request has been aborted
// the request remains in-flight and headers hasn't been received yet
// for those scenarios, best effort is to destroy the stream immediately
// as there's no value to keep it open.
if (request.aborted || request.completed) {
const err = new RequestAbortedError()
errorRequest(client, request, err)
util.destroy(stream, err)
return
}

if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
stream.pause()
}
Expand Down
5 changes: 4 additions & 1 deletion lib/mock/pending-interceptors-formatter.js
Expand Up @@ -3,6 +3,9 @@
const { Transform } = require('node:stream')
const { Console } = require('node:console')

const PERSISTENT = process.versions.icu ? '✅' : 'Y '
const NOT_PERSISTENT = process.versions.icu ? '❌' : 'N '

/**
* Gets the output of `console.table(…)` as a string.
*/
Expand All @@ -29,7 +32,7 @@ module.exports = class PendingInterceptorsFormatter {
Origin: origin,
Path: path,
'Status code': statusCode,
Persistent: persist ? '✅' : '❌',
Persistent: persist ? PERSISTENT : NOT_PERSISTENT,
Invocations: timesInvoked,
Remaining: persist ? Infinity : times - timesInvoked
}))
Expand Down
2 changes: 1 addition & 1 deletion lib/web/fetch/headers.js
Expand Up @@ -12,7 +12,7 @@ const {
} = require('./util')
const { webidl } = require('./webidl')
const assert = require('node:assert')
const util = require('util')
const util = require('node:util')

const kHeadersMap = Symbol('headers map')
const kHeadersSortedMap = Symbol('headers map sorted')
Expand Down
8 changes: 6 additions & 2 deletions package.json
Expand Up @@ -69,10 +69,13 @@
"lint:fix": "standard --fix | snazzy",
"test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript",
"test:javascript": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:jest",
"test:javascript:withoutintl": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch:nobuild && npm run test:cookies && npm run test:eventsource:nobuild && npm run test:wpt:withoutintl && npm run test:node-test",
"test:cookies": "borp -p \"test/cookie/*.js\"",
"test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"",
"test:eventsource": "npm run build:node && borp --expose-gc -p \"test/eventsource/*.js\"",
"test:fetch": "npm run build:node && borp --expose-gc -p \"test/fetch/*.js\" && borp -p \"test/webidl/*.js\" && borp -p \"test/busboy/*.js\"",
"test:eventsource": "npm run build:node && npm run test:eventsource:nobuild",
"test:eventsource:nobuild": "borp --expose-gc -p \"test/eventsource/*.js\"",
"test:fetch": "npm run build:node && npm run test:fetch:nobuild",
"test:fetch:nobuild": "borp --expose-gc -p \"test/fetch/*.js\" && borp -p \"test/webidl/*.js\" && borp -p \"test/busboy/*.js\"",
"test:jest": "cross-env NODE_V8_COVERAGE= jest",
"test:unit": "borp --expose-gc -p \"test/*.js\"",
"test:node-test": "borp -p \"test/node-test/**/*.js\"",
Expand All @@ -81,6 +84,7 @@
"test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
"test:websocket": "borp -p \"test/websocket/*.js\"",
"test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
"test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
"coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",
"coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report:ci",
"coverage:clean": "node ./scripts/clean-coverage.js",
Expand Down
2 changes: 1 addition & 1 deletion test/connect-timeout.js
Expand Up @@ -9,7 +9,7 @@ const assert = require('node:assert')
// Using describe instead of test to avoid the timeout
describe('prioritize socket errors over timeouts', async () => {
const t = tspl({ ...assert, after: () => {} }, { plan: 1 })
const client = new Pool('http://foobar.bar:1234', { connectTimeout: 1 })
const client = new Pool('http://foorbar.invalid:1234', { connectTimeout: 1 })

client.request({ method: 'GET', path: '/foobar' })
.then(() => t.fail())
Expand Down
2 changes: 1 addition & 1 deletion test/fetch/headers-inspect-custom.js
Expand Up @@ -3,7 +3,7 @@
const { Headers } = require('../../lib/web/fetch/headers')
const { test } = require('node:test')
const assert = require('node:assert')
const util = require('util')
const util = require('node:util')

test('Headers class custom inspection', () => {
const headers = new Headers()
Expand Down
4 changes: 2 additions & 2 deletions test/fetch/request-inspect-custom.js
@@ -1,8 +1,8 @@
'use strict'

const { describe, it } = require('node:test')
const assert = require('assert')
const util = require('util')
const assert = require('node:assert')
const util = require('node:util')
const { Request } = require('../../')

describe('Request custom inspection', () => {
Expand Down
4 changes: 2 additions & 2 deletions test/fetch/response-inspect-custom.js
@@ -1,8 +1,8 @@
'use strict'

const { describe, it } = require('node:test')
const assert = require('assert')
const util = require('util')
const assert = require('node:assert')
const util = require('node:util')
const { Response } = require('../../')

describe('Response custom inspection', () => {
Expand Down
85 changes: 85 additions & 0 deletions test/http2.js
Expand Up @@ -1296,3 +1296,88 @@ test('Should throw informational error on half-closed streams (remote)', async t
t.strictEqual(err.code, 'UND_ERR_INFO')
})
})

test('#2364 - Concurrent aborts', async t => {
const server = createSecureServer(pem)

server.on('stream', (stream, headers, _flags, rawHeaders) => {
t.strictEqual(headers['x-my-header'], 'foo')
t.strictEqual(headers[':method'], 'GET')
setTimeout(() => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200
})
stream.end('hello h2!')
}, 100)
})

server.listen(0)
await once(server, 'listening')

const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})

t = tspl(t, { plan: 18 })
after(() => server.close())
after(() => client.close())
const controller = new AbortController()

client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
}, (err, response) => {
t.ifError(err)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(response.statusCode, 200)
response.body.dump()
})

client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
},
signal: controller.signal
}, (err, response) => {
t.strictEqual(err.name, 'AbortError')
})

client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
}
}, (err, response) => {
t.ifError(err)
t.strictEqual(response.headers['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(response.headers['x-custom-h2'], 'hello')
t.strictEqual(response.statusCode, 200)
})

client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
},
signal: controller.signal
}, (err, response) => {
t.strictEqual(err.name, 'AbortError')
})

controller.abort()

await t.completed
})