19 Commits

Author SHA1 Message Date
crowlkats
70768ed567 fmt 2024-10-02 05:16:43 +02:00
crowlkats
5f06e8aafb feat: add warning for setup-deno v1 2024-10-02 05:13:56 +02:00
Luca Casonato
5e01c016a8 1.5.1 (#78) 2024-09-13 18:59:54 +02:00
Luca Casonato
90ced52839 fix: use npm install (#77) 2024-09-13 18:57:49 +02:00
Luca Casonato
084daf99a8 1.5.0 (#75) 2024-09-13 17:32:26 +02:00
Luca Casonato
8b162a5755 chore: improve readme (#74) 2024-09-13 17:32:16 +02:00
Luca Casonato
f1ac2c87b8 chore: migrate code to ESM (#73) 2024-09-13 17:15:41 +02:00
Leo Kettmeir
f8480e68ca feat: support rc version (#72) 2024-09-13 16:41:34 +02:00
Leo Kettmeir
3a041055d2 feat: allow specifying binary name (#71) 2024-09-12 03:46:45 -07:00
Luca Casonato
fa660b328d 1.4.1 (#69) 2024-09-03 10:36:21 +02:00
Luca Casonato
cce4306590 Don't put file versions at the top 2024-09-02 15:46:47 +02:00
Leo Kettmeir
916edb9a40 1.4.0 (#68) 2024-08-21 10:32:07 -07:00
Leo Kettmeir
b8a676db36 fix: use dl.deno.land for downloading binaries (#67) 2024-08-21 10:24:39 -07:00
Leo Kettmeir
ba9dcf3bc3 1.3.0 (#66) 2024-07-12 04:38:48 -07:00
Leo Kettmeir
2bca7ce523 feat: add "latest" as possible version (#65) 2024-07-12 04:32:45 -07:00
Yoshiya Hinosawa
f99b7edee3 1.2.0 (#63) 2024-07-05 16:19:39 +09:00
Jesse Dijkstra
edde9366ea feat: add .tool-versions and .dvmrc support (#61)
---------

Signed-off-by: Jesse Dijkstra <mail@jessedijkstra.nl>
Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
2024-07-05 15:33:22 +09:00
Yoshiya Hinosawa
041b854f97 1.1.4 (#57) 2024-01-29 13:20:15 +09:00
ctdeakin
ef28d469f1 chore: update node.js version from 16 to 20 (#56) 2024-01-28 21:01:15 +09:00
631 changed files with 68119 additions and 54015 deletions

1
.dvmrc Normal file
View File

@@ -0,0 +1 @@
1.43.1

View File

@@ -11,9 +11,17 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os:
- ubuntu-latest
- windows-latest
- macos-latest
deno: deno:
[1.x, "1.33.1", canary, ~1.32, b31cf9fde6ad5398c20370c136695db77df6beeb] - "1.x"
- "1.33.1"
- "canary"
- "~1.32"
- "b290fd01f3f5d32f9d010fc719ced0240759c049"
- "rc"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -26,15 +34,65 @@ jobs:
- name: Test Deno - name: Test Deno
run: deno run https://deno.land/std@0.198.0/examples/welcome.ts run: deno run https://deno.land/std@0.198.0/examples/welcome.ts
- name: Test `deno install` - name: Test `deno install -g` (2.0)
if: matrix.deno == 'canary' || matrix.deno == 'latest'
run: |
deno install -g --allow-net -n deno_curl https://deno.land/std@0.198.0/examples/curl.ts
deno_curl https://deno.land/std@0.198.0/examples/curl.ts
- name: Test `deno install (1.0)
if: matrix.deno == '1.x' || matrix.deno == '1.33.1' || matrix.deno == '~1.32' || matrix.deno == 'b290fd01f3f5d32f9d010fc719ced0240759c049'
run: | run: |
deno install --allow-net -n deno_curl https://deno.land/std@0.198.0/examples/curl.ts deno install --allow-net -n deno_curl https://deno.land/std@0.198.0/examples/curl.ts
deno_curl https://deno.land/std@0.198.0/examples/curl.ts deno_curl https://deno.land/std@0.198.0/examples/curl.ts
- name: Format test-version-file:
if: runner.os == 'Linux' && matrix.deno == 'canary' runs-on: ubuntu-latest
run: npm run fmt:check strategy:
matrix:
deno-version-file: [.dvmrc, .tool-versions]
steps:
- uses: actions/checkout@v3
- name: Setup Deno
uses: ./
with:
deno-version-file: ${{ matrix.deno-version-file }}
- name: Check version
run: deno -V | grep -q "deno 1\.43\.1"
test-binary-name:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: Setup Deno
uses: ./
with:
deno-binary-name: deno_foo
- name: Check binary exists
run: deno_foo -V
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Deno
uses: ./
with:
deno-version: "rc"
- name: Lint - name: Lint
if: runner.os == 'Linux' && matrix.deno == 'canary' run: deno lint
run: npm run lint
- name: Format
run: deno fmt --check
- name: Check types
run: deno check main.mjs

7
.tool-versions Normal file
View File

@@ -0,0 +1,7 @@
nodejs 20.5.1
bun 1.1.4
ruby 3.3.0
lua 5.4.4
deno 1.43.1
rust 1.65.0
python 3.11.0

1
CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
* @lucacasonato

View File

@@ -53,3 +53,83 @@ Targets the latest major, minor and patch version of Deno.
with: with:
deno-version: e7b7129b7a92b7500ded88f8f5baa25a7f59e56e deno-version: e7b7129b7a92b7500ded88f8f5baa25a7f59e56e
``` ```
### Latest release candidate
```yaml
- uses: denoland/setup-deno@v1
with:
deno-version: rc
```
### Specific release candidate
```yaml
- uses: denoland/setup-deno@v1
with:
deno-version: 2.0.0-rc.1
```
### Version from file
The extension can also automatically read the version file from
[`.tool-versions`](https://asdf-vm.com/manage/configuration.html#tool-versions)
```yaml
- uses: denoland/setup-deno@v1
with:
deno-version-file: .tool-versions
```
The extension can also automatically read the file from
[`dvm`](https://github.com/justjavac/dvm).
```yaml
- uses: denoland/setup-deno@v1
with:
deno-version-file: .dvmrc
```
### Specifying binary name
This is useful when you want to install different versions of Deno side by side.
```yaml
- uses: denoland/setup-deno@v1
with:
deno-version: canary
deno-binary-name: deno_canary
```
### Determining the release channel
You can determine the release channel reading back the `release-channel` output.
Valid values are `stable`, `canary` and `rc`.
```yaml
- uses: denoland/setup-deno@v1
id: deno
with:
deno-version: canary
- run: echo "Deno release channel is ${{ steps.deno.outputs.release-channel }}"
```
### Determining the installed version
You can determine the installed version reading back the `deno-version` output.
For canary versions, the output will be in the form `0.0.0-GIT_HASH`.
For stable and rc versions, the output will be the regular semver version
number.
```yaml
- uses: denoland/setup-deno@v1
id: deno
with:
deno-version: canary
- run: echo "Deno version is ${{ steps.deno.outputs.deno-version }}"
```

View File

@@ -8,11 +8,18 @@ inputs:
deno-version: deno-version:
description: The Deno version to install. Can be a semver version of a stable release, "canary" for the latest canary, or the Git hash of a specific canary release. description: The Deno version to install. Can be a semver version of a stable release, "canary" for the latest canary, or the Git hash of a specific canary release.
default: "1.x" default: "1.x"
deno-version-file:
description: File containing the Deno version to install such as .dvmrc or .tool-versions.
deno-binary-name:
description: The name to use for the binary.
default: "deno"
outputs: outputs:
deno-version: deno-version:
description: "The Deno version that was installed." description: "The Deno version that was installed."
is-canary: is-canary:
description: "If the installed Deno version was a canary version." description: "If the installed Deno version was a canary version."
release-channel:
description: "The release channel of the installed version."
runs: runs:
using: "node16" using: "node20"
main: "main.js" main: "main.mjs"

6
deno.json Normal file
View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"checkJs": true,
"allowJs": true
}
}

46
main.js
View File

@@ -1,46 +0,0 @@
const process = require("process");
const core = require("@actions/core");
const { parseVersionRange, resolveVersion } = require("./src/version.js");
const { install } = require("./src/install.js");
/**
* @param {string} message
* @returns {never}
*/
function exit(message) {
core.setFailed(message);
process.exit();
}
async function main() {
try {
const range = parseVersionRange(core.getInput("deno-version"));
if (range === null) {
exit("The passed version range is not valid.");
}
const version = await resolveVersion(range);
if (version === null) {
exit("Could not resolve a version for the given range.");
}
core.info(
`Going to install ${
version.isCanary ? "canary" : "stable"
} version ${version.version}.`,
);
await install(version);
core.setOutput("deno-version", version.version);
core.setOutput("is-canary", version.isCanary);
core.info("Installation complete.");
} catch (err) {
core.setFailed(err);
process.exit();
}
}
main();

57
main.mjs Normal file
View File

@@ -0,0 +1,57 @@
import process from "node:process";
import core from "@actions/core";
import {
getDenoVersionFromFile,
parseVersionRange,
resolveVersion,
} from "./src/version.mjs";
import { install } from "./src/install.mjs";
/**
* @param {string} message
* @returns {never}
*/
function exit(message) {
core.setFailed(message);
process.exit();
}
async function main() {
core.warning(
"Running on setup-deno@1, which is deprecated.\nPlease update to setup-deno@2, which defaults to Deno 2.0",
);
try {
const denoVersionFile = core.getInput("deno-version-file");
const range = parseVersionRange(
denoVersionFile
? getDenoVersionFromFile(denoVersionFile)
: core.getInput("deno-version"),
);
if (range === null) {
exit("The passed version range is not valid.");
}
const version = await resolveVersion(range);
if (version === null) {
exit("Could not resolve a version for the given range.");
}
core.info(`Going to install ${version.kind} version ${version.version}.`);
await install(version);
core.setOutput("deno-version", version.version);
// TODO(@crowlKats): remove in 2.0
core.setOutput("is-canary", version.kind === "canary");
core.setOutput("release-channel", version.kind);
core.info("Installation complete.");
} catch (err) {
core.setFailed((err instanceof Error) ? err : String(err));
process.exit();
}
}
main();

128
node_modules/.package-lock.json generated vendored
View File

@@ -1,13 +1,13 @@
{ {
"name": "setup-deno", "name": "setup-deno",
"version": "1.1.0", "version": "1.5.0",
"lockfileVersion": 2, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "1.10.0", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==",
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^2.0.1",
"uuid": "^8.3.2" "uuid": "^8.3.2"
@@ -22,17 +22,29 @@
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/http-client": {
"version": "2.0.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6" "tunnel": "^0.0.6",
"undici": "^5.25.4"
}
},
"node_modules/@actions/http-client/node_modules/undici": {
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"engines": {
"node": ">=14.0"
} }
}, },
"node_modules/@actions/io": { "node_modules/@actions/io": {
"version": "1.1.2", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="
}, },
"node_modules/@actions/tool-cache": { "node_modules/@actions/tool-cache": {
"version": "2.0.1", "version": "2.0.1",
@@ -48,9 +60,9 @@
} }
}, },
"node_modules/@actions/tool-cache/node_modules/semver": { "node_modules/@actions/tool-cache/node_modules/semver": {
"version": "6.3.0", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
@@ -64,47 +76,33 @@
"uuid": "bin/uuid" "uuid": "bin/uuid"
} }
}, },
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "16.11.66", "version": "20.16.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.66.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
"integrity": "sha512-+xvMrGl3eAygKcf5jm+4zA4tbfEgmKM9o6/glTmN0RFVdu2VuFXMYYtRmuv3zTGCgAYMnEZLde3B7BTp+Yxcig==", "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
"dev": true "dev": true,
"dependencies": {
"undici-types": "~6.19.2"
}
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.3.12", "version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==", "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true "dev": true
}, },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.3.8", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@@ -112,14 +110,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/tunnel": { "node_modules/tunnel": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -129,16 +119,19 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.11.0", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.11.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
"integrity": "sha512-oWjWJHzFet0Ow4YZBkyiJwiK5vWqEYoH7BINzJAJOLedZ++JpAlCbUktW2GQ2DS2FpKmxD/JMtWUUWl1BtghGw==", "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
"dependencies": {
"busboy": "^1.6.0"
},
"engines": { "engines": {
"node": ">=12.18" "node": ">=18.17"
} }
}, },
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -146,11 +139,6 @@
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
} }
} }
} }

View File

@@ -121,7 +121,7 @@ const result = await core.group('Do something async', async () => {
This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run). This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run).
```js ```js
core.error('This is a bad error. This will also fail the build.') core.error('This is a bad error, action may still succeed though.')
core.warning('Something went wrong, but it\'s not bad enough to fail the build.') core.warning('Something went wrong, but it\'s not bad enough to fail the build.')
@@ -163,7 +163,7 @@ export interface AnnotationProperties {
startColumn?: number startColumn?: number
/** /**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values. * The end column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
* Defaults to `startColumn` when `startColumn` is provided. * Defaults to `startColumn` when `startColumn` is provided.
*/ */
endColumn?: number endColumn?: number

View File

@@ -21,7 +21,7 @@ export declare enum ExitCode {
Failure = 1 Failure = 1
} }
/** /**
* Optional properties that can be sent with annotatation commands (notice, error, and warning) * Optional properties that can be sent with annotation commands (notice, error, and warning)
* See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations. * See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations.
*/ */
export interface AnnotationProperties { export interface AnnotationProperties {
@@ -46,7 +46,7 @@ export interface AnnotationProperties {
*/ */
startColumn?: number; startColumn?: number;
/** /**
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values. * The end column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
* Defaults to `startColumn` when `startColumn` is provided. * Defaults to `startColumn` when `startColumn` is provided.
*/ */
endColumn?: number; endColumn?: number;

View File

@@ -44,7 +44,7 @@ class OidcClient {
.catch(error => { .catch(error => {
throw new Error(`Failed to get ID Token. \n throw new Error(`Failed to get ID Token. \n
Error Code : ${error.statusCode}\n Error Code : ${error.statusCode}\n
Error Message: ${error.result.message}`); Error Message: ${error.message}`);
}); });
const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value;
if (!id_token) { if (!id_token) {

View File

@@ -1 +1 @@
{"version":3,"file":"oidc-utils.js","sourceRoot":"","sources":["../src/oidc-utils.ts"],"names":[],"mappings":";;;;;;;;;;;;AAGA,sDAA+C;AAC/C,wDAAqE;AACrE,iCAAuC;AAKvC,MAAa,UAAU;IACb,MAAM,CAAC,gBAAgB,CAC7B,UAAU,GAAG,IAAI,EACjB,QAAQ,GAAG,EAAE;QAEb,MAAM,cAAc,GAAmB;YACrC,YAAY,EAAE,UAAU;YACxB,UAAU,EAAE,QAAQ;SACrB,CAAA;QAED,OAAO,IAAI,wBAAU,CACnB,qBAAqB,EACrB,CAAC,IAAI,8BAAuB,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,CAAC,EAC3D,cAAc,CACf,CAAA;IACH,CAAC;IAEO,MAAM,CAAC,eAAe;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAA;QAC3D,IAAI,CAAC,KAAK,EAAE;YACV,MAAM,IAAI,KAAK,CACb,2DAA2D,CAC5D,CAAA;SACF;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAEO,MAAM,CAAC,aAAa;QAC1B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAA;QAC9D,IAAI,CAAC,UAAU,EAAE;YACf,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;SAC3E;QACD,OAAO,UAAU,CAAA;IACnB,CAAC;IAEO,MAAM,CAAO,OAAO,CAAC,YAAoB;;;YAC/C,MAAM,UAAU,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAA;YAEhD,MAAM,GAAG,GAAG,MAAM,UAAU;iBACzB,OAAO,CAAgB,YAAY,CAAC;iBACpC,KAAK,CAAC,KAAK,CAAC,EAAE;gBACb,MAAM,IAAI,KAAK,CACb;uBACa,KAAK,CAAC,UAAU;yBACd,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CACtC,CAAA;YACH,CAAC,CAAC,CAAA;YAEJ,MAAM,QAAQ,SAAG,GAAG,CAAC,MAAM,0CAAE,KAAK,CAAA;YAClC,IAAI,CAAC,QAAQ,EAAE;gBACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;aACjE;YACD,OAAO,QAAQ,CAAA;;KAChB;IAED,MAAM,CAAO,UAAU,CAAC,QAAiB;;YACvC,IAAI;gBACF,gDAAgD;gBAChD,IAAI,YAAY,GAAW,UAAU,CAAC,aAAa,EAAE,CAAA;gBACrD,IAAI,QAAQ,EAAE;oBACZ,MAAM,eAAe,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAA;oBACpD,YAAY,GAAG,GAAG,YAAY,aAAa,eAAe,EAAE,CAAA;iBAC7D;gBAED,YAAK,CAAC,mBAAmB,YAAY,EAAE,CAAC,CAAA;gBAExC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;gBACvD,gBAAS,CAAC,QAAQ,CAAC,CAAA;gBACnB,OAAO,QAAQ,CAAA;aAChB;YAAC,OAAO,KAAK,EAAE;gBACd,MAAM,IAAI,KAAK,CAAC,kBAAkB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;aACnD;QACH,CAAC;KAAA;CACF;AAzED,gCAyEC"} {"version":3,"file":"oidc-utils.js","sourceRoot":"","sources":["../src/oidc-utils.ts"],"names":[],"mappings":";;;;;;;;;;;;AAGA,sDAA+C;AAC/C,wDAAqE;AACrE,iCAAuC;AAKvC,MAAa,UAAU;IACb,MAAM,CAAC,gBAAgB,CAC7B,UAAU,GAAG,IAAI,EACjB,QAAQ,GAAG,EAAE;QAEb,MAAM,cAAc,GAAmB;YACrC,YAAY,EAAE,UAAU;YACxB,UAAU,EAAE,QAAQ;SACrB,CAAA;QAED,OAAO,IAAI,wBAAU,CACnB,qBAAqB,EACrB,CAAC,IAAI,8BAAuB,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,CAAC,EAC3D,cAAc,CACf,CAAA;IACH,CAAC;IAEO,MAAM,CAAC,eAAe;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAA;QAC3D,IAAI,CAAC,KAAK,EAAE;YACV,MAAM,IAAI,KAAK,CACb,2DAA2D,CAC5D,CAAA;SACF;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAEO,MAAM,CAAC,aAAa;QAC1B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAA;QAC9D,IAAI,CAAC,UAAU,EAAE;YACf,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;SAC3E;QACD,OAAO,UAAU,CAAA;IACnB,CAAC;IAEO,MAAM,CAAO,OAAO,CAAC,YAAoB;;;YAC/C,MAAM,UAAU,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAA;YAEhD,MAAM,GAAG,GAAG,MAAM,UAAU;iBACzB,OAAO,CAAgB,YAAY,CAAC;iBACpC,KAAK,CAAC,KAAK,CAAC,EAAE;gBACb,MAAM,IAAI,KAAK,CACb;uBACa,KAAK,CAAC,UAAU;yBACd,KAAK,CAAC,OAAO,EAAE,CAC/B,CAAA;YACH,CAAC,CAAC,CAAA;YAEJ,MAAM,QAAQ,SAAG,GAAG,CAAC,MAAM,0CAAE,KAAK,CAAA;YAClC,IAAI,CAAC,QAAQ,EAAE;gBACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;aACjE;YACD,OAAO,QAAQ,CAAA;;KAChB;IAED,MAAM,CAAO,UAAU,CAAC,QAAiB;;YACvC,IAAI;gBACF,gDAAgD;gBAChD,IAAI,YAAY,GAAW,UAAU,CAAC,aAAa,EAAE,CAAA;gBACrD,IAAI,QAAQ,EAAE;oBACZ,MAAM,eAAe,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAA;oBACpD,YAAY,GAAG,GAAG,YAAY,aAAa,eAAe,EAAE,CAAA;iBAC7D;gBAED,YAAK,CAAC,mBAAmB,YAAY,EAAE,CAAC,CAAA;gBAExC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;gBACvD,gBAAS,CAAC,QAAQ,CAAC,CAAA;gBACnB,OAAO,QAAQ,CAAA;aAChB;YAAC,OAAO,KAAK,EAAE;gBACd,MAAM,IAAI,KAAK,CAAC,kBAAkB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;aACnD;QACH,CAAC;KAAA;CACF;AAzED,gCAyEC"}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.10.0", "version": "1.10.1",
"description": "Actions core lib", "description": "Actions core lib",
"keywords": [ "keywords": [
"github", "github",
@@ -30,7 +30,7 @@
"scripts": { "scripts": {
"audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json", "audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json",
"test": "echo \"Error: run tests from root\" && exit 1", "test": "echo \"Error: run tests from root\" && exit 1",
"tsc": "tsc" "tsc": "tsc -p tsconfig.json"
}, },
"bugs": { "bugs": {
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
@@ -43,4 +43,4 @@
"@types/node": "^12.0.2", "@types/node": "^12.0.2",
"@types/uuid": "^8.3.4" "@types/uuid": "^8.3.4"
} }
} }

View File

@@ -1 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":";;;;;;;;;;;;AAIA,MAAa,sBAAsB;IAIjC,YAAY,QAAgB,EAAE,QAAgB;QAC5C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAED,cAAc,CAAC,OAA4B;QACzC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YACpB,MAAM,KAAK,CAAC,4BAA4B,CAAC,CAAA;SAC1C;QACD,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,SAAS,MAAM,CAAC,IAAI,CACrD,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,EAAE,CACpC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAA;IACxB,CAAC;IAED,iCAAiC;IACjC,uBAAuB;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAEK,oBAAoB;;YACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;QACpC,CAAC;KAAA;CACF;AA1BD,wDA0BC;AAED,MAAa,uBAAuB;IAGlC,YAAY,KAAa;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;IAED,yCAAyC;IACzC,sDAAsD;IACtD,cAAc,CAAC,OAA4B;QACzC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YACpB,MAAM,KAAK,CAAC,4BAA4B,CAAC,CAAA;SAC1C;QACD,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,IAAI,CAAC,KAAK,EAAE,CAAA;IAC3D,CAAC;IAED,iCAAiC;IACjC,uBAAuB;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAEK,oBAAoB;;YACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;QACpC,CAAC;KAAA;CACF;AAxBD,0DAwBC;AAED,MAAa,oCAAoC;IAI/C,YAAY,KAAa;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;IAED,yCAAyC;IACzC,sDAAsD;IACtD,cAAc,CAAC,OAA4B;QACzC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YACpB,MAAM,KAAK,CAAC,4BAA4B,CAAC,CAAA;SAC1C;QACD,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,SAAS,MAAM,CAAC,IAAI,CACrD,OAAO,IAAI,CAAC,KAAK,EAAE,CACpB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAA;IACxB,CAAC;IAED,iCAAiC;IACjC,uBAAuB;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAEK,oBAAoB;;YACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;QACpC,CAAC;KAAA;CACF;AA3BD,oFA2BC"} {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":";;;;;;;;;;;;AAIA,MAAa,sBAAsB;IAIjC,YAAY,QAAgB,EAAE,QAAgB;QAC5C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAED,cAAc,CAAC,OAA4B;QACzC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YACpB,MAAM,KAAK,CAAC,4BAA4B,CAAC,CAAA;SAC1C;QACD,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,SAAS,MAAM,CAAC,IAAI,CACrD,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,EAAE,CACpC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAA;IACxB,CAAC;IAED,iCAAiC;IACjC,uBAAuB;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAEK,oBAAoB;;YACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;QACpC,CAAC;KAAA;CACF;AA1BD,wDA0BC;AAED,MAAa,uBAAuB;IAGlC,YAAY,KAAa;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;IAED,yCAAyC;IACzC,sDAAsD;IACtD,cAAc,CAAC,OAA4B;QACzC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YACpB,MAAM,KAAK,CAAC,4BAA4B,CAAC,CAAA;SAC1C;QACD,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,IAAI,CAAC,KAAK,EAAE,CAAA;IAC3D,CAAC;IAED,iCAAiC;IACjC,uBAAuB;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAEK,oBAAoB;;YACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;QACpC,CAAC;KAAA;CACF;AAxBD,0DAwBC;AAED,MAAa,oCAAoC;IAK/C,YAAY,KAAa;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;IAED,yCAAyC;IACzC,sDAAsD;IACtD,cAAc,CAAC,OAA4B;QACzC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YACpB,MAAM,KAAK,CAAC,4BAA4B,CAAC,CAAA;SAC1C;QACD,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,SAAS,MAAM,CAAC,IAAI,CACrD,OAAO,IAAI,CAAC,KAAK,EAAE,CACpB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAA;IACxB,CAAC;IAED,iCAAiC;IACjC,uBAAuB;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAEK,oBAAoB;;YACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAA;QACpC,CAAC;KAAA;CACF;AA5BD,oFA4BC"}

View File

@@ -1,6 +1,9 @@
/// <reference types="node" /> /// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
import * as http from 'http'; import * as http from 'http';
import * as ifm from './interfaces'; import * as ifm from './interfaces';
import { ProxyAgent } from 'undici';
export declare enum HttpCodes { export declare enum HttpCodes {
OK = 200, OK = 200,
MultipleChoices = 300, MultipleChoices = 300,
@@ -51,6 +54,7 @@ export declare class HttpClientResponse {
constructor(message: http.IncomingMessage); constructor(message: http.IncomingMessage);
message: http.IncomingMessage; message: http.IncomingMessage;
readBody(): Promise<string>; readBody(): Promise<string>;
readBodyBuffer?(): Promise<Buffer>;
} }
export declare function isHttps(requestUrl: string): boolean; export declare function isHttps(requestUrl: string): boolean;
export declare class HttpClient { export declare class HttpClient {
@@ -66,6 +70,7 @@ export declare class HttpClient {
private _maxRetries; private _maxRetries;
private _agent; private _agent;
private _proxyAgent; private _proxyAgent;
private _proxyAgentDispatcher;
private _keepAlive; private _keepAlive;
private _disposed; private _disposed;
constructor(userAgent?: string, handlers?: ifm.RequestHandler[], requestOptions?: ifm.RequestOptions); constructor(userAgent?: string, handlers?: ifm.RequestHandler[], requestOptions?: ifm.RequestOptions);
@@ -114,10 +119,12 @@ export declare class HttpClient {
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
*/ */
getAgent(serverUrl: string): http.Agent; getAgent(serverUrl: string): http.Agent;
getAgentDispatcher(serverUrl: string): ProxyAgent | undefined;
private _prepareRequest; private _prepareRequest;
private _mergeHeaders; private _mergeHeaders;
private _getExistingOrDefaultHeader; private _getExistingOrDefaultHeader;
private _getAgent; private _getAgent;
private _getProxyAgentDispatcher;
private _performExponentialBackoff; private _performExponentialBackoff;
private _processResponse; private _processResponse;
} }

View File

@@ -2,7 +2,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k; if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) { }) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k; if (k2 === undefined) k2 = k;
o[k2] = m[k]; o[k2] = m[k];
@@ -15,7 +19,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
var __importStar = (this && this.__importStar) || function (mod) { var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod; if (mod && mod.__esModule) return mod;
var result = {}; var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod); __setModuleDefault(result, mod);
return result; return result;
}; };
@@ -34,6 +38,7 @@ const http = __importStar(require("http"));
const https = __importStar(require("https")); const https = __importStar(require("https"));
const pm = __importStar(require("./proxy")); const pm = __importStar(require("./proxy"));
const tunnel = __importStar(require("tunnel")); const tunnel = __importStar(require("tunnel"));
const undici_1 = require("undici");
var HttpCodes; var HttpCodes;
(function (HttpCodes) { (function (HttpCodes) {
HttpCodes[HttpCodes["OK"] = 200] = "OK"; HttpCodes[HttpCodes["OK"] = 200] = "OK";
@@ -63,16 +68,16 @@ var HttpCodes;
HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway";
HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable";
HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout";
})(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {})); })(HttpCodes || (exports.HttpCodes = HttpCodes = {}));
var Headers; var Headers;
(function (Headers) { (function (Headers) {
Headers["Accept"] = "accept"; Headers["Accept"] = "accept";
Headers["ContentType"] = "content-type"; Headers["ContentType"] = "content-type";
})(Headers = exports.Headers || (exports.Headers = {})); })(Headers || (exports.Headers = Headers = {}));
var MediaTypes; var MediaTypes;
(function (MediaTypes) { (function (MediaTypes) {
MediaTypes["ApplicationJson"] = "application/json"; MediaTypes["ApplicationJson"] = "application/json";
})(MediaTypes = exports.MediaTypes || (exports.MediaTypes = {})); })(MediaTypes || (exports.MediaTypes = MediaTypes = {}));
/** /**
* Returns the proxy URL, depending upon the supplied url and proxy environment variables. * Returns the proxy URL, depending upon the supplied url and proxy environment variables.
* @param serverUrl The server URL where the request will be sent. For example, https://api.github.com * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
@@ -123,6 +128,19 @@ class HttpClientResponse {
})); }));
}); });
} }
readBodyBuffer() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
const chunks = [];
this.message.on('data', (chunk) => {
chunks.push(chunk);
});
this.message.on('end', () => {
resolve(Buffer.concat(chunks));
});
}));
});
}
} }
exports.HttpClientResponse = HttpClientResponse; exports.HttpClientResponse = HttpClientResponse;
function isHttps(requestUrl) { function isHttps(requestUrl) {
@@ -428,6 +446,15 @@ class HttpClient {
const parsedUrl = new URL(serverUrl); const parsedUrl = new URL(serverUrl);
return this._getAgent(parsedUrl); return this._getAgent(parsedUrl);
} }
getAgentDispatcher(serverUrl) {
const parsedUrl = new URL(serverUrl);
const proxyUrl = pm.getProxyUrl(parsedUrl);
const useProxy = proxyUrl && proxyUrl.hostname;
if (!useProxy) {
return;
}
return this._getProxyAgentDispatcher(parsedUrl, proxyUrl);
}
_prepareRequest(method, requestUrl, headers) { _prepareRequest(method, requestUrl, headers) {
const info = {}; const info = {};
info.parsedUrl = requestUrl; info.parsedUrl = requestUrl;
@@ -475,7 +502,7 @@ class HttpClient {
if (this._keepAlive && useProxy) { if (this._keepAlive && useProxy) {
agent = this._proxyAgent; agent = this._proxyAgent;
} }
if (this._keepAlive && !useProxy) { if (!useProxy) {
agent = this._agent; agent = this._agent;
} }
// if agent is already assigned use that agent. // if agent is already assigned use that agent.
@@ -507,16 +534,12 @@ class HttpClient {
agent = tunnelAgent(agentOptions); agent = tunnelAgent(agentOptions);
this._proxyAgent = agent; this._proxyAgent = agent;
} }
// if reusing agent across request and tunneling agent isn't assigned create a new agent // if tunneling agent isn't assigned create a new agent
if (this._keepAlive && !agent) { if (!agent) {
const options = { keepAlive: this._keepAlive, maxSockets }; const options = { keepAlive: this._keepAlive, maxSockets };
agent = usingSsl ? new https.Agent(options) : new http.Agent(options); agent = usingSsl ? new https.Agent(options) : new http.Agent(options);
this._agent = agent; this._agent = agent;
} }
// if not using private agent and tunnel agent isn't setup then use global agent
if (!agent) {
agent = usingSsl ? https.globalAgent : http.globalAgent;
}
if (usingSsl && this._ignoreSslError) { if (usingSsl && this._ignoreSslError) {
// we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
// http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
@@ -527,6 +550,30 @@ class HttpClient {
} }
return agent; return agent;
} }
_getProxyAgentDispatcher(parsedUrl, proxyUrl) {
let proxyAgent;
if (this._keepAlive) {
proxyAgent = this._proxyAgentDispatcher;
}
// if agent is already assigned use that agent.
if (proxyAgent) {
return proxyAgent;
}
const usingSsl = parsedUrl.protocol === 'https:';
proxyAgent = new undici_1.ProxyAgent(Object.assign({ uri: proxyUrl.href, pipelining: !this._keepAlive ? 0 : 1 }, ((proxyUrl.username || proxyUrl.password) && {
token: `Basic ${Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`).toString('base64')}`
})));
this._proxyAgentDispatcher = proxyAgent;
if (usingSsl && this._ignoreSslError) {
// we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
// http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
// we have to cast it to any and change it directly
proxyAgent.options = Object.assign(proxyAgent.options.requestTls || {}, {
rejectUnauthorized: false
});
}
return proxyAgent;
}
_performExponentialBackoff(retryNumber) { _performExponentialBackoff(retryNumber) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber);

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,6 @@
/// <reference types="node" /> /// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { HttpClientResponse } from './index'; import { HttpClientResponse } from './index';

View File

@@ -15,7 +15,13 @@ function getProxyUrl(reqUrl) {
} }
})(); })();
if (proxyVar) { if (proxyVar) {
return new URL(proxyVar); try {
return new DecodedURL(proxyVar);
}
catch (_a) {
if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://'))
return new DecodedURL(`http://${proxyVar}`);
}
} }
else { else {
return undefined; return undefined;
@@ -26,6 +32,10 @@ function checkBypass(reqUrl) {
if (!reqUrl.hostname) { if (!reqUrl.hostname) {
return false; return false;
} }
const reqHost = reqUrl.hostname;
if (isLoopbackAddress(reqHost)) {
return true;
}
const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || '';
if (!noProxy) { if (!noProxy) {
return false; return false;
@@ -51,11 +61,35 @@ function checkBypass(reqUrl) {
.split(',') .split(',')
.map(x => x.trim().toUpperCase()) .map(x => x.trim().toUpperCase())
.filter(x => x)) { .filter(x => x)) {
if (upperReqHosts.some(x => x === upperNoProxyItem)) { if (upperNoProxyItem === '*' ||
upperReqHosts.some(x => x === upperNoProxyItem ||
x.endsWith(`.${upperNoProxyItem}`) ||
(upperNoProxyItem.startsWith('.') &&
x.endsWith(`${upperNoProxyItem}`)))) {
return true; return true;
} }
} }
return false; return false;
} }
exports.checkBypass = checkBypass; exports.checkBypass = checkBypass;
function isLoopbackAddress(host) {
const hostLower = host.toLowerCase();
return (hostLower === 'localhost' ||
hostLower.startsWith('127.') ||
hostLower.startsWith('[::1]') ||
hostLower.startsWith('[0:0:0:0:0:0:0:1]'));
}
class DecodedURL extends URL {
constructor(url, base) {
super(url, base);
this._decodedUsername = decodeURIComponent(super.username);
this._decodedPassword = decodeURIComponent(super.password);
}
get username() {
return this._decodedUsername;
}
get password() {
return this._decodedPassword;
}
}
//# sourceMappingURL=proxy.js.map //# sourceMappingURL=proxy.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":";;;AAAA,SAAgB,WAAW,CAAC,MAAW;IACrC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAA;IAE7C,IAAI,WAAW,CAAC,MAAM,CAAC,EAAE;QACvB,OAAO,SAAS,CAAA;KACjB;IAED,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE;QACrB,IAAI,QAAQ,EAAE;YACZ,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;SAChE;aAAM;YACL,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;SAC9D;IACH,CAAC,CAAC,EAAE,CAAA;IAEJ,IAAI,QAAQ,EAAE;QACZ,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAA;KACzB;SAAM;QACL,OAAO,SAAS,CAAA;KACjB;AACH,CAAC;AApBD,kCAoBC;AAED,SAAgB,WAAW,CAAC,MAAW;IACrC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;QACpB,OAAO,KAAK,CAAA;KACb;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAA;IACxE,IAAI,CAAC,OAAO,EAAE;QACZ,OAAO,KAAK,CAAA;KACb;IAED,6BAA6B;IAC7B,IAAI,OAA2B,CAAA;IAC/B,IAAI,MAAM,CAAC,IAAI,EAAE;QACf,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;KAC9B;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE;QACtC,OAAO,GAAG,EAAE,CAAA;KACb;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE;QACvC,OAAO,GAAG,GAAG,CAAA;KACd;IAED,qDAAqD;IACrD,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;IACrD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE;QAC/B,aAAa,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC,CAAA;KACrD;IAED,uCAAuC;IACvC,KAAK,MAAM,gBAAgB,IAAI,OAAO;SACnC,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;SAChC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;QACjB,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,gBAAgB,CAAC,EAAE;YACnD,OAAO,IAAI,CAAA;SACZ;KACF;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AArCD,kCAqCC"} {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":";;;AAAA,SAAgB,WAAW,CAAC,MAAW;IACrC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAA;IAE7C,IAAI,WAAW,CAAC,MAAM,CAAC,EAAE;QACvB,OAAO,SAAS,CAAA;KACjB;IAED,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE;QACrB,IAAI,QAAQ,EAAE;YACZ,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;SAChE;aAAM;YACL,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;SAC9D;IACH,CAAC,CAAC,EAAE,CAAA;IAEJ,IAAI,QAAQ,EAAE;QACZ,IAAI;YACF,OAAO,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAA;SAChC;QAAC,WAAM;YACN,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC;gBACrE,OAAO,IAAI,UAAU,CAAC,UAAU,QAAQ,EAAE,CAAC,CAAA;SAC9C;KACF;SAAM;QACL,OAAO,SAAS,CAAA;KACjB;AACH,CAAC;AAzBD,kCAyBC;AAED,SAAgB,WAAW,CAAC,MAAW;IACrC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;QACpB,OAAO,KAAK,CAAA;KACb;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAA;IAC/B,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE;QAC9B,OAAO,IAAI,CAAA;KACZ;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAA;IACxE,IAAI,CAAC,OAAO,EAAE;QACZ,OAAO,KAAK,CAAA;KACb;IAED,6BAA6B;IAC7B,IAAI,OAA2B,CAAA;IAC/B,IAAI,MAAM,CAAC,IAAI,EAAE;QACf,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;KAC9B;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE;QACtC,OAAO,GAAG,EAAE,CAAA;KACb;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE;QACvC,OAAO,GAAG,GAAG,CAAA;KACd;IAED,qDAAqD;IACrD,MAAM,aAAa,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;IACrD,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE;QAC/B,aAAa,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC,CAAA;KACrD;IAED,uCAAuC;IACvC,KAAK,MAAM,gBAAgB,IAAI,OAAO;SACnC,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;SAChC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;QACjB,IACE,gBAAgB,KAAK,GAAG;YACxB,aAAa,CAAC,IAAI,CAChB,CAAC,CAAC,EAAE,CACF,CAAC,KAAK,gBAAgB;gBACtB,CAAC,CAAC,QAAQ,CAAC,IAAI,gBAAgB,EAAE,CAAC;gBAClC,CAAC,gBAAgB,CAAC,UAAU,CAAC,GAAG,CAAC;oBAC/B,CAAC,CAAC,QAAQ,CAAC,GAAG,gBAAgB,EAAE,CAAC,CAAC,CACvC,EACD;YACA,OAAO,IAAI,CAAA;SACZ;KACF;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAnDD,kCAmDC;AAED,SAAS,iBAAiB,CAAC,IAAY;IACrC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;IACpC,OAAO,CACL,SAAS,KAAK,WAAW;QACzB,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC;QAC5B,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC;QAC7B,SAAS,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAC1C,CAAA;AACH,CAAC;AAED,MAAM,UAAW,SAAQ,GAAG;IAI1B,YAAY,GAAiB,EAAE,IAAmB;QAChD,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QAChB,IAAI,CAAC,gBAAgB,GAAG,kBAAkB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QAC1D,IAAI,CAAC,gBAAgB,GAAG,kBAAkB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC5D,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,gBAAgB,CAAA;IAC9B,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,gBAAgB,CAAA;IAC9B,CAAC;CACF"}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Matteo Collina and Undici contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,443 @@
# undici
[![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) [![npm version](https://badge.fury.io/js/undici.svg)](https://badge.fury.io/js/undici) [![codecov](https://codecov.io/gh/nodejs/undici/branch/main/graph/badge.svg?token=yZL6LtXkOA)](https://codecov.io/gh/nodejs/undici)
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.
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.
## Install
```
npm i undici
```
## Benchmarks
The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a
number of unix sockets (connections) with a pipelining depth of 10 running on Node 20.6.0.
### Connections 1
| Tests | Samples | Result | Tolerance | Difference with slowest |
|---------------------|---------|---------------|-----------|-------------------------|
| http - no keepalive | 15 | 5.32 req/sec | ± 2.61 % | - |
| http - keepalive | 10 | 5.35 req/sec | ± 2.47 % | + 0.44 % |
| undici - fetch | 15 | 41.85 req/sec | ± 2.49 % | + 686.04 % |
| undici - pipeline | 40 | 50.36 req/sec | ± 2.77 % | + 845.92 % |
| undici - stream | 15 | 60.58 req/sec | ± 2.75 % | + 1037.72 % |
| undici - request | 10 | 61.19 req/sec | ± 2.60 % | + 1049.24 % |
| undici - dispatch | 20 | 64.84 req/sec | ± 2.81 % | + 1117.81 % |
### Connections 50
| Tests | Samples | Result | Tolerance | Difference with slowest |
|---------------------|---------|------------------|-----------|-------------------------|
| undici - fetch | 30 | 2107.19 req/sec | ± 2.69 % | - |
| http - no keepalive | 10 | 2698.90 req/sec | ± 2.68 % | + 28.08 % |
| http - keepalive | 10 | 4639.49 req/sec | ± 2.55 % | + 120.17 % |
| undici - pipeline | 40 | 6123.33 req/sec | ± 2.97 % | + 190.59 % |
| undici - stream | 50 | 9426.51 req/sec | ± 2.92 % | + 347.35 % |
| undici - request | 10 | 10162.88 req/sec | ± 2.13 % | + 382.29 % |
| undici - dispatch | 50 | 11191.11 req/sec | ± 2.98 % | + 431.09 % |
## Quick Start
```js
import { request } from 'undici'
const {
statusCode,
headers,
trailers,
body
} = await request('http://localhost:3000/foo')
console.log('response received', statusCode)
console.log('headers', headers)
for await (const data of body) {
console.log('data', data)
}
console.log('trailers', trailers)
```
## Body Mixins
The `body` mixins are the most common way to format the request/response body. Mixins include:
- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
Example usage:
```js
import { request } from 'undici'
const {
statusCode,
headers,
trailers,
body
} = await request('http://localhost:3000/foo')
console.log('response received', statusCode)
console.log('headers', headers)
console.log('data', await body.json())
console.log('trailers', trailers)
```
_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._
Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format.
For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
## Common API Methods
This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
### `undici.request([url, options]): Promise`
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
* **maxRedirections** `Integer` - Default: `0`
Returns a promise with the result of the `Dispatcher.request` method.
Calls `options.dispatcher.request(options)`.
See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details.
### `undici.stream([url, options, ]factory): Promise`
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
* **maxRedirections** `Integer` - Default: `0`
* **factory** `Dispatcher.stream.factory`
Returns a promise with the result of the `Dispatcher.stream` method.
Calls `options.dispatcher.stream(options, factory)`.
See [Dispatcher.stream](docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
### `undici.pipeline([url, options, ]handler): Duplex`
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`PipelineOptions`](docs/api/Dispatcher.md#parameter-pipelineoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
* **maxRedirections** `Integer` - Default: `0`
* **handler** `Dispatcher.pipeline.handler`
Returns: `stream.Duplex`
Calls `options.dispatch.pipeline(options, handler)`.
See [Dispatcher.pipeline](docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
### `undici.connect([url, options]): Promise`
Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT).
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`ConnectOptions`](docs/api/Dispatcher.md#parameter-connectoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **maxRedirections** `Integer` - Default: `0`
* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
Returns a promise with the result of the `Dispatcher.connect` method.
Calls `options.dispatch.connect(options)`.
See [Dispatcher.connect](docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
### `undici.fetch(input[, init]): Promise`
Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
* https://fetch.spec.whatwg.org/#fetch-method
Only supported on Node 16.8+.
Basic usage example:
```js
import { fetch } from 'undici'
const res = await fetch('https://example.com')
const json = await res.json()
console.log(json)
```
You can pass an optional dispatcher to `fetch` as:
```js
import { fetch, Agent } from 'undici'
const res = await fetch('https://example.com', {
// Mocks are also supported
dispatcher: new Agent({
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10
})
})
const json = await res.json()
console.log(json)
```
#### `request.body`
A body can be of the following types:
- ArrayBuffer
- ArrayBufferView
- AsyncIterables
- Blob
- Iterables
- String
- URLSearchParams
- FormData
In this implementation of fetch, ```request.body``` now accepts ```Async Iterables```. It is not present in the [Fetch Standard.](https://fetch.spec.whatwg.org)
```js
import { fetch } from 'undici'
const data = {
async *[Symbol.asyncIterator]() {
yield 'hello'
yield 'world'
},
}
await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
```
#### `request.duplex`
- half
In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`. And fetch requests are currently always be full duplex. More detail refer to [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex)
#### `response.body`
Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
```js
import { fetch } from 'undici'
import { Readable } from 'node:stream'
const response = await fetch('https://example.com')
const readableWebStream = response.body
const readableNodeStream = Readable.fromWeb(readableWebStream)
```
#### Specification Compliance
This section documents parts of the [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
not support or does not fully implement.
##### Garbage Collection
* https://fetch.spec.whatwg.org/#garbage-collection
The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body.
Garbage collection in Node is less aggressive and deterministic
(due to the lack of clear idle periods that browsers have through the rendering refresh rate)
which means that leaving the release of connection resources to the garbage collector can lead
to excessive connection usage, reduced performance (due to less connection re-use), and even
stalls or deadlocks when running out of connections.
```js
// Do
const headers = await fetch(url)
.then(async res => {
for await (const chunk of res.body) {
// force consumption of body
}
return res.headers
})
// Do not
const headers = await fetch(url)
.then(res => res.headers)
```
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
```js
const headers = await fetch(url, { method: 'HEAD' })
.then(res => res.headers)
```
##### Forbidden and Safelisted Header Names
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
* https://fetch.spec.whatwg.org/#forbidden-header-name
* https://fetch.spec.whatwg.org/#forbidden-response-header-name
* https://github.com/wintercg/fetch/issues/6
The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
### `undici.upgrade([url, options]): Promise`
Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
Arguments:
* **url** `string | URL | UrlObject`
* **options** [`UpgradeOptions`](docs/api/Dispatcher.md#parameter-upgradeoptions)
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
* **maxRedirections** `Integer` - Default: `0`
* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
Returns a promise with the result of the `Dispatcher.upgrade` method.
Calls `options.dispatcher.upgrade(options)`.
See [Dispatcher.upgrade](docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
### `undici.setGlobalDispatcher(dispatcher)`
* dispatcher `Dispatcher`
Sets the global dispatcher used by Common API Methods.
### `undici.getGlobalDispatcher()`
Gets the global dispatcher used by Common API Methods.
Returns: `Dispatcher`
### `undici.setGlobalOrigin(origin)`
* origin `string | URL | undefined`
Sets the global origin used in `fetch`.
If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed.
```js
setGlobalOrigin('http://localhost:3000')
const response = await fetch('/api/ping')
console.log(response.url) // http://localhost:3000/api/ping
```
### `undici.getGlobalOrigin()`
Gets the global origin used in `fetch`.
Returns: `URL`
### `UrlObject`
* **port** `string | number` (optional)
* **path** `string` (optional)
* **pathname** `string` (optional)
* **hostname** `string` (optional)
* **origin** `string` (optional)
* **protocol** `string` (optional)
* **search** `string` (optional)
## Specification Compliance
This section documents parts of the HTTP/1.1 specification that Undici does
not support or does not fully implement.
### Expect
Undici does not support the `Expect` request header field. The request
body is always immediately sent and the `100 Continue` response will be
ignored.
Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
### Pipelining
Undici will only use pipelining if configured with a `pipelining` factor
greater than `1`.
Undici always assumes that connections are persistent and will immediately
pipeline requests, without checking whether the connection is persistent.
Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is
not supported.
Undici will immediately pipeline when retrying requests after a failed
connection. However, Undici will not retry the first remaining requests in
the prior pipeline and instead error the corresponding callback/promise/stream.
Undici will abort all running requests in the pipeline when any of them are
aborted.
* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2
* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
### Manual Redirect
Since it is not possible to manually follow an HTTP redirect on the server-side,
Undici returns the actual response instead of an `opaqueredirect` filtered one
when invoked with a `manual` redirect. This aligns `fetch()` with the other
implementations in Deno and Cloudflare Workers.
Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
## Workarounds
### Network address family autoselection.
If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record)
first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case
undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`.
If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version
(18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request`
and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection.
## Collaborators
* [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
* [__Szymon Marczak__](https://github.com/szmarczak), <https://www.npmjs.com/~szmarczak>
* [__Tomas Della Vedova__](https://github.com/delvedor), <https://www.npmjs.com/~delvedor>
### Releasers
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
## License
MIT

View File

@@ -0,0 +1,30 @@
# CacheStorage
Undici exposes a W3C spec-compliant implementation of [CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) and [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
## Opening a Cache
Undici exports a top-level CacheStorage instance. You can open a new Cache, or duplicate a Cache with an existing name, by using `CacheStorage.prototype.open`. If you open a Cache with the same name as an already-existing Cache, its list of cached Responses will be shared between both instances.
```mjs
import { caches } from 'undici'
const cache_1 = await caches.open('v1')
const cache_2 = await caches.open('v1')
// Although .open() creates a new instance,
assert(cache_1 !== cache_2)
// The same Response is matched in both.
assert.deepStrictEqual(await cache_1.match('/req'), await cache_2.match('/req'))
```
## Deleting a Cache
If a Cache is deleted, the cached Responses/Requests can still be used.
```mjs
const response = await cache_1.match('/req')
await caches.delete('v1')
await response.text() // the Response's body
```

View File

@@ -17,16 +17,23 @@ Returns: `Client`
### Parameter: `ClientOptions` ### Parameter: `ClientOptions`
* **bodyTimeout** `number | null` (optional) - Default: `30e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds. > ⚠️ Warning: The `H2` support is experimental.
* **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds.
* **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout` when overridden by *keep-alive* hints from the server. Defaults to 10 minutes. * **bodyTimeout** `number | null` (optional) - Default: `300e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
* **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds. * **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second. * **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. Defaults to 10 minutes.
* **maxHeaderSize** `number | null` (optional) - Default: `16384` - The maximum length of request headers in bytes. Defaults to 16KiB. * **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second.
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
* **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time. * **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time.
* **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
* **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
#### Parameter: `ConnectOptions` #### Parameter: `ConnectOptions`
@@ -35,8 +42,10 @@ Furthermore, the following options can be passed:
* **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe. * **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: 100. * **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: 100.
* **timeout** `number | null` (optional) - Default `10e3` * **timeout** `number | null` (optional) - In milliseconds, Default `10e3`.
* **servername** `string | null` (optional) * **servername** `string | null` (optional)
* **keepAlive** `boolean | null` (optional) - Default: `true` - TCP keep-alive enabled
* **keepAliveInitialDelay** `number | null` (optional) - Default: `60000` - TCP keep-alive interval for the socket in milliseconds
### Example - Basic Client instantiation ### Example - Basic Client instantiation

View File

@@ -13,8 +13,8 @@ Every Tls option, see [here](https://nodejs.org/api/tls.html#tls_tls_connect_opt
Furthermore, the following options can be passed: Furthermore, the following options can be passed:
* **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe. * **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: 100. * **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: `100`.
* **timeout** `number | null` (optional) - Default `10e3` * **timeout** `number | null` (optional) - In milliseconds. Default `10e3`.
* **servername** `string | null` (optional) * **servername** `string | null` (optional)
Once you call `buildConnector`, it will return a connector function, which takes the following parameters. Once you call `buildConnector`, it will return a connector function, which takes the following parameters.
@@ -24,8 +24,10 @@ Once you call `buildConnector`, it will return a connector function, which takes
* **hostname** `string` (required) * **hostname** `string` (required)
* **host** `string` (optional) * **host** `string` (optional)
* **protocol** `string` (required) * **protocol** `string` (required)
* **port** `number` (required) * **port** `string` (required)
* **servername** `string` (optional) * **servername** `string` (optional)
* **localAddress** `string | null` (optional) Local address the socket should connect from.
* **httpSocket** `Socket` (optional) Establish secure connection on a given socket rather than creating a new socket. It can only be sent on TLS update.
### Basic example ### Basic example

View File

@@ -0,0 +1,57 @@
# MIME Type Parsing
## `MIMEType` interface
* **type** `string`
* **subtype** `string`
* **parameters** `Map<string, string>`
* **essence** `string`
## `parseMIMEType(input)`
Implements [parse a MIME type](https://mimesniff.spec.whatwg.org/#parse-a-mime-type).
Parses a MIME type, returning its type, subtype, and any associated parameters. If the parser can't parse an input it returns the string literal `'failure'`.
```js
import { parseMIMEType } from 'undici'
parseMIMEType('text/html; charset=gbk')
// {
// type: 'text',
// subtype: 'html',
// parameters: Map(1) { 'charset' => 'gbk' },
// essence: 'text/html'
// }
```
Arguments:
* **input** `string`
Returns: `MIMEType|'failure'`
## `serializeAMimeType(input)`
Implements [serialize a MIME type](https://mimesniff.spec.whatwg.org/#serialize-a-mime-type).
Serializes a MIMEType object.
```js
import { serializeAMimeType } from 'undici'
serializeAMimeType({
type: 'text',
subtype: 'html',
parameters: new Map([['charset', 'gbk']]),
essence: 'text/html'
})
// text/html;charset=gbk
```
Arguments:
* **mimeType** `MIMEType`
Returns: `string`

View File

@@ -0,0 +1,101 @@
# Cookie Handling
## `Cookie` interface
* **name** `string`
* **value** `string`
* **expires** `Date|number` (optional)
* **maxAge** `number` (optional)
* **domain** `string` (optional)
* **path** `string` (optional)
* **secure** `boolean` (optional)
* **httpOnly** `boolean` (optional)
* **sameSite** `'String'|'Lax'|'None'` (optional)
* **unparsed** `string[]` (optional) Left over attributes that weren't parsed.
## `deleteCookie(headers, name[, attributes])`
Sets the expiry time of the cookie to the unix epoch, causing browsers to delete it when received.
```js
import { deleteCookie, Headers } from 'undici'
const headers = new Headers()
deleteCookie(headers, 'name')
console.log(headers.get('set-cookie')) // name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT
```
Arguments:
* **headers** `Headers`
* **name** `string`
* **attributes** `{ path?: string, domain?: string }` (optional)
Returns: `void`
## `getCookies(headers)`
Parses the `Cookie` header and returns a list of attributes and values.
```js
import { getCookies, Headers } from 'undici'
const headers = new Headers({
cookie: 'get=cookies; and=attributes'
})
console.log(getCookies(headers)) // { get: 'cookies', and: 'attributes' }
```
Arguments:
* **headers** `Headers`
Returns: `Record<string, string>`
## `getSetCookies(headers)`
Parses all `Set-Cookie` headers.
```js
import { getSetCookies, Headers } from 'undici'
const headers = new Headers({ 'set-cookie': 'undici=getSetCookies; Secure' })
console.log(getSetCookies(headers))
// [
// {
// name: 'undici',
// value: 'getSetCookies',
// secure: true
// }
// ]
```
Arguments:
* **headers** `Headers`
Returns: `Cookie[]`
## `setCookie(headers, cookie)`
Appends a cookie to the `Set-Cookie` header.
```js
import { setCookie, Headers } from 'undici'
const headers = new Headers()
setCookie(headers, { name: 'undici', value: 'setCookie' })
console.log(headers.get('Set-Cookie')) // undici=setCookie
```
Arguments:
* **headers** `Headers`
* **cookie** `Cookie`
Returns: `void`

View File

@@ -135,3 +135,70 @@ diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, soc
// connector is a function that creates the socket // connector is a function that creates the socket
console.log(`Connect failed with ${error.message}`) console.log(`Connect failed with ${error.message}`)
}) })
```
## `undici:websocket:open`
This message is published after the client has successfully connected to a server.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:open').subscribe(({ address, protocol, extensions }) => {
console.log(address) // address, family, and port
console.log(protocol) // negotiated subprotocols
console.log(extensions) // negotiated extensions
})
```
## `undici:websocket:close`
This message is published after the connection has closed.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:close').subscribe(({ websocket, code, reason }) => {
console.log(websocket) // the WebSocket object
console.log(code) // the closing status code
console.log(reason) // the closing reason
})
```
## `undici:websocket:socket_error`
This message is published if the socket experiences an error.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:socket_error').subscribe((error) => {
console.log(error)
})
```
## `undici:websocket:ping`
This message is published after the client receives a ping frame, if the connection is not closing.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:ping').subscribe(({ payload }) => {
// a Buffer or undefined, containing the optional application data of the frame
console.log(payload)
})
```
## `undici:websocket:pong`
This message is published after the client receives a pong frame.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:websocket:pong').subscribe(({ payload }) => {
// a Buffer or undefined, containing the optional application data of the frame
console.log(payload)
})
```

View File

@@ -1,4 +1,4 @@
#Interface: DispatchInterceptor # Interface: DispatchInterceptor
Extends: `Function` Extends: `Function`

View File

@@ -74,7 +74,7 @@ Returns: `void | Promise<ConnectData>` - Only returns a `Promise` if no `callbac
#### Parameter: `ConnectData` #### Parameter: `ConnectData`
* **statusCode** `number` * **statusCode** `number`
* **headers** `http.IncomingHttpHeaders` * **headers** `Record<string, string | string[] | undefined>`
* **socket** `stream.Duplex` * **socket** `stream.Duplex`
* **opaque** `unknown` * **opaque** `unknown`
@@ -192,15 +192,17 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
* **origin** `string | URL` * **origin** `string | URL`
* **path** `string` * **path** `string`
* **method** `string` * **method** `string`
* **reset** `boolean` (optional) - Default: `false` - If `false`, the request will attempt to create a long-living connection by sending the `connection: keep-alive` header,otherwise will attempt to close it immediately after response by sending `connection: close` within the request and closing the socket afterwards.
* **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null`
* **headers** `UndiciHeaders | string[]` (optional) - Default: `null`. * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`.
* **query** `Record<string, any> | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead. * **query** `Record<string, any> | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead.
* **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed. * **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed.
* **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received. * **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
* **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
* **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds. * **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
* **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds. * **headersTimeout** `number | null` (optional) - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
* **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. * **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.
* **expectContinue** `boolean` (optional) - Default: `false` - For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server
#### Parameter: `DispatchHandler` #### Parameter: `DispatchHandler`
@@ -382,7 +384,7 @@ Extends: [`RequestOptions`](#parameter-requestoptions)
#### Parameter: PipelineHandlerData #### Parameter: PipelineHandlerData
* **statusCode** `number` * **statusCode** `number`
* **headers** `IncomingHttpHeaders` * **headers** `Record<string, string | string[] | undefined>`
* **opaque** `unknown` * **opaque** `unknown`
* **body** `stream.Readable` * **body** `stream.Readable`
* **context** `object` * **context** `object`
@@ -476,7 +478,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
#### Parameter: `ResponseData` #### Parameter: `ResponseData`
* **statusCode** `number` * **statusCode** `number`
* **headers** `http.IncomingHttpHeaders` - Note that all header keys are lower-cased, e. g. `content-type`. * **headers** `Record<string, string | string[]>` - Note that all header keys are lower-cased, e. g. `content-type`.
* **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin). * **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
* **trailers** `Record<string, string>` - This object starts out * **trailers** `Record<string, string>` - This object starts out
as empty and will be mutated to contain trailers after `body` has emitted `'end'`. as empty and will be mutated to contain trailers after `body` has emitted `'end'`.
@@ -630,7 +632,7 @@ try {
A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream. A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream.
As demonstrated in [Example 1 - Basic GET stream request](#example-1-basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](#example-2-stream-to-fastify-response) for more details. As demonstrated in [Example 1 - Basic GET stream request](#example-1---basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](#example-2---stream-to-fastify-response) for more details.
Arguments: Arguments:
@@ -643,7 +645,7 @@ Returns: `void | Promise<StreamData>` - Only returns a `Promise` if no `callback
#### Parameter: `StreamFactoryData` #### Parameter: `StreamFactoryData`
* **statusCode** `number` * **statusCode** `number`
* **headers** `http.IncomingHttpHeaders` * **headers** `Record<string, string | string[] | undefined>`
* **opaque** `unknown` * **opaque** `unknown`
* **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received. * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
@@ -852,9 +854,9 @@ Emitted when dispatcher is no longer busy.
## Parameter: `UndiciHeaders` ## Parameter: `UndiciHeaders`
* `http.IncomingHttpHeaders | string[] | null` * `Record<string, string | string[] | undefined> | string[] | null`
Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `http.IncomingHttpHeaders` type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown. Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `Record<string, string | string[] | undefined>` (`IncomingHttpHeaders`) type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown.
Keys are lowercase and values are not modified. Keys are lowercase and values are not modified.

View File

@@ -7,18 +7,25 @@ You can find all the error objects inside the `errors` key.
import { errors } from 'undici' import { errors } from 'undici'
``` ```
| Error | Error Codes | Description | | Error | Error Codes | Description |
| ------------------------------------ | ------------------------------------- | -------------------------------------------------- | | ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------- |
| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. | | `UndiciError` | `UND_ERR` | all errors below are extended from `UndiciError`. |
| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. | | `ConnectTimeoutError` | `UND_ERR_CONNECT_TIMEOUT` | socket is destroyed due to connect timeout. |
| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user | | `HeadersTimeoutError` | `UND_ERR_HEADERS_TIMEOUT` | socket is destroyed due to headers timeout. |
| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. | | `HeadersOverflowError` | `UND_ERR_HEADERS_OVERFLOW` | socket is destroyed due to headers' max size being exceeded. |
| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. | | `BodyTimeoutError` | `UND_ERR_BODY_TIMEOUT` | socket is destroyed due to body timeout. |
| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. | | `ResponseStatusCodeError` | `UND_ERR_RESPONSE_STATUS_CODE` | an error is thrown when `throwOnError` is `true` for status codes >= 400. |
| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. | | `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. |
| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header | | `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. |
| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header | | `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user |
| `InformationalError` | `UND_ERR_INFO` | expected error with reason | | `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. |
| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. |
| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. |
| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. |
| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header |
| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header |
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
### `SocketError` ### `SocketError`

View File

@@ -0,0 +1,27 @@
# Fetch
Undici exposes a fetch() method starts the process of fetching a resource from the network.
Documentation and examples can be found on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
## File
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/File)
In Node versions v18.13.0 and above and v19.2.0 and above, undici will default to using Node's [File](https://nodejs.org/api/buffer.html#class-file) class. In versions where it's not available, it will default to the undici one.
## FormData
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
## Response
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Response)
## Request
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Request)
## Header
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Headers)

View File

@@ -35,7 +35,7 @@ const mockPool = mockAgent.get('http://localhost:3000')
### `MockPool.intercept(options)` ### `MockPool.intercept(options)`
This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance. This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once. For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once.
When defining interception rules, all the rules must pass for a request to be intercepted. If a request is not intercepted, a real request will be attempted. When defining interception rules, all the rules must pass for a request to be intercepted. If a request is not intercepted, a real request will be attempted.
@@ -53,17 +53,18 @@ Returns: `MockInterceptor` corresponding to the input options.
### Parameter: `MockPoolInterceptOptions` ### Parameter: `MockPoolInterceptOptions`
* **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. * **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. When a `RegExp` or callback is used, it will match against the request path including all query parameters in alphabetical order. When a `string` is provided, the query parameters can be conveniently specified through the `MockPoolInterceptOptions.query` setting.
* **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`. * **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`.
* **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body. * **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
* **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way. * **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
* **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params. * **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params. Only applies when a `string` was provided for `MockPoolInterceptOptions.path`.
### Return: `MockInterceptor` ### Return: `MockInterceptor`
We can define the behaviour of an intercepted request with the following options. We can define the behaviour of an intercepted request with the following options.
* **reply** `(statusCode: number, replyData: string | Buffer | object | MockInterceptor.MockResponseDataHandler, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. You can define this as a callback to read incoming request data. Default for `responseOptions` is `{}`. * **reply** `(statusCode: number, replyData: string | Buffer | object | MockInterceptor.MockResponseDataHandler, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. You can define the replyData as a callback to read incoming request data. Default for `responseOptions` is `{}`.
* **reply** `(callback: MockInterceptor.MockReplyOptionsCallback) => MockScope` - define a reply for a matching request, allowing dynamic mocking of all reply options rather than just the data.
* **replyWithError** `(error: Error) => MockScope` - define an error for a matching request to throw. * **replyWithError** `(error: Error) => MockScope` - define an error for a matching request to throw.
* **defaultReplyHeaders** `(headers: Record<string, string>) => MockInterceptor` - define default headers to be included in subsequent replies. These are in addition to headers on a specific reply. * **defaultReplyHeaders** `(headers: Record<string, string>) => MockInterceptor` - define default headers to be included in subsequent replies. These are in addition to headers on a specific reply.
* **defaultReplyTrailers** `(trailers: Record<string, string>) => MockInterceptor` - define default trailers to be included in subsequent replies. These are in addition to trailers on a specific reply. * **defaultReplyTrailers** `(trailers: Record<string, string>) => MockInterceptor` - define default trailers to be included in subsequent replies. These are in addition to trailers on a specific reply.
@@ -456,6 +457,41 @@ const result3 = await request('http://localhost:3000/foo')
// Will not match and make attempt a real request // Will not match and make attempt a real request
``` ```
#### Example - Mocked request with path callback
```js
import { MockAgent, setGlobalDispatcher, request } from 'undici'
import querystring from 'querystring'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const mockPool = mockAgent.get('http://localhost:3000')
const matchPath = requestPath => {
const [pathname, search] = requestPath.split('?')
const requestQuery = querystring.parse(search)
if (!pathname.startsWith('/foo')) {
return false
}
if (!Object.keys(requestQuery).includes('foo') || requestQuery.foo !== 'bar') {
return false
}
return true
}
mockPool.intercept({
path: matchPath,
method: 'GET'
}).reply(200, 'foo')
const result = await request('http://localhost:3000/foo?foo=bar')
// Will match and return mocked data
```
### `MockPool.close()` ### `MockPool.close()`
Closes the mock pool and de-registers from associated MockAgent. Closes the mock pool and de-registers from associated MockAgent.

View File

@@ -17,6 +17,11 @@ Returns: `ProxyAgent`
Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) Extends: [`AgentOptions`](Agent.md#parameter-agentoptions)
* **uri** `string` (required) - It can be passed either by a string or a object containing `uri` as string. * **uri** `string` (required) - It can be passed either by a string or a object containing `uri` as string.
* **token** `string` (optional) - It can be passed by a string of token for authentication.
* **auth** `string` (**deprecated**) - Use token.
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. See [TLS](https://nodejs.org/api/tls.html#tlsconnectoptions-callback).
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. See [TLS](https://nodejs.org/api/tls.html#tlsconnectoptions-callback).
Examples: Examples:
@@ -74,6 +79,27 @@ for await (const data of body) {
} }
``` ```
#### Example - Basic Proxy Request with authentication
```js
import { setGlobalDispatcher, request, ProxyAgent } from 'undici';
const proxyAgent = new ProxyAgent({
uri: 'my.proxy.server',
// token: 'Bearer xxxx'
token: `Basic ${Buffer.from('username:password').toString('base64')}`
});
setGlobalDispatcher(proxyAgent);
const { statusCode, body } = await request('http://localhost:3000/foo');
console.log('response received', statusCode); // response received 200
for await (const data of body) {
console.log('data', data.toString('utf8')); // data foo
}
```
### `ProxyAgent.close()` ### `ProxyAgent.close()`
Closes the proxy agent and waits for registered pools and clients to also close before resolving. Closes the proxy agent and waits for registered pools and clients to also close before resolving.

View File

@@ -0,0 +1,108 @@
# Class: RetryHandler
Extends: `undici.DispatcherHandlers`
A handler class that implements the retry logic for a request.
## `new RetryHandler(dispatchOptions, retryHandlers, [retryOptions])`
Arguments:
- **options** `Dispatch.DispatchOptions & RetryOptions` (required) - It is an intersection of `Dispatcher.DispatchOptions` and `RetryOptions`.
- **retryHandlers** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle.
Returns: `retryHandler`
### Parameter: `Dispatch.DispatchOptions & RetryOptions`
Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
#### `RetryOptions`
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
-
- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN',
**`RetryContext`**
- `state`: `RetryState` - Current retry state. It can be mutated.
- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
### Parameter `RetryHandlers`
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
- **handler** Extends [`Dispatch.DispatchHandlers`](Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
Examples:
```js
const client = new Client(`http://localhost:${server.address().port}`);
const chunks = [];
const handler = new RetryHandler(
{
...dispatchOptions,
retryOptions: {
// custom retry function
retry: function (err, state, callback) {
counter++;
if (err.code && err.code === "UND_ERR_DESTROYED") {
callback(err);
return;
}
if (err.statusCode === 206) {
callback(err);
return;
}
setTimeout(() => callback(null), 1000);
},
},
},
{
dispatch: (...args) => {
return client.dispatch(...args);
},
handler: {
onConnect() {},
onBodySent() {},
onHeaders(status, _rawHeaders, resume, _statusMessage) {
// do something with headers
},
onData(chunk) {
chunks.push(chunk);
return true;
},
onComplete() {},
onError() {
// handle error properly
},
},
}
);
```
#### Example - Basic RetryHandler with defaults
```js
const client = new Client(`http://localhost:${server.address().port}`);
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect() {},
onBodySent() {},
onHeaders(status, _rawHeaders, resume, _statusMessage) {},
onData(chunk) {},
onComplete() {},
onError(err) {},
},
});
```

View File

@@ -0,0 +1,43 @@
# Class: WebSocket
> ⚠️ Warning: the WebSocket API is experimental.
Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455).
## `new WebSocket(url[, protocol])`
Arguments:
* **url** `URL | string` - The url's protocol *must* be `ws` or `wss`.
* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md).
### Example:
This example will not work in browsers or other platforms that don't allow passing an object.
```mjs
import { WebSocket, ProxyAgent } from 'undici'
const proxyAgent = new ProxyAgent('my.proxy.server')
const ws = new WebSocket('wss://echo.websocket.events', {
dispatcher: proxyAgent,
protocols: ['echo', 'chat']
})
```
If you do not need a custom Dispatcher, it's recommended to use the following pattern:
```mjs
import { WebSocket } from 'undici'
const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
```
## Read More
- [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
- [The WebSocket Specification](https://www.rfc-editor.org/rfc/rfc6455)
- [The WHATWG WebSocket Specification](https://websockets.spec.whatwg.org/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,15 @@
'use strict'
const fetchImpl = require('./lib/fetch').fetch
module.exports.fetch = function fetch (resource, init = undefined) {
return fetchImpl(resource, init).catch((err) => {
Error.captureStackTrace(err, this)
throw err
})
}
module.exports.FormData = require('./lib/fetch/formdata').FormData
module.exports.Headers = require('./lib/fetch/headers').Headers
module.exports.Response = require('./lib/fetch/response').Response
module.exports.Request = require('./lib/fetch/request').Request
module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket

View File

@@ -0,0 +1,3 @@
export * from './types/index'
import Undici from './types/index'
export default Undici

View File

@@ -0,0 +1,167 @@
'use strict'
const Client = require('./lib/client')
const Dispatcher = require('./lib/dispatcher')
const errors = require('./lib/core/errors')
const Pool = require('./lib/pool')
const BalancedPool = require('./lib/balanced-pool')
const Agent = require('./lib/agent')
const util = require('./lib/core/util')
const { InvalidArgumentError } = errors
const api = require('./lib/api')
const buildConnector = require('./lib/core/connect')
const MockClient = require('./lib/mock/mock-client')
const MockAgent = require('./lib/mock/mock-agent')
const MockPool = require('./lib/mock/mock-pool')
const mockErrors = require('./lib/mock/mock-errors')
const ProxyAgent = require('./lib/proxy-agent')
const RetryHandler = require('./lib/handler/RetryHandler')
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
const DecoratorHandler = require('./lib/handler/DecoratorHandler')
const RedirectHandler = require('./lib/handler/RedirectHandler')
const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor')
let hasCrypto
try {
require('crypto')
hasCrypto = true
} catch {
hasCrypto = false
}
Object.assign(Dispatcher.prototype, api)
module.exports.Dispatcher = Dispatcher
module.exports.Client = Client
module.exports.Pool = Pool
module.exports.BalancedPool = BalancedPool
module.exports.Agent = Agent
module.exports.ProxyAgent = ProxyAgent
module.exports.RetryHandler = RetryHandler
module.exports.DecoratorHandler = DecoratorHandler
module.exports.RedirectHandler = RedirectHandler
module.exports.createRedirectInterceptor = createRedirectInterceptor
module.exports.buildConnector = buildConnector
module.exports.errors = errors
function makeDispatcher (fn) {
return (url, opts, handler) => {
if (typeof opts === 'function') {
handler = opts
opts = null
}
if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) {
throw new InvalidArgumentError('invalid url')
}
if (opts != null && typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
if (opts && opts.path != null) {
if (typeof opts.path !== 'string') {
throw new InvalidArgumentError('invalid opts.path')
}
let path = opts.path
if (!opts.path.startsWith('/')) {
path = `/${path}`
}
url = new URL(util.parseOrigin(url).origin + path)
} else {
if (!opts) {
opts = typeof url === 'object' ? url : {}
}
url = util.parseURL(url)
}
const { agent, dispatcher = getGlobalDispatcher() } = opts
if (agent) {
throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?')
}
return fn.call(dispatcher, {
...opts,
origin: url.origin,
path: url.search ? `${url.pathname}${url.search}` : url.pathname,
method: opts.method || (opts.body ? 'PUT' : 'GET')
}, handler)
}
}
module.exports.setGlobalDispatcher = setGlobalDispatcher
module.exports.getGlobalDispatcher = getGlobalDispatcher
if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) {
let fetchImpl = null
module.exports.fetch = async function fetch (resource) {
if (!fetchImpl) {
fetchImpl = require('./lib/fetch').fetch
}
try {
return await fetchImpl(...arguments)
} catch (err) {
if (typeof err === 'object') {
Error.captureStackTrace(err, this)
}
throw err
}
}
module.exports.Headers = require('./lib/fetch/headers').Headers
module.exports.Response = require('./lib/fetch/response').Response
module.exports.Request = require('./lib/fetch/request').Request
module.exports.FormData = require('./lib/fetch/formdata').FormData
module.exports.File = require('./lib/fetch/file').File
module.exports.FileReader = require('./lib/fileapi/filereader').FileReader
const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global')
module.exports.setGlobalOrigin = setGlobalOrigin
module.exports.getGlobalOrigin = getGlobalOrigin
const { CacheStorage } = require('./lib/cache/cachestorage')
const { kConstruct } = require('./lib/cache/symbols')
// Cache & CacheStorage are tightly coupled with fetch. Even if it may run
// in an older version of Node, it doesn't have any use without fetch.
module.exports.caches = new CacheStorage(kConstruct)
}
if (util.nodeMajor >= 16) {
const { deleteCookie, getCookies, getSetCookies, setCookie } = require('./lib/cookies')
module.exports.deleteCookie = deleteCookie
module.exports.getCookies = getCookies
module.exports.getSetCookies = getSetCookies
module.exports.setCookie = setCookie
const { parseMIMEType, serializeAMimeType } = require('./lib/fetch/dataURL')
module.exports.parseMIMEType = parseMIMEType
module.exports.serializeAMimeType = serializeAMimeType
}
if (util.nodeMajor >= 18 && hasCrypto) {
const { WebSocket } = require('./lib/websocket/websocket')
module.exports.WebSocket = WebSocket
}
module.exports.request = makeDispatcher(api.request)
module.exports.stream = makeDispatcher(api.stream)
module.exports.pipeline = makeDispatcher(api.pipeline)
module.exports.connect = makeDispatcher(api.connect)
module.exports.upgrade = makeDispatcher(api.upgrade)
module.exports.MockClient = MockClient
module.exports.MockPool = MockPool
module.exports.MockAgent = MockAgent
module.exports.mockErrors = mockErrors

View File

@@ -0,0 +1,54 @@
const { addAbortListener } = require('../core/util')
const { RequestAbortedError } = require('../core/errors')
const kListener = Symbol('kListener')
const kSignal = Symbol('kSignal')
function abort (self) {
if (self.abort) {
self.abort()
} else {
self.onError(new RequestAbortedError())
}
}
function addSignal (self, signal) {
self[kSignal] = null
self[kListener] = null
if (!signal) {
return
}
if (signal.aborted) {
abort(self)
return
}
self[kSignal] = signal
self[kListener] = () => {
abort(self)
}
addAbortListener(self[kSignal], self[kListener])
}
function removeSignal (self) {
if (!self[kSignal]) {
return
}
if ('removeEventListener' in self[kSignal]) {
self[kSignal].removeEventListener('abort', self[kListener])
} else {
self[kSignal].removeListener('abort', self[kListener])
}
self[kSignal] = null
self[kListener] = null
}
module.exports = {
addSignal,
removeSignal
}

View File

@@ -0,0 +1,104 @@
'use strict'
const { AsyncResource } = require('async_hooks')
const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors')
const util = require('../core/util')
const { addSignal, removeSignal } = require('./abort-signal')
class ConnectHandler extends AsyncResource {
constructor (opts, callback) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
const { signal, opaque, responseHeaders } = opts
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
super('UNDICI_CONNECT')
this.opaque = opaque || null
this.responseHeaders = responseHeaders || null
this.callback = callback
this.abort = null
addSignal(this, signal)
}
onConnect (abort, context) {
if (!this.callback) {
throw new RequestAbortedError()
}
this.abort = abort
this.context = context
}
onHeaders () {
throw new SocketError('bad connect', null)
}
onUpgrade (statusCode, rawHeaders, socket) {
const { callback, opaque, context } = this
removeSignal(this)
this.callback = null
let headers = rawHeaders
// Indicates is an HTTP2Session
if (headers != null) {
headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
}
this.runInAsyncScope(callback, null, null, {
statusCode,
headers,
socket,
opaque,
context
})
}
onError (err) {
const { callback, opaque } = this
removeSignal(this)
if (callback) {
this.callback = null
queueMicrotask(() => {
this.runInAsyncScope(callback, null, err, { opaque })
})
}
}
}
function connect (opts, callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
connect.call(this, opts, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
try {
const connectHandler = new ConnectHandler(opts, callback)
this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler)
} catch (err) {
if (typeof callback !== 'function') {
throw err
}
const opaque = opts && opts.opaque
queueMicrotask(() => callback(err, { opaque }))
}
}
module.exports = connect

View File

@@ -0,0 +1,249 @@
'use strict'
const {
Readable,
Duplex,
PassThrough
} = require('stream')
const {
InvalidArgumentError,
InvalidReturnValueError,
RequestAbortedError
} = require('../core/errors')
const util = require('../core/util')
const { AsyncResource } = require('async_hooks')
const { addSignal, removeSignal } = require('./abort-signal')
const assert = require('assert')
const kResume = Symbol('resume')
class PipelineRequest extends Readable {
constructor () {
super({ autoDestroy: true })
this[kResume] = null
}
_read () {
const { [kResume]: resume } = this
if (resume) {
this[kResume] = null
resume()
}
}
_destroy (err, callback) {
this._read()
callback(err)
}
}
class PipelineResponse extends Readable {
constructor (resume) {
super({ autoDestroy: true })
this[kResume] = resume
}
_read () {
this[kResume]()
}
_destroy (err, callback) {
if (!err && !this._readableState.endEmitted) {
err = new RequestAbortedError()
}
callback(err)
}
}
class PipelineHandler extends AsyncResource {
constructor (opts, handler) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
if (typeof handler !== 'function') {
throw new InvalidArgumentError('invalid handler')
}
const { signal, method, opaque, onInfo, responseHeaders } = opts
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
if (method === 'CONNECT') {
throw new InvalidArgumentError('invalid method')
}
if (onInfo && typeof onInfo !== 'function') {
throw new InvalidArgumentError('invalid onInfo callback')
}
super('UNDICI_PIPELINE')
this.opaque = opaque || null
this.responseHeaders = responseHeaders || null
this.handler = handler
this.abort = null
this.context = null
this.onInfo = onInfo || null
this.req = new PipelineRequest().on('error', util.nop)
this.ret = new Duplex({
readableObjectMode: opts.objectMode,
autoDestroy: true,
read: () => {
const { body } = this
if (body && body.resume) {
body.resume()
}
},
write: (chunk, encoding, callback) => {
const { req } = this
if (req.push(chunk, encoding) || req._readableState.destroyed) {
callback()
} else {
req[kResume] = callback
}
},
destroy: (err, callback) => {
const { body, req, res, ret, abort } = this
if (!err && !ret._readableState.endEmitted) {
err = new RequestAbortedError()
}
if (abort && err) {
abort()
}
util.destroy(body, err)
util.destroy(req, err)
util.destroy(res, err)
removeSignal(this)
callback(err)
}
}).on('prefinish', () => {
const { req } = this
// Node < 15 does not call _final in same tick.
req.push(null)
})
this.res = null
addSignal(this, signal)
}
onConnect (abort, context) {
const { ret, res } = this
assert(!res, 'pipeline cannot be retried')
if (ret.destroyed) {
throw new RequestAbortedError()
}
this.abort = abort
this.context = context
}
onHeaders (statusCode, rawHeaders, resume) {
const { opaque, handler, context } = this
if (statusCode < 200) {
if (this.onInfo) {
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
this.onInfo({ statusCode, headers })
}
return
}
this.res = new PipelineResponse(resume)
let body
try {
this.handler = null
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
body = this.runInAsyncScope(handler, null, {
statusCode,
headers,
opaque,
body: this.res,
context
})
} catch (err) {
this.res.on('error', util.nop)
throw err
}
if (!body || typeof body.on !== 'function') {
throw new InvalidReturnValueError('expected Readable')
}
body
.on('data', (chunk) => {
const { ret, body } = this
if (!ret.push(chunk) && body.pause) {
body.pause()
}
})
.on('error', (err) => {
const { ret } = this
util.destroy(ret, err)
})
.on('end', () => {
const { ret } = this
ret.push(null)
})
.on('close', () => {
const { ret } = this
if (!ret._readableState.ended) {
util.destroy(ret, new RequestAbortedError())
}
})
this.body = body
}
onData (chunk) {
const { res } = this
return res.push(chunk)
}
onComplete (trailers) {
const { res } = this
res.push(null)
}
onError (err) {
const { ret } = this
this.handler = null
util.destroy(ret, err)
}
}
function pipeline (opts, handler) {
try {
const pipelineHandler = new PipelineHandler(opts, handler)
this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler)
return pipelineHandler.ret
} catch (err) {
return new PassThrough().destroy(err)
}
}
module.exports = pipeline

View File

@@ -0,0 +1,180 @@
'use strict'
const Readable = require('./readable')
const {
InvalidArgumentError,
RequestAbortedError
} = require('../core/errors')
const util = require('../core/util')
const { getResolveErrorBodyCallback } = require('./util')
const { AsyncResource } = require('async_hooks')
const { addSignal, removeSignal } = require('./abort-signal')
class RequestHandler extends AsyncResource {
constructor (opts, callback) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts
try {
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) {
throw new InvalidArgumentError('invalid highWaterMark')
}
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
if (method === 'CONNECT') {
throw new InvalidArgumentError('invalid method')
}
if (onInfo && typeof onInfo !== 'function') {
throw new InvalidArgumentError('invalid onInfo callback')
}
super('UNDICI_REQUEST')
} catch (err) {
if (util.isStream(body)) {
util.destroy(body.on('error', util.nop), err)
}
throw err
}
this.responseHeaders = responseHeaders || null
this.opaque = opaque || null
this.callback = callback
this.res = null
this.abort = null
this.body = body
this.trailers = {}
this.context = null
this.onInfo = onInfo || null
this.throwOnError = throwOnError
this.highWaterMark = highWaterMark
if (util.isStream(body)) {
body.on('error', (err) => {
this.onError(err)
})
}
addSignal(this, signal)
}
onConnect (abort, context) {
if (!this.callback) {
throw new RequestAbortedError()
}
this.abort = abort
this.context = context
}
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
if (statusCode < 200) {
if (this.onInfo) {
this.onInfo({ statusCode, headers })
}
return
}
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
const contentType = parsedHeaders['content-type']
const body = new Readable({ resume, abort, contentType, highWaterMark })
this.callback = null
this.res = body
if (callback !== null) {
if (this.throwOnError && statusCode >= 400) {
this.runInAsyncScope(getResolveErrorBodyCallback, null,
{ callback, body, contentType, statusCode, statusMessage, headers }
)
} else {
this.runInAsyncScope(callback, null, null, {
statusCode,
headers,
trailers: this.trailers,
opaque,
body,
context
})
}
}
}
onData (chunk) {
const { res } = this
return res.push(chunk)
}
onComplete (trailers) {
const { res } = this
removeSignal(this)
util.parseHeaders(trailers, this.trailers)
res.push(null)
}
onError (err) {
const { res, callback, body, opaque } = this
removeSignal(this)
if (callback) {
// TODO: Does this need queueMicrotask?
this.callback = null
queueMicrotask(() => {
this.runInAsyncScope(callback, null, err, { opaque })
})
}
if (res) {
this.res = null
// Ensure all queued handlers are invoked before destroying res.
queueMicrotask(() => {
util.destroy(res, err)
})
}
if (body) {
this.body = null
util.destroy(body, err)
}
}
}
function request (opts, callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
request.call(this, opts, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
try {
this.dispatch(opts, new RequestHandler(opts, callback))
} catch (err) {
if (typeof callback !== 'function') {
throw err
}
const opaque = opts && opts.opaque
queueMicrotask(() => callback(err, { opaque }))
}
}
module.exports = request
module.exports.RequestHandler = RequestHandler

View File

@@ -0,0 +1,220 @@
'use strict'
const { finished, PassThrough } = require('stream')
const {
InvalidArgumentError,
InvalidReturnValueError,
RequestAbortedError
} = require('../core/errors')
const util = require('../core/util')
const { getResolveErrorBodyCallback } = require('./util')
const { AsyncResource } = require('async_hooks')
const { addSignal, removeSignal } = require('./abort-signal')
class StreamHandler extends AsyncResource {
constructor (opts, factory, callback) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts
try {
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
if (typeof factory !== 'function') {
throw new InvalidArgumentError('invalid factory')
}
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
if (method === 'CONNECT') {
throw new InvalidArgumentError('invalid method')
}
if (onInfo && typeof onInfo !== 'function') {
throw new InvalidArgumentError('invalid onInfo callback')
}
super('UNDICI_STREAM')
} catch (err) {
if (util.isStream(body)) {
util.destroy(body.on('error', util.nop), err)
}
throw err
}
this.responseHeaders = responseHeaders || null
this.opaque = opaque || null
this.factory = factory
this.callback = callback
this.res = null
this.abort = null
this.context = null
this.trailers = null
this.body = body
this.onInfo = onInfo || null
this.throwOnError = throwOnError || false
if (util.isStream(body)) {
body.on('error', (err) => {
this.onError(err)
})
}
addSignal(this, signal)
}
onConnect (abort, context) {
if (!this.callback) {
throw new RequestAbortedError()
}
this.abort = abort
this.context = context
}
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
const { factory, opaque, context, callback, responseHeaders } = this
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
if (statusCode < 200) {
if (this.onInfo) {
this.onInfo({ statusCode, headers })
}
return
}
this.factory = null
let res
if (this.throwOnError && statusCode >= 400) {
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
const contentType = parsedHeaders['content-type']
res = new PassThrough()
this.callback = null
this.runInAsyncScope(getResolveErrorBodyCallback, null,
{ callback, body: res, contentType, statusCode, statusMessage, headers }
)
} else {
if (factory === null) {
return
}
res = this.runInAsyncScope(factory, null, {
statusCode,
headers,
opaque,
context
})
if (
!res ||
typeof res.write !== 'function' ||
typeof res.end !== 'function' ||
typeof res.on !== 'function'
) {
throw new InvalidReturnValueError('expected Writable')
}
// TODO: Avoid finished. It registers an unnecessary amount of listeners.
finished(res, { readable: false }, (err) => {
const { callback, res, opaque, trailers, abort } = this
this.res = null
if (err || !res.readable) {
util.destroy(res, err)
}
this.callback = null
this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
if (err) {
abort()
}
})
}
res.on('drain', resume)
this.res = res
const needDrain = res.writableNeedDrain !== undefined
? res.writableNeedDrain
: res._writableState && res._writableState.needDrain
return needDrain !== true
}
onData (chunk) {
const { res } = this
return res ? res.write(chunk) : true
}
onComplete (trailers) {
const { res } = this
removeSignal(this)
if (!res) {
return
}
this.trailers = util.parseHeaders(trailers)
res.end()
}
onError (err) {
const { res, callback, opaque, body } = this
removeSignal(this)
this.factory = null
if (res) {
this.res = null
util.destroy(res, err)
} else if (callback) {
this.callback = null
queueMicrotask(() => {
this.runInAsyncScope(callback, null, err, { opaque })
})
}
if (body) {
this.body = null
util.destroy(body, err)
}
}
}
function stream (opts, factory, callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
stream.call(this, opts, factory, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
try {
this.dispatch(opts, new StreamHandler(opts, factory, callback))
} catch (err) {
if (typeof callback !== 'function') {
throw err
}
const opaque = opts && opts.opaque
queueMicrotask(() => callback(err, { opaque }))
}
}
module.exports = stream

View File

@@ -0,0 +1,105 @@
'use strict'
const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors')
const { AsyncResource } = require('async_hooks')
const util = require('../core/util')
const { addSignal, removeSignal } = require('./abort-signal')
const assert = require('assert')
class UpgradeHandler extends AsyncResource {
constructor (opts, callback) {
if (!opts || typeof opts !== 'object') {
throw new InvalidArgumentError('invalid opts')
}
if (typeof callback !== 'function') {
throw new InvalidArgumentError('invalid callback')
}
const { signal, opaque, responseHeaders } = opts
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
}
super('UNDICI_UPGRADE')
this.responseHeaders = responseHeaders || null
this.opaque = opaque || null
this.callback = callback
this.abort = null
this.context = null
addSignal(this, signal)
}
onConnect (abort, context) {
if (!this.callback) {
throw new RequestAbortedError()
}
this.abort = abort
this.context = null
}
onHeaders () {
throw new SocketError('bad upgrade', null)
}
onUpgrade (statusCode, rawHeaders, socket) {
const { callback, opaque, context } = this
assert.strictEqual(statusCode, 101)
removeSignal(this)
this.callback = null
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
this.runInAsyncScope(callback, null, null, {
headers,
socket,
opaque,
context
})
}
onError (err) {
const { callback, opaque } = this
removeSignal(this)
if (callback) {
this.callback = null
queueMicrotask(() => {
this.runInAsyncScope(callback, null, err, { opaque })
})
}
}
}
function upgrade (opts, callback) {
if (callback === undefined) {
return new Promise((resolve, reject) => {
upgrade.call(this, opts, (err, data) => {
return err ? reject(err) : resolve(data)
})
})
}
try {
const upgradeHandler = new UpgradeHandler(opts, callback)
this.dispatch({
...opts,
method: opts.method || 'GET',
upgrade: opts.protocol || 'Websocket'
}, upgradeHandler)
} catch (err) {
if (typeof callback !== 'function') {
throw err
}
const opaque = opts && opts.opaque
queueMicrotask(() => callback(err, { opaque }))
}
}
module.exports = upgrade

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports.request = require('./api-request')
module.exports.stream = require('./api-stream')
module.exports.pipeline = require('./api-pipeline')
module.exports.upgrade = require('./api-upgrade')
module.exports.connect = require('./api-connect')

View File

@@ -0,0 +1,322 @@
// Ported from https://github.com/nodejs/undici/pull/907
'use strict'
const assert = require('assert')
const { Readable } = require('stream')
const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors')
const util = require('../core/util')
const { ReadableStreamFrom, toUSVString } = require('../core/util')
let Blob
const kConsume = Symbol('kConsume')
const kReading = Symbol('kReading')
const kBody = Symbol('kBody')
const kAbort = Symbol('abort')
const kContentType = Symbol('kContentType')
const noop = () => {}
module.exports = class BodyReadable extends Readable {
constructor ({
resume,
abort,
contentType = '',
highWaterMark = 64 * 1024 // Same as nodejs fs streams.
}) {
super({
autoDestroy: true,
read: resume,
highWaterMark
})
this._readableState.dataEmitted = false
this[kAbort] = abort
this[kConsume] = null
this[kBody] = null
this[kContentType] = contentType
// Is stream being consumed through Readable API?
// This is an optimization so that we avoid checking
// for 'data' and 'readable' listeners in the hot path
// inside push().
this[kReading] = false
}
destroy (err) {
if (this.destroyed) {
// Node < 16
return this
}
if (!err && !this._readableState.endEmitted) {
err = new RequestAbortedError()
}
if (err) {
this[kAbort]()
}
return super.destroy(err)
}
emit (ev, ...args) {
if (ev === 'data') {
// Node < 16.7
this._readableState.dataEmitted = true
} else if (ev === 'error') {
// Node < 16
this._readableState.errorEmitted = true
}
return super.emit(ev, ...args)
}
on (ev, ...args) {
if (ev === 'data' || ev === 'readable') {
this[kReading] = true
}
return super.on(ev, ...args)
}
addListener (ev, ...args) {
return this.on(ev, ...args)
}
off (ev, ...args) {
const ret = super.off(ev, ...args)
if (ev === 'data' || ev === 'readable') {
this[kReading] = (
this.listenerCount('data') > 0 ||
this.listenerCount('readable') > 0
)
}
return ret
}
removeListener (ev, ...args) {
return this.off(ev, ...args)
}
push (chunk) {
if (this[kConsume] && chunk !== null && this.readableLength === 0) {
consumePush(this[kConsume], chunk)
return this[kReading] ? super.push(chunk) : true
}
return super.push(chunk)
}
// https://fetch.spec.whatwg.org/#dom-body-text
async text () {
return consume(this, 'text')
}
// https://fetch.spec.whatwg.org/#dom-body-json
async json () {
return consume(this, 'json')
}
// https://fetch.spec.whatwg.org/#dom-body-blob
async blob () {
return consume(this, 'blob')
}
// https://fetch.spec.whatwg.org/#dom-body-arraybuffer
async arrayBuffer () {
return consume(this, 'arrayBuffer')
}
// https://fetch.spec.whatwg.org/#dom-body-formdata
async formData () {
// TODO: Implement.
throw new NotSupportedError()
}
// https://fetch.spec.whatwg.org/#dom-body-bodyused
get bodyUsed () {
return util.isDisturbed(this)
}
// https://fetch.spec.whatwg.org/#dom-body-body
get body () {
if (!this[kBody]) {
this[kBody] = ReadableStreamFrom(this)
if (this[kConsume]) {
// TODO: Is this the best way to force a lock?
this[kBody].getReader() // Ensure stream is locked.
assert(this[kBody].locked)
}
}
return this[kBody]
}
dump (opts) {
let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
const signal = opts && opts.signal
if (signal) {
try {
if (typeof signal !== 'object' || !('aborted' in signal)) {
throw new InvalidArgumentError('signal must be an AbortSignal')
}
util.throwIfAborted(signal)
} catch (err) {
return Promise.reject(err)
}
}
if (this.closed) {
return Promise.resolve(null)
}
return new Promise((resolve, reject) => {
const signalListenerCleanup = signal
? util.addAbortListener(signal, () => {
this.destroy()
})
: noop
this
.on('close', function () {
signalListenerCleanup()
if (signal && signal.aborted) {
reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }))
} else {
resolve(null)
}
})
.on('error', noop)
.on('data', function (chunk) {
limit -= chunk.length
if (limit <= 0) {
this.destroy()
}
})
.resume()
})
}
}
// https://streams.spec.whatwg.org/#readablestream-locked
function isLocked (self) {
// Consume is an implicit lock.
return (self[kBody] && self[kBody].locked === true) || self[kConsume]
}
// https://fetch.spec.whatwg.org/#body-unusable
function isUnusable (self) {
return util.isDisturbed(self) || isLocked(self)
}
async function consume (stream, type) {
if (isUnusable(stream)) {
throw new TypeError('unusable')
}
assert(!stream[kConsume])
return new Promise((resolve, reject) => {
stream[kConsume] = {
type,
stream,
resolve,
reject,
length: 0,
body: []
}
stream
.on('error', function (err) {
consumeFinish(this[kConsume], err)
})
.on('close', function () {
if (this[kConsume].body !== null) {
consumeFinish(this[kConsume], new RequestAbortedError())
}
})
process.nextTick(consumeStart, stream[kConsume])
})
}
function consumeStart (consume) {
if (consume.body === null) {
return
}
const { _readableState: state } = consume.stream
for (const chunk of state.buffer) {
consumePush(consume, chunk)
}
if (state.endEmitted) {
consumeEnd(this[kConsume])
} else {
consume.stream.on('end', function () {
consumeEnd(this[kConsume])
})
}
consume.stream.resume()
while (consume.stream.read() != null) {
// Loop
}
}
function consumeEnd (consume) {
const { type, body, resolve, stream, length } = consume
try {
if (type === 'text') {
resolve(toUSVString(Buffer.concat(body)))
} else if (type === 'json') {
resolve(JSON.parse(Buffer.concat(body)))
} else if (type === 'arrayBuffer') {
const dst = new Uint8Array(length)
let pos = 0
for (const buf of body) {
dst.set(buf, pos)
pos += buf.byteLength
}
resolve(dst.buffer)
} else if (type === 'blob') {
if (!Blob) {
Blob = require('buffer').Blob
}
resolve(new Blob(body, { type: stream[kContentType] }))
}
consumeFinish(consume)
} catch (err) {
stream.destroy(err)
}
}
function consumePush (consume, chunk) {
consume.length += chunk.length
consume.body.push(chunk)
}
function consumeFinish (consume, err) {
if (consume.body === null) {
return
}
if (err) {
consume.reject(err)
} else {
consume.resolve()
}
consume.type = null
consume.stream = null
consume.resolve = null
consume.reject = null
consume.length = 0
consume.body = null
}

View File

@@ -0,0 +1,46 @@
const assert = require('assert')
const {
ResponseStatusCodeError
} = require('../core/errors')
const { toUSVString } = require('../core/util')
async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) {
assert(body)
let chunks = []
let limit = 0
for await (const chunk of body) {
chunks.push(chunk)
limit += chunk.length
if (limit > 128 * 1024) {
chunks = null
break
}
}
if (statusCode === 204 || !contentType || !chunks) {
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
return
}
try {
if (contentType.startsWith('application/json')) {
const payload = JSON.parse(toUSVString(Buffer.concat(chunks)))
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
return
}
if (contentType.startsWith('text/')) {
const payload = toUSVString(Buffer.concat(chunks))
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
return
}
} catch (err) {
// Process in a fallback if error
}
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
}
module.exports = { getResolveErrorBodyCallback }

View File

@@ -0,0 +1,838 @@
'use strict'
const { kConstruct } = require('./symbols')
const { urlEquals, fieldValues: getFieldValues } = require('./util')
const { kEnumerableProperty, isDisturbed } = require('../core/util')
const { kHeadersList } = require('../core/symbols')
const { webidl } = require('../fetch/webidl')
const { Response, cloneResponse } = require('../fetch/response')
const { Request } = require('../fetch/request')
const { kState, kHeaders, kGuard, kRealm } = require('../fetch/symbols')
const { fetching } = require('../fetch/index')
const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util')
const assert = require('assert')
const { getGlobalDispatcher } = require('../global')
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation
* @typedef {Object} CacheBatchOperation
* @property {'delete' | 'put'} type
* @property {any} request
* @property {any} response
* @property {import('../../types/cache').CacheQueryOptions} options
*/
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list
* @typedef {[any, any][]} requestResponseList
*/
class Cache {
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list
* @type {requestResponseList}
*/
#relevantRequestResponseList
constructor () {
if (arguments[0] !== kConstruct) {
webidl.illegalConstructor()
}
this.#relevantRequestResponseList = arguments[1]
}
async match (request, options = {}) {
webidl.brandCheck(this, Cache)
webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' })
request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options)
const p = await this.matchAll(request, options)
if (p.length === 0) {
return
}
return p[0]
}
async matchAll (request = undefined, options = {}) {
webidl.brandCheck(this, Cache)
if (request !== undefined) request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options)
// 1.
let r = null
// 2.
if (request !== undefined) {
if (request instanceof Request) {
// 2.1.1
r = request[kState]
// 2.1.2
if (r.method !== 'GET' && !options.ignoreMethod) {
return []
}
} else if (typeof request === 'string') {
// 2.2.1
r = new Request(request)[kState]
}
}
// 5.
// 5.1
const responses = []
// 5.2
if (request === undefined) {
// 5.2.1
for (const requestResponse of this.#relevantRequestResponseList) {
responses.push(requestResponse[1])
}
} else { // 5.3
// 5.3.1
const requestResponses = this.#queryCache(r, options)
// 5.3.2
for (const requestResponse of requestResponses) {
responses.push(requestResponse[1])
}
}
// 5.4
// We don't implement CORs so we don't need to loop over the responses, yay!
// 5.5.1
const responseList = []
// 5.5.2
for (const response of responses) {
// 5.5.2.1
const responseObject = new Response(response.body?.source ?? null)
const body = responseObject[kState].body
responseObject[kState] = response
responseObject[kState].body = body
responseObject[kHeaders][kHeadersList] = response.headersList
responseObject[kHeaders][kGuard] = 'immutable'
responseList.push(responseObject)
}
// 6.
return Object.freeze(responseList)
}
async add (request) {
webidl.brandCheck(this, Cache)
webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' })
request = webidl.converters.RequestInfo(request)
// 1.
const requests = [request]
// 2.
const responseArrayPromise = this.addAll(requests)
// 3.
return await responseArrayPromise
}
async addAll (requests) {
webidl.brandCheck(this, Cache)
webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' })
requests = webidl.converters['sequence<RequestInfo>'](requests)
// 1.
const responsePromises = []
// 2.
const requestList = []
// 3.
for (const request of requests) {
if (typeof request === 'string') {
continue
}
// 3.1
const r = request[kState]
// 3.2
if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') {
throw webidl.errors.exception({
header: 'Cache.addAll',
message: 'Expected http/s scheme when method is not GET.'
})
}
}
// 4.
/** @type {ReturnType<typeof fetching>[]} */
const fetchControllers = []
// 5.
for (const request of requests) {
// 5.1
const r = new Request(request)[kState]
// 5.2
if (!urlIsHttpHttpsScheme(r.url)) {
throw webidl.errors.exception({
header: 'Cache.addAll',
message: 'Expected http/s scheme.'
})
}
// 5.4
r.initiator = 'fetch'
r.destination = 'subresource'
// 5.5
requestList.push(r)
// 5.6
const responsePromise = createDeferredPromise()
// 5.7
fetchControllers.push(fetching({
request: r,
dispatcher: getGlobalDispatcher(),
processResponse (response) {
// 1.
if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) {
responsePromise.reject(webidl.errors.exception({
header: 'Cache.addAll',
message: 'Received an invalid status code or the request failed.'
}))
} else if (response.headersList.contains('vary')) { // 2.
// 2.1
const fieldValues = getFieldValues(response.headersList.get('vary'))
// 2.2
for (const fieldValue of fieldValues) {
// 2.2.1
if (fieldValue === '*') {
responsePromise.reject(webidl.errors.exception({
header: 'Cache.addAll',
message: 'invalid vary field value'
}))
for (const controller of fetchControllers) {
controller.abort()
}
return
}
}
}
},
processResponseEndOfBody (response) {
// 1.
if (response.aborted) {
responsePromise.reject(new DOMException('aborted', 'AbortError'))
return
}
// 2.
responsePromise.resolve(response)
}
}))
// 5.8
responsePromises.push(responsePromise.promise)
}
// 6.
const p = Promise.all(responsePromises)
// 7.
const responses = await p
// 7.1
const operations = []
// 7.2
let index = 0
// 7.3
for (const response of responses) {
// 7.3.1
/** @type {CacheBatchOperation} */
const operation = {
type: 'put', // 7.3.2
request: requestList[index], // 7.3.3
response // 7.3.4
}
operations.push(operation) // 7.3.5
index++ // 7.3.6
}
// 7.5
const cacheJobPromise = createDeferredPromise()
// 7.6.1
let errorData = null
// 7.6.2
try {
this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
// 7.6.3
queueMicrotask(() => {
// 7.6.3.1
if (errorData === null) {
cacheJobPromise.resolve(undefined)
} else {
// 7.6.3.2
cacheJobPromise.reject(errorData)
}
})
// 7.7
return cacheJobPromise.promise
}
async put (request, response) {
webidl.brandCheck(this, Cache)
webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' })
request = webidl.converters.RequestInfo(request)
response = webidl.converters.Response(response)
// 1.
let innerRequest = null
// 2.
if (request instanceof Request) {
innerRequest = request[kState]
} else { // 3.
innerRequest = new Request(request)[kState]
}
// 4.
if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') {
throw webidl.errors.exception({
header: 'Cache.put',
message: 'Expected an http/s scheme when method is not GET'
})
}
// 5.
const innerResponse = response[kState]
// 6.
if (innerResponse.status === 206) {
throw webidl.errors.exception({
header: 'Cache.put',
message: 'Got 206 status'
})
}
// 7.
if (innerResponse.headersList.contains('vary')) {
// 7.1.
const fieldValues = getFieldValues(innerResponse.headersList.get('vary'))
// 7.2.
for (const fieldValue of fieldValues) {
// 7.2.1
if (fieldValue === '*') {
throw webidl.errors.exception({
header: 'Cache.put',
message: 'Got * vary field value'
})
}
}
}
// 8.
if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) {
throw webidl.errors.exception({
header: 'Cache.put',
message: 'Response body is locked or disturbed'
})
}
// 9.
const clonedResponse = cloneResponse(innerResponse)
// 10.
const bodyReadPromise = createDeferredPromise()
// 11.
if (innerResponse.body != null) {
// 11.1
const stream = innerResponse.body.stream
// 11.2
const reader = stream.getReader()
// 11.3
readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject)
} else {
bodyReadPromise.resolve(undefined)
}
// 12.
/** @type {CacheBatchOperation[]} */
const operations = []
// 13.
/** @type {CacheBatchOperation} */
const operation = {
type: 'put', // 14.
request: innerRequest, // 15.
response: clonedResponse // 16.
}
// 17.
operations.push(operation)
// 19.
const bytes = await bodyReadPromise.promise
if (clonedResponse.body != null) {
clonedResponse.body.source = bytes
}
// 19.1
const cacheJobPromise = createDeferredPromise()
// 19.2.1
let errorData = null
// 19.2.2
try {
this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
// 19.2.3
queueMicrotask(() => {
// 19.2.3.1
if (errorData === null) {
cacheJobPromise.resolve()
} else { // 19.2.3.2
cacheJobPromise.reject(errorData)
}
})
return cacheJobPromise.promise
}
async delete (request, options = {}) {
webidl.brandCheck(this, Cache)
webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' })
request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options)
/**
* @type {Request}
*/
let r = null
if (request instanceof Request) {
r = request[kState]
if (r.method !== 'GET' && !options.ignoreMethod) {
return false
}
} else {
assert(typeof request === 'string')
r = new Request(request)[kState]
}
/** @type {CacheBatchOperation[]} */
const operations = []
/** @type {CacheBatchOperation} */
const operation = {
type: 'delete',
request: r,
options
}
operations.push(operation)
const cacheJobPromise = createDeferredPromise()
let errorData = null
let requestResponses
try {
requestResponses = this.#batchCacheOperations(operations)
} catch (e) {
errorData = e
}
queueMicrotask(() => {
if (errorData === null) {
cacheJobPromise.resolve(!!requestResponses?.length)
} else {
cacheJobPromise.reject(errorData)
}
})
return cacheJobPromise.promise
}
/**
* @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
* @param {any} request
* @param {import('../../types/cache').CacheQueryOptions} options
* @returns {readonly Request[]}
*/
async keys (request = undefined, options = {}) {
webidl.brandCheck(this, Cache)
if (request !== undefined) request = webidl.converters.RequestInfo(request)
options = webidl.converters.CacheQueryOptions(options)
// 1.
let r = null
// 2.
if (request !== undefined) {
// 2.1
if (request instanceof Request) {
// 2.1.1
r = request[kState]
// 2.1.2
if (r.method !== 'GET' && !options.ignoreMethod) {
return []
}
} else if (typeof request === 'string') { // 2.2
r = new Request(request)[kState]
}
}
// 4.
const promise = createDeferredPromise()
// 5.
// 5.1
const requests = []
// 5.2
if (request === undefined) {
// 5.2.1
for (const requestResponse of this.#relevantRequestResponseList) {
// 5.2.1.1
requests.push(requestResponse[0])
}
} else { // 5.3
// 5.3.1
const requestResponses = this.#queryCache(r, options)
// 5.3.2
for (const requestResponse of requestResponses) {
// 5.3.2.1
requests.push(requestResponse[0])
}
}
// 5.4
queueMicrotask(() => {
// 5.4.1
const requestList = []
// 5.4.2
for (const request of requests) {
const requestObject = new Request('https://a')
requestObject[kState] = request
requestObject[kHeaders][kHeadersList] = request.headersList
requestObject[kHeaders][kGuard] = 'immutable'
requestObject[kRealm] = request.client
// 5.4.2.1
requestList.push(requestObject)
}
// 5.4.3
promise.resolve(Object.freeze(requestList))
})
return promise.promise
}
/**
* @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm
* @param {CacheBatchOperation[]} operations
* @returns {requestResponseList}
*/
#batchCacheOperations (operations) {
// 1.
const cache = this.#relevantRequestResponseList
// 2.
const backupCache = [...cache]
// 3.
const addedItems = []
// 4.1
const resultList = []
try {
// 4.2
for (const operation of operations) {
// 4.2.1
if (operation.type !== 'delete' && operation.type !== 'put') {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'operation type does not match "delete" or "put"'
})
}
// 4.2.2
if (operation.type === 'delete' && operation.response != null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'delete operation should not have an associated response'
})
}
// 4.2.3
if (this.#queryCache(operation.request, operation.options, addedItems).length) {
throw new DOMException('???', 'InvalidStateError')
}
// 4.2.4
let requestResponses
// 4.2.5
if (operation.type === 'delete') {
// 4.2.5.1
requestResponses = this.#queryCache(operation.request, operation.options)
// TODO: the spec is wrong, this is needed to pass WPTs
if (requestResponses.length === 0) {
return []
}
// 4.2.5.2
for (const requestResponse of requestResponses) {
const idx = cache.indexOf(requestResponse)
assert(idx !== -1)
// 4.2.5.2.1
cache.splice(idx, 1)
}
} else if (operation.type === 'put') { // 4.2.6
// 4.2.6.1
if (operation.response == null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'put operation should have an associated response'
})
}
// 4.2.6.2
const r = operation.request
// 4.2.6.3
if (!urlIsHttpHttpsScheme(r.url)) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'expected http or https scheme'
})
}
// 4.2.6.4
if (r.method !== 'GET') {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'not get method'
})
}
// 4.2.6.5
if (operation.options != null) {
throw webidl.errors.exception({
header: 'Cache.#batchCacheOperations',
message: 'options must not be defined'
})
}
// 4.2.6.6
requestResponses = this.#queryCache(operation.request)
// 4.2.6.7
for (const requestResponse of requestResponses) {
const idx = cache.indexOf(requestResponse)
assert(idx !== -1)
// 4.2.6.7.1
cache.splice(idx, 1)
}
// 4.2.6.8
cache.push([operation.request, operation.response])
// 4.2.6.10
addedItems.push([operation.request, operation.response])
}
// 4.2.7
resultList.push([operation.request, operation.response])
}
// 4.3
return resultList
} catch (e) { // 5.
// 5.1
this.#relevantRequestResponseList.length = 0
// 5.2
this.#relevantRequestResponseList = backupCache
// 5.3
throw e
}
}
/**
* @see https://w3c.github.io/ServiceWorker/#query-cache
* @param {any} requestQuery
* @param {import('../../types/cache').CacheQueryOptions} options
* @param {requestResponseList} targetStorage
* @returns {requestResponseList}
*/
#queryCache (requestQuery, options, targetStorage) {
/** @type {requestResponseList} */
const resultList = []
const storage = targetStorage ?? this.#relevantRequestResponseList
for (const requestResponse of storage) {
const [cachedRequest, cachedResponse] = requestResponse
if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) {
resultList.push(requestResponse)
}
}
return resultList
}
/**
* @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
* @param {any} requestQuery
* @param {any} request
* @param {any | null} response
* @param {import('../../types/cache').CacheQueryOptions | undefined} options
* @returns {boolean}
*/
#requestMatchesCachedItem (requestQuery, request, response = null, options) {
// if (options?.ignoreMethod === false && request.method === 'GET') {
// return false
// }
const queryURL = new URL(requestQuery.url)
const cachedURL = new URL(request.url)
if (options?.ignoreSearch) {
cachedURL.search = ''
queryURL.search = ''
}
if (!urlEquals(queryURL, cachedURL, true)) {
return false
}
if (
response == null ||
options?.ignoreVary ||
!response.headersList.contains('vary')
) {
return true
}
const fieldValues = getFieldValues(response.headersList.get('vary'))
for (const fieldValue of fieldValues) {
if (fieldValue === '*') {
return false
}
const requestValue = request.headersList.get(fieldValue)
const queryValue = requestQuery.headersList.get(fieldValue)
// If one has the header and the other doesn't, or one has
// a different value than the other, return false
if (requestValue !== queryValue) {
return false
}
}
return true
}
}
Object.defineProperties(Cache.prototype, {
[Symbol.toStringTag]: {
value: 'Cache',
configurable: true
},
match: kEnumerableProperty,
matchAll: kEnumerableProperty,
add: kEnumerableProperty,
addAll: kEnumerableProperty,
put: kEnumerableProperty,
delete: kEnumerableProperty,
keys: kEnumerableProperty
})
const cacheQueryOptionConverters = [
{
key: 'ignoreSearch',
converter: webidl.converters.boolean,
defaultValue: false
},
{
key: 'ignoreMethod',
converter: webidl.converters.boolean,
defaultValue: false
},
{
key: 'ignoreVary',
converter: webidl.converters.boolean,
defaultValue: false
}
]
webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters)
webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([
...cacheQueryOptionConverters,
{
key: 'cacheName',
converter: webidl.converters.DOMString
}
])
webidl.converters.Response = webidl.interfaceConverter(Response)
webidl.converters['sequence<RequestInfo>'] = webidl.sequenceConverter(
webidl.converters.RequestInfo
)
module.exports = {
Cache
}

View File

@@ -0,0 +1,144 @@
'use strict'
const { kConstruct } = require('./symbols')
const { Cache } = require('./cache')
const { webidl } = require('../fetch/webidl')
const { kEnumerableProperty } = require('../core/util')
class CacheStorage {
/**
* @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map
* @type {Map<string, import('./cache').requestResponseList}
*/
#caches = new Map()
constructor () {
if (arguments[0] !== kConstruct) {
webidl.illegalConstructor()
}
}
async match (request, options = {}) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.match' })
request = webidl.converters.RequestInfo(request)
options = webidl.converters.MultiCacheQueryOptions(options)
// 1.
if (options.cacheName != null) {
// 1.1.1.1
if (this.#caches.has(options.cacheName)) {
// 1.1.1.1.1
const cacheList = this.#caches.get(options.cacheName)
const cache = new Cache(kConstruct, cacheList)
return await cache.match(request, options)
}
} else { // 2.
// 2.2
for (const cacheList of this.#caches.values()) {
const cache = new Cache(kConstruct, cacheList)
// 2.2.1.2
const response = await cache.match(request, options)
if (response !== undefined) {
return response
}
}
}
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-has
* @param {string} cacheName
* @returns {Promise<boolean>}
*/
async has (cacheName) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' })
cacheName = webidl.converters.DOMString(cacheName)
// 2.1.1
// 2.2
return this.#caches.has(cacheName)
}
/**
* @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open
* @param {string} cacheName
* @returns {Promise<Cache>}
*/
async open (cacheName) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' })
cacheName = webidl.converters.DOMString(cacheName)
// 2.1
if (this.#caches.has(cacheName)) {
// await caches.open('v1') !== await caches.open('v1')
// 2.1.1
const cache = this.#caches.get(cacheName)
// 2.1.1.1
return new Cache(kConstruct, cache)
}
// 2.2
const cache = []
// 2.3
this.#caches.set(cacheName, cache)
// 2.4
return new Cache(kConstruct, cache)
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-delete
* @param {string} cacheName
* @returns {Promise<boolean>}
*/
async delete (cacheName) {
webidl.brandCheck(this, CacheStorage)
webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' })
cacheName = webidl.converters.DOMString(cacheName)
return this.#caches.delete(cacheName)
}
/**
* @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
* @returns {string[]}
*/
async keys () {
webidl.brandCheck(this, CacheStorage)
// 2.1
const keys = this.#caches.keys()
// 2.2
return [...keys]
}
}
Object.defineProperties(CacheStorage.prototype, {
[Symbol.toStringTag]: {
value: 'CacheStorage',
configurable: true
},
match: kEnumerableProperty,
has: kEnumerableProperty,
open: kEnumerableProperty,
delete: kEnumerableProperty,
keys: kEnumerableProperty
})
module.exports = {
CacheStorage
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
kConstruct: require('../core/symbols').kConstruct
}

View File

@@ -0,0 +1,49 @@
'use strict'
const assert = require('assert')
const { URLSerializer } = require('../fetch/dataURL')
const { isValidHeaderName } = require('../fetch/util')
/**
* @see https://url.spec.whatwg.org/#concept-url-equals
* @param {URL} A
* @param {URL} B
* @param {boolean | undefined} excludeFragment
* @returns {boolean}
*/
function urlEquals (A, B, excludeFragment = false) {
const serializedA = URLSerializer(A, excludeFragment)
const serializedB = URLSerializer(B, excludeFragment)
return serializedA === serializedB
}
/**
* @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262
* @param {string} header
*/
function fieldValues (header) {
assert(header !== null)
const values = []
for (let value of header.split(',')) {
value = value.trim()
if (!value.length) {
continue
} else if (!isValidHeaderName(value)) {
continue
}
values.push(value)
}
return values
}
module.exports = {
urlEquals,
fieldValues
}

View File

@@ -22,15 +22,25 @@ class CompatFinalizer {
} }
register (dispatcher, key) { register (dispatcher, key) {
dispatcher.on('disconnect', () => { if (dispatcher.on) {
if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { dispatcher.on('disconnect', () => {
this.finalizer(key) if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) {
} this.finalizer(key)
}) }
})
}
} }
} }
module.exports = function () { module.exports = function () {
// FIXME: remove workaround when the Node bug is fixed
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
if (process.env.NODE_V8_COVERAGE) {
return {
WeakRef: CompatWeakRef,
FinalizationRegistry: CompatFinalizer
}
}
return { return {
WeakRef: global.WeakRef || CompatWeakRef, WeakRef: global.WeakRef || CompatWeakRef,
FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer

View File

@@ -0,0 +1,12 @@
'use strict'
// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size
const maxAttributeValueSize = 1024
// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size
const maxNameValuePairSize = 4096
module.exports = {
maxAttributeValueSize,
maxNameValuePairSize
}

View File

@@ -0,0 +1,184 @@
'use strict'
const { parseSetCookie } = require('./parse')
const { stringify, getHeadersList } = require('./util')
const { webidl } = require('../fetch/webidl')
const { Headers } = require('../fetch/headers')
/**
* @typedef {Object} Cookie
* @property {string} name
* @property {string} value
* @property {Date|number|undefined} expires
* @property {number|undefined} maxAge
* @property {string|undefined} domain
* @property {string|undefined} path
* @property {boolean|undefined} secure
* @property {boolean|undefined} httpOnly
* @property {'Strict'|'Lax'|'None'} sameSite
* @property {string[]} unparsed
*/
/**
* @param {Headers} headers
* @returns {Record<string, string>}
*/
function getCookies (headers) {
webidl.argumentLengthCheck(arguments, 1, { header: 'getCookies' })
webidl.brandCheck(headers, Headers, { strict: false })
const cookie = headers.get('cookie')
const out = {}
if (!cookie) {
return out
}
for (const piece of cookie.split(';')) {
const [name, ...value] = piece.split('=')
out[name.trim()] = value.join('=')
}
return out
}
/**
* @param {Headers} headers
* @param {string} name
* @param {{ path?: string, domain?: string }|undefined} attributes
* @returns {void}
*/
function deleteCookie (headers, name, attributes) {
webidl.argumentLengthCheck(arguments, 2, { header: 'deleteCookie' })
webidl.brandCheck(headers, Headers, { strict: false })
name = webidl.converters.DOMString(name)
attributes = webidl.converters.DeleteCookieAttributes(attributes)
// Matches behavior of
// https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278
setCookie(headers, {
name,
value: '',
expires: new Date(0),
...attributes
})
}
/**
* @param {Headers} headers
* @returns {Cookie[]}
*/
function getSetCookies (headers) {
webidl.argumentLengthCheck(arguments, 1, { header: 'getSetCookies' })
webidl.brandCheck(headers, Headers, { strict: false })
const cookies = getHeadersList(headers).cookies
if (!cookies) {
return []
}
// In older versions of undici, cookies is a list of name:value.
return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair))
}
/**
* @param {Headers} headers
* @param {Cookie} cookie
* @returns {void}
*/
function setCookie (headers, cookie) {
webidl.argumentLengthCheck(arguments, 2, { header: 'setCookie' })
webidl.brandCheck(headers, Headers, { strict: false })
cookie = webidl.converters.Cookie(cookie)
const str = stringify(cookie)
if (str) {
headers.append('Set-Cookie', stringify(cookie))
}
}
webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'path',
defaultValue: null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'domain',
defaultValue: null
}
])
webidl.converters.Cookie = webidl.dictionaryConverter([
{
converter: webidl.converters.DOMString,
key: 'name'
},
{
converter: webidl.converters.DOMString,
key: 'value'
},
{
converter: webidl.nullableConverter((value) => {
if (typeof value === 'number') {
return webidl.converters['unsigned long long'](value)
}
return new Date(value)
}),
key: 'expires',
defaultValue: null
},
{
converter: webidl.nullableConverter(webidl.converters['long long']),
key: 'maxAge',
defaultValue: null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'domain',
defaultValue: null
},
{
converter: webidl.nullableConverter(webidl.converters.DOMString),
key: 'path',
defaultValue: null
},
{
converter: webidl.nullableConverter(webidl.converters.boolean),
key: 'secure',
defaultValue: null
},
{
converter: webidl.nullableConverter(webidl.converters.boolean),
key: 'httpOnly',
defaultValue: null
},
{
converter: webidl.converters.USVString,
key: 'sameSite',
allowedValues: ['Strict', 'Lax', 'None']
},
{
converter: webidl.sequenceConverter(webidl.converters.DOMString),
key: 'unparsed',
defaultValue: []
}
])
module.exports = {
getCookies,
deleteCookie,
getSetCookies,
setCookie
}

View File

@@ -0,0 +1,317 @@
'use strict'
const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
const { isCTLExcludingHtab } = require('./util')
const { collectASequenceOfCodePointsFast } = require('../fetch/dataURL')
const assert = require('assert')
/**
* @description Parses the field-value attributes of a set-cookie header string.
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
* @param {string} header
* @returns if the header is invalid, null will be returned
*/
function parseSetCookie (header) {
// 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F
// character (CTL characters excluding HTAB): Abort these steps and
// ignore the set-cookie-string entirely.
if (isCTLExcludingHtab(header)) {
return null
}
let nameValuePair = ''
let unparsedAttributes = ''
let name = ''
let value = ''
// 2. If the set-cookie-string contains a %x3B (";") character:
if (header.includes(';')) {
// 1. The name-value-pair string consists of the characters up to,
// but not including, the first %x3B (";"), and the unparsed-
// attributes consist of the remainder of the set-cookie-string
// (including the %x3B (";") in question).
const position = { position: 0 }
nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
unparsedAttributes = header.slice(position.position)
} else {
// Otherwise:
// 1. The name-value-pair string consists of all the characters
// contained in the set-cookie-string, and the unparsed-
// attributes is the empty string.
nameValuePair = header
}
// 3. If the name-value-pair string lacks a %x3D ("=") character, then
// the name string is empty, and the value string is the value of
// name-value-pair.
if (!nameValuePair.includes('=')) {
value = nameValuePair
} else {
// Otherwise, the name string consists of the characters up to, but
// not including, the first %x3D ("=") character, and the (possibly
// empty) value string consists of the characters after the first
// %x3D ("=") character.
const position = { position: 0 }
name = collectASequenceOfCodePointsFast(
'=',
nameValuePair,
position
)
value = nameValuePair.slice(position.position + 1)
}
// 4. Remove any leading or trailing WSP characters from the name
// string and the value string.
name = name.trim()
value = value.trim()
// 5. If the sum of the lengths of the name string and the value string
// is more than 4096 octets, abort these steps and ignore the set-
// cookie-string entirely.
if (name.length + value.length > maxNameValuePairSize) {
return null
}
// 6. The cookie-name is the name string, and the cookie-value is the
// value string.
return {
name, value, ...parseUnparsedAttributes(unparsedAttributes)
}
}
/**
* Parses the remaining attributes of a set-cookie header
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
* @param {string} unparsedAttributes
* @param {[Object.<string, unknown>]={}} cookieAttributeList
*/
function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) {
// 1. If the unparsed-attributes string is empty, skip the rest of
// these steps.
if (unparsedAttributes.length === 0) {
return cookieAttributeList
}
// 2. Discard the first character of the unparsed-attributes (which
// will be a %x3B (";") character).
assert(unparsedAttributes[0] === ';')
unparsedAttributes = unparsedAttributes.slice(1)
let cookieAv = ''
// 3. If the remaining unparsed-attributes contains a %x3B (";")
// character:
if (unparsedAttributes.includes(';')) {
// 1. Consume the characters of the unparsed-attributes up to, but
// not including, the first %x3B (";") character.
cookieAv = collectASequenceOfCodePointsFast(
';',
unparsedAttributes,
{ position: 0 }
)
unparsedAttributes = unparsedAttributes.slice(cookieAv.length)
} else {
// Otherwise:
// 1. Consume the remainder of the unparsed-attributes.
cookieAv = unparsedAttributes
unparsedAttributes = ''
}
// Let the cookie-av string be the characters consumed in this step.
let attributeName = ''
let attributeValue = ''
// 4. If the cookie-av string contains a %x3D ("=") character:
if (cookieAv.includes('=')) {
// 1. The (possibly empty) attribute-name string consists of the
// characters up to, but not including, the first %x3D ("=")
// character, and the (possibly empty) attribute-value string
// consists of the characters after the first %x3D ("=")
// character.
const position = { position: 0 }
attributeName = collectASequenceOfCodePointsFast(
'=',
cookieAv,
position
)
attributeValue = cookieAv.slice(position.position + 1)
} else {
// Otherwise:
// 1. The attribute-name string consists of the entire cookie-av
// string, and the attribute-value string is empty.
attributeName = cookieAv
}
// 5. Remove any leading or trailing WSP characters from the attribute-
// name string and the attribute-value string.
attributeName = attributeName.trim()
attributeValue = attributeValue.trim()
// 6. If the attribute-value is longer than 1024 octets, ignore the
// cookie-av string and return to Step 1 of this algorithm.
if (attributeValue.length > maxAttributeValueSize) {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 7. Process the attribute-name and attribute-value according to the
// requirements in the following subsections. (Notice that
// attributes with unrecognized attribute-names are ignored.)
const attributeNameLowercase = attributeName.toLowerCase()
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1
// If the attribute-name case-insensitively matches the string
// "Expires", the user agent MUST process the cookie-av as follows.
if (attributeNameLowercase === 'expires') {
// 1. Let the expiry-time be the result of parsing the attribute-value
// as cookie-date (see Section 5.1.1).
const expiryTime = new Date(attributeValue)
// 2. If the attribute-value failed to parse as a cookie date, ignore
// the cookie-av.
cookieAttributeList.expires = expiryTime
} else if (attributeNameLowercase === 'max-age') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2
// If the attribute-name case-insensitively matches the string "Max-
// Age", the user agent MUST process the cookie-av as follows.
// 1. If the first character of the attribute-value is not a DIGIT or a
// "-" character, ignore the cookie-av.
const charCode = attributeValue.charCodeAt(0)
if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 2. If the remainder of attribute-value contains a non-DIGIT
// character, ignore the cookie-av.
if (!/^\d+$/.test(attributeValue)) {
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
// 3. Let delta-seconds be the attribute-value converted to an integer.
const deltaSeconds = Number(attributeValue)
// 4. Let cookie-age-limit be the maximum age of the cookie (which
// SHOULD be 400 days or less, see Section 4.1.2.2).
// 5. Set delta-seconds to the smaller of its present value and cookie-
// age-limit.
// deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs)
// 6. If delta-seconds is less than or equal to zero (0), let expiry-
// time be the earliest representable date and time. Otherwise, let
// the expiry-time be the current date and time plus delta-seconds
// seconds.
// const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds
// 7. Append an attribute to the cookie-attribute-list with an
// attribute-name of Max-Age and an attribute-value of expiry-time.
cookieAttributeList.maxAge = deltaSeconds
} else if (attributeNameLowercase === 'domain') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3
// If the attribute-name case-insensitively matches the string "Domain",
// the user agent MUST process the cookie-av as follows.
// 1. Let cookie-domain be the attribute-value.
let cookieDomain = attributeValue
// 2. If cookie-domain starts with %x2E ("."), let cookie-domain be
// cookie-domain without its leading %x2E (".").
if (cookieDomain[0] === '.') {
cookieDomain = cookieDomain.slice(1)
}
// 3. Convert the cookie-domain to lower case.
cookieDomain = cookieDomain.toLowerCase()
// 4. Append an attribute to the cookie-attribute-list with an
// attribute-name of Domain and an attribute-value of cookie-domain.
cookieAttributeList.domain = cookieDomain
} else if (attributeNameLowercase === 'path') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4
// If the attribute-name case-insensitively matches the string "Path",
// the user agent MUST process the cookie-av as follows.
// 1. If the attribute-value is empty or if the first character of the
// attribute-value is not %x2F ("/"):
let cookiePath = ''
if (attributeValue.length === 0 || attributeValue[0] !== '/') {
// 1. Let cookie-path be the default-path.
cookiePath = '/'
} else {
// Otherwise:
// 1. Let cookie-path be the attribute-value.
cookiePath = attributeValue
}
// 2. Append an attribute to the cookie-attribute-list with an
// attribute-name of Path and an attribute-value of cookie-path.
cookieAttributeList.path = cookiePath
} else if (attributeNameLowercase === 'secure') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5
// If the attribute-name case-insensitively matches the string "Secure",
// the user agent MUST append an attribute to the cookie-attribute-list
// with an attribute-name of Secure and an empty attribute-value.
cookieAttributeList.secure = true
} else if (attributeNameLowercase === 'httponly') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6
// If the attribute-name case-insensitively matches the string
// "HttpOnly", the user agent MUST append an attribute to the cookie-
// attribute-list with an attribute-name of HttpOnly and an empty
// attribute-value.
cookieAttributeList.httpOnly = true
} else if (attributeNameLowercase === 'samesite') {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7
// If the attribute-name case-insensitively matches the string
// "SameSite", the user agent MUST process the cookie-av as follows:
// 1. Let enforcement be "Default".
let enforcement = 'Default'
const attributeValueLowercase = attributeValue.toLowerCase()
// 2. If cookie-av's attribute-value is a case-insensitive match for
// "None", set enforcement to "None".
if (attributeValueLowercase.includes('none')) {
enforcement = 'None'
}
// 3. If cookie-av's attribute-value is a case-insensitive match for
// "Strict", set enforcement to "Strict".
if (attributeValueLowercase.includes('strict')) {
enforcement = 'Strict'
}
// 4. If cookie-av's attribute-value is a case-insensitive match for
// "Lax", set enforcement to "Lax".
if (attributeValueLowercase.includes('lax')) {
enforcement = 'Lax'
}
// 5. Append an attribute to the cookie-attribute-list with an
// attribute-name of "SameSite" and an attribute-value of
// enforcement.
cookieAttributeList.sameSite = enforcement
} else {
cookieAttributeList.unparsed ??= []
cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`)
}
// 8. Return to Step 1 of this algorithm.
return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
}
module.exports = {
parseSetCookie,
parseUnparsedAttributes
}

View File

@@ -0,0 +1,291 @@
'use strict'
const assert = require('assert')
const { kHeadersList } = require('../core/symbols')
function isCTLExcludingHtab (value) {
if (value.length === 0) {
return false
}
for (const char of value) {
const code = char.charCodeAt(0)
if (
(code >= 0x00 || code <= 0x08) ||
(code >= 0x0A || code <= 0x1F) ||
code === 0x7F
) {
return false
}
}
}
/**
CHAR = <any US-ASCII character (octets 0 - 127)>
token = 1*<any CHAR except CTLs or separators>
separators = "(" | ")" | "<" | ">" | "@"
| "," | ";" | ":" | "\" | <">
| "/" | "[" | "]" | "?" | "="
| "{" | "}" | SP | HT
* @param {string} name
*/
function validateCookieName (name) {
for (const char of name) {
const code = char.charCodeAt(0)
if (
(code <= 0x20 || code > 0x7F) ||
char === '(' ||
char === ')' ||
char === '>' ||
char === '<' ||
char === '@' ||
char === ',' ||
char === ';' ||
char === ':' ||
char === '\\' ||
char === '"' ||
char === '/' ||
char === '[' ||
char === ']' ||
char === '?' ||
char === '=' ||
char === '{' ||
char === '}'
) {
throw new Error('Invalid cookie name')
}
}
}
/**
cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
; US-ASCII characters excluding CTLs,
; whitespace DQUOTE, comma, semicolon,
; and backslash
* @param {string} value
*/
function validateCookieValue (value) {
for (const char of value) {
const code = char.charCodeAt(0)
if (
code < 0x21 || // exclude CTLs (0-31)
code === 0x22 ||
code === 0x2C ||
code === 0x3B ||
code === 0x5C ||
code > 0x7E // non-ascii
) {
throw new Error('Invalid header value')
}
}
}
/**
* path-value = <any CHAR except CTLs or ";">
* @param {string} path
*/
function validateCookiePath (path) {
for (const char of path) {
const code = char.charCodeAt(0)
if (code < 0x21 || char === ';') {
throw new Error('Invalid cookie path')
}
}
}
/**
* I have no idea why these values aren't allowed to be honest,
* but Deno tests these. - Khafra
* @param {string} domain
*/
function validateCookieDomain (domain) {
if (
domain.startsWith('-') ||
domain.endsWith('.') ||
domain.endsWith('-')
) {
throw new Error('Invalid cookie domain')
}
}
/**
* @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
* @param {number|Date} date
IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
; fixed length/zone/capitalization subset of the format
; see Section 3.3 of [RFC5322]
day-name = %x4D.6F.6E ; "Mon", case-sensitive
/ %x54.75.65 ; "Tue", case-sensitive
/ %x57.65.64 ; "Wed", case-sensitive
/ %x54.68.75 ; "Thu", case-sensitive
/ %x46.72.69 ; "Fri", case-sensitive
/ %x53.61.74 ; "Sat", case-sensitive
/ %x53.75.6E ; "Sun", case-sensitive
date1 = day SP month SP year
; e.g., 02 Jun 1982
day = 2DIGIT
month = %x4A.61.6E ; "Jan", case-sensitive
/ %x46.65.62 ; "Feb", case-sensitive
/ %x4D.61.72 ; "Mar", case-sensitive
/ %x41.70.72 ; "Apr", case-sensitive
/ %x4D.61.79 ; "May", case-sensitive
/ %x4A.75.6E ; "Jun", case-sensitive
/ %x4A.75.6C ; "Jul", case-sensitive
/ %x41.75.67 ; "Aug", case-sensitive
/ %x53.65.70 ; "Sep", case-sensitive
/ %x4F.63.74 ; "Oct", case-sensitive
/ %x4E.6F.76 ; "Nov", case-sensitive
/ %x44.65.63 ; "Dec", case-sensitive
year = 4DIGIT
GMT = %x47.4D.54 ; "GMT", case-sensitive
time-of-day = hour ":" minute ":" second
; 00:00:00 - 23:59:60 (leap second)
hour = 2DIGIT
minute = 2DIGIT
second = 2DIGIT
*/
function toIMFDate (date) {
if (typeof date === 'number') {
date = new Date(date)
}
const days = [
'Sun', 'Mon', 'Tue', 'Wed',
'Thu', 'Fri', 'Sat'
]
const months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]
const dayName = days[date.getUTCDay()]
const day = date.getUTCDate().toString().padStart(2, '0')
const month = months[date.getUTCMonth()]
const year = date.getUTCFullYear()
const hour = date.getUTCHours().toString().padStart(2, '0')
const minute = date.getUTCMinutes().toString().padStart(2, '0')
const second = date.getUTCSeconds().toString().padStart(2, '0')
return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT`
}
/**
max-age-av = "Max-Age=" non-zero-digit *DIGIT
; In practice, both expires-av and max-age-av
; are limited to dates representable by the
; user agent.
* @param {number} maxAge
*/
function validateCookieMaxAge (maxAge) {
if (maxAge < 0) {
throw new Error('Invalid cookie max-age')
}
}
/**
* @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
* @param {import('./index').Cookie} cookie
*/
function stringify (cookie) {
if (cookie.name.length === 0) {
return null
}
validateCookieName(cookie.name)
validateCookieValue(cookie.value)
const out = [`${cookie.name}=${cookie.value}`]
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2
if (cookie.name.startsWith('__Secure-')) {
cookie.secure = true
}
if (cookie.name.startsWith('__Host-')) {
cookie.secure = true
cookie.domain = null
cookie.path = '/'
}
if (cookie.secure) {
out.push('Secure')
}
if (cookie.httpOnly) {
out.push('HttpOnly')
}
if (typeof cookie.maxAge === 'number') {
validateCookieMaxAge(cookie.maxAge)
out.push(`Max-Age=${cookie.maxAge}`)
}
if (cookie.domain) {
validateCookieDomain(cookie.domain)
out.push(`Domain=${cookie.domain}`)
}
if (cookie.path) {
validateCookiePath(cookie.path)
out.push(`Path=${cookie.path}`)
}
if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
out.push(`Expires=${toIMFDate(cookie.expires)}`)
}
if (cookie.sameSite) {
out.push(`SameSite=${cookie.sameSite}`)
}
for (const part of cookie.unparsed) {
if (!part.includes('=')) {
throw new Error('Invalid unparsed')
}
const [key, ...value] = part.split('=')
out.push(`${key.trim()}=${value.join('=')}`)
}
return out.join('; ')
}
let kHeadersListNode
function getHeadersList (headers) {
if (headers[kHeadersList]) {
return headers[kHeadersList]
}
if (!kHeadersListNode) {
kHeadersListNode = Object.getOwnPropertySymbols(headers).find(
(symbol) => symbol.description === 'headers list'
)
assert(kHeadersListNode, 'Headers cannot be parsed')
}
const headersList = headers[kHeadersListNode]
assert(headersList)
return headersList
}
module.exports = {
isCTLExcludingHtab,
stringify,
getHeadersList
}

View File

@@ -0,0 +1,189 @@
'use strict'
const net = require('net')
const assert = require('assert')
const util = require('./util')
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
let tls // include tls conditionally since it is not always available
// TODO: session re-use does not wait for the first
// connection to resolve the session and might therefore
// resolve the same servername multiple times even when
// re-use is enabled.
let SessionCache
// FIXME: remove workaround when the Node bug is fixed
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) {
SessionCache = class WeakSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
this._sessionCache = new Map()
this._sessionRegistry = new global.FinalizationRegistry((key) => {
if (this._sessionCache.size < this._maxCachedSessions) {
return
}
const ref = this._sessionCache.get(key)
if (ref !== undefined && ref.deref() === undefined) {
this._sessionCache.delete(key)
}
})
}
get (sessionKey) {
const ref = this._sessionCache.get(sessionKey)
return ref ? ref.deref() : null
}
set (sessionKey, session) {
if (this._maxCachedSessions === 0) {
return
}
this._sessionCache.set(sessionKey, new WeakRef(session))
this._sessionRegistry.register(session, sessionKey)
}
}
} else {
SessionCache = class SimpleSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
this._sessionCache = new Map()
}
get (sessionKey) {
return this._sessionCache.get(sessionKey)
}
set (sessionKey, session) {
if (this._maxCachedSessions === 0) {
return
}
if (this._sessionCache.size >= this._maxCachedSessions) {
// remove the oldest session
const { value: oldestKey } = this._sessionCache.keys().next()
this._sessionCache.delete(oldestKey)
}
this._sessionCache.set(sessionKey, session)
}
}
}
function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) {
if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
}
const options = { path: socketPath, ...opts }
const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
timeout = timeout == null ? 10e3 : timeout
allowH2 = allowH2 != null ? allowH2 : false
return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
let socket
if (protocol === 'https:') {
if (!tls) {
tls = require('tls')
}
servername = servername || options.servername || util.getServerName(host) || null
const sessionKey = servername || hostname
const session = sessionCache.get(sessionKey) || null
assert(sessionKey)
socket = tls.connect({
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
...options,
servername,
session,
localAddress,
// TODO(HTTP/2): Add support for h2c
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
socket: httpSocket, // upgrade socket connection
port: port || 443,
host: hostname
})
socket
.on('session', function (session) {
// TODO (fix): Can a session become invalid once established? Don't think so?
sessionCache.set(sessionKey, session)
})
} else {
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
socket = net.connect({
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
...options,
localAddress,
port: port || 80,
host: hostname
})
}
// Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
if (options.keepAlive == null || options.keepAlive) {
const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay
socket.setKeepAlive(true, keepAliveInitialDelay)
}
const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
socket
.setNoDelay(true)
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
cancelTimeout()
if (callback) {
const cb = callback
callback = null
cb(null, this)
}
})
.on('error', function (err) {
cancelTimeout()
if (callback) {
const cb = callback
callback = null
cb(err)
}
})
return socket
}
}
function setupTimeout (onConnectTimeout, timeout) {
if (!timeout) {
return () => {}
}
let s1 = null
let s2 = null
const timeoutId = setTimeout(() => {
// setImmediate is added to make sure that we priotorise socket error events over timeouts
s1 = setImmediate(() => {
if (process.platform === 'win32') {
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
s2 = setImmediate(() => onConnectTimeout())
} else {
onConnectTimeout()
}
})
}, timeout)
return () => {
clearTimeout(timeoutId)
clearImmediate(s1)
clearImmediate(s2)
}
}
function onConnectTimeout (socket) {
util.destroy(socket, new ConnectTimeoutError())
}
module.exports = buildConnector

View File

@@ -0,0 +1,118 @@
'use strict'
/** @type {Record<string, string | undefined>} */
const headerNameLowerCasedRecord = {}
// https://developer.mozilla.org/docs/Web/HTTP/Headers
const wellknownHeaderNames = [
'Accept',
'Accept-Encoding',
'Accept-Language',
'Accept-Ranges',
'Access-Control-Allow-Credentials',
'Access-Control-Allow-Headers',
'Access-Control-Allow-Methods',
'Access-Control-Allow-Origin',
'Access-Control-Expose-Headers',
'Access-Control-Max-Age',
'Access-Control-Request-Headers',
'Access-Control-Request-Method',
'Age',
'Allow',
'Alt-Svc',
'Alt-Used',
'Authorization',
'Cache-Control',
'Clear-Site-Data',
'Connection',
'Content-Disposition',
'Content-Encoding',
'Content-Language',
'Content-Length',
'Content-Location',
'Content-Range',
'Content-Security-Policy',
'Content-Security-Policy-Report-Only',
'Content-Type',
'Cookie',
'Cross-Origin-Embedder-Policy',
'Cross-Origin-Opener-Policy',
'Cross-Origin-Resource-Policy',
'Date',
'Device-Memory',
'Downlink',
'ECT',
'ETag',
'Expect',
'Expect-CT',
'Expires',
'Forwarded',
'From',
'Host',
'If-Match',
'If-Modified-Since',
'If-None-Match',
'If-Range',
'If-Unmodified-Since',
'Keep-Alive',
'Last-Modified',
'Link',
'Location',
'Max-Forwards',
'Origin',
'Permissions-Policy',
'Pragma',
'Proxy-Authenticate',
'Proxy-Authorization',
'RTT',
'Range',
'Referer',
'Referrer-Policy',
'Refresh',
'Retry-After',
'Sec-WebSocket-Accept',
'Sec-WebSocket-Extensions',
'Sec-WebSocket-Key',
'Sec-WebSocket-Protocol',
'Sec-WebSocket-Version',
'Server',
'Server-Timing',
'Service-Worker-Allowed',
'Service-Worker-Navigation-Preload',
'Set-Cookie',
'SourceMap',
'Strict-Transport-Security',
'Supports-Loading-Mode',
'TE',
'Timing-Allow-Origin',
'Trailer',
'Transfer-Encoding',
'Upgrade',
'Upgrade-Insecure-Requests',
'User-Agent',
'Vary',
'Via',
'WWW-Authenticate',
'X-Content-Type-Options',
'X-DNS-Prefetch-Control',
'X-Frame-Options',
'X-Permitted-Cross-Domain-Policies',
'X-Powered-By',
'X-Requested-With',
'X-XSS-Protection'
]
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
const key = wellknownHeaderNames[i]
const lowerCasedKey = key.toLowerCase()
headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] =
lowerCasedKey
}
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
module.exports = {
wellknownHeaderNames,
headerNameLowerCasedRecord
}

View File

@@ -0,0 +1,230 @@
'use strict'
class UndiciError extends Error {
constructor (message) {
super(message)
this.name = 'UndiciError'
this.code = 'UND_ERR'
}
}
class ConnectTimeoutError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, ConnectTimeoutError)
this.name = 'ConnectTimeoutError'
this.message = message || 'Connect Timeout Error'
this.code = 'UND_ERR_CONNECT_TIMEOUT'
}
}
class HeadersTimeoutError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, HeadersTimeoutError)
this.name = 'HeadersTimeoutError'
this.message = message || 'Headers Timeout Error'
this.code = 'UND_ERR_HEADERS_TIMEOUT'
}
}
class HeadersOverflowError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, HeadersOverflowError)
this.name = 'HeadersOverflowError'
this.message = message || 'Headers Overflow Error'
this.code = 'UND_ERR_HEADERS_OVERFLOW'
}
}
class BodyTimeoutError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, BodyTimeoutError)
this.name = 'BodyTimeoutError'
this.message = message || 'Body Timeout Error'
this.code = 'UND_ERR_BODY_TIMEOUT'
}
}
class ResponseStatusCodeError extends UndiciError {
constructor (message, statusCode, headers, body) {
super(message)
Error.captureStackTrace(this, ResponseStatusCodeError)
this.name = 'ResponseStatusCodeError'
this.message = message || 'Response Status Code Error'
this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
this.body = body
this.status = statusCode
this.statusCode = statusCode
this.headers = headers
}
}
class InvalidArgumentError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, InvalidArgumentError)
this.name = 'InvalidArgumentError'
this.message = message || 'Invalid Argument Error'
this.code = 'UND_ERR_INVALID_ARG'
}
}
class InvalidReturnValueError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, InvalidReturnValueError)
this.name = 'InvalidReturnValueError'
this.message = message || 'Invalid Return Value Error'
this.code = 'UND_ERR_INVALID_RETURN_VALUE'
}
}
class RequestAbortedError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, RequestAbortedError)
this.name = 'AbortError'
this.message = message || 'Request aborted'
this.code = 'UND_ERR_ABORTED'
}
}
class InformationalError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, InformationalError)
this.name = 'InformationalError'
this.message = message || 'Request information'
this.code = 'UND_ERR_INFO'
}
}
class RequestContentLengthMismatchError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, RequestContentLengthMismatchError)
this.name = 'RequestContentLengthMismatchError'
this.message = message || 'Request body length does not match content-length header'
this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
}
}
class ResponseContentLengthMismatchError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, ResponseContentLengthMismatchError)
this.name = 'ResponseContentLengthMismatchError'
this.message = message || 'Response body length does not match content-length header'
this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'
}
}
class ClientDestroyedError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, ClientDestroyedError)
this.name = 'ClientDestroyedError'
this.message = message || 'The client is destroyed'
this.code = 'UND_ERR_DESTROYED'
}
}
class ClientClosedError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, ClientClosedError)
this.name = 'ClientClosedError'
this.message = message || 'The client is closed'
this.code = 'UND_ERR_CLOSED'
}
}
class SocketError extends UndiciError {
constructor (message, socket) {
super(message)
Error.captureStackTrace(this, SocketError)
this.name = 'SocketError'
this.message = message || 'Socket error'
this.code = 'UND_ERR_SOCKET'
this.socket = socket
}
}
class NotSupportedError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, NotSupportedError)
this.name = 'NotSupportedError'
this.message = message || 'Not supported error'
this.code = 'UND_ERR_NOT_SUPPORTED'
}
}
class BalancedPoolMissingUpstreamError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, NotSupportedError)
this.name = 'MissingUpstreamError'
this.message = message || 'No upstream has been added to the BalancedPool'
this.code = 'UND_ERR_BPL_MISSING_UPSTREAM'
}
}
class HTTPParserError extends Error {
constructor (message, code, data) {
super(message)
Error.captureStackTrace(this, HTTPParserError)
this.name = 'HTTPParserError'
this.code = code ? `HPE_${code}` : undefined
this.data = data ? data.toString() : undefined
}
}
class ResponseExceededMaxSizeError extends UndiciError {
constructor (message) {
super(message)
Error.captureStackTrace(this, ResponseExceededMaxSizeError)
this.name = 'ResponseExceededMaxSizeError'
this.message = message || 'Response content exceeded max size'
this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE'
}
}
class RequestRetryError extends UndiciError {
constructor (message, code, { headers, data }) {
super(message)
Error.captureStackTrace(this, RequestRetryError)
this.name = 'RequestRetryError'
this.message = message || 'Request retry error'
this.code = 'UND_ERR_REQ_RETRY'
this.statusCode = code
this.data = data
this.headers = headers
}
}
module.exports = {
HTTPParserError,
UndiciError,
HeadersTimeoutError,
HeadersOverflowError,
BodyTimeoutError,
RequestContentLengthMismatchError,
ConnectTimeoutError,
ResponseStatusCodeError,
InvalidArgumentError,
InvalidReturnValueError,
RequestAbortedError,
ClientDestroyedError,
ClientClosedError,
InformationalError,
SocketError,
NotSupportedError,
ResponseContentLengthMismatchError,
BalancedPoolMissingUpstreamError,
ResponseExceededMaxSizeError,
RequestRetryError
}

View File

@@ -0,0 +1,499 @@
'use strict'
const {
InvalidArgumentError,
NotSupportedError
} = require('./errors')
const assert = require('assert')
const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols')
const util = require('./util')
// tokenRegExp and headerCharRegex have been lifted from
// https://github.com/nodejs/node/blob/main/lib/_http_common.js
/**
* Verifies that the given val is a valid HTTP token
* per the rules defined in RFC 7230
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
*/
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
/**
* Matches if val contains an invalid field-vchar
* field-value = *( field-content / obs-fold )
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
*/
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
// Verifies that a given path is valid does not contain control chars \x00 to \x20
const invalidPathRegex = /[^\u0021-\u00ff]/
const kHandler = Symbol('handler')
const channels = {}
let extractBody
try {
const diagnosticsChannel = require('diagnostics_channel')
channels.create = diagnosticsChannel.channel('undici:request:create')
channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent')
channels.headers = diagnosticsChannel.channel('undici:request:headers')
channels.trailers = diagnosticsChannel.channel('undici:request:trailers')
channels.error = diagnosticsChannel.channel('undici:request:error')
} catch {
channels.create = { hasSubscribers: false }
channels.bodySent = { hasSubscribers: false }
channels.headers = { hasSubscribers: false }
channels.trailers = { hasSubscribers: false }
channels.error = { hasSubscribers: false }
}
class Request {
constructor (origin, {
path,
method,
body,
headers,
query,
idempotent,
blocking,
upgrade,
headersTimeout,
bodyTimeout,
reset,
throwOnError,
expectContinue
}, handler) {
if (typeof path !== 'string') {
throw new InvalidArgumentError('path must be a string')
} else if (
path[0] !== '/' &&
!(path.startsWith('http://') || path.startsWith('https://')) &&
method !== 'CONNECT'
) {
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
} else if (invalidPathRegex.exec(path) !== null) {
throw new InvalidArgumentError('invalid request path')
}
if (typeof method !== 'string') {
throw new InvalidArgumentError('method must be a string')
} else if (tokenRegExp.exec(method) === null) {
throw new InvalidArgumentError('invalid request method')
}
if (upgrade && typeof upgrade !== 'string') {
throw new InvalidArgumentError('upgrade must be a string')
}
if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
throw new InvalidArgumentError('invalid headersTimeout')
}
if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
throw new InvalidArgumentError('invalid bodyTimeout')
}
if (reset != null && typeof reset !== 'boolean') {
throw new InvalidArgumentError('invalid reset')
}
if (expectContinue != null && typeof expectContinue !== 'boolean') {
throw new InvalidArgumentError('invalid expectContinue')
}
this.headersTimeout = headersTimeout
this.bodyTimeout = bodyTimeout
this.throwOnError = throwOnError === true
this.method = method
this.abort = null
if (body == null) {
this.body = null
} else if (util.isStream(body)) {
this.body = body
const rState = this.body._readableState
if (!rState || !rState.autoDestroy) {
this.endHandler = function autoDestroy () {
util.destroy(this)
}
this.body.on('end', this.endHandler)
}
this.errorHandler = err => {
if (this.abort) {
this.abort(err)
} else {
this.error = err
}
}
this.body.on('error', this.errorHandler)
} else if (util.isBuffer(body)) {
this.body = body.byteLength ? body : null
} else if (ArrayBuffer.isView(body)) {
this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
} else if (body instanceof ArrayBuffer) {
this.body = body.byteLength ? Buffer.from(body) : null
} else if (typeof body === 'string') {
this.body = body.length ? Buffer.from(body) : null
} else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
this.body = body
} else {
throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
}
this.completed = false
this.aborted = false
this.upgrade = upgrade || null
this.path = query ? util.buildURL(path, query) : path
this.origin = origin
this.idempotent = idempotent == null
? method === 'HEAD' || method === 'GET'
: idempotent
this.blocking = blocking == null ? false : blocking
this.reset = reset == null ? null : reset
this.host = null
this.contentLength = null
this.contentType = null
this.headers = ''
// Only for H2
this.expectContinue = expectContinue != null ? expectContinue : false
if (Array.isArray(headers)) {
if (headers.length % 2 !== 0) {
throw new InvalidArgumentError('headers array must be even')
}
for (let i = 0; i < headers.length; i += 2) {
processHeader(this, headers[i], headers[i + 1])
}
} else if (headers && typeof headers === 'object') {
const keys = Object.keys(headers)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
processHeader(this, key, headers[key])
}
} else if (headers != null) {
throw new InvalidArgumentError('headers must be an object or an array')
}
if (util.isFormDataLike(this.body)) {
if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) {
throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
}
if (!extractBody) {
extractBody = require('../fetch/body.js').extractBody
}
const [bodyStream, contentType] = extractBody(body)
if (this.contentType == null) {
this.contentType = contentType
this.headers += `content-type: ${contentType}\r\n`
}
this.body = bodyStream.stream
this.contentLength = bodyStream.length
} else if (util.isBlobLike(body) && this.contentType == null && body.type) {
this.contentType = body.type
this.headers += `content-type: ${body.type}\r\n`
}
util.validateHandler(handler, method, upgrade)
this.servername = util.getServerName(this.host)
this[kHandler] = handler
if (channels.create.hasSubscribers) {
channels.create.publish({ request: this })
}
}
onBodySent (chunk) {
if (this[kHandler].onBodySent) {
try {
return this[kHandler].onBodySent(chunk)
} catch (err) {
this.abort(err)
}
}
}
onRequestSent () {
if (channels.bodySent.hasSubscribers) {
channels.bodySent.publish({ request: this })
}
if (this[kHandler].onRequestSent) {
try {
return this[kHandler].onRequestSent()
} catch (err) {
this.abort(err)
}
}
}
onConnect (abort) {
assert(!this.aborted)
assert(!this.completed)
if (this.error) {
abort(this.error)
} else {
this.abort = abort
return this[kHandler].onConnect(abort)
}
}
onHeaders (statusCode, headers, resume, statusText) {
assert(!this.aborted)
assert(!this.completed)
if (channels.headers.hasSubscribers) {
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
}
try {
return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
} catch (err) {
this.abort(err)
}
}
onData (chunk) {
assert(!this.aborted)
assert(!this.completed)
try {
return this[kHandler].onData(chunk)
} catch (err) {
this.abort(err)
return false
}
}
onUpgrade (statusCode, headers, socket) {
assert(!this.aborted)
assert(!this.completed)
return this[kHandler].onUpgrade(statusCode, headers, socket)
}
onComplete (trailers) {
this.onFinally()
assert(!this.aborted)
this.completed = true
if (channels.trailers.hasSubscribers) {
channels.trailers.publish({ request: this, trailers })
}
try {
return this[kHandler].onComplete(trailers)
} catch (err) {
// TODO (fix): This might be a bad idea?
this.onError(err)
}
}
onError (error) {
this.onFinally()
if (channels.error.hasSubscribers) {
channels.error.publish({ request: this, error })
}
if (this.aborted) {
return
}
this.aborted = true
return this[kHandler].onError(error)
}
onFinally () {
if (this.errorHandler) {
this.body.off('error', this.errorHandler)
this.errorHandler = null
}
if (this.endHandler) {
this.body.off('end', this.endHandler)
this.endHandler = null
}
}
// TODO: adjust to support H2
addHeader (key, value) {
processHeader(this, key, value)
return this
}
static [kHTTP1BuildRequest] (origin, opts, handler) {
// TODO: Migrate header parsing here, to make Requests
// HTTP agnostic
return new Request(origin, opts, handler)
}
static [kHTTP2BuildRequest] (origin, opts, handler) {
const headers = opts.headers
opts = { ...opts, headers: null }
const request = new Request(origin, opts, handler)
request.headers = {}
if (Array.isArray(headers)) {
if (headers.length % 2 !== 0) {
throw new InvalidArgumentError('headers array must be even')
}
for (let i = 0; i < headers.length; i += 2) {
processHeader(request, headers[i], headers[i + 1], true)
}
} else if (headers && typeof headers === 'object') {
const keys = Object.keys(headers)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
processHeader(request, key, headers[key], true)
}
} else if (headers != null) {
throw new InvalidArgumentError('headers must be an object or an array')
}
return request
}
static [kHTTP2CopyHeaders] (raw) {
const rawHeaders = raw.split('\r\n')
const headers = {}
for (const header of rawHeaders) {
const [key, value] = header.split(': ')
if (value == null || value.length === 0) continue
if (headers[key]) headers[key] += `,${value}`
else headers[key] = value
}
return headers
}
}
function processHeaderValue (key, val, skipAppend) {
if (val && typeof val === 'object') {
throw new InvalidArgumentError(`invalid ${key} header`)
}
val = val != null ? `${val}` : ''
if (headerCharRegex.exec(val) !== null) {
throw new InvalidArgumentError(`invalid ${key} header`)
}
return skipAppend ? val : `${key}: ${val}\r\n`
}
function processHeader (request, key, val, skipAppend = false) {
if (val && (typeof val === 'object' && !Array.isArray(val))) {
throw new InvalidArgumentError(`invalid ${key} header`)
} else if (val === undefined) {
return
}
if (
request.host === null &&
key.length === 4 &&
key.toLowerCase() === 'host'
) {
if (headerCharRegex.exec(val) !== null) {
throw new InvalidArgumentError(`invalid ${key} header`)
}
// Consumed by Client
request.host = val
} else if (
request.contentLength === null &&
key.length === 14 &&
key.toLowerCase() === 'content-length'
) {
request.contentLength = parseInt(val, 10)
if (!Number.isFinite(request.contentLength)) {
throw new InvalidArgumentError('invalid content-length header')
}
} else if (
request.contentType === null &&
key.length === 12 &&
key.toLowerCase() === 'content-type'
) {
request.contentType = val
if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
else request.headers += processHeaderValue(key, val)
} else if (
key.length === 17 &&
key.toLowerCase() === 'transfer-encoding'
) {
throw new InvalidArgumentError('invalid transfer-encoding header')
} else if (
key.length === 10 &&
key.toLowerCase() === 'connection'
) {
const value = typeof val === 'string' ? val.toLowerCase() : null
if (value !== 'close' && value !== 'keep-alive') {
throw new InvalidArgumentError('invalid connection header')
} else if (value === 'close') {
request.reset = true
}
} else if (
key.length === 10 &&
key.toLowerCase() === 'keep-alive'
) {
throw new InvalidArgumentError('invalid keep-alive header')
} else if (
key.length === 7 &&
key.toLowerCase() === 'upgrade'
) {
throw new InvalidArgumentError('invalid upgrade header')
} else if (
key.length === 6 &&
key.toLowerCase() === 'expect'
) {
throw new NotSupportedError('expect header not supported')
} else if (tokenRegExp.exec(key) === null) {
throw new InvalidArgumentError('invalid header key')
} else {
if (Array.isArray(val)) {
for (let i = 0; i < val.length; i++) {
if (skipAppend) {
if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}`
else request.headers[key] = processHeaderValue(key, val[i], skipAppend)
} else {
request.headers += processHeaderValue(key, val[i])
}
}
} else {
if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
else request.headers += processHeaderValue(key, val)
}
}
}
module.exports = Request

View File

@@ -0,0 +1,63 @@
module.exports = {
kClose: Symbol('close'),
kDestroy: Symbol('destroy'),
kDispatch: Symbol('dispatch'),
kUrl: Symbol('url'),
kWriting: Symbol('writing'),
kResuming: Symbol('resuming'),
kQueue: Symbol('queue'),
kConnect: Symbol('connect'),
kConnecting: Symbol('connecting'),
kHeadersList: Symbol('headers list'),
kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
kKeepAliveTimeoutValue: Symbol('keep alive timeout'),
kKeepAlive: Symbol('keep alive'),
kHeadersTimeout: Symbol('headers timeout'),
kBodyTimeout: Symbol('body timeout'),
kServerName: Symbol('server name'),
kLocalAddress: Symbol('local address'),
kHost: Symbol('host'),
kNoRef: Symbol('no ref'),
kBodyUsed: Symbol('used'),
kRunning: Symbol('running'),
kBlocking: Symbol('blocking'),
kPending: Symbol('pending'),
kSize: Symbol('size'),
kBusy: Symbol('busy'),
kQueued: Symbol('queued'),
kFree: Symbol('free'),
kConnected: Symbol('connected'),
kClosed: Symbol('closed'),
kNeedDrain: Symbol('need drain'),
kReset: Symbol('reset'),
kDestroyed: Symbol.for('nodejs.stream.destroyed'),
kMaxHeadersSize: Symbol('max headers size'),
kRunningIdx: Symbol('running index'),
kPendingIdx: Symbol('pending index'),
kError: Symbol('error'),
kClients: Symbol('clients'),
kClient: Symbol('client'),
kParser: Symbol('parser'),
kOnDestroyed: Symbol('destroy callbacks'),
kPipelining: Symbol('pipelining'),
kSocket: Symbol('socket'),
kHostHeader: Symbol('host header'),
kConnector: Symbol('connector'),
kStrictContentLength: Symbol('strict content length'),
kMaxRedirections: Symbol('maxRedirections'),
kMaxRequests: Symbol('maxRequestsPerClient'),
kProxy: Symbol('proxy agent options'),
kCounter: Symbol('socket request counter'),
kInterceptors: Symbol('dispatch interceptors'),
kMaxResponseSize: Symbol('max response size'),
kHTTP2Session: Symbol('http2Session'),
kHTTP2SessionState: Symbol('http2Session state'),
kHTTP2BuildRequest: Symbol('http2 build request'),
kHTTP1BuildRequest: Symbol('http1 build request'),
kHTTP2CopyHeaders: Symbol('http2 copy headers'),
kHTTPConnVersion: Symbol('http connection version'),
kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
kConstruct: Symbol('constructable')
}

View File

@@ -0,0 +1,522 @@
'use strict'
const assert = require('assert')
const { kDestroyed, kBodyUsed } = require('./symbols')
const { IncomingMessage } = require('http')
const stream = require('stream')
const net = require('net')
const { InvalidArgumentError } = require('./errors')
const { Blob } = require('buffer')
const nodeUtil = require('util')
const { stringify } = require('querystring')
const { headerNameLowerCasedRecord } = require('./constants')
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
function nop () {}
function isStream (obj) {
return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
}
// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
function isBlobLike (object) {
return (Blob && object instanceof Blob) || (
object &&
typeof object === 'object' &&
(typeof object.stream === 'function' ||
typeof object.arrayBuffer === 'function') &&
/^(Blob|File)$/.test(object[Symbol.toStringTag])
)
}
function buildURL (url, queryParams) {
if (url.includes('?') || url.includes('#')) {
throw new Error('Query params cannot be passed when url already contains "?" or "#".')
}
const stringified = stringify(queryParams)
if (stringified) {
url += '?' + stringified
}
return url
}
function parseURL (url) {
if (typeof url === 'string') {
url = new URL(url)
if (!/^https?:/.test(url.origin || url.protocol)) {
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}
return url
}
if (!url || typeof url !== 'object') {
throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
}
if (!/^https?:/.test(url.origin || url.protocol)) {
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}
if (!(url instanceof URL)) {
if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
}
if (url.path != null && typeof url.path !== 'string') {
throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
}
if (url.pathname != null && typeof url.pathname !== 'string') {
throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
}
if (url.hostname != null && typeof url.hostname !== 'string') {
throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
}
if (url.origin != null && typeof url.origin !== 'string') {
throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
}
const port = url.port != null
? url.port
: (url.protocol === 'https:' ? 443 : 80)
let origin = url.origin != null
? url.origin
: `${url.protocol}//${url.hostname}:${port}`
let path = url.path != null
? url.path
: `${url.pathname || ''}${url.search || ''}`
if (origin.endsWith('/')) {
origin = origin.substring(0, origin.length - 1)
}
if (path && !path.startsWith('/')) {
path = `/${path}`
}
// new URL(path, origin) is unsafe when `path` contains an absolute URL
// From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
// If first parameter is a relative URL, second param is required, and will be used as the base URL.
// If first parameter is an absolute URL, a given second param will be ignored.
url = new URL(origin + path)
}
return url
}
function parseOrigin (url) {
url = parseURL(url)
if (url.pathname !== '/' || url.search || url.hash) {
throw new InvalidArgumentError('invalid url')
}
return url
}
function getHostname (host) {
if (host[0] === '[') {
const idx = host.indexOf(']')
assert(idx !== -1)
return host.substring(1, idx)
}
const idx = host.indexOf(':')
if (idx === -1) return host
return host.substring(0, idx)
}
// IP addresses are not valid server names per RFC6066
// > Currently, the only server names supported are DNS hostnames
function getServerName (host) {
if (!host) {
return null
}
assert.strictEqual(typeof host, 'string')
const servername = getHostname(host)
if (net.isIP(servername)) {
return ''
}
return servername
}
function deepClone (obj) {
return JSON.parse(JSON.stringify(obj))
}
function isAsyncIterable (obj) {
return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function')
}
function isIterable (obj) {
return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function'))
}
function bodyLength (body) {
if (body == null) {
return 0
} else if (isStream(body)) {
const state = body._readableState
return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length)
? state.length
: null
} else if (isBlobLike(body)) {
return body.size != null ? body.size : null
} else if (isBuffer(body)) {
return body.byteLength
}
return null
}
function isDestroyed (stream) {
return !stream || !!(stream.destroyed || stream[kDestroyed])
}
function isReadableAborted (stream) {
const state = stream && stream._readableState
return isDestroyed(stream) && state && !state.endEmitted
}
function destroy (stream, err) {
if (stream == null || !isStream(stream) || isDestroyed(stream)) {
return
}
if (typeof stream.destroy === 'function') {
if (Object.getPrototypeOf(stream).constructor === IncomingMessage) {
// See: https://github.com/nodejs/node/pull/38505/files
stream.socket = null
}
stream.destroy(err)
} else if (err) {
process.nextTick((stream, err) => {
stream.emit('error', err)
}, stream, err)
}
if (stream.destroyed !== true) {
stream[kDestroyed] = true
}
}
const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
function parseKeepAliveTimeout (val) {
const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR)
return m ? parseInt(m[1], 10) * 1000 : null
}
/**
* Retrieves a header name and returns its lowercase value.
* @param {string | Buffer} value Header name
* @returns {string}
*/
function headerNameToString (value) {
return headerNameLowerCasedRecord[value] || value.toLowerCase()
}
function parseHeaders (headers, obj = {}) {
// For H2 support
if (!Array.isArray(headers)) return headers
for (let i = 0; i < headers.length; i += 2) {
const key = headers[i].toString().toLowerCase()
let val = obj[key]
if (!val) {
if (Array.isArray(headers[i + 1])) {
obj[key] = headers[i + 1].map(x => x.toString('utf8'))
} else {
obj[key] = headers[i + 1].toString('utf8')
}
} else {
if (!Array.isArray(val)) {
val = [val]
obj[key] = val
}
val.push(headers[i + 1].toString('utf8'))
}
}
// See https://github.com/nodejs/node/pull/46528
if ('content-length' in obj && 'content-disposition' in obj) {
obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1')
}
return obj
}
function parseRawHeaders (headers) {
const ret = []
let hasContentLength = false
let contentDispositionIdx = -1
for (let n = 0; n < headers.length; n += 2) {
const key = headers[n + 0].toString()
const val = headers[n + 1].toString('utf8')
if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) {
ret.push(key, val)
hasContentLength = true
} else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) {
contentDispositionIdx = ret.push(key, val) - 1
} else {
ret.push(key, val)
}
}
// See https://github.com/nodejs/node/pull/46528
if (hasContentLength && contentDispositionIdx !== -1) {
ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1')
}
return ret
}
function isBuffer (buffer) {
// See, https://github.com/mcollina/undici/pull/319
return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
}
function validateHandler (handler, method, upgrade) {
if (!handler || typeof handler !== 'object') {
throw new InvalidArgumentError('handler must be an object')
}
if (typeof handler.onConnect !== 'function') {
throw new InvalidArgumentError('invalid onConnect method')
}
if (typeof handler.onError !== 'function') {
throw new InvalidArgumentError('invalid onError method')
}
if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) {
throw new InvalidArgumentError('invalid onBodySent method')
}
if (upgrade || method === 'CONNECT') {
if (typeof handler.onUpgrade !== 'function') {
throw new InvalidArgumentError('invalid onUpgrade method')
}
} else {
if (typeof handler.onHeaders !== 'function') {
throw new InvalidArgumentError('invalid onHeaders method')
}
if (typeof handler.onData !== 'function') {
throw new InvalidArgumentError('invalid onData method')
}
if (typeof handler.onComplete !== 'function') {
throw new InvalidArgumentError('invalid onComplete method')
}
}
}
// A body is disturbed if it has been read from and it cannot
// be re-used without losing state or data.
function isDisturbed (body) {
return !!(body && (
stream.isDisturbed
? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed?
: body[kBodyUsed] ||
body.readableDidRead ||
(body._readableState && body._readableState.dataEmitted) ||
isReadableAborted(body)
))
}
function isErrored (body) {
return !!(body && (
stream.isErrored
? stream.isErrored(body)
: /state: 'errored'/.test(nodeUtil.inspect(body)
)))
}
function isReadable (body) {
return !!(body && (
stream.isReadable
? stream.isReadable(body)
: /state: 'readable'/.test(nodeUtil.inspect(body)
)))
}
function getSocketInfo (socket) {
return {
localAddress: socket.localAddress,
localPort: socket.localPort,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
remoteFamily: socket.remoteFamily,
timeout: socket.timeout,
bytesWritten: socket.bytesWritten,
bytesRead: socket.bytesRead
}
}
async function * convertIterableToBuffer (iterable) {
for await (const chunk of iterable) {
yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
}
}
let ReadableStream
function ReadableStreamFrom (iterable) {
if (!ReadableStream) {
ReadableStream = require('stream/web').ReadableStream
}
if (ReadableStream.from) {
return ReadableStream.from(convertIterableToBuffer(iterable))
}
let iterator
return new ReadableStream(
{
async start () {
iterator = iterable[Symbol.asyncIterator]()
},
async pull (controller) {
const { done, value } = await iterator.next()
if (done) {
queueMicrotask(() => {
controller.close()
})
} else {
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
controller.enqueue(new Uint8Array(buf))
}
return controller.desiredSize > 0
},
async cancel (reason) {
await iterator.return()
}
},
0
)
}
// The chunk should be a FormData instance and contains
// all the required methods.
function isFormDataLike (object) {
return (
object &&
typeof object === 'object' &&
typeof object.append === 'function' &&
typeof object.delete === 'function' &&
typeof object.get === 'function' &&
typeof object.getAll === 'function' &&
typeof object.has === 'function' &&
typeof object.set === 'function' &&
object[Symbol.toStringTag] === 'FormData'
)
}
function throwIfAborted (signal) {
if (!signal) { return }
if (typeof signal.throwIfAborted === 'function') {
signal.throwIfAborted()
} else {
if (signal.aborted) {
// DOMException not available < v17.0.0
const err = new Error('The operation was aborted')
err.name = 'AbortError'
throw err
}
}
}
function addAbortListener (signal, listener) {
if ('addEventListener' in signal) {
signal.addEventListener('abort', listener, { once: true })
return () => signal.removeEventListener('abort', listener)
}
signal.addListener('abort', listener)
return () => signal.removeListener('abort', listener)
}
const hasToWellFormed = !!String.prototype.toWellFormed
/**
* @param {string} val
*/
function toUSVString (val) {
if (hasToWellFormed) {
return `${val}`.toWellFormed()
} else if (nodeUtil.toUSVString) {
return nodeUtil.toUSVString(val)
}
return `${val}`
}
// Parsed accordingly to RFC 9110
// https://www.rfc-editor.org/rfc/rfc9110#field.content-range
function parseRangeHeader (range) {
if (range == null || range === '') return { start: 0, end: null, size: null }
const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
return m
? {
start: parseInt(m[1]),
end: m[2] ? parseInt(m[2]) : null,
size: m[3] ? parseInt(m[3]) : null
}
: null
}
const kEnumerableProperty = Object.create(null)
kEnumerableProperty.enumerable = true
module.exports = {
kEnumerableProperty,
nop,
isDisturbed,
isErrored,
isReadable,
toUSVString,
isReadableAborted,
isBlobLike,
parseOrigin,
parseURL,
getServerName,
isStream,
isIterable,
isAsyncIterable,
isDestroyed,
headerNameToString,
parseRawHeaders,
parseHeaders,
parseKeepAliveTimeout,
destroy,
bodyLength,
deepClone,
ReadableStreamFrom,
isBuffer,
validateHandler,
getSocketInfo,
isFormDataLike,
buildURL,
throwIfAborted,
addAbortListener,
parseRangeHeader,
nodeMajor,
nodeMinor,
nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13),
safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE']
}

View File

@@ -19,7 +19,7 @@ class DispatcherBase extends Dispatcher {
super() super()
this[kDestroyed] = false this[kDestroyed] = false
this[kOnDestroyed] = [] this[kOnDestroyed] = null
this[kClosed] = false this[kClosed] = false
this[kOnClosed] = [] this[kOnClosed] = []
} }
@@ -127,6 +127,7 @@ class DispatcherBase extends Dispatcher {
} }
this[kDestroyed] = true this[kDestroyed] = true
this[kOnDestroyed] = this[kOnDestroyed] || []
this[kOnDestroyed].push(callback) this[kOnDestroyed].push(callback)
const onDestroyed = () => { const onDestroyed = () => {
@@ -167,7 +168,7 @@ class DispatcherBase extends Dispatcher {
throw new InvalidArgumentError('opts must be an object.') throw new InvalidArgumentError('opts must be an object.')
} }
if (this[kDestroyed]) { if (this[kDestroyed] || this[kOnDestroyed]) {
throw new ClientDestroyedError() throw new ClientDestroyedError()
} }

View File

@@ -1,24 +1,33 @@
'use strict' 'use strict'
const Busboy = require('busboy') const Busboy = require('@fastify/busboy')
const util = require('../core/util') const util = require('../core/util')
const { ReadableStreamFrom, toUSVString, isBlobLike } = require('./util') const {
ReadableStreamFrom,
isBlobLike,
isReadableStreamLike,
readableStreamClose,
createDeferredPromise,
fullyReadBody
} = require('./util')
const { FormData } = require('./formdata') const { FormData } = require('./formdata')
const { kState } = require('./symbols') const { kState } = require('./symbols')
const { webidl } = require('./webidl') const { webidl } = require('./webidl')
const { DOMException } = require('./constants') const { DOMException, structuredClone } = require('./constants')
const { Blob } = require('buffer') const { Blob, File: NativeFile } = require('buffer')
const { kBodyUsed } = require('../core/symbols') const { kBodyUsed } = require('../core/symbols')
const assert = require('assert') const assert = require('assert')
const { isErrored } = require('../core/util') const { isErrored } = require('../core/util')
const { isUint8Array, isArrayBuffer } = require('util/types') const { isUint8Array, isArrayBuffer } = require('util/types')
const { File } = require('./file') const { File: UndiciFile } = require('./file')
const { parseMIMEType, serializeAMimeType } = require('./dataURL')
let ReadableStream let ReadableStream = globalThis.ReadableStream
async function * blobGen (blob) { /** @type {globalThis['File']} */
yield * blob.stream() const File = NativeFile ?? UndiciFile
} const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
function extractBody (object, keepalive = false) { function extractBody (object, keepalive = false) {
@@ -26,26 +35,54 @@ function extractBody (object, keepalive = false) {
ReadableStream = require('stream/web').ReadableStream ReadableStream = require('stream/web').ReadableStream
} }
// 1. Let stream be object if object is a ReadableStream object. // 1. Let stream be null.
// Otherwise, let stream be a new ReadableStream, and set up stream.
let stream = null let stream = null
// 2. Let action be null. // 2. If object is a ReadableStream object, then set stream to object.
if (object instanceof ReadableStream) {
stream = object
} else if (isBlobLike(object)) {
// 3. Otherwise, if object is a Blob object, set stream to the
// result of running objects get stream.
stream = object.stream()
} else {
// 4. Otherwise, set stream to a new ReadableStream object, and set
// up stream.
stream = new ReadableStream({
async pull (controller) {
controller.enqueue(
typeof source === 'string' ? textEncoder.encode(source) : source
)
queueMicrotask(() => readableStreamClose(controller))
},
start () {},
type: undefined
})
}
// 5. Assert: stream is a ReadableStream object.
assert(isReadableStreamLike(stream))
// 6. Let action be null.
let action = null let action = null
// 3. Let source be null. // 7. Let source be null.
let source = null let source = null
// 4. Let length be null. // 8. Let length be null.
let length = null let length = null
// 5. Let Content-Type be null. // 9. Let type be null.
let contentType = null let type = null
// 6. Switch on object: // 10. Switch on object:
if (object == null) { if (typeof object === 'string') {
// Note: The IDL processor cannot handle this situation. See // Set source to the UTF-8 encoding of object.
// https://crbug.com/335871. // Note: setting source to a Uint8Array here breaks some mocking assumptions.
source = object
// Set type to `text/plain;charset=UTF-8`.
type = 'text/plain;charset=UTF-8'
} else if (object instanceof URLSearchParams) { } else if (object instanceof URLSearchParams) {
// URLSearchParams // URLSearchParams
@@ -57,8 +94,8 @@ function extractBody (object, keepalive = false) {
// Set source to the result of running the application/x-www-form-urlencoded serializer with objects list. // Set source to the result of running the application/x-www-form-urlencoded serializer with objects list.
source = object.toString() source = object.toString()
// Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`. // Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
contentType = 'application/x-www-form-urlencoded;charset=UTF-8' type = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (isArrayBuffer(object)) { } else if (isArrayBuffer(object)) {
// BufferSource/ArrayBuffer // BufferSource/ArrayBuffer
@@ -70,7 +107,7 @@ function extractBody (object, keepalive = false) {
// Set source to a copy of the bytes held by object. // Set source to a copy of the bytes held by object.
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
} else if (util.isFormDataLike(object)) { } else if (util.isFormDataLike(object)) {
const boundary = '----formdata-undici-' + Math.random() const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data` const prefix = `--${boundary}\r\nContent-Disposition: form-data`
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */ /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
@@ -80,52 +117,64 @@ function extractBody (object, keepalive = false) {
// Set action to this step: run the multipart/form-data // Set action to this step: run the multipart/form-data
// encoding algorithm, with objects entry list and UTF-8. // encoding algorithm, with objects entry list and UTF-8.
action = async function * (object) { // - This ensures that the body is immutable and can't be changed afterwords
const enc = new TextEncoder() // - That the content-length is calculated in advance.
// - And that all parts are pre-encoded and ready to be sent.
for (const [name, value] of object) { const blobParts = []
if (typeof value === 'string') { const rn = new Uint8Array([13, 10]) // '\r\n'
yield enc.encode( length = 0
prefix + let hasUnknownSizeValue = false
`; name="${escape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n` for (const [name, value] of object) {
) if (typeof value === 'string') {
const chunk = textEncoder.encode(prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
} else {
const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`)
blobParts.push(chunk, value, rn)
if (typeof value.size === 'number') {
length += chunk.byteLength + value.size + rn.byteLength
} else { } else {
yield enc.encode( hasUnknownSizeValue = true
prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${escape(value.name)}"` : '') +
'\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`
)
yield * blobGen(value)
yield enc.encode('\r\n')
} }
} }
}
yield enc.encode(`--${boundary}--`) const chunk = textEncoder.encode(`--${boundary}--`)
blobParts.push(chunk)
length += chunk.byteLength
if (hasUnknownSizeValue) {
length = null
} }
// Set source to object. // Set source to object.
source = object source = object
// Set length to unclear, see html/6424 for improving this. action = async function * () {
// TODO for (const part of blobParts) {
if (part.stream) {
yield * part.stream()
} else {
yield part
}
}
}
// Set Content-Type to `multipart/form-data; boundary=`, // Set type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated // followed by the multipart/form-data boundary string generated
// by the multipart/form-data encoding algorithm. // by the multipart/form-data encoding algorithm.
contentType = 'multipart/form-data; boundary=' + boundary type = 'multipart/form-data; boundary=' + boundary
} else if (isBlobLike(object)) { } else if (isBlobLike(object)) {
// Blob // Blob
// Set action to this step: read object.
action = blobGen
// Set source to object. // Set source to object.
source = object source = object
@@ -133,9 +182,9 @@ function extractBody (object, keepalive = false) {
length = object.size length = object.size
// If objects type attribute is not the empty byte sequence, set // If objects type attribute is not the empty byte sequence, set
// Content-Type to its value. // type to its value.
if (object.type) { if (object.type) {
contentType = object.type type = object.type
} }
} else if (typeof object[Symbol.asyncIterator] === 'function') { } else if (typeof object[Symbol.asyncIterator] === 'function') {
// If keepalive is true, then throw a TypeError. // If keepalive is true, then throw a TypeError.
@@ -152,22 +201,15 @@ function extractBody (object, keepalive = false) {
stream = stream =
object instanceof ReadableStream ? object : ReadableStreamFrom(object) object instanceof ReadableStream ? object : ReadableStreamFrom(object)
} else {
// TODO: byte sequence?
// TODO: scalar value string?
// TODO: else?
source = toUSVString(object)
contentType = 'text/plain;charset=UTF-8'
} }
// 7. If source is a byte sequence, then set action to a // 11. If source is a byte sequence, then set action to a
// step that returns source and length to sources length. // step that returns source and length to sources length.
// TODO: What is a "byte sequence?"
if (typeof source === 'string' || util.isBuffer(source)) { if (typeof source === 'string' || util.isBuffer(source)) {
length = Buffer.byteLength(source) length = Buffer.byteLength(source)
} }
// 8. If action is non-null, then run these steps in in parallel: // 12. If action is non-null, then run these steps in in parallel:
if (action != null) { if (action != null) {
// Run action. // Run action.
let iterator let iterator
@@ -194,28 +236,17 @@ function extractBody (object, keepalive = false) {
}, },
async cancel (reason) { async cancel (reason) {
await iterator.return() await iterator.return()
} },
}) type: undefined
} else if (!stream) {
// TODO: Spec doesn't say anything about this?
stream = new ReadableStream({
async pull (controller) {
controller.enqueue(
typeof source === 'string' ? new TextEncoder().encode(source) : source
)
queueMicrotask(() => {
controller.close()
})
}
}) })
} }
// 9. Let body be a body whose stream is stream, source is source, // 13. Let body be a body whose stream is stream, source is source,
// and length is length. // and length is length.
const body = { stream, source, length } const body = { stream, source, length }
// 10. Return body and Content-Type. // 14. Return (body, type).
return [body, contentType] return [body, type]
} }
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract // https://fetch.spec.whatwg.org/#bodyinit-safely-extract
@@ -248,13 +279,17 @@ function cloneBody (body) {
// 1. Let « out1, out2 » be the result of teeing bodys stream. // 1. Let « out1, out2 » be the result of teeing bodys stream.
const [out1, out2] = body.stream.tee() const [out1, out2] = body.stream.tee()
const out2Clone = structuredClone(out2, { transfer: [out2] })
// This, for whatever reasons, unrefs out2Clone which allows
// the process to exit by itself.
const [, finalClone] = out2Clone.tee()
// 2. Set bodys stream to out1. // 2. Set bodys stream to out1.
body.stream = out1 body.stream = out1
// 3. Return a body whose stream is out2 and other members are copied from body. // 3. Return a body whose stream is out2 and other members are copied from body.
return { return {
stream: out2, stream: finalClone,
length: body.length, length: body.length,
source: body.source source: body.source
} }
@@ -291,123 +326,51 @@ function throwIfAborted (state) {
function bodyMixinMethods (instance) { function bodyMixinMethods (instance) {
const methods = { const methods = {
async blob () { blob () {
if (!(this instanceof instance)) { // The blob() method steps are to return the result of
throw new TypeError('Illegal invocation') // running consume body with this and the following step
} // given a byte sequence bytes: return a Blob whose
// contents are bytes and whose type attribute is thiss
// MIME type.
return specConsumeBody(this, (bytes) => {
let mimeType = bodyMimeType(this)
throwIfAborted(this[kState]) if (mimeType === 'failure') {
mimeType = ''
const chunks = [] } else if (mimeType) {
mimeType = serializeAMimeType(mimeType)
for await (const chunk of consumeBody(this[kState].body)) {
if (!isUint8Array(chunk)) {
throw new TypeError('Expected Uint8Array chunk')
} }
// Assemble one final large blob with Uint8Array's can exhaust memory. // Return a Blob whose contents are bytes and type attribute
// That's why we create create multiple blob's and using references // is mimeType.
chunks.push(new Blob([chunk])) return new Blob([bytes], { type: mimeType })
} }, instance)
return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
}, },
async arrayBuffer () { arrayBuffer () {
if (!(this instanceof instance)) { // The arrayBuffer() method steps are to return the result
throw new TypeError('Illegal invocation') // of running consume body with this and the following step
} // given a byte sequence bytes: return a new ArrayBuffer
// whose contents are bytes.
throwIfAborted(this[kState]) return specConsumeBody(this, (bytes) => {
return new Uint8Array(bytes).buffer
const contentLength = this.headers.get('content-length') }, instance)
const encoded = this.headers.has('content-encoding')
// if we have content length and no encoding, then we can
// pre allocate the buffer and just read the data into it
if (!encoded && contentLength) {
const buffer = new Uint8Array(contentLength)
let offset = 0
for await (const chunk of consumeBody(this[kState].body)) {
if (!isUint8Array(chunk)) {
throw new TypeError('Expected Uint8Array chunk')
}
buffer.set(chunk, offset)
offset += chunk.length
}
return buffer.buffer
}
// if we don't have content length, then we have to allocate 2x the
// size of the body, once for consumed data, and once for the final buffer
// This could be optimized by using growable ArrayBuffer, but it's not
// implemented yet. https://github.com/tc39/proposal-resizablearraybuffer
const chunks = []
let size = 0
for await (const chunk of consumeBody(this[kState].body)) {
if (!isUint8Array(chunk)) {
throw new TypeError('Expected Uint8Array chunk')
}
chunks.push(chunk)
size += chunk.byteLength
}
const buffer = new Uint8Array(size)
let offset = 0
for (const chunk of chunks) {
buffer.set(chunk, offset)
offset += chunk.byteLength
}
return buffer.buffer
}, },
async text () { text () {
if (!(this instanceof instance)) { // The text() method steps are to return the result of running
throw new TypeError('Illegal invocation') // consume body with this and UTF-8 decode.
} return specConsumeBody(this, utf8DecodeBytes, instance)
throwIfAborted(this[kState])
let result = ''
const textDecoder = new TextDecoder()
for await (const chunk of consumeBody(this[kState].body)) {
if (!isUint8Array(chunk)) {
throw new TypeError('Expected Uint8Array chunk')
}
result += textDecoder.decode(chunk, { stream: true })
}
// flush
result += textDecoder.decode()
return result
}, },
async json () { json () {
if (!(this instanceof instance)) { // The json() method steps are to return the result of running
throw new TypeError('Illegal invocation') // consume body with this and parse JSON from bytes.
} return specConsumeBody(this, parseJSONFromBytes, instance)
throwIfAborted(this[kState])
return JSON.parse(await this.text())
}, },
async formData () { async formData () {
if (!(this instanceof instance)) { webidl.brandCheck(this, instance)
throw new TypeError('Illegal invocation')
}
throwIfAborted(this[kState]) throwIfAborted(this[kState])
@@ -423,20 +386,21 @@ function bodyMixinMethods (instance) {
let busboy let busboy
try { try {
busboy = Busboy({ headers }) busboy = new Busboy({
headers,
preservePath: true
})
} catch (err) { } catch (err) {
// Error due to headers: throw new DOMException(`${err}`, 'AbortError')
throw Object.assign(new TypeError(), { cause: err })
} }
busboy.on('field', (name, value) => { busboy.on('field', (name, value) => {
responseFormData.append(name, value) responseFormData.append(name, value)
}) })
busboy.on('file', (name, value, info) => { busboy.on('file', (name, value, filename, encoding, mimeType) => {
const { filename, encoding, mimeType } = info
const chunks = [] const chunks = []
if (encoding.toLowerCase() === 'base64') { if (encoding === 'base64' || encoding.toLowerCase() === 'base64') {
let base64chunk = '' let base64chunk = ''
value.on('data', (chunk) => { value.on('data', (chunk) => {
@@ -463,7 +427,7 @@ function bodyMixinMethods (instance) {
const busboyResolve = new Promise((resolve, reject) => { const busboyResolve = new Promise((resolve, reject) => {
busboy.on('finish', resolve) busboy.on('finish', resolve)
busboy.on('error', (err) => reject(err)) busboy.on('error', (err) => reject(new TypeError(err)))
}) })
if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk) if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk)
@@ -480,14 +444,16 @@ function bodyMixinMethods (instance) {
let text = '' let text = ''
// application/x-www-form-urlencoded parser will keep the BOM. // application/x-www-form-urlencoded parser will keep the BOM.
// https://url.spec.whatwg.org/#concept-urlencoded-parser // https://url.spec.whatwg.org/#concept-urlencoded-parser
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) // Note that streaming decoder is stateful and cannot be reused
const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
for await (const chunk of consumeBody(this[kState].body)) { for await (const chunk of consumeBody(this[kState].body)) {
if (!isUint8Array(chunk)) { if (!isUint8Array(chunk)) {
throw new TypeError('Expected Uint8Array chunk') throw new TypeError('Expected Uint8Array chunk')
} }
text += textDecoder.decode(chunk, { stream: true }) text += streamingDecoder.decode(chunk, { stream: true })
} }
text += textDecoder.decode() text += streamingDecoder.decode()
entries = new URLSearchParams(text) entries = new URLSearchParams(text)
} catch (err) { } catch (err) {
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string. // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
@@ -509,7 +475,7 @@ function bodyMixinMethods (instance) {
throwIfAborted(this[kState]) throwIfAborted(this[kState])
// Otherwise, throw a TypeError. // Otherwise, throw a TypeError.
webidl.errors.exception({ throw webidl.errors.exception({
header: `${instance.name}.formData`, header: `${instance.name}.formData`,
message: 'Could not parse content as FormData.' message: 'Could not parse content as FormData.'
}) })
@@ -520,32 +486,115 @@ function bodyMixinMethods (instance) {
return methods return methods
} }
const properties = {
body: {
enumerable: true,
get () {
if (!this || !this[kState]) {
throw new TypeError('Illegal invocation')
}
return this[kState].body ? this[kState].body.stream : null
}
},
bodyUsed: {
enumerable: true,
get () {
if (!this || !this[kState]) {
throw new TypeError('Illegal invocation')
}
return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
}
}
}
function mixinBody (prototype) { function mixinBody (prototype) {
Object.assign(prototype.prototype, bodyMixinMethods(prototype)) Object.assign(prototype.prototype, bodyMixinMethods(prototype))
Object.defineProperties(prototype.prototype, properties) }
/**
* @see https://fetch.spec.whatwg.org/#concept-body-consume-body
* @param {Response|Request} object
* @param {(value: unknown) => unknown} convertBytesToJSValue
* @param {Response|Request} instance
*/
async function specConsumeBody (object, convertBytesToJSValue, instance) {
webidl.brandCheck(object, instance)
throwIfAborted(object[kState])
// 1. If object is unusable, then return a promise rejected
// with a TypeError.
if (bodyUnusable(object[kState].body)) {
throw new TypeError('Body is unusable')
}
// 2. Let promise be a new promise.
const promise = createDeferredPromise()
// 3. Let errorSteps given error be to reject promise with error.
const errorSteps = (error) => promise.reject(error)
// 4. Let successSteps given a byte sequence data be to resolve
// promise with the result of running convertBytesToJSValue
// with data. If that threw an exception, then run errorSteps
// with that exception.
const successSteps = (data) => {
try {
promise.resolve(convertBytesToJSValue(data))
} catch (e) {
errorSteps(e)
}
}
// 5. If objects body is null, then run successSteps with an
// empty byte sequence.
if (object[kState].body == null) {
successSteps(new Uint8Array())
return promise.promise
}
// 6. Otherwise, fully read objects body given successSteps,
// errorSteps, and objects relevant global object.
await fullyReadBody(object[kState].body, successSteps, errorSteps)
// 7. Return promise.
return promise.promise
}
// https://fetch.spec.whatwg.org/#body-unusable
function bodyUnusable (body) {
// An object including the Body interface mixin is
// said to be unusable if its body is non-null and
// its bodys stream is disturbed or locked.
return body != null && (body.stream.locked || util.isDisturbed(body.stream))
}
/**
* @see https://encoding.spec.whatwg.org/#utf-8-decode
* @param {Buffer} buffer
*/
function utf8DecodeBytes (buffer) {
if (buffer.length === 0) {
return ''
}
// 1. Let buffer be the result of peeking three bytes from
// ioQueue, converted to a byte sequence.
// 2. If buffer is 0xEF 0xBB 0xBF, then read three
// bytes from ioQueue. (Do nothing with those bytes.)
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
buffer = buffer.subarray(3)
}
// 3. Process a queue with an instance of UTF-8s
// decoder, ioQueue, output, and "replacement".
const output = textDecoder.decode(buffer)
// 4. Return output.
return output
}
/**
* @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
* @param {Uint8Array} bytes
*/
function parseJSONFromBytes (bytes) {
return JSON.parse(utf8DecodeBytes(bytes))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-mime-type
* @param {import('./response').Response|import('./request').Request} object
*/
function bodyMimeType (object) {
const { headersList } = object[kState]
const contentType = headersList.get('content-type')
if (contentType === null) {
return 'failure'
}
return parseMIMEType(contentType)
} }
module.exports = { module.exports = {

View File

@@ -0,0 +1,151 @@
'use strict'
const { MessageChannel, receiveMessageOnPort } = require('worker_threads')
const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
const nullBodyStatus = [101, 204, 205, 304]
const redirectStatus = [301, 302, 303, 307, 308]
const redirectStatusSet = new Set(redirectStatus)
// https://fetch.spec.whatwg.org/#block-bad-port
const badPorts = [
'1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
'87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
'139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
'540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
'2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697',
'10080'
]
const badPortsSet = new Set(badPorts)
// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
const referrerPolicy = [
'',
'no-referrer',
'no-referrer-when-downgrade',
'same-origin',
'origin',
'strict-origin',
'origin-when-cross-origin',
'strict-origin-when-cross-origin',
'unsafe-url'
]
const referrerPolicySet = new Set(referrerPolicy)
const requestRedirect = ['follow', 'manual', 'error']
const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE']
const safeMethodsSet = new Set(safeMethods)
const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors']
const requestCredentials = ['omit', 'same-origin', 'include']
const requestCache = [
'default',
'no-store',
'reload',
'no-cache',
'force-cache',
'only-if-cached'
]
// https://fetch.spec.whatwg.org/#request-body-header-name
const requestBodyHeader = [
'content-encoding',
'content-language',
'content-location',
'content-type',
// See https://github.com/nodejs/undici/issues/2021
// 'Content-Length' is a forbidden header name, which is typically
// removed in the Headers implementation. However, undici doesn't
// filter out headers, so we add it here.
'content-length'
]
// https://fetch.spec.whatwg.org/#enumdef-requestduplex
const requestDuplex = [
'half'
]
// http://fetch.spec.whatwg.org/#forbidden-method
const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']
const forbiddenMethodsSet = new Set(forbiddenMethods)
const subresource = [
'audio',
'audioworklet',
'font',
'image',
'manifest',
'paintworklet',
'script',
'style',
'track',
'video',
'xslt',
''
]
const subresourceSet = new Set(subresource)
/** @type {globalThis['DOMException']} */
const DOMException = globalThis.DOMException ?? (() => {
// DOMException was only made a global in Node v17.0.0,
// but fetch supports >= v16.8.
try {
atob('~')
} catch (err) {
return Object.getPrototypeOf(err).constructor
}
})()
let channel
/** @type {globalThis['structuredClone']} */
const structuredClone =
globalThis.structuredClone ??
// https://github.com/nodejs/node/blob/b27ae24dcc4251bad726d9d84baf678d1f707fed/lib/internal/structured_clone.js
// structuredClone was added in v17.0.0, but fetch supports v16.8
function structuredClone (value, options = undefined) {
if (arguments.length === 0) {
throw new TypeError('missing argument')
}
if (!channel) {
channel = new MessageChannel()
}
channel.port1.unref()
channel.port2.unref()
channel.port1.postMessage(value, options?.transfer)
return receiveMessageOnPort(channel.port2).message
}
module.exports = {
DOMException,
structuredClone,
subresource,
forbiddenMethods,
requestBodyHeader,
referrerPolicy,
requestRedirect,
requestMode,
requestCredentials,
requestCache,
redirectStatus,
corsSafeListedMethods,
nullBodyStatus,
safeMethods,
badPorts,
requestDuplex,
subresourceSet,
badPortsSet,
redirectStatusSet,
corsSafeListedMethodsSet,
safeMethodsSet,
forbiddenMethodsSet,
referrerPolicySet
}

View File

@@ -1,9 +1,19 @@
const assert = require('assert') const assert = require('assert')
const { atob } = require('buffer') const { atob } = require('buffer')
const { isValidHTTPToken } = require('./util') const { isomorphicDecode } = require('./util')
const encoder = new TextEncoder() const encoder = new TextEncoder()
/**
* @see https://mimesniff.spec.whatwg.org/#http-token-code-point
*/
const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/
const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line
/**
* @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
*/
const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line
// https://fetch.spec.whatwg.org/#data-url-processor // https://fetch.spec.whatwg.org/#data-url-processor
/** @param {URL} dataURL */ /** @param {URL} dataURL */
function dataURLProcessor (dataURL) { function dataURLProcessor (dataURL) {
@@ -24,22 +34,20 @@ function dataURLProcessor (dataURL) {
// 5. Let mimeType be the result of collecting a // 5. Let mimeType be the result of collecting a
// sequence of code points that are not equal // sequence of code points that are not equal
// to U+002C (,), given position. // to U+002C (,), given position.
let mimeType = collectASequenceOfCodePoints( let mimeType = collectASequenceOfCodePointsFast(
(char) => char !== ',', ',',
input, input,
position position
) )
// 6. Strip leading and trailing ASCII whitespace // 6. Strip leading and trailing ASCII whitespace
// from mimeType. // from mimeType.
// Note: This will only remove U+0020 SPACE code
// points, if any.
// Undici implementation note: we need to store the // Undici implementation note: we need to store the
// length because if the mimetype has spaces removed, // length because if the mimetype has spaces removed,
// the wrong amount will be sliced from the input in // the wrong amount will be sliced from the input in
// step #9 // step #9
const mimeTypeLength = mimeType.length const mimeTypeLength = mimeType.length
mimeType = mimeType.replace(/^(\u0020)+|(\u0020)+$/g, '') mimeType = removeASCIIWhitespace(mimeType, true, true)
// 7. If position is past the end of input, then // 7. If position is past the end of input, then
// return failure // return failure
@@ -54,7 +62,6 @@ function dataURLProcessor (dataURL) {
const encodedBody = input.slice(mimeTypeLength + 1) const encodedBody = input.slice(mimeTypeLength + 1)
// 10. Let body be the percent-decoding of encodedBody. // 10. Let body be the percent-decoding of encodedBody.
/** @type {Uint8Array|string} */
let body = stringPercentDecode(encodedBody) let body = stringPercentDecode(encodedBody)
// 11. If mimeType ends with U+003B (;), followed by // 11. If mimeType ends with U+003B (;), followed by
@@ -62,7 +69,8 @@ function dataURLProcessor (dataURL) {
// case-insensitive match for "base64", then: // case-insensitive match for "base64", then:
if (/;(\u0020){0,}base64$/i.test(mimeType)) { if (/;(\u0020){0,}base64$/i.test(mimeType)) {
// 1. Let stringBody be the isomorphic decode of body. // 1. Let stringBody be the isomorphic decode of body.
const stringBody = decodeURIComponent(new TextDecoder('utf-8').decode(body)) const stringBody = isomorphicDecode(body)
// 2. Set body to the forgiving-base64 decode of // 2. Set body to the forgiving-base64 decode of
// stringBody. // stringBody.
body = forgivingBase64(stringBody) body = forgivingBase64(stringBody)
@@ -111,73 +119,14 @@ function dataURLProcessor (dataURL) {
* @param {boolean} excludeFragment * @param {boolean} excludeFragment
*/ */
function URLSerializer (url, excludeFragment = false) { function URLSerializer (url, excludeFragment = false) {
// 1. Let output be urls scheme and U+003A (:) concatenated. if (!excludeFragment) {
let output = url.protocol return url.href
// 2. If urls host is non-null:
if (url.host.length > 0) {
// 1. Append "//" to output.
output += '//'
// 2. If url includes credentials, then:
if (url.username.length > 0 || url.password.length > 0) {
// 1. Append urls username to output.
output += url.username
// 2. If urls password is not the empty string, then append U+003A (:),
// followed by urls password, to output.
if (url.password.length > 0) {
output += ':' + url.password
}
// 3. Append U+0040 (@) to output.
output += '@'
}
// 3. Append urls host, serialized, to output.
output += decodeURIComponent(url.host)
// 4. If urls port is non-null, append U+003A (:) followed by urls port,
// serialized, to output.
if (url.port.length > 0) {
output += ':' + url.port
}
} }
// 3. If urls host is null, url does not have an opaque path, const href = url.href
// urls paths size is greater than 1, and urls path[0] const hashLength = url.hash.length
// is the empty string, then append U+002F (/) followed by
// U+002E (.) to output.
// Note: This prevents web+demo:/.//not-a-host/ or web+demo:/path/..//not-a-host/,
// when parsed and then serialized, from ending up as web+demo://not-a-host/
// (they end up as web+demo:/.//not-a-host/).
// Undici implementation note: url's path[0] can never be an
// empty string, so we have to slightly alter what the spec says.
if (
url.host.length === 0 &&
url.pathname.length > 1 &&
url.href.slice(url.protocol.length + 1)[0] === '.'
) {
output += '/.'
}
// 4. Append the result of URL path serializing url to output. return hashLength === 0 ? href : href.substring(0, href.length - hashLength)
output += url.pathname
// 5. If urls query is non-null, append U+003F (?),
// followed by urls query, to output.
if (url.search.length > 0) {
output += url.search
}
// 6. If exclude fragment is false and urls fragment is non-null,
// then append U+0023 (#), followed by urls fragment, to output.
if (excludeFragment === false && url.hash.length > 0) {
output += url.hash
}
// 7. Return output.
return output
} }
// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
@@ -204,6 +153,25 @@ function collectASequenceOfCodePoints (condition, input, position) {
return result return result
} }
/**
* A faster collectASequenceOfCodePoints that only works when comparing a single character.
* @param {string} char
* @param {string} input
* @param {{ position: number }} position
*/
function collectASequenceOfCodePointsFast (char, input, position) {
const idx = input.indexOf(char, position.position)
const start = position.position
if (idx === -1) {
position.position = input.length
return input.slice(start)
}
position.position = idx
return input.slice(start, position.position)
}
// https://url.spec.whatwg.org/#string-percent-decode // https://url.spec.whatwg.org/#string-percent-decode
/** @param {string} input */ /** @param {string} input */
function stringPercentDecode (input) { function stringPercentDecode (input) {
@@ -264,7 +232,7 @@ function percentDecode (input) {
function parseMIMEType (input) { function parseMIMEType (input) {
// 1. Remove any leading and trailing HTTP whitespace // 1. Remove any leading and trailing HTTP whitespace
// from input. // from input.
input = input.trim() input = removeHTTPWhitespace(input, true, true)
// 2. Let position be a position variable for input, // 2. Let position be a position variable for input,
// initially pointing at the start of input. // initially pointing at the start of input.
@@ -273,8 +241,8 @@ function parseMIMEType (input) {
// 3. Let type be the result of collecting a sequence // 3. Let type be the result of collecting a sequence
// of code points that are not U+002F (/) from // of code points that are not U+002F (/) from
// input, given position. // input, given position.
const type = collectASequenceOfCodePoints( const type = collectASequenceOfCodePointsFast(
(char) => char !== '/', '/',
input, input,
position position
) )
@@ -282,7 +250,7 @@ function parseMIMEType (input) {
// 4. If type is the empty string or does not solely // 4. If type is the empty string or does not solely
// contain HTTP token code points, then return failure. // contain HTTP token code points, then return failure.
// https://mimesniff.spec.whatwg.org/#http-token-code-point // https://mimesniff.spec.whatwg.org/#http-token-code-point
if (type.length === 0 || !/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(type)) { if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) {
return 'failure' return 'failure'
} }
@@ -298,30 +266,35 @@ function parseMIMEType (input) {
// 7. Let subtype be the result of collecting a sequence of // 7. Let subtype be the result of collecting a sequence of
// code points that are not U+003B (;) from input, given // code points that are not U+003B (;) from input, given
// position. // position.
let subtype = collectASequenceOfCodePoints( let subtype = collectASequenceOfCodePointsFast(
(char) => char !== ';', ';',
input, input,
position position
) )
// 8. Remove any trailing HTTP whitespace from subtype. // 8. Remove any trailing HTTP whitespace from subtype.
subtype = subtype.trim() subtype = removeHTTPWhitespace(subtype, false, true)
// 9. If subtype is the empty string or does not solely // 9. If subtype is the empty string or does not solely
// contain HTTP token code points, then return failure. // contain HTTP token code points, then return failure.
if (subtype.length === 0 || !/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(subtype)) { if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) {
return 'failure' return 'failure'
} }
const typeLowercase = type.toLowerCase()
const subtypeLowercase = subtype.toLowerCase()
// 10. Let mimeType be a new MIME type record whose type // 10. Let mimeType be a new MIME type record whose type
// is type, in ASCII lowercase, and subtype is subtype, // is type, in ASCII lowercase, and subtype is subtype,
// in ASCII lowercase. // in ASCII lowercase.
// https://mimesniff.spec.whatwg.org/#mime-type // https://mimesniff.spec.whatwg.org/#mime-type
const mimeType = { const mimeType = {
type: type.toLowerCase(), type: typeLowercase,
subtype: subtype.toLowerCase(), subtype: subtypeLowercase,
/** @type {Map<string, string>} */ /** @type {Map<string, string>} */
parameters: new Map() parameters: new Map(),
// https://mimesniff.spec.whatwg.org/#mime-type-essence
essence: `${typeLowercase}/${subtypeLowercase}`
} }
// 11. While position is not past the end of input: // 11. While position is not past the end of input:
@@ -333,7 +306,7 @@ function parseMIMEType (input) {
// whitespace from input given position. // whitespace from input given position.
collectASequenceOfCodePoints( collectASequenceOfCodePoints(
// https://fetch.spec.whatwg.org/#http-whitespace // https://fetch.spec.whatwg.org/#http-whitespace
(char) => /(\u000A|\u000D|\u0009|\u0020)/.test(char), // eslint-disable-line char => HTTP_WHITESPACE_REGEX.test(char),
input, input,
position position
) )
@@ -381,8 +354,8 @@ function parseMIMEType (input) {
// 2. Collect a sequence of code points that are not // 2. Collect a sequence of code points that are not
// U+003B (;) from input, given position. // U+003B (;) from input, given position.
collectASequenceOfCodePoints( collectASequenceOfCodePointsFast(
(char) => char !== ';', ';',
input, input,
position position
) )
@@ -392,15 +365,14 @@ function parseMIMEType (input) {
// 1. Set parameterValue to the result of collecting // 1. Set parameterValue to the result of collecting
// a sequence of code points that are not U+003B (;) // a sequence of code points that are not U+003B (;)
// from input, given position. // from input, given position.
parameterValue = collectASequenceOfCodePoints( parameterValue = collectASequenceOfCodePointsFast(
(char) => char !== ';', ';',
input, input,
position position
) )
// 2. Remove any trailing HTTP whitespace from parameterValue. // 2. Remove any trailing HTTP whitespace from parameterValue.
// Note: it says "trailing" whitespace; leading is fine. parameterValue = removeHTTPWhitespace(parameterValue, false, true)
parameterValue = parameterValue.trimEnd()
// 3. If parameterValue is the empty string, then continue. // 3. If parameterValue is the empty string, then continue.
if (parameterValue.length === 0) { if (parameterValue.length === 0) {
@@ -416,9 +388,8 @@ function parseMIMEType (input) {
// then set mimeTypes parameters[parameterName] to parameterValue. // then set mimeTypes parameters[parameterName] to parameterValue.
if ( if (
parameterName.length !== 0 && parameterName.length !== 0 &&
/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(parameterName) && HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
// https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
!/^(\u0009|\x{0020}-\x{007E}|\x{0080}-\x{00FF})+$/.test(parameterValue) && // eslint-disable-line
!mimeType.parameters.has(parameterName) !mimeType.parameters.has(parameterName)
) { ) {
mimeType.parameters.set(parameterName, parameterValue) mimeType.parameters.set(parameterName, parameterValue)
@@ -552,11 +523,11 @@ function collectAnHTTPQuotedString (input, position, extractValue) {
*/ */
function serializeAMimeType (mimeType) { function serializeAMimeType (mimeType) {
assert(mimeType !== 'failure') assert(mimeType !== 'failure')
const { type, subtype, parameters } = mimeType const { parameters, essence } = mimeType
// 1. Let serialization be the concatenation of mimeTypes // 1. Let serialization be the concatenation of mimeTypes
// type, U+002F (/), and mimeTypes subtype. // type, U+002F (/), and mimeTypes subtype.
let serialization = `${type}/${subtype}` let serialization = essence
// 2. For each name → value of mimeTypes parameters: // 2. For each name → value of mimeTypes parameters:
for (let [name, value] of parameters.entries()) { for (let [name, value] of parameters.entries()) {
@@ -571,7 +542,7 @@ function serializeAMimeType (mimeType) {
// 4. If value does not solely contain HTTP token code // 4. If value does not solely contain HTTP token code
// points or value is the empty string, then: // points or value is the empty string, then:
if (!isValidHTTPToken(value)) { if (!HTTP_TOKEN_CODEPOINTS.test(value)) {
// 1. Precede each occurence of U+0022 (") or // 1. Precede each occurence of U+0022 (") or
// U+005C (\) in value with U+005C (\). // U+005C (\) in value with U+005C (\).
value = value.replace(/(\\|")/g, '\\$1') value = value.replace(/(\\|")/g, '\\$1')
@@ -591,10 +562,64 @@ function serializeAMimeType (mimeType) {
return serialization return serialization
} }
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {string} char
*/
function isHTTPWhiteSpace (char) {
return char === '\r' || char === '\n' || char === '\t' || char === ' '
}
/**
* @see https://fetch.spec.whatwg.org/#http-whitespace
* @param {string} str
*/
function removeHTTPWhitespace (str, leading = true, trailing = true) {
let lead = 0
let trail = str.length - 1
if (leading) {
for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++);
}
if (trailing) {
for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--);
}
return str.slice(lead, trail + 1)
}
/**
* @see https://infra.spec.whatwg.org/#ascii-whitespace
* @param {string} char
*/
function isASCIIWhitespace (char) {
return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' '
}
/**
* @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
*/
function removeASCIIWhitespace (str, leading = true, trailing = true) {
let lead = 0
let trail = str.length - 1
if (leading) {
for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++);
}
if (trailing) {
for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--);
}
return str.slice(lead, trail + 1)
}
module.exports = { module.exports = {
dataURLProcessor, dataURLProcessor,
URLSerializer, URLSerializer,
collectASequenceOfCodePoints, collectASequenceOfCodePoints,
collectASequenceOfCodePointsFast,
stringPercentDecode, stringPercentDecode,
parseMIMEType, parseMIMEType,
collectAnHTTPQuotedString, collectAnHTTPQuotedString,

View File

@@ -1,19 +1,20 @@
'use strict' 'use strict'
const { Blob } = require('buffer') const { Blob, File: NativeFile } = require('buffer')
const { types } = require('util') const { types } = require('util')
const { kState } = require('./symbols') const { kState } = require('./symbols')
const { isBlobLike } = require('./util') const { isBlobLike } = require('./util')
const { webidl } = require('./webidl') const { webidl } = require('./webidl')
const { parseMIMEType, serializeAMimeType } = require('./dataURL')
const { kEnumerableProperty } = require('../core/util')
const encoder = new TextEncoder()
class File extends Blob { class File extends Blob {
constructor (fileBits, fileName, options = {}) { constructor (fileBits, fileName, options = {}) {
// The File constructor is invoked with two or three parameters, depending // The File constructor is invoked with two or three parameters, depending
// on whether the optional dictionary parameter is used. When the File() // on whether the optional dictionary parameter is used. When the File()
// constructor is invoked, user agents must run the following steps: // constructor is invoked, user agents must run the following steps:
if (arguments.length < 2) { webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' })
throw new TypeError('2 arguments required')
}
fileBits = webidl.converters['sequence<BlobPart>'](fileBits) fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
fileName = webidl.converters.USVString(fileName) fileName = webidl.converters.USVString(fileName)
@@ -34,13 +35,29 @@ class File extends Blob {
// outside the range U+0020 to U+007E, then set t to the empty string // outside the range U+0020 to U+007E, then set t to the empty string
// and return from these substeps. // and return from these substeps.
// 2. Convert every character in t to ASCII lowercase. // 2. Convert every character in t to ASCII lowercase.
// Note: Blob handles both of these steps for us let t = options.type
let d
// 3. If the lastModified member is provided, let d be set to the // eslint-disable-next-line no-labels
// lastModified dictionary member. If it is not provided, set d to the substep: {
// current date and time represented as the number of milliseconds since if (t) {
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). t = parseMIMEType(t)
const d = options.lastModified
if (t === 'failure') {
t = ''
// eslint-disable-next-line no-labels
break substep
}
t = serializeAMimeType(t).toLowerCase()
}
// 3. If the lastModified member is provided, let d be set to the
// lastModified dictionary member. If it is not provided, set d to the
// current date and time represented as the number of milliseconds since
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
d = options.lastModified
}
// 4. Return a new File object F such that: // 4. Return a new File object F such that:
// F refers to the bytes byte sequence. // F refers to the bytes byte sequence.
@@ -49,31 +66,30 @@ class File extends Blob {
// F.type is set to t. // F.type is set to t.
// F.lastModified is set to d. // F.lastModified is set to d.
super(processBlobParts(fileBits, options), { type: options.type }) super(processBlobParts(fileBits, options), { type: t })
this[kState] = { this[kState] = {
name: n, name: n,
lastModified: d lastModified: d,
type: t
} }
} }
get name () { get name () {
if (!(this instanceof File)) { webidl.brandCheck(this, File)
throw new TypeError('Illegal invocation')
}
return this[kState].name return this[kState].name
} }
get lastModified () { get lastModified () {
if (!(this instanceof File)) { webidl.brandCheck(this, File)
throw new TypeError('Illegal invocation')
}
return this[kState].lastModified return this[kState].lastModified
} }
get [Symbol.toStringTag] () { get type () {
return this.constructor.name webidl.brandCheck(this, File)
return this[kState].type
} }
} }
@@ -126,65 +142,49 @@ class FileLike {
} }
stream (...args) { stream (...args) {
if (!(this instanceof FileLike)) { webidl.brandCheck(this, FileLike)
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.stream(...args) return this[kState].blobLike.stream(...args)
} }
arrayBuffer (...args) { arrayBuffer (...args) {
if (!(this instanceof FileLike)) { webidl.brandCheck(this, FileLike)
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.arrayBuffer(...args) return this[kState].blobLike.arrayBuffer(...args)
} }
slice (...args) { slice (...args) {
if (!(this instanceof FileLike)) { webidl.brandCheck(this, FileLike)
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.slice(...args) return this[kState].blobLike.slice(...args)
} }
text (...args) { text (...args) {
if (!(this instanceof FileLike)) { webidl.brandCheck(this, FileLike)
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.text(...args) return this[kState].blobLike.text(...args)
} }
get size () { get size () {
if (!(this instanceof FileLike)) { webidl.brandCheck(this, FileLike)
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.size return this[kState].blobLike.size
} }
get type () { get type () {
if (!(this instanceof FileLike)) { webidl.brandCheck(this, FileLike)
throw new TypeError('Illegal invocation')
}
return this[kState].blobLike.type return this[kState].blobLike.type
} }
get name () { get name () {
if (!(this instanceof FileLike)) { webidl.brandCheck(this, FileLike)
throw new TypeError('Illegal invocation')
}
return this[kState].name return this[kState].name
} }
get lastModified () { get lastModified () {
if (!(this instanceof FileLike)) { webidl.brandCheck(this, FileLike)
throw new TypeError('Illegal invocation')
}
return this[kState].lastModified return this[kState].lastModified
} }
@@ -194,6 +194,15 @@ class FileLike {
} }
} }
Object.defineProperties(File.prototype, {
[Symbol.toStringTag]: {
value: 'File',
configurable: true
},
name: kEnumerableProperty,
lastModified: kEnumerableProperty
})
webidl.converters.Blob = webidl.interfaceConverter(Blob) webidl.converters.Blob = webidl.interfaceConverter(Blob)
webidl.converters.BlobPart = function (V, opts) { webidl.converters.BlobPart = function (V, opts) {
@@ -202,10 +211,15 @@ webidl.converters.BlobPart = function (V, opts) {
return webidl.converters.Blob(V, { strict: false }) return webidl.converters.Blob(V, { strict: false })
} }
return webidl.converters.BufferSource(V, opts) if (
} else { ArrayBuffer.isView(V) ||
return webidl.converters.USVString(V, opts) types.isAnyArrayBuffer(V)
) {
return webidl.converters.BufferSource(V, opts)
}
} }
return webidl.converters.USVString(V, opts)
} }
webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter( webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
@@ -267,7 +281,7 @@ function processBlobParts (parts, options) {
} }
// 3. Append the result of UTF-8 encoding s to bytes. // 3. Append the result of UTF-8 encoding s to bytes.
bytes.push(new TextEncoder().encode(s)) bytes.push(encoder.encode(s))
} else if ( } else if (
types.isAnyArrayBuffer(element) || types.isAnyArrayBuffer(element) ||
types.isTypedArray(element) types.isTypedArray(element)
@@ -316,11 +330,14 @@ function convertLineEndingsNative (s) {
// rollup) will warn about circular dependencies. See: // rollup) will warn about circular dependencies. See:
// https://github.com/nodejs/undici/issues/1629 // https://github.com/nodejs/undici/issues/1629
function isFileLike (object) { function isFileLike (object) {
return object instanceof File || ( return (
object && (NativeFile && object instanceof NativeFile) ||
(typeof object.stream === 'function' || object instanceof File || (
typeof object.arrayBuffer === 'function') && object &&
object[Symbol.toStringTag] === 'File' (typeof object.stream === 'function' ||
typeof object.arrayBuffer === 'function') &&
object[Symbol.toStringTag] === 'File'
)
) )
} }

View File

@@ -2,20 +2,21 @@
const { isBlobLike, toUSVString, makeIterator } = require('./util') const { isBlobLike, toUSVString, makeIterator } = require('./util')
const { kState } = require('./symbols') const { kState } = require('./symbols')
const { File, FileLike, isFileLike } = require('./file') const { File: UndiciFile, FileLike, isFileLike } = require('./file')
const { webidl } = require('./webidl') const { webidl } = require('./webidl')
const { Blob } = require('buffer') const { Blob, File: NativeFile } = require('buffer')
/** @type {globalThis['File']} */
const File = NativeFile ?? UndiciFile
// https://xhr.spec.whatwg.org/#formdata // https://xhr.spec.whatwg.org/#formdata
class FormData { class FormData {
static name = 'FormData'
constructor (form) { constructor (form) {
if (arguments.length > 0 && form != null) { if (form !== undefined) {
webidl.errors.conversionFailed({ throw webidl.errors.conversionFailed({
prefix: 'FormData constructor', prefix: 'FormData constructor',
argument: 'Argument 1', argument: 'Argument 1',
types: ['null'] types: ['undefined']
}) })
} }
@@ -23,15 +24,9 @@ class FormData {
} }
append (name, value, filename = undefined) { append (name, value, filename = undefined) {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 2) { webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' })
throw new TypeError(
`Failed to execute 'append' on 'FormData': 2 arguments required, but only ${arguments.length} present.`
)
}
if (arguments.length === 3 && !isBlobLike(value)) { if (arguments.length === 3 && !isBlobLike(value)) {
throw new TypeError( throw new TypeError(
@@ -58,40 +53,21 @@ class FormData {
} }
delete (name) { delete (name) {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' })
throw new TypeError(
`Failed to execute 'delete' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.USVString(name) name = webidl.converters.USVString(name)
// The delete(name) method steps are to remove all entries whose name // The delete(name) method steps are to remove all entries whose name
// is name from thiss entry list. // is name from thiss entry list.
const next = [] this[kState] = this[kState].filter(entry => entry.name !== name)
for (const entry of this[kState]) {
if (entry.name !== name) {
next.push(entry)
}
}
this[kState] = next
} }
get (name) { get (name) {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' })
throw new TypeError(
`Failed to execute 'get' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.USVString(name) name = webidl.converters.USVString(name)
@@ -108,15 +84,9 @@ class FormData {
} }
getAll (name) { getAll (name) {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' })
throw new TypeError(
`Failed to execute 'getAll' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.USVString(name) name = webidl.converters.USVString(name)
@@ -130,15 +100,9 @@ class FormData {
} }
has (name) { has (name) {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' })
throw new TypeError(
`Failed to execute 'has' on 'FormData': 1 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.USVString(name) name = webidl.converters.USVString(name)
@@ -148,15 +112,9 @@ class FormData {
} }
set (name, value, filename = undefined) { set (name, value, filename = undefined) {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 2) { webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' })
throw new TypeError(
`Failed to execute 'set' on 'FormData': 2 arguments required, but only ${arguments.length} present.`
)
}
if (arguments.length === 3 && !isBlobLike(value)) { if (arguments.length === 3 && !isBlobLike(value)) {
throw new TypeError( throw new TypeError(
@@ -196,40 +154,33 @@ class FormData {
} }
} }
get [Symbol.toStringTag] () {
return this.constructor.name
}
entries () { entries () {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
return makeIterator( return makeIterator(
makeIterable(this[kState], 'entries'), () => this[kState].map(pair => [pair.name, pair.value]),
'FormData' 'FormData',
'key+value'
) )
} }
keys () { keys () {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
return makeIterator( return makeIterator(
makeIterable(this[kState], 'keys'), () => this[kState].map(pair => [pair.name, pair.value]),
'FormData' 'FormData',
'key'
) )
} }
values () { values () {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
return makeIterator( return makeIterator(
makeIterable(this[kState], 'values'), () => this[kState].map(pair => [pair.name, pair.value]),
'FormData' 'FormData',
'value'
) )
} }
@@ -238,15 +189,9 @@ class FormData {
* @param {unknown} thisArg * @param {unknown} thisArg
*/ */
forEach (callbackFn, thisArg = globalThis) { forEach (callbackFn, thisArg = globalThis) {
if (!(this instanceof FormData)) { webidl.brandCheck(this, FormData)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' })
throw new TypeError(
`Failed to execute 'forEach' on 'FormData': 1 argument required, but only ${arguments.length} present.`
)
}
if (typeof callbackFn !== 'function') { if (typeof callbackFn !== 'function') {
throw new TypeError( throw new TypeError(
@@ -262,6 +207,13 @@ class FormData {
FormData.prototype[Symbol.iterator] = FormData.prototype.entries FormData.prototype[Symbol.iterator] = FormData.prototype.entries
Object.defineProperties(FormData.prototype, {
[Symbol.toStringTag]: {
value: 'FormData',
configurable: true
}
})
/** /**
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
* @param {string} name * @param {string} name
@@ -294,9 +246,15 @@ function makeEntry (name, value, filename) {
// 2. If filename is given, then set value to a new File object, // 2. If filename is given, then set value to a new File object,
// representing the same bytes, whose name attribute is filename. // representing the same bytes, whose name attribute is filename.
if (filename !== undefined) { if (filename !== undefined) {
value = value instanceof File /** @type {FilePropertyBag} */
? new File([value], filename, { type: value.type }) const options = {
: new FileLike(value, filename, { type: value.type }) type: value.type,
lastModified: value.lastModified
}
value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile
? new File([value], filename, options)
: new FileLike(value, filename, options)
} }
} }
@@ -304,18 +262,4 @@ function makeEntry (name, value, filename) {
return { name, value } return { name, value }
} }
function * makeIterable (entries, type) {
// The value pairs to iterate over are thiss entry lists entries
// with the key being the name and the value being the value.
for (const { name, value } of entries) {
if (type === 'entries') {
yield [name, value]
} else if (type === 'values') {
yield value
} else {
yield name
}
}
}
module.exports = { FormData } module.exports = { FormData }

View File

@@ -9,14 +9,6 @@ function getGlobalOrigin () {
} }
function setGlobalOrigin (newOrigin) { function setGlobalOrigin (newOrigin) {
if (
newOrigin !== undefined &&
typeof newOrigin !== 'string' &&
!(newOrigin instanceof URL)
) {
throw new Error('Invalid base url')
}
if (newOrigin === undefined) { if (newOrigin === undefined) {
Object.defineProperty(globalThis, globalOrigin, { Object.defineProperty(globalThis, globalOrigin, {
value: undefined, value: undefined,

View File

@@ -2,7 +2,7 @@
'use strict' 'use strict'
const { kHeadersList } = require('../core/symbols') const { kHeadersList, kConstruct } = require('../core/symbols')
const { kGuard } = require('./symbols') const { kGuard } = require('./symbols')
const { kEnumerableProperty } = require('../core/util') const { kEnumerableProperty } = require('../core/util')
const { const {
@@ -11,10 +11,18 @@ const {
isValidHeaderValue isValidHeaderValue
} = require('./util') } = require('./util')
const { webidl } = require('./webidl') const { webidl } = require('./webidl')
const assert = require('assert')
const kHeadersMap = Symbol('headers map') const kHeadersMap = Symbol('headers map')
const kHeadersSortedMap = Symbol('headers map sorted') const kHeadersSortedMap = Symbol('headers map sorted')
/**
* @param {number} code
*/
function isHTTPWhiteSpaceCharCode (code) {
return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020
}
/** /**
* @see https://fetch.spec.whatwg.org/#concept-header-value-normalize * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
* @param {string} potentialValue * @param {string} potentialValue
@@ -23,10 +31,12 @@ function headerValueNormalize (potentialValue) {
// To normalize a byte sequence potentialValue, remove // To normalize a byte sequence potentialValue, remove
// any leading and trailing HTTP whitespace bytes from // any leading and trailing HTTP whitespace bytes from
// potentialValue. // potentialValue.
return potentialValue.replace( let i = 0; let j = potentialValue.length
/^[\r\n\t ]+|[\r\n\t ]+$/g,
'' while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
) while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
} }
function fill (headers, object) { function fill (headers, object) {
@@ -35,28 +45,30 @@ function fill (headers, object) {
// 1. If object is a sequence, then for each header in object: // 1. If object is a sequence, then for each header in object:
// Note: webidl conversion to array has already been done. // Note: webidl conversion to array has already been done.
if (Array.isArray(object)) { if (Array.isArray(object)) {
for (const header of object) { for (let i = 0; i < object.length; ++i) {
const header = object[i]
// 1. If header does not contain exactly two items, then throw a TypeError. // 1. If header does not contain exactly two items, then throw a TypeError.
if (header.length !== 2) { if (header.length !== 2) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Headers constructor', header: 'Headers constructor',
message: `expected name/value pair to be length 2, found ${header.length}.` message: `expected name/value pair to be length 2, found ${header.length}.`
}) })
} }
// 2. Append (headers first item, headers second item) to headers. // 2. Append (headers first item, headers second item) to headers.
headers.append(header[0], header[1]) appendHeader(headers, header[0], header[1])
} }
} else if (typeof object === 'object' && object !== null) { } else if (typeof object === 'object' && object !== null) {
// Note: null should throw // Note: null should throw
// 2. Otherwise, object is a record, then for each key → value in object, // 2. Otherwise, object is a record, then for each key → value in object,
// append (key, value) to headers // append (key, value) to headers
for (const [key, value] of Object.entries(object)) { const keys = Object.keys(object)
headers.append(key, value) for (let i = 0; i < keys.length; ++i) {
appendHeader(headers, keys[i], object[keys[i]])
} }
} else { } else {
webidl.errors.conversionFailed({ throw webidl.errors.conversionFailed({
prefix: 'Headers constructor', prefix: 'Headers constructor',
argument: 'Argument 1', argument: 'Argument 1',
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>'] types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
@@ -64,11 +76,59 @@ function fill (headers, object) {
} }
} }
/**
* @see https://fetch.spec.whatwg.org/#concept-headers-append
*/
function appendHeader (headers, name, value) {
// 1. Normalize value.
value = headerValueNormalize(value)
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
if (!isValidHeaderName(name)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.append',
value: name,
type: 'header name'
})
} else if (!isValidHeaderValue(value)) {
throw webidl.errors.invalidArgument({
prefix: 'Headers.append',
value,
type: 'header value'
})
}
// 3. If headerss guard is "immutable", then throw a TypeError.
// 4. Otherwise, if headerss guard is "request" and name is a
// forbidden header name, return.
// Note: undici does not implement forbidden header names
if (headers[kGuard] === 'immutable') {
throw new TypeError('immutable')
} else if (headers[kGuard] === 'request-no-cors') {
// 5. Otherwise, if headerss guard is "request-no-cors":
// TODO
}
// 6. Otherwise, if headerss guard is "response" and name is a
// forbidden response-header name, return.
// 7. Append (name, value) to headerss header list.
return headers[kHeadersList].append(name, value)
// 8. If headerss guard is "request-no-cors", then remove
// privileged no-CORS request headers from headers
}
class HeadersList { class HeadersList {
/** @type {[string, string][]|null} */
cookies = null
constructor (init) { constructor (init) {
if (init instanceof HeadersList) { if (init instanceof HeadersList) {
this[kHeadersMap] = new Map(init[kHeadersMap]) this[kHeadersMap] = new Map(init[kHeadersMap])
this[kHeadersSortedMap] = init[kHeadersSortedMap] this[kHeadersSortedMap] = init[kHeadersSortedMap]
this.cookies = init.cookies === null ? null : [...init.cookies]
} else { } else {
this[kHeadersMap] = new Map(init) this[kHeadersMap] = new Map(init)
this[kHeadersSortedMap] = null this[kHeadersSortedMap] = null
@@ -88,6 +148,7 @@ class HeadersList {
clear () { clear () {
this[kHeadersMap].clear() this[kHeadersMap].clear()
this[kHeadersSortedMap] = null this[kHeadersSortedMap] = null
this.cookies = null
} }
// https://fetch.spec.whatwg.org/#concept-header-list-append // https://fetch.spec.whatwg.org/#concept-header-list-append
@@ -96,27 +157,40 @@ class HeadersList {
// 1. If list contains name, then set name to the first such // 1. If list contains name, then set name to the first such
// headers name. // headers name.
name = name.toLowerCase() const lowercaseName = name.toLowerCase()
const exists = this[kHeadersMap].get(name) const exists = this[kHeadersMap].get(lowercaseName)
// 2. Append (name, value) to list. // 2. Append (name, value) to list.
if (exists) { if (exists) {
this[kHeadersMap].set(name, `${exists}, ${value}`) const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
this[kHeadersMap].set(lowercaseName, {
name: exists.name,
value: `${exists.value}${delimiter}${value}`
})
} else { } else {
this[kHeadersMap].set(name, `${value}`) this[kHeadersMap].set(lowercaseName, { name, value })
}
if (lowercaseName === 'set-cookie') {
this.cookies ??= []
this.cookies.push(value)
} }
} }
// https://fetch.spec.whatwg.org/#concept-header-list-set // https://fetch.spec.whatwg.org/#concept-header-list-set
set (name, value) { set (name, value) {
this[kHeadersSortedMap] = null this[kHeadersSortedMap] = null
name = name.toLowerCase() const lowercaseName = name.toLowerCase()
if (lowercaseName === 'set-cookie') {
this.cookies = [value]
}
// 1. If list contains name, then set the value of // 1. If list contains name, then set the value of
// the first such header to value and remove the // the first such header to value and remove the
// others. // others.
// 2. Otherwise, append header (name, value) to list. // 2. Otherwise, append header (name, value) to list.
return this[kHeadersMap].set(name, value) this[kHeadersMap].set(lowercaseName, { name, value })
} }
// https://fetch.spec.whatwg.org/#concept-header-list-delete // https://fetch.spec.whatwg.org/#concept-header-list-delete
@@ -124,49 +198,51 @@ class HeadersList {
this[kHeadersSortedMap] = null this[kHeadersSortedMap] = null
name = name.toLowerCase() name = name.toLowerCase()
return this[kHeadersMap].delete(name)
if (name === 'set-cookie') {
this.cookies = null
}
this[kHeadersMap].delete(name)
} }
// https://fetch.spec.whatwg.org/#concept-header-list-get // https://fetch.spec.whatwg.org/#concept-header-list-get
get (name) { get (name) {
name = name.toLowerCase() const value = this[kHeadersMap].get(name.toLowerCase())
// 1. If list does not contain name, then return null. // 1. If list does not contain name, then return null.
if (!this.contains(name)) {
return null
}
// 2. Return the values of all headers in list whose name // 2. Return the values of all headers in list whose name
// is a byte-case-insensitive match for name, // is a byte-case-insensitive match for name,
// separated from each other by 0x2C 0x20, in order. // separated from each other by 0x2C 0x20, in order.
return this[kHeadersMap].get(name) ?? null return value === undefined ? null : value.value
} }
has (name) { * [Symbol.iterator] () {
name = name.toLowerCase() // use the lowercased name
return this[kHeadersMap].has(name) for (const [name, { value }] of this[kHeadersMap]) {
yield [name, value]
}
} }
keys () { get entries () {
return this[kHeadersMap].keys() const headers = {}
}
values () { if (this[kHeadersMap].size) {
return this[kHeadersMap].values() for (const { name, value } of this[kHeadersMap].values()) {
} headers[name] = value
}
}
entries () { return headers
return this[kHeadersMap].entries()
}
[Symbol.iterator] () {
return this[kHeadersMap][Symbol.iterator]()
} }
} }
// https://fetch.spec.whatwg.org/#headers-class // https://fetch.spec.whatwg.org/#headers-class
class Headers { class Headers {
constructor (init = undefined) { constructor (init = undefined) {
if (init === kConstruct) {
return
}
this[kHeadersList] = new HeadersList() this[kHeadersList] = new HeadersList()
// The new Headers(init) constructor steps are: // The new Headers(init) constructor steps are:
@@ -181,81 +257,29 @@ class Headers {
} }
} }
get [Symbol.toStringTag] () {
return this.constructor.name
}
// https://fetch.spec.whatwg.org/#dom-headers-append // https://fetch.spec.whatwg.org/#dom-headers-append
append (name, value) { append (name, value) {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 2) { webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' })
throw new TypeError(
`Failed to execute 'append' on 'Headers': 2 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name) name = webidl.converters.ByteString(name)
value = webidl.converters.ByteString(value) value = webidl.converters.ByteString(value)
// 1. Normalize value. return appendHeader(this, name, value)
value = headerValueNormalize(value)
// 2. If name is not a header name or value is not a
// header value, then throw a TypeError.
if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({
prefix: 'Headers.append',
value: name,
type: 'header name'
})
} else if (!isValidHeaderValue(value)) {
webidl.errors.invalidArgument({
prefix: 'Headers.append',
value,
type: 'header value'
})
}
// 3. If headerss guard is "immutable", then throw a TypeError.
// 4. Otherwise, if headerss guard is "request" and name is a
// forbidden header name, return.
// Note: undici does not implement forbidden header names
if (this[kGuard] === 'immutable') {
throw new TypeError('immutable')
} else if (this[kGuard] === 'request-no-cors') {
// 5. Otherwise, if headerss guard is "request-no-cors":
// TODO
}
// 6. Otherwise, if headerss guard is "response" and name is a
// forbidden response-header name, return.
// 7. Append (name, value) to headerss header list.
// 8. If headerss guard is "request-no-cors", then remove
// privileged no-CORS request headers from headers
return this[kHeadersList].append(name, value)
} }
// https://fetch.spec.whatwg.org/#dom-headers-delete // https://fetch.spec.whatwg.org/#dom-headers-delete
delete (name) { delete (name) {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' })
throw new TypeError(
`Failed to execute 'delete' on 'Headers': 1 argument required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name) name = webidl.converters.ByteString(name)
// 1. If name is not a header name, then throw a TypeError. // 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) { if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({ throw webidl.errors.invalidArgument({
prefix: 'Headers.delete', prefix: 'Headers.delete',
value: name, value: name,
type: 'header name' type: 'header name'
@@ -287,26 +311,20 @@ class Headers {
// 7. Delete name from thiss header list. // 7. Delete name from thiss header list.
// 8. If thiss guard is "request-no-cors", then remove // 8. If thiss guard is "request-no-cors", then remove
// privileged no-CORS request headers from this. // privileged no-CORS request headers from this.
return this[kHeadersList].delete(name) this[kHeadersList].delete(name)
} }
// https://fetch.spec.whatwg.org/#dom-headers-get // https://fetch.spec.whatwg.org/#dom-headers-get
get (name) { get (name) {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' })
throw new TypeError(
`Failed to execute 'get' on 'Headers': 1 argument required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name) name = webidl.converters.ByteString(name)
// 1. If name is not a header name, then throw a TypeError. // 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) { if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({ throw webidl.errors.invalidArgument({
prefix: 'Headers.get', prefix: 'Headers.get',
value: name, value: name,
type: 'header name' type: 'header name'
@@ -320,21 +338,15 @@ class Headers {
// https://fetch.spec.whatwg.org/#dom-headers-has // https://fetch.spec.whatwg.org/#dom-headers-has
has (name) { has (name) {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' })
throw new TypeError(
`Failed to execute 'has' on 'Headers': 1 argument required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name) name = webidl.converters.ByteString(name)
// 1. If name is not a header name, then throw a TypeError. // 1. If name is not a header name, then throw a TypeError.
if (!isValidHeaderName(name)) { if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({ throw webidl.errors.invalidArgument({
prefix: 'Headers.has', prefix: 'Headers.has',
value: name, value: name,
type: 'header name' type: 'header name'
@@ -348,15 +360,9 @@ class Headers {
// https://fetch.spec.whatwg.org/#dom-headers-set // https://fetch.spec.whatwg.org/#dom-headers-set
set (name, value) { set (name, value) {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 2) { webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' })
throw new TypeError(
`Failed to execute 'set' on 'Headers': 2 arguments required, but only ${arguments.length} present.`
)
}
name = webidl.converters.ByteString(name) name = webidl.converters.ByteString(name)
value = webidl.converters.ByteString(value) value = webidl.converters.ByteString(value)
@@ -367,13 +373,13 @@ class Headers {
// 2. If name is not a header name or value is not a // 2. If name is not a header name or value is not a
// header value, then throw a TypeError. // header value, then throw a TypeError.
if (!isValidHeaderName(name)) { if (!isValidHeaderName(name)) {
webidl.errors.invalidArgument({ throw webidl.errors.invalidArgument({
prefix: 'Headers.set', prefix: 'Headers.set',
value: name, value: name,
type: 'header name' type: 'header name'
}) })
} else if (!isValidHeaderValue(value)) { } else if (!isValidHeaderValue(value)) {
webidl.errors.invalidArgument({ throw webidl.errors.invalidArgument({
prefix: 'Headers.set', prefix: 'Headers.set',
value, value,
type: 'header value' type: 'header value'
@@ -398,38 +404,119 @@ class Headers {
// 7. Set (name, value) in thiss header list. // 7. Set (name, value) in thiss header list.
// 8. If thiss guard is "request-no-cors", then remove // 8. If thiss guard is "request-no-cors", then remove
// privileged no-CORS request headers from this // privileged no-CORS request headers from this
return this[kHeadersList].set(name, value) this[kHeadersList].set(name, value)
} }
get [kHeadersSortedMap] () { // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
if (!this[kHeadersList][kHeadersSortedMap]) { getSetCookie () {
this[kHeadersList][kHeadersSortedMap] = new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)) webidl.brandCheck(this, Headers)
// 1. If thiss header list does not contain `Set-Cookie`, then return « ».
// 2. Return the values of all headers in thiss header list whose name is
// a byte-case-insensitive match for `Set-Cookie`, in order.
const list = this[kHeadersList].cookies
if (list) {
return [...list]
} }
return this[kHeadersList][kHeadersSortedMap]
return []
}
// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
get [kHeadersSortedMap] () {
if (this[kHeadersList][kHeadersSortedMap]) {
return this[kHeadersList][kHeadersSortedMap]
}
// 1. Let headers be an empty list of headers with the key being the name
// and value the value.
const headers = []
// 2. Let names be the result of convert header names to a sorted-lowercase
// set with all the names of the headers in list.
const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)
const cookies = this[kHeadersList].cookies
// 3. For each name of names:
for (let i = 0; i < names.length; ++i) {
const [name, value] = names[i]
// 1. If name is `set-cookie`, then:
if (name === 'set-cookie') {
// 1. Let values be a list of all values of headers in list whose name
// is a byte-case-insensitive match for name, in order.
// 2. For each value of values:
// 1. Append (name, value) to headers.
for (let j = 0; j < cookies.length; ++j) {
headers.push([name, cookies[j]])
}
} else {
// 2. Otherwise:
// 1. Let value be the result of getting name from list.
// 2. Assert: value is non-null.
assert(value !== null)
// 3. Append (name, value) to headers.
headers.push([name, value])
}
}
this[kHeadersList][kHeadersSortedMap] = headers
// 4. Return headers.
return headers
} }
keys () { keys () {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
if (this[kGuard] === 'immutable') {
const value = this[kHeadersSortedMap]
return makeIterator(() => value, 'Headers',
'key')
} }
return makeIterator(this[kHeadersSortedMap].keys(), 'Headers') return makeIterator(
() => [...this[kHeadersSortedMap].values()],
'Headers',
'key'
)
} }
values () { values () {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
if (this[kGuard] === 'immutable') {
const value = this[kHeadersSortedMap]
return makeIterator(() => value, 'Headers',
'value')
} }
return makeIterator(this[kHeadersSortedMap].values(), 'Headers') return makeIterator(
() => [...this[kHeadersSortedMap].values()],
'Headers',
'value'
)
} }
entries () { entries () {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
if (this[kGuard] === 'immutable') {
const value = this[kHeadersSortedMap]
return makeIterator(() => value, 'Headers',
'key+value')
} }
return makeIterator(this[kHeadersSortedMap].entries(), 'Headers') return makeIterator(
() => [...this[kHeadersSortedMap].values()],
'Headers',
'key+value'
)
} }
/** /**
@@ -437,15 +524,9 @@ class Headers {
* @param {unknown} thisArg * @param {unknown} thisArg
*/ */
forEach (callbackFn, thisArg = globalThis) { forEach (callbackFn, thisArg = globalThis) {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
}
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' })
throw new TypeError(
`Failed to execute 'forEach' on 'Headers': 1 argument required, but only ${arguments.length} present.`
)
}
if (typeof callbackFn !== 'function') { if (typeof callbackFn !== 'function') {
throw new TypeError( throw new TypeError(
@@ -459,9 +540,7 @@ class Headers {
} }
[Symbol.for('nodejs.util.inspect.custom')] () { [Symbol.for('nodejs.util.inspect.custom')] () {
if (!(this instanceof Headers)) { webidl.brandCheck(this, Headers)
throw new TypeError('Illegal invocation')
}
return this[kHeadersList] return this[kHeadersList]
} }
@@ -475,10 +554,16 @@ Object.defineProperties(Headers.prototype, {
get: kEnumerableProperty, get: kEnumerableProperty,
has: kEnumerableProperty, has: kEnumerableProperty,
set: kEnumerableProperty, set: kEnumerableProperty,
getSetCookie: kEnumerableProperty,
keys: kEnumerableProperty, keys: kEnumerableProperty,
values: kEnumerableProperty, values: kEnumerableProperty,
entries: kEnumerableProperty, entries: kEnumerableProperty,
forEach: kEnumerableProperty forEach: kEnumerableProperty,
[Symbol.iterator]: { enumerable: false },
[Symbol.toStringTag]: {
value: 'Headers',
configurable: true
}
}) })
webidl.converters.HeadersInit = function (V) { webidl.converters.HeadersInit = function (V) {
@@ -490,7 +575,7 @@ webidl.converters.HeadersInit = function (V) {
return webidl.converters['record<ByteString, ByteString>'](V) return webidl.converters['record<ByteString, ByteString>'](V)
} }
webidl.errors.conversionFailed({ throw webidl.errors.conversionFailed({
prefix: 'Headers constructor', prefix: 'Headers constructor',
argument: 'Argument 1', argument: 'Argument 1',
types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>'] types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']

View File

@@ -9,27 +9,32 @@ const util = require('../core/util')
const { const {
isValidHTTPToken, isValidHTTPToken,
sameOrigin, sameOrigin,
normalizeMethod normalizeMethod,
makePolicyContainer,
normalizeMethodRecord
} = require('./util') } = require('./util')
const { const {
forbiddenMethods, forbiddenMethodsSet,
corsSafeListedMethods, corsSafeListedMethodsSet,
referrerPolicy, referrerPolicy,
requestRedirect, requestRedirect,
requestMode, requestMode,
requestCredentials, requestCredentials,
requestCache requestCache,
requestDuplex
} = require('./constants') } = require('./constants')
const { kEnumerableProperty } = util const { kEnumerableProperty } = util
const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols') const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols')
const { webidl } = require('./webidl') const { webidl } = require('./webidl')
const { getGlobalOrigin } = require('./global') const { getGlobalOrigin } = require('./global')
const { kHeadersList } = require('../core/symbols') const { URLSerializer } = require('./dataURL')
const { kHeadersList, kConstruct } = require('../core/symbols')
const assert = require('assert') const assert = require('assert')
const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('events')
let TransformStream let TransformStream = globalThis.TransformStream
const kInit = Symbol('init') const kAbortController = Symbol('abortController')
const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
signal.removeEventListener('abort', abort) signal.removeEventListener('abort', abort)
@@ -39,23 +44,23 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
class Request { class Request {
// https://fetch.spec.whatwg.org/#dom-request // https://fetch.spec.whatwg.org/#dom-request
constructor (input, init = {}) { constructor (input, init = {}) {
if (input === kInit) { if (input === kConstruct) {
return return
} }
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' })
throw new TypeError(
`Failed to construct 'Request': 1 argument required, but only ${arguments.length} present.`
)
}
input = webidl.converters.RequestInfo(input) input = webidl.converters.RequestInfo(input)
init = webidl.converters.RequestInit(init) init = webidl.converters.RequestInit(init)
// TODO // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
this[kRealm] = { this[kRealm] = {
settingsObject: { settingsObject: {
baseUrl: getGlobalOrigin() baseUrl: getGlobalOrigin(),
get origin () {
return this.baseUrl?.origin
},
policyContainer: makePolicyContainer()
} }
} }
@@ -124,12 +129,12 @@ class Request {
} }
// 10. If init["window"] exists and is non-null, then throw a TypeError. // 10. If init["window"] exists and is non-null, then throw a TypeError.
if (init.window !== undefined && init.window != null) { if (init.window != null) {
throw new TypeError(`'window' option '${window}' must be null`) throw new TypeError(`'window' option '${window}' must be null`)
} }
// 11. If init["window"] exists, then set window to "no-window". // 11. If init["window"] exists, then set window to "no-window".
if (init.window !== undefined) { if ('window' in init) {
window = 'no-window' window = 'no-window'
} }
@@ -178,8 +183,10 @@ class Request {
urlList: [...request.urlList] urlList: [...request.urlList]
}) })
const initHasKey = Object.keys(init).length !== 0
// 13. If init is not empty, then: // 13. If init is not empty, then:
if (Object.keys(init).length > 0) { if (initHasKey) {
// 1. If requests mode is "navigate", then set it to "same-origin". // 1. If requests mode is "navigate", then set it to "same-origin".
if (request.mode === 'navigate') { if (request.mode === 'navigate') {
request.mode = 'same-origin' request.mode = 'same-origin'
@@ -227,14 +234,18 @@ class Request {
} }
// 3. If one of the following is true // 3. If one of the following is true
// parsedReferrers cannot-be-a-base-URL is true, scheme is "about", // - parsedReferrers scheme is "about" and path is the string "client"
// and path contains a single string "client" // - parsedReferrers origin is not same origin with origin
// parsedReferrers origin is not same origin with origin
// then set requests referrer to "client". // then set requests referrer to "client".
// TODO if (
(parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') ||
// 4. Otherwise, set requests referrer to parsedReferrer. (origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl))
request.referrer = parsedReferrer ) {
request.referrer = 'client'
} else {
// 4. Otherwise, set requests referrer to parsedReferrer.
request.referrer = parsedReferrer
}
} }
} }
@@ -242,29 +253,19 @@ class Request {
// to it. // to it.
if (init.referrerPolicy !== undefined) { if (init.referrerPolicy !== undefined) {
request.referrerPolicy = init.referrerPolicy request.referrerPolicy = init.referrerPolicy
if (!referrerPolicy.includes(request.referrerPolicy)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.referrerPolicy}' is not a valid enum value of type ReferrerPolicy.`
)
}
} }
// 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise. // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise.
let mode let mode
if (init.mode !== undefined) { if (init.mode !== undefined) {
mode = init.mode mode = init.mode
if (!requestMode.includes(mode)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.mode}' is not a valid enum value of type RequestMode.`
)
}
} else { } else {
mode = fallbackMode mode = fallbackMode
} }
// 17. If mode is "navigate", then throw a TypeError. // 17. If mode is "navigate", then throw a TypeError.
if (mode === 'navigate') { if (mode === 'navigate') {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Request constructor', header: 'Request constructor',
message: 'invalid request mode navigate.' message: 'invalid request mode navigate.'
}) })
@@ -279,21 +280,11 @@ class Request {
// to it. // to it.
if (init.credentials !== undefined) { if (init.credentials !== undefined) {
request.credentials = init.credentials request.credentials = init.credentials
if (!requestCredentials.includes(request.credentials)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.credentials}' is not a valid enum value of type RequestCredentials.`
)
}
} }
// 18. If init["cache"] exists, then set requests cache mode to it. // 18. If init["cache"] exists, then set requests cache mode to it.
if (init.cache !== undefined) { if (init.cache !== undefined) {
request.cache = init.cache request.cache = init.cache
if (!requestCache.includes(request.cache)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.cache}' is not a valid enum value of type RequestCache.`
)
}
} }
// 21. If requests cache mode is "only-if-cached" and requests mode is // 21. If requests cache mode is "only-if-cached" and requests mode is
@@ -307,15 +298,10 @@ class Request {
// 22. If init["redirect"] exists, then set requests redirect mode to it. // 22. If init["redirect"] exists, then set requests redirect mode to it.
if (init.redirect !== undefined) { if (init.redirect !== undefined) {
request.redirect = init.redirect request.redirect = init.redirect
if (!requestRedirect.includes(request.redirect)) {
throw new TypeError(
`Failed to construct 'Request': The provided value '${request.redirect}' is not a valid enum value of type RequestRedirect.`
)
}
} }
// 23. If init["integrity"] exists, then set requests integrity metadata to it. // 23. If init["integrity"] exists, then set requests integrity metadata to it.
if (init.integrity !== undefined && init.integrity != null) { if (init.integrity != null) {
request.integrity = String(init.integrity) request.integrity = String(init.integrity)
} }
@@ -331,16 +317,16 @@ class Request {
// 2. If method is not a method or method is a forbidden method, then // 2. If method is not a method or method is a forbidden method, then
// throw a TypeError. // throw a TypeError.
if (!isValidHTTPToken(init.method)) { if (!isValidHTTPToken(method)) {
throw TypeError(`'${init.method}' is not a valid HTTP method.`) throw new TypeError(`'${method}' is not a valid HTTP method.`)
} }
if (forbiddenMethods.indexOf(method.toUpperCase()) !== -1) { if (forbiddenMethodsSet.has(method.toUpperCase())) {
throw TypeError(`'${init.method}' HTTP method is unsupported.`) throw new TypeError(`'${method}' HTTP method is unsupported.`)
} }
// 3. Normalize method. // 3. Normalize method.
method = normalizeMethod(init.method) method = normalizeMethodRecord[method] ?? normalizeMethod(method)
// 4. Set requests method to method. // 4. Set requests method to method.
request.method = method request.method = method
@@ -356,6 +342,8 @@ class Request {
// 28. Set thiss signal to a new AbortSignal object with thiss relevant // 28. Set thiss signal to a new AbortSignal object with thiss relevant
// Realm. // Realm.
// TODO: could this be simplified with AbortSignal.any
// (https://dom.spec.whatwg.org/#dom-abortsignal-any)
const ac = new AbortController() const ac = new AbortController()
this[kSignal] = ac.signal this[kSignal] = ac.signal
this[kSignal][kRealm] = this[kRealm] this[kSignal][kRealm] = this[kRealm]
@@ -375,16 +363,41 @@ class Request {
if (signal.aborted) { if (signal.aborted) {
ac.abort(signal.reason) ac.abort(signal.reason)
} else { } else {
const abort = () => ac.abort(signal.reason) // Keep a strong ref to ac while request object
signal.addEventListener('abort', abort, { once: true }) // is alive. This is needed to prevent AbortController
requestFinalizer.register(this, { signal, abort }) // from being prematurely garbage collected.
// See, https://github.com/nodejs/undici/issues/1926.
this[kAbortController] = ac
const acRef = new WeakRef(ac)
const abort = function () {
const ac = acRef.deref()
if (ac !== undefined) {
ac.abort(this.reason)
}
}
// Third-party AbortControllers may not work with these.
// See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619.
try {
// If the max amount of listeners is equal to the default, increase it
// This is only available in node >= v19.9.0
if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) {
setMaxListeners(100, signal)
} else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
setMaxListeners(100, signal)
}
} catch {}
util.addAbortListener(signal, abort)
requestFinalizer.register(ac, { signal, abort })
} }
} }
// 30. Set thiss headers to a new Headers object with thiss relevant // 30. Set thiss headers to a new Headers object with thiss relevant
// Realm, whose header list is requests header list and guard is // Realm, whose header list is requests header list and guard is
// "request". // "request".
this[kHeaders] = new Headers() this[kHeaders] = new Headers(kConstruct)
this[kHeaders][kHeadersList] = request.headersList this[kHeaders][kHeadersList] = request.headersList
this[kHeaders][kGuard] = 'request' this[kHeaders][kGuard] = 'request'
this[kHeaders][kRealm] = this[kRealm] this[kHeaders][kRealm] = this[kRealm]
@@ -393,7 +406,7 @@ class Request {
if (mode === 'no-cors') { if (mode === 'no-cors') {
// 1. If thiss requests method is not a CORS-safelisted method, // 1. If thiss requests method is not a CORS-safelisted method,
// then throw a TypeError. // then throw a TypeError.
if (!corsSafeListedMethods.includes(request.method)) { if (!corsSafeListedMethodsSet.has(request.method)) {
throw new TypeError( throw new TypeError(
`'${request.method} is unsupported in no-cors mode.` `'${request.method} is unsupported in no-cors mode.`
) )
@@ -404,25 +417,25 @@ class Request {
} }
// 32. If init is not empty, then: // 32. If init is not empty, then:
if (Object.keys(init).length !== 0) { if (initHasKey) {
/** @type {HeadersList} */
const headersList = this[kHeaders][kHeadersList]
// 1. Let headers be a copy of thiss headers and its associated header // 1. Let headers be a copy of thiss headers and its associated header
// list. // list.
let headers = new Headers(this[kHeaders])
// 2. If init["headers"] exists, then set headers to init["headers"]. // 2. If init["headers"] exists, then set headers to init["headers"].
if (init.headers !== undefined) { const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList)
headers = init.headers
}
// 3. Empty thiss headerss header list. // 3. Empty thiss headerss header list.
this[kHeaders][kHeadersList].clear() headersList.clear()
// 4. If headers is a Headers object, then for each header in its header // 4. If headers is a Headers object, then for each header in its header
// list, append headers name/headers value to thiss headers. // list, append headers name/headers value to thiss headers.
if (headers.constructor.name === 'Headers') { if (headers instanceof HeadersList) {
for (const [key, val] of headers) { for (const [key, val] of headers) {
this[kHeaders].append(key, val) headersList.append(key, val)
} }
// Note: Copy the `set-cookie` meta-data.
headersList.cookies = headers.cookies
} else { } else {
// 5. Otherwise, fill thiss headers with headers. // 5. Otherwise, fill thiss headers with headers.
fillHeaders(this[kHeaders], headers) fillHeaders(this[kHeaders], headers)
@@ -437,7 +450,7 @@ class Request {
// non-null, and requests method is `GET` or `HEAD`, then throw a // non-null, and requests method is `GET` or `HEAD`, then throw a
// TypeError. // TypeError.
if ( if (
((init.body !== undefined && init.body != null) || inputBody != null) && (init.body != null || inputBody != null) &&
(request.method === 'GET' || request.method === 'HEAD') (request.method === 'GET' || request.method === 'HEAD')
) { ) {
throw new TypeError('Request with GET/HEAD method cannot have body.') throw new TypeError('Request with GET/HEAD method cannot have body.')
@@ -447,7 +460,7 @@ class Request {
let initBody = null let initBody = null
// 36. If init["body"] exists and is non-null, then: // 36. If init["body"] exists and is non-null, then:
if (init.body !== undefined && init.body != null) { if (init.body != null) {
// 1. Let Content-Type be null. // 1. Let Content-Type be null.
// 2. Set initBody and Content-Type to the result of extracting // 2. Set initBody and Content-Type to the result of extracting
// init["body"], with keepalive set to requests keepalive. // init["body"], with keepalive set to requests keepalive.
@@ -460,7 +473,7 @@ class Request {
// 3, If Content-Type is non-null and thiss headerss header list does // 3, If Content-Type is non-null and thiss headerss header list does
// not contain `Content-Type`, then append `Content-Type`/Content-Type to // not contain `Content-Type`, then append `Content-Type`/Content-Type to
// thiss headers. // thiss headers.
if (contentType && !this[kHeaders].has('content-type')) { if (contentType && !this[kHeaders][kHeadersList].contains('content-type')) {
this[kHeaders].append('content-type', contentType) this[kHeaders].append('content-type', contentType)
} }
} }
@@ -472,7 +485,13 @@ class Request {
// 38. If inputOrInitBody is non-null and inputOrInitBodys source is // 38. If inputOrInitBody is non-null and inputOrInitBodys source is
// null, then: // null, then:
if (inputOrInitBody != null && inputOrInitBody.source == null) { if (inputOrInitBody != null && inputOrInitBody.source == null) {
// 1. If thiss requests mode is neither "same-origin" nor "cors", // 1. If initBody is non-null and init["duplex"] does not exist,
// then throw a TypeError.
if (initBody != null && init.duplex == null) {
throw new TypeError('RequestInit: duplex option is required when sending a body.')
}
// 2. If thiss requests mode is neither "same-origin" nor "cors",
// then throw a TypeError. // then throw a TypeError.
if (request.mode !== 'same-origin' && request.mode !== 'cors') { if (request.mode !== 'same-origin' && request.mode !== 'cors') {
throw new TypeError( throw new TypeError(
@@ -480,7 +499,7 @@ class Request {
) )
} }
// 2. Set thiss requests use-CORS-preflight flag. // 3. Set thiss requests use-CORS-preflight flag.
request.useCORSPreflightFlag = true request.useCORSPreflightFlag = true
} }
@@ -515,15 +534,9 @@ class Request {
this[kState].body = finalBody this[kState].body = finalBody
} }
get [Symbol.toStringTag] () {
return this.constructor.name
}
// Returns requests HTTP method, which is "GET" by default. // Returns requests HTTP method, which is "GET" by default.
get method () { get method () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The method getter steps are to return thiss requests method. // The method getter steps are to return thiss requests method.
return this[kState].method return this[kState].method
@@ -531,21 +544,17 @@ class Request {
// Returns the URL of request as a string. // Returns the URL of request as a string.
get url () { get url () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The url getter steps are to return thiss requests URL, serialized. // The url getter steps are to return thiss requests URL, serialized.
return this[kState].url.toString() return URLSerializer(this[kState].url)
} }
// Returns a Headers object consisting of the headers associated with request. // Returns a Headers object consisting of the headers associated with request.
// Note that headers added in the network layer by the user agent will not // Note that headers added in the network layer by the user agent will not
// be accounted for in this object, e.g., the "Host" header. // be accounted for in this object, e.g., the "Host" header.
get headers () { get headers () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The headers getter steps are to return thiss headers. // The headers getter steps are to return thiss headers.
return this[kHeaders] return this[kHeaders]
@@ -554,9 +563,7 @@ class Request {
// Returns the kind of resource requested by request, e.g., "document" // Returns the kind of resource requested by request, e.g., "document"
// or "script". // or "script".
get destination () { get destination () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The destination getter are to return thiss requests destination. // The destination getter are to return thiss requests destination.
return this[kState].destination return this[kState].destination
@@ -568,9 +575,7 @@ class Request {
// during fetching to determine the value of the `Referer` header of the // during fetching to determine the value of the `Referer` header of the
// request being made. // request being made.
get referrer () { get referrer () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// 1. If thiss requests referrer is "no-referrer", then return the // 1. If thiss requests referrer is "no-referrer", then return the
// empty string. // empty string.
@@ -592,9 +597,7 @@ class Request {
// This is used during fetching to compute the value of the requests // This is used during fetching to compute the value of the requests
// referrer. // referrer.
get referrerPolicy () { get referrerPolicy () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The referrerPolicy getter steps are to return thiss requests referrer policy. // The referrerPolicy getter steps are to return thiss requests referrer policy.
return this[kState].referrerPolicy return this[kState].referrerPolicy
@@ -604,9 +607,7 @@ class Request {
// whether the request will use CORS, or will be restricted to same-origin // whether the request will use CORS, or will be restricted to same-origin
// URLs. // URLs.
get mode () { get mode () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The mode getter steps are to return thiss requests mode. // The mode getter steps are to return thiss requests mode.
return this[kState].mode return this[kState].mode
@@ -624,9 +625,7 @@ class Request {
// which is a string indicating how the request will // which is a string indicating how the request will
// interact with the browsers cache when fetching. // interact with the browsers cache when fetching.
get cache () { get cache () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The cache getter steps are to return thiss requests cache mode. // The cache getter steps are to return thiss requests cache mode.
return this[kState].cache return this[kState].cache
@@ -637,9 +636,7 @@ class Request {
// request will be handled during fetching. A request // request will be handled during fetching. A request
// will follow redirects by default. // will follow redirects by default.
get redirect () { get redirect () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The redirect getter steps are to return thiss requests redirect mode. // The redirect getter steps are to return thiss requests redirect mode.
return this[kState].redirect return this[kState].redirect
@@ -649,9 +646,7 @@ class Request {
// cryptographic hash of the resource being fetched. Its value // cryptographic hash of the resource being fetched. Its value
// consists of multiple hashes separated by whitespace. [SRI] // consists of multiple hashes separated by whitespace. [SRI]
get integrity () { get integrity () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The integrity getter steps are to return thiss requests integrity // The integrity getter steps are to return thiss requests integrity
// metadata. // metadata.
@@ -661,9 +656,7 @@ class Request {
// Returns a boolean indicating whether or not request can outlive the // Returns a boolean indicating whether or not request can outlive the
// global in which it was created. // global in which it was created.
get keepalive () { get keepalive () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The keepalive getter steps are to return thiss requests keepalive. // The keepalive getter steps are to return thiss requests keepalive.
return this[kState].keepalive return this[kState].keepalive
@@ -672,9 +665,7 @@ class Request {
// Returns a boolean indicating whether or not request is for a reload // Returns a boolean indicating whether or not request is for a reload
// navigation. // navigation.
get isReloadNavigation () { get isReloadNavigation () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The isReloadNavigation getter steps are to return true if thiss // The isReloadNavigation getter steps are to return true if thiss
// requests reload-navigation flag is set; otherwise false. // requests reload-navigation flag is set; otherwise false.
@@ -684,9 +675,7 @@ class Request {
// Returns a boolean indicating whether or not request is for a history // Returns a boolean indicating whether or not request is for a history
// navigation (a.k.a. back-foward navigation). // navigation (a.k.a. back-foward navigation).
get isHistoryNavigation () { get isHistoryNavigation () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The isHistoryNavigation getter steps are to return true if thiss requests // The isHistoryNavigation getter steps are to return true if thiss requests
// history-navigation flag is set; otherwise false. // history-navigation flag is set; otherwise false.
@@ -697,19 +686,33 @@ class Request {
// object indicating whether or not request has been aborted, and its // object indicating whether or not request has been aborted, and its
// abort event handler. // abort event handler.
get signal () { get signal () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// The signal getter steps are to return thiss signal. // The signal getter steps are to return thiss signal.
return this[kSignal] return this[kSignal]
} }
get body () {
webidl.brandCheck(this, Request)
return this[kState].body ? this[kState].body.stream : null
}
get bodyUsed () {
webidl.brandCheck(this, Request)
return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
}
get duplex () {
webidl.brandCheck(this, Request)
return 'half'
}
// Returns a clone of request. // Returns a clone of request.
clone () { clone () {
if (!(this instanceof Request)) { webidl.brandCheck(this, Request)
throw new TypeError('Illegal invocation')
}
// 1. If this is unusable, then throw a TypeError. // 1. If this is unusable, then throw a TypeError.
if (this.bodyUsed || this.body?.locked) { if (this.bodyUsed || this.body?.locked) {
@@ -721,10 +724,10 @@ class Request {
// 3. Let clonedRequestObject be the result of creating a Request object, // 3. Let clonedRequestObject be the result of creating a Request object,
// given clonedRequest, thiss headerss guard, and thiss relevant Realm. // given clonedRequest, thiss headerss guard, and thiss relevant Realm.
const clonedRequestObject = new Request(kInit) const clonedRequestObject = new Request(kConstruct)
clonedRequestObject[kState] = clonedRequest clonedRequestObject[kState] = clonedRequest
clonedRequestObject[kRealm] = this[kRealm] clonedRequestObject[kRealm] = this[kRealm]
clonedRequestObject[kHeaders] = new Headers() clonedRequestObject[kHeaders] = new Headers(kConstruct)
clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList
clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard] clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard]
clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm] clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm]
@@ -734,12 +737,11 @@ class Request {
if (this.signal.aborted) { if (this.signal.aborted) {
ac.abort(this.signal.reason) ac.abort(this.signal.reason)
} else { } else {
this.signal.addEventListener( util.addAbortListener(
'abort', this.signal,
() => { () => {
ac.abort(this.signal.reason) ac.abort(this.signal.reason)
}, }
{ once: true }
) )
} }
clonedRequestObject[kSignal] = ac.signal clonedRequestObject[kSignal] = ac.signal
@@ -821,7 +823,25 @@ Object.defineProperties(Request.prototype, {
headers: kEnumerableProperty, headers: kEnumerableProperty,
redirect: kEnumerableProperty, redirect: kEnumerableProperty,
clone: kEnumerableProperty, clone: kEnumerableProperty,
signal: kEnumerableProperty signal: kEnumerableProperty,
duplex: kEnumerableProperty,
destination: kEnumerableProperty,
body: kEnumerableProperty,
bodyUsed: kEnumerableProperty,
isHistoryNavigation: kEnumerableProperty,
isReloadNavigation: kEnumerableProperty,
keepalive: kEnumerableProperty,
integrity: kEnumerableProperty,
cache: kEnumerableProperty,
credentials: kEnumerableProperty,
attribute: kEnumerableProperty,
referrerPolicy: kEnumerableProperty,
referrer: kEnumerableProperty,
mode: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Request',
configurable: true
}
}) })
webidl.converters.Request = webidl.interfaceConverter( webidl.converters.Request = webidl.interfaceConverter(
@@ -869,45 +889,31 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([
key: 'referrerPolicy', key: 'referrerPolicy',
converter: webidl.converters.DOMString, converter: webidl.converters.DOMString,
// https://w3c.github.io/webappsec-referrer-policy/#referrer-policy // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
allowedValues: [ allowedValues: referrerPolicy
'', 'no-referrer', 'no-referrer-when-downgrade',
'same-origin', 'origin', 'strict-origin',
'origin-when-cross-origin', 'strict-origin-when-cross-origin',
'unsafe-url'
]
}, },
{ {
key: 'mode', key: 'mode',
converter: webidl.converters.DOMString, converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#concept-request-mode // https://fetch.spec.whatwg.org/#concept-request-mode
allowedValues: [ allowedValues: requestMode
'same-origin', 'cors', 'no-cors', 'navigate', 'websocket'
]
}, },
{ {
key: 'credentials', key: 'credentials',
converter: webidl.converters.DOMString, converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestcredentials // https://fetch.spec.whatwg.org/#requestcredentials
allowedValues: [ allowedValues: requestCredentials
'omit', 'same-origin', 'include'
]
}, },
{ {
key: 'cache', key: 'cache',
converter: webidl.converters.DOMString, converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestcache // https://fetch.spec.whatwg.org/#requestcache
allowedValues: [ allowedValues: requestCache
'default', 'no-store', 'reload', 'no-cache', 'force-cache',
'only-if-cached'
]
}, },
{ {
key: 'redirect', key: 'redirect',
converter: webidl.converters.DOMString, converter: webidl.converters.DOMString,
// https://fetch.spec.whatwg.org/#requestredirect // https://fetch.spec.whatwg.org/#requestredirect
allowedValues: [ allowedValues: requestRedirect
'follow', 'error', 'manual'
]
}, },
{ {
key: 'integrity', key: 'integrity',
@@ -929,6 +935,11 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([
{ {
key: 'window', key: 'window',
converter: webidl.converters.any converter: webidl.converters.any
},
{
key: 'duplex',
converter: webidl.converters.DOMString,
allowedValues: requestDuplex
} }
]) ])

View File

@@ -5,16 +5,16 @@ const { extractBody, cloneBody, mixinBody } = require('./body')
const util = require('../core/util') const util = require('../core/util')
const { kEnumerableProperty } = util const { kEnumerableProperty } = util
const { const {
responseURL,
isValidReasonPhrase, isValidReasonPhrase,
isCancelled, isCancelled,
isAborted, isAborted,
isBlobLike, isBlobLike,
serializeJavascriptValueToJSONString, serializeJavascriptValueToJSONString,
isErrorLike isErrorLike,
isomorphicEncode
} = require('./util') } = require('./util')
const { const {
redirectStatus, redirectStatusSet,
nullBodyStatus, nullBodyStatus,
DOMException DOMException
} = require('./constants') } = require('./constants')
@@ -22,11 +22,13 @@ const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
const { webidl } = require('./webidl') const { webidl } = require('./webidl')
const { FormData } = require('./formdata') const { FormData } = require('./formdata')
const { getGlobalOrigin } = require('./global') const { getGlobalOrigin } = require('./global')
const { kHeadersList } = require('../core/symbols') const { URLSerializer } = require('./dataURL')
const { kHeadersList, kConstruct } = require('../core/symbols')
const assert = require('assert') const assert = require('assert')
const { types } = require('util') const { types } = require('util')
const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
const textEncoder = new TextEncoder('utf-8')
// https://fetch.spec.whatwg.org/#response-class // https://fetch.spec.whatwg.org/#response-class
class Response { class Response {
@@ -49,18 +51,14 @@ class Response {
// https://fetch.spec.whatwg.org/#dom-response-json // https://fetch.spec.whatwg.org/#dom-response-json
static json (data, init = {}) { static json (data, init = {}) {
if (arguments.length === 0) { webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' })
throw new TypeError(
'Failed to execute \'json\' on \'Response\': 1 argument required, but 0 present.'
)
}
if (init !== null) { if (init !== null) {
init = webidl.converters.ResponseInit(init) init = webidl.converters.ResponseInit(init)
} }
// 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
const bytes = new TextEncoder('utf-8').encode( const bytes = textEncoder.encode(
serializeJavascriptValueToJSONString(data) serializeJavascriptValueToJSONString(data)
) )
@@ -86,11 +84,7 @@ class Response {
static redirect (url, status = 302) { static redirect (url, status = 302) {
const relevantRealm = { settingsObject: {} } const relevantRealm = { settingsObject: {} }
if (arguments.length < 1) { webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' })
throw new TypeError(
`Failed to execute 'redirect' on 'Response': 1 argument required, but only ${arguments.length} present.`
)
}
url = webidl.converters.USVString(url) url = webidl.converters.USVString(url)
status = webidl.converters['unsigned short'](status) status = webidl.converters['unsigned short'](status)
@@ -109,8 +103,8 @@ class Response {
} }
// 3. If status is not a redirect status, then throw a RangeError. // 3. If status is not a redirect status, then throw a RangeError.
if (!redirectStatus.includes(status)) { if (!redirectStatusSet.has(status)) {
throw new RangeError('Invalid status code') throw new RangeError('Invalid status code ' + status)
} }
// 4. Let responseObject be the result of creating a Response object, // 4. Let responseObject be the result of creating a Response object,
@@ -124,8 +118,7 @@ class Response {
responseObject[kState].status = status responseObject[kState].status = status
// 6. Let value be parsedURL, serialized and isomorphic encoded. // 6. Let value be parsedURL, serialized and isomorphic encoded.
// TODO: isomorphic encoded? const value = isomorphicEncode(URLSerializer(parsedURL))
const value = parsedURL.toString()
// 7. Append `Location`/value to responseObjects responses header list. // 7. Append `Location`/value to responseObjects responses header list.
responseObject[kState].headersList.append('location', value) responseObject[kState].headersList.append('location', value)
@@ -151,7 +144,7 @@ class Response {
// 2. Set thiss headers to a new Headers object with thiss relevant // 2. Set thiss headers to a new Headers object with thiss relevant
// Realm, whose header list is thiss responses header list and guard // Realm, whose header list is thiss responses header list and guard
// is "response". // is "response".
this[kHeaders] = new Headers() this[kHeaders] = new Headers(kConstruct)
this[kHeaders][kGuard] = 'response' this[kHeaders][kGuard] = 'response'
this[kHeaders][kHeadersList] = this[kState].headersList this[kHeaders][kHeadersList] = this[kState].headersList
this[kHeaders][kRealm] = this[kRealm] this[kHeaders][kRealm] = this[kRealm]
@@ -169,15 +162,9 @@ class Response {
initializeResponse(this, init, bodyWithType) initializeResponse(this, init, bodyWithType)
} }
get [Symbol.toStringTag] () {
return this.constructor.name
}
// Returns responses type, e.g., "cors". // Returns responses type, e.g., "cors".
get type () { get type () {
if (!(this instanceof Response)) { webidl.brandCheck(this, Response)
throw new TypeError('Illegal invocation')
}
// The type getter steps are to return thiss responses type. // The type getter steps are to return thiss responses type.
return this[kState].type return this[kState].type
@@ -185,32 +172,25 @@ class Response {
// Returns responses URL, if it has one; otherwise the empty string. // Returns responses URL, if it has one; otherwise the empty string.
get url () { get url () {
if (!(this instanceof Response)) { webidl.brandCheck(this, Response)
throw new TypeError('Illegal invocation')
} const urlList = this[kState].urlList
// The url getter steps are to return the empty string if thiss // The url getter steps are to return the empty string if thiss
// responses URL is null; otherwise thiss responses URL, // responses URL is null; otherwise thiss responses URL,
// serialized with exclude fragment set to true. // serialized with exclude fragment set to true.
let url = responseURL(this[kState]) const url = urlList[urlList.length - 1] ?? null
if (url == null) { if (url === null) {
return '' return ''
} }
if (url.hash) { return URLSerializer(url, true)
url = new URL(url)
url.hash = ''
}
return url.toString()
} }
// Returns whether response was obtained through a redirect. // Returns whether response was obtained through a redirect.
get redirected () { get redirected () {
if (!(this instanceof Response)) { webidl.brandCheck(this, Response)
throw new TypeError('Illegal invocation')
}
// The redirected getter steps are to return true if thiss responses URL // The redirected getter steps are to return true if thiss responses URL
// list has more than one item; otherwise false. // list has more than one item; otherwise false.
@@ -219,9 +199,7 @@ class Response {
// Returns responses status. // Returns responses status.
get status () { get status () {
if (!(this instanceof Response)) { webidl.brandCheck(this, Response)
throw new TypeError('Illegal invocation')
}
// The status getter steps are to return thiss responses status. // The status getter steps are to return thiss responses status.
return this[kState].status return this[kState].status
@@ -229,9 +207,7 @@ class Response {
// Returns whether responses status is an ok status. // Returns whether responses status is an ok status.
get ok () { get ok () {
if (!(this instanceof Response)) { webidl.brandCheck(this, Response)
throw new TypeError('Illegal invocation')
}
// The ok getter steps are to return true if thiss responses status is an // The ok getter steps are to return true if thiss responses status is an
// ok status; otherwise false. // ok status; otherwise false.
@@ -240,9 +216,7 @@ class Response {
// Returns responses status message. // Returns responses status message.
get statusText () { get statusText () {
if (!(this instanceof Response)) { webidl.brandCheck(this, Response)
throw new TypeError('Illegal invocation')
}
// The statusText getter steps are to return thiss responses status // The statusText getter steps are to return thiss responses status
// message. // message.
@@ -251,23 +225,31 @@ class Response {
// Returns responses headers as Headers. // Returns responses headers as Headers.
get headers () { get headers () {
if (!(this instanceof Response)) { webidl.brandCheck(this, Response)
throw new TypeError('Illegal invocation')
}
// The headers getter steps are to return thiss headers. // The headers getter steps are to return thiss headers.
return this[kHeaders] return this[kHeaders]
} }
get body () {
webidl.brandCheck(this, Response)
return this[kState].body ? this[kState].body.stream : null
}
get bodyUsed () {
webidl.brandCheck(this, Response)
return !!this[kState].body && util.isDisturbed(this[kState].body.stream)
}
// Returns a clone of response. // Returns a clone of response.
clone () { clone () {
if (!(this instanceof Response)) { webidl.brandCheck(this, Response)
throw new TypeError('Illegal invocation')
}
// 1. If this is unusable, then throw a TypeError. // 1. If this is unusable, then throw a TypeError.
if (this.bodyUsed || (this.body && this.body.locked)) { if (this.bodyUsed || (this.body && this.body.locked)) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Response.clone', header: 'Response.clone',
message: 'Body has already been consumed.' message: 'Body has already been consumed.'
}) })
@@ -299,7 +281,19 @@ Object.defineProperties(Response.prototype, {
redirected: kEnumerableProperty, redirected: kEnumerableProperty,
statusText: kEnumerableProperty, statusText: kEnumerableProperty,
headers: kEnumerableProperty, headers: kEnumerableProperty,
clone: kEnumerableProperty clone: kEnumerableProperty,
body: kEnumerableProperty,
bodyUsed: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'Response',
configurable: true
}
})
Object.defineProperties(Response, {
json: kEnumerableProperty,
redirect: kEnumerableProperty,
error: kEnumerableProperty
}) })
// https://fetch.spec.whatwg.org/#concept-response-clone // https://fetch.spec.whatwg.org/#concept-response-clone
@@ -355,9 +349,7 @@ function makeNetworkError (reason) {
status: 0, status: 0,
error: isError error: isError
? reason ? reason
: new Error(reason ? String(reason) : reason, { : new Error(reason ? String(reason) : reason),
cause: isError ? reason : undefined
}),
aborted: reason && reason.name === 'AbortError' aborted: reason && reason.name === 'AbortError'
}) })
} }
@@ -435,15 +427,15 @@ function filterResponse (response, type) {
} }
// https://fetch.spec.whatwg.org/#appropriate-network-error // https://fetch.spec.whatwg.org/#appropriate-network-error
function makeAppropriateNetworkError (fetchParams) { function makeAppropriateNetworkError (fetchParams, err = null) {
// 1. Assert: fetchParams is canceled. // 1. Assert: fetchParams is canceled.
assert(isCancelled(fetchParams)) assert(isCancelled(fetchParams))
// 2. Return an aborted network error if fetchParams is aborted; // 2. Return an aborted network error if fetchParams is aborted;
// otherwise return a network error. // otherwise return a network error.
return isAborted(fetchParams) return isAborted(fetchParams)
? makeNetworkError(new DOMException('The operation was aborted.', 'AbortError')) ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
: makeNetworkError(fetchParams.controller.terminated.reason) : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
} }
// https://whatpr.org/fetch/1392.html#initialize-a-response // https://whatpr.org/fetch/1392.html#initialize-a-response
@@ -476,16 +468,16 @@ function initializeResponse (response, init, body) {
// 5. If init["headers"] exists, then fill responses headers with init["headers"]. // 5. If init["headers"] exists, then fill responses headers with init["headers"].
if ('headers' in init && init.headers != null) { if ('headers' in init && init.headers != null) {
fill(response[kState].headersList, init.headers) fill(response[kHeaders], init.headers)
} }
// 6. If body was given, then: // 6. If body was given, then:
if (body) { if (body) {
// 1. If response's status is a null body status, then throw a TypeError. // 1. If response's status is a null body status, then throw a TypeError.
if (nullBodyStatus.includes(response.status)) { if (nullBodyStatus.includes(response.status)) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Response constructor', header: 'Response constructor',
message: 'Invalid response status code.' message: 'Invalid response status code ' + response.status
}) })
} }
@@ -494,7 +486,7 @@ function initializeResponse (response, init, body) {
// 3. If body's type is non-null and response's header list does not contain // 3. If body's type is non-null and response's header list does not contain
// `Content-Type`, then append (`Content-Type`, body's type) to response's header list. // `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
if (body.type != null && !response[kState].headersList.has('Content-Type')) { if (body.type != null && !response[kState].headersList.contains('Content-Type')) {
response[kState].headersList.append('content-type', body.type) response[kState].headersList.append('content-type', body.type)
} }
} }
@@ -522,11 +514,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V) {
return webidl.converters.Blob(V, { strict: false }) return webidl.converters.Blob(V, { strict: false })
} }
if ( if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) {
types.isAnyArrayBuffer(V) ||
types.isTypedArray(V) ||
types.isDataView(V)
) {
return webidl.converters.BufferSource(V) return webidl.converters.BufferSource(V)
} }
@@ -578,5 +566,6 @@ module.exports = {
makeResponse, makeResponse,
makeAppropriateNetworkError, makeAppropriateNetworkError,
filterResponse, filterResponse,
Response Response,
cloneResponse
} }

View File

@@ -1,31 +1,26 @@
'use strict' 'use strict'
const { redirectStatus } = require('./constants') const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants')
const { getGlobalOrigin } = require('./global')
const { performance } = require('perf_hooks') const { performance } = require('perf_hooks')
const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util') const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
const assert = require('assert') const assert = require('assert')
const { isUint8Array } = require('util/types') const { isUint8Array } = require('util/types')
let supportedHashes = []
// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
/** @type {import('crypto')|undefined} */ /** @type {import('crypto')|undefined} */
let crypto let crypto
try { try {
crypto = require('crypto') crypto = require('crypto')
const possibleRelevantHashes = ['sha256', 'sha384', 'sha512']
supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash))
/* c8 ignore next 3 */
} catch { } catch {
} }
// https://fetch.spec.whatwg.org/#block-bad-port
const badPorts = [
'1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
'87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
'139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
'540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
'2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697',
'10080'
]
function responseURL (response) { function responseURL (response) {
// https://fetch.spec.whatwg.org/#responses // https://fetch.spec.whatwg.org/#responses
// A response has an associated URL. It is a pointer to the last URL // A response has an associated URL. It is a pointer to the last URL
@@ -38,7 +33,7 @@ function responseURL (response) {
// https://fetch.spec.whatwg.org/#concept-response-location-url // https://fetch.spec.whatwg.org/#concept-response-location-url
function responseLocationURL (response, requestFragment) { function responseLocationURL (response, requestFragment) {
// 1. If responses status is not a redirect status, then return null. // 1. If responses status is not a redirect status, then return null.
if (!redirectStatus.includes(response.status)) { if (!redirectStatusSet.has(response.status)) {
return null return null
} }
@@ -46,9 +41,11 @@ function responseLocationURL (response, requestFragment) {
// `Location` and responses header list. // `Location` and responses header list.
let location = response.headersList.get('location') let location = response.headersList.get('location')
// 3. If location is a value, then set location to the result of parsing // 3. If location is a header value, then set location to the result of
// location with responses URL. // parsing location with responses URL.
location = location ? new URL(location, responseURL(response)) : null if (location !== null && isValidHeaderValue(location)) {
location = new URL(location, responseURL(response))
}
// 4. If location is a URL whose fragment is null, then set locations // 4. If location is a URL whose fragment is null, then set locations
// fragment to requestFragment. // fragment to requestFragment.
@@ -71,7 +68,7 @@ function requestBadPort (request) {
// 2. If urls scheme is an HTTP(S) scheme and urls port is a bad port, // 2. If urls scheme is an HTTP(S) scheme and urls port is a bad port,
// then return blocked. // then return blocked.
if (/^https?:/.test(url.protocol) && badPorts.includes(url.port)) { if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
return 'blocked' return 'blocked'
} }
@@ -110,59 +107,58 @@ function isValidReasonPhrase (statusText) {
return true return true
} }
function isTokenChar (c) { /**
return !( * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
c >= 0x7f || * @param {number} c
c <= 0x20 || */
c === '(' || function isTokenCharCode (c) {
c === ')' || switch (c) {
c === '<' || case 0x22:
c === '>' || case 0x28:
c === '@' || case 0x29:
c === ',' || case 0x2c:
c === ';' || case 0x2f:
c === ':' || case 0x3a:
c === '\\' || case 0x3b:
c === '"' || case 0x3c:
c === '/' || case 0x3d:
c === '[' || case 0x3e:
c === ']' || case 0x3f:
c === '?' || case 0x40:
c === '=' || case 0x5b:
c === '{' || case 0x5c:
c === '}' case 0x5d:
) case 0x7b:
case 0x7d:
// DQUOTE and "(),/:;<=>?@[\]{}"
return false
default:
// VCHAR %x21-7E
return c >= 0x21 && c <= 0x7e
}
} }
// See RFC 7230, Section 3.2.6. /**
// https://github.com/chromium/chromium/blob/d7da0240cae77824d1eda25745c4022757499131/third_party/blink/renderer/platform/network/http_parsers.cc#L321 * @param {string} characters
*/
function isValidHTTPToken (characters) { function isValidHTTPToken (characters) {
if (!characters || typeof characters !== 'string') { if (characters.length === 0) {
return false return false
} }
for (let i = 0; i < characters.length; ++i) { for (let i = 0; i < characters.length; ++i) {
const c = characters.charCodeAt(i) if (!isTokenCharCode(characters.charCodeAt(i))) {
if (c > 0x7f || !isTokenChar(c)) {
return false return false
} }
} }
return true return true
} }
// https://fetch.spec.whatwg.org/#header-name /**
// https://github.com/chromium/chromium/blob/b3d37e6f94f87d59e44662d6078f6a12de845d17/net/http/http_util.cc#L342 * @see https://fetch.spec.whatwg.org/#header-name
* @param {string} potentialValue
*/
function isValidHeaderName (potentialValue) { function isValidHeaderName (potentialValue) {
if (potentialValue.length === 0) { return isValidHTTPToken(potentialValue)
return false
}
for (const char of potentialValue) {
if (!isValidHTTPToken(char)) {
return false
}
}
return true
} }
/** /**
@@ -200,8 +196,31 @@ function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
// 1. Let policy be the result of executing § 8.1 Parse a referrer policy // 1. Let policy be the result of executing § 8.1 Parse a referrer policy
// from a Referrer-Policy header on actualResponse. // from a Referrer-Policy header on actualResponse.
// TODO: https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header
const policy = '' // 8.1 Parse a referrer policy from a Referrer-Policy header
// 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and responses header list.
const { headersList } = actualResponse
// 2. Let policy be the empty string.
// 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
// 4. Return policy.
const policyHeader = (headersList.get('referrer-policy') ?? '').split(',')
// Note: As the referrer-policy can contain multiple policies
// separated by comma, we need to loop through all of them
// and pick the first valid one.
// Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy
let policy = ''
if (policyHeader.length > 0) {
// The right-most policy takes precedence.
// The left-most policy is the fallback.
for (let i = policyHeader.length; i !== 0; i--) {
const token = policyHeader[i - 1].trim()
if (referrerPolicyTokens.has(token)) {
policy = token
break
}
}
}
// 2. If policy is not the empty string, then set requests referrer policy to policy. // 2. If policy is not the empty string, then set requests referrer policy to policy.
if (policy !== '') { if (policy !== '') {
@@ -260,7 +279,7 @@ function appendRequestOriginHeader (request) {
// 2. If requests response tainting is "cors" or requests mode is "websocket", then append (`Origin`, serializedOrigin) to requests header list. // 2. If requests response tainting is "cors" or requests mode is "websocket", then append (`Origin`, serializedOrigin) to requests header list.
if (request.responseTainting === 'cors' || request.mode === 'websocket') { if (request.responseTainting === 'cors' || request.mode === 'websocket') {
if (serializedOrigin) { if (serializedOrigin) {
request.headersList.append('Origin', serializedOrigin) request.headersList.append('origin', serializedOrigin)
} }
// 3. Otherwise, if requests method is neither `GET` nor `HEAD`, then: // 3. Otherwise, if requests method is neither `GET` nor `HEAD`, then:
@@ -275,7 +294,7 @@ function appendRequestOriginHeader (request) {
case 'strict-origin': case 'strict-origin':
case 'strict-origin-when-cross-origin': case 'strict-origin-when-cross-origin':
// If requests origin is a tuple origin, its scheme is "https", and requests current URLs scheme is not "https", then set serializedOrigin to `null`. // If requests origin is a tuple origin, its scheme is "https", and requests current URLs scheme is not "https", then set serializedOrigin to `null`.
if (/^https:/.test(request.origin) && !/^https:/.test(requestCurrentURL(request))) { if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
serializedOrigin = null serializedOrigin = null
} }
break break
@@ -291,7 +310,7 @@ function appendRequestOriginHeader (request) {
if (serializedOrigin) { if (serializedOrigin) {
// 2. Append (`Origin`, serializedOrigin) to requests header list. // 2. Append (`Origin`, serializedOrigin) to requests header list.
request.headersList.append('Origin', serializedOrigin) request.headersList.append('origin', serializedOrigin)
} }
} }
} }
@@ -320,14 +339,17 @@ function createOpaqueTimingInfo (timingInfo) {
// https://html.spec.whatwg.org/multipage/origin.html#policy-container // https://html.spec.whatwg.org/multipage/origin.html#policy-container
function makePolicyContainer () { function makePolicyContainer () {
// TODO // Note: the fetch spec doesn't make use of embedder policy or CSP list
return {} return {
referrerPolicy: 'strict-origin-when-cross-origin'
}
} }
// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container // https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
function clonePolicyContainer () { function clonePolicyContainer (policyContainer) {
// TODO return {
return {} referrerPolicy: policyContainer.referrerPolicy
}
} }
// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
@@ -335,104 +357,76 @@ function determineRequestsReferrer (request) {
// 1. Let policy be request's referrer policy. // 1. Let policy be request's referrer policy.
const policy = request.referrerPolicy const policy = request.referrerPolicy
// Return no-referrer when empty or policy says so // Note: policy cannot (shouldn't) be null or an empty string.
if (policy == null || policy === '' || policy === 'no-referrer') { assert(policy)
return 'no-referrer'
} // 2. Let environment be requests client.
// 2. Let environment be the request client
const environment = request.client
let referrerSource = null let referrerSource = null
/** // 3. Switch on requests referrer:
* 3, Switch on requests referrer:
"client"
If environments global object is a Window object, then
Let document be the associated Document of environments global object.
If documents origin is an opaque origin, return no referrer.
While document is an iframe srcdoc document,
let document be documents browsing contexts browsing context containers node document.
Let referrerSource be documents URL.
Otherwise, let referrerSource be environments creation URL.
a URL
Let referrerSource be requests referrer.
*/
if (request.referrer === 'client') { if (request.referrer === 'client') {
// Not defined in Node but part of the spec // Note: node isn't a browser and doesn't implement document/iframes,
if (request.client?.globalObject?.constructor?.name === 'Window' ) { // eslint-disable-line // so we bypass this step and replace it with our own.
const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin
// If documents origin is an opaque origin, return no referrer. const globalOrigin = getGlobalOrigin()
if (origin == null || origin === 'null') return 'no-referrer'
// Let referrerSource be documents URL. if (!globalOrigin || globalOrigin.origin === 'null') {
referrerSource = new URL(environment.globalObject.location.href) return 'no-referrer'
} else {
// 3(a)(II) If environment's global object is not Window,
// Let referrerSource be environments creationURL
if (environment?.globalObject?.location == null) {
return 'no-referrer'
}
referrerSource = new URL(environment.globalObject.location.href)
} }
// note: we need to clone it as it's mutated
referrerSource = new URL(globalOrigin)
} else if (request.referrer instanceof URL) { } else if (request.referrer instanceof URL) {
// 3(b) If requests's referrer is a URL instance, then make // Let referrerSource be requests referrer.
// referrerSource be requests's referrer.
referrerSource = request.referrer referrerSource = request.referrer
} else {
// If referrerSource neither client nor instance of URL
// then return "no-referrer".
return 'no-referrer'
} }
const urlProtocol = referrerSource.protocol // 4. Let requests referrerURL be the result of stripping referrerSource for
// use as a referrer.
let referrerURL = stripURLForReferrer(referrerSource)
// If url's scheme is a local scheme (i.e. one of "about", "data", "javascript", "file") // 5. Let referrerOrigin be the result of stripping referrerSource for use as
// then return "no-referrer". // a referrer, with the origin-only flag set to true.
if ( const referrerOrigin = stripURLForReferrer(referrerSource, true)
urlProtocol === 'about:' || urlProtocol === 'data:' ||
urlProtocol === 'blob:' // 6. If the result of serializing referrerURL is a string whose length is
) { // greater than 4096, set referrerURL to referrerOrigin.
return 'no-referrer' if (referrerURL.toString().length > 4096) {
referrerURL = referrerOrigin
} }
let temp const areSameOrigin = sameOrigin(request, referrerURL)
let referrerOrigin const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) &&
// 4. Let requests's referrerURL be the result of stripping referrer
// source for use as referrer (using util function, without origin only)
const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096
// 5. Let referrerOrigin be the result of stripping referrer
// source for use as referrer (using util function, with originOnly true)
? (referrerOrigin = stripURLForReferrer(referrerSource, true))
// 6. If result of seralizing referrerUrl is a string whose length is greater than
// 4096, then set referrerURL to referrerOrigin
: temp
const areSameOrigin = sameOrigin(request, referrerUrl)
const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) &&
!isURLPotentiallyTrustworthy(request.url) !isURLPotentiallyTrustworthy(request.url)
// NOTE: How to treat step 7?
// 8. Execute the switch statements corresponding to the value of policy: // 8. Execute the switch statements corresponding to the value of policy:
switch (policy) { switch (policy) {
case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true)
case 'unsafe-url': return referrerUrl case 'unsafe-url': return referrerURL
case 'same-origin': case 'same-origin':
return areSameOrigin ? referrerOrigin : 'no-referrer' return areSameOrigin ? referrerOrigin : 'no-referrer'
case 'origin-when-cross-origin': case 'origin-when-cross-origin':
return areSameOrigin ? referrerUrl : referrerOrigin return areSameOrigin ? referrerURL : referrerOrigin
case 'strict-origin-when-cross-origin': case 'strict-origin-when-cross-origin': {
/** const currentURL = requestCurrentURL(request)
* 1. If the origin of referrerURL and the origin of requests current URL are the same,
* then return referrerURL. // 1. If the origin of referrerURL and the origin of requests current
* 2. If referrerURL is a potentially trustworthy URL and requests current URL is not a // URL are the same, then return referrerURL.
* potentially trustworthy URL, then return no referrer. if (sameOrigin(referrerURL, currentURL)) {
* 3. Return referrerOrigin return referrerURL
*/ }
if (areSameOrigin) return referrerOrigin
// else return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin // 2. If referrerURL is a potentially trustworthy URL and requests
// current URL is not a potentially trustworthy URL, then return no
// referrer.
if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
return 'no-referrer'
}
// 3. Return referrerOrigin.
return referrerOrigin
}
case 'strict-origin': // eslint-disable-line case 'strict-origin': // eslint-disable-line
/** /**
* 1. If referrerURL is a potentially trustworthy URL and * 1. If referrerURL is a potentially trustworthy URL and
@@ -451,15 +445,42 @@ function determineRequestsReferrer (request) {
default: // eslint-disable-line default: // eslint-disable-line
return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
} }
}
function stripURLForReferrer (url, originOnly = false) { /**
const urlObject = new URL(url.href) * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
urlObject.username = '' * @param {URL} url
urlObject.password = '' * @param {boolean|undefined} originOnly
urlObject.hash = '' */
function stripURLForReferrer (url, originOnly) {
// 1. Assert: url is a URL.
assert(url instanceof URL)
return originOnly ? urlObject.origin : urlObject.href // 2. If urls scheme is a local scheme, then return no referrer.
if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') {
return 'no-referrer'
} }
// 3. Set urls username to the empty string.
url.username = ''
// 4. Set urls password to the empty string.
url.password = ''
// 5. Set urls fragment to null.
url.hash = ''
// 6. If the origin-only flag is true, then:
if (originOnly) {
// 1. Set urls path to « the empty string ».
url.pathname = ''
// 2. Set urls query to null.
url.search = ''
}
// 7. Return url.
return url
} }
function isURLPotentiallyTrustworthy (url) { function isURLPotentiallyTrustworthy (url) {
@@ -525,17 +546,20 @@ function bytesMatch (bytes, metadataList) {
return true return true
} }
// 3. If parsedMetadata is the empty set, return true. // 3. If response is not eligible for integrity validation, return false.
// TODO
// 4. If parsedMetadata is the empty set, return true.
if (parsedMetadata.length === 0) { if (parsedMetadata.length === 0) {
return true return true
} }
// 4. Let metadata be the result of getting the strongest // 5. Let metadata be the result of getting the strongest
// metadata from parsedMetadata. // metadata from parsedMetadata.
// Note: this will only work for SHA- algorithms and it's lazy *at best*. const strongest = getStrongestMetadata(parsedMetadata)
const metadata = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest)
// 5. For each item in metadata: // 6. For each item in metadata:
for (const item of metadata) { for (const item of metadata) {
// 1. Let algorithm be the alg component of item. // 1. Let algorithm be the alg component of item.
const algorithm = item.algo const algorithm = item.algo
@@ -543,26 +567,35 @@ function bytesMatch (bytes, metadataList) {
// 2. Let expectedValue be the val component of item. // 2. Let expectedValue be the val component of item.
const expectedValue = item.hash const expectedValue = item.hash
// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
// "be liberal with padding". This is annoying, and it's not even in the spec.
// 3. Let actualValue be the result of applying algorithm to bytes. // 3. Let actualValue be the result of applying algorithm to bytes.
// Note: "applying algorithm to bytes" converts the result to base64 let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
const actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
if (actualValue[actualValue.length - 1] === '=') {
if (actualValue[actualValue.length - 2] === '=') {
actualValue = actualValue.slice(0, -2)
} else {
actualValue = actualValue.slice(0, -1)
}
}
// 4. If actualValue is a case-sensitive match for expectedValue, // 4. If actualValue is a case-sensitive match for expectedValue,
// return true. // return true.
if (actualValue === expectedValue) { if (compareBase64Mixed(actualValue, expectedValue)) {
return true return true
} }
} }
// 6. Return false. // 7. Return false.
return false return false
} }
// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
// hash-algo is defined in Content Security Policy 2 Section 4.2 // https://www.w3.org/TR/CSP2/#source-list-syntax
// base64-value is similary defined there // https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
// VCHAR is defined https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 const parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i
const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={1,2}))( +[\x21-\x7e]?)?/i
/** /**
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
@@ -576,8 +609,6 @@ function parseMetadata (metadata) {
// 2. Let empty be equal to true. // 2. Let empty be equal to true.
let empty = true let empty = true
const supportedHashes = crypto.getHashes()
// 3. For each token returned by splitting metadata on spaces: // 3. For each token returned by splitting metadata on spaces:
for (const token of metadata.split(' ')) { for (const token of metadata.split(' ')) {
// 1. Set empty to false. // 1. Set empty to false.
@@ -587,7 +618,11 @@ function parseMetadata (metadata) {
const parsedToken = parseHashWithOptions.exec(token) const parsedToken = parseHashWithOptions.exec(token)
// 3. If token does not parse, continue to the next token. // 3. If token does not parse, continue to the next token.
if (parsedToken === null || parsedToken.groups === undefined) { if (
parsedToken === null ||
parsedToken.groups === undefined ||
parsedToken.groups.algo === undefined
) {
// Note: Chromium blocks the request at this point, but Firefox // Note: Chromium blocks the request at this point, but Firefox
// gives a warning that an invalid integrity was given. The // gives a warning that an invalid integrity was given. The
// correct behavior is to ignore these, and subsequently not // correct behavior is to ignore these, and subsequently not
@@ -596,11 +631,11 @@ function parseMetadata (metadata) {
} }
// 4. Let algorithm be the hash-algo component of token. // 4. Let algorithm be the hash-algo component of token.
const algorithm = parsedToken.groups.algo const algorithm = parsedToken.groups.algo.toLowerCase()
// 5. If algorithm is a hash function recognized by the user // 5. If algorithm is a hash function recognized by the user
// agent, add the parsed token to result. // agent, add the parsed token to result.
if (supportedHashes.includes(algorithm.toLowerCase())) { if (supportedHashes.includes(algorithm)) {
result.push(parsedToken.groups) result.push(parsedToken.groups)
} }
} }
@@ -613,6 +648,82 @@ function parseMetadata (metadata) {
return result return result
} }
/**
* @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList
*/
function getStrongestMetadata (metadataList) {
// Let algorithm be the algo component of the first item in metadataList.
// Can be sha256
let algorithm = metadataList[0].algo
// If the algorithm is sha512, then it is the strongest
// and we can return immediately
if (algorithm[3] === '5') {
return algorithm
}
for (let i = 1; i < metadataList.length; ++i) {
const metadata = metadataList[i]
// If the algorithm is sha512, then it is the strongest
// and we can break the loop immediately
if (metadata.algo[3] === '5') {
algorithm = 'sha512'
break
// If the algorithm is sha384, then a potential sha256 or sha384 is ignored
} else if (algorithm[3] === '3') {
continue
// algorithm is sha256, check if algorithm is sha384 and if so, set it as
// the strongest
} else if (metadata.algo[3] === '3') {
algorithm = 'sha384'
}
}
return algorithm
}
function filterMetadataListByAlgorithm (metadataList, algorithm) {
if (metadataList.length === 1) {
return metadataList
}
let pos = 0
for (let i = 0; i < metadataList.length; ++i) {
if (metadataList[i].algo === algorithm) {
metadataList[pos++] = metadataList[i]
}
}
metadataList.length = pos
return metadataList
}
/**
* Compares two base64 strings, allowing for base64url
* in the second string.
*
* @param {string} actualValue always base64
* @param {string} expectedValue base64 or base64url
* @returns {boolean}
*/
function compareBase64Mixed (actualValue, expectedValue) {
if (actualValue.length !== expectedValue.length) {
return false
}
for (let i = 0; i < actualValue.length; ++i) {
if (actualValue[i] !== expectedValue[i]) {
if (
(actualValue[i] === '+' && expectedValue[i] === '-') ||
(actualValue[i] === '/' && expectedValue[i] === '_')
) {
continue
}
return false
}
}
return true
}
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request // https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
// TODO // TODO
@@ -625,7 +736,9 @@ function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
*/ */
function sameOrigin (A, B) { function sameOrigin (A, B) {
// 1. If A and B are the same opaque origin, then return true. // 1. If A and B are the same opaque origin, then return true.
// "opaque origin" is an internal value we cannot access, ignore. if (A.origin === B.origin && A.origin === 'null') {
return true
}
// 2. If A and B are both tuple origins and their schemes, // 2. If A and B are both tuple origins and their schemes,
// hosts, and port are identical, then return true. // hosts, and port are identical, then return true.
@@ -657,11 +770,30 @@ function isCancelled (fetchParams) {
fetchParams.controller.state === 'terminated' fetchParams.controller.state === 'terminated'
} }
// https://fetch.spec.whatwg.org/#concept-method-normalize const normalizeMethodRecord = {
delete: 'DELETE',
DELETE: 'DELETE',
get: 'GET',
GET: 'GET',
head: 'HEAD',
HEAD: 'HEAD',
options: 'OPTIONS',
OPTIONS: 'OPTIONS',
post: 'POST',
POST: 'POST',
put: 'PUT',
PUT: 'PUT'
}
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
Object.setPrototypeOf(normalizeMethodRecord, null)
/**
* @see https://fetch.spec.whatwg.org/#concept-method-normalize
* @param {string} method
*/
function normalizeMethod (method) { function normalizeMethod (method) {
return /^(DELETE|GET|HEAD|OPTIONS|POST|PUT)$/i.test(method) return normalizeMethodRecord[method.toLowerCase()] ?? method
? method.toUpperCase()
: method
} }
// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
@@ -684,17 +816,61 @@ function serializeJavascriptValueToJSONString (value) {
// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object // https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
// https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object /**
function makeIterator (iterator, name) { * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
* @param {() => unknown[]} iterator
* @param {string} name name of the instance
* @param {'key'|'value'|'key+value'} kind
*/
function makeIterator (iterator, name, kind) {
const object = {
index: 0,
kind,
target: iterator
}
const i = { const i = {
next () { next () {
// 1. Let interface be the interface for which the iterator prototype object exists.
// 2. Let thisValue be the this value.
// 3. Let object be ? ToObject(thisValue).
// 4. If object is a platform object, then perform a security
// check, passing:
// 5. If object is not a default iterator object for interface,
// then throw a TypeError.
if (Object.getPrototypeOf(this) !== i) { if (Object.getPrototypeOf(this) !== i) {
throw new TypeError( throw new TypeError(
`'next' called on an object that does not implement interface ${name} Iterator.` `'next' called on an object that does not implement interface ${name} Iterator.`
) )
} }
return iterator.next() // 6. Let index be objects index.
// 7. Let kind be objects kind.
// 8. Let values be objects target's value pairs to iterate over.
const { index, kind, target } = object
const values = target()
// 9. Let len be the length of values.
const len = values.length
// 10. If index is greater than or equal to len, then return
// CreateIterResultObject(undefined, true).
if (index >= len) {
return { value: undefined, done: true }
}
// 11. Let pair be the entry in values at index index.
const pair = values[index]
// 12. Set objects index to index + 1.
object.index = index + 1
// 13. Return the iterator result for pair and kind.
return iteratorResult(pair, kind)
}, },
// The class string of an iterator prototype object for a given interface is the // The class string of an iterator prototype object for a given interface is the
// result of concatenating the identifier of the interface and the string " Iterator". // result of concatenating the identifier of the interface and the string " Iterator".
@@ -708,6 +884,48 @@ function makeIterator (iterator, name) {
return Object.setPrototypeOf({}, i) return Object.setPrototypeOf({}, i)
} }
// https://webidl.spec.whatwg.org/#iterator-result
function iteratorResult (pair, kind) {
let result
// 1. Let result be a value determined by the value of kind:
switch (kind) {
case 'key': {
// 1. Let idlKey be pairs key.
// 2. Let key be the result of converting idlKey to an
// ECMAScript value.
// 3. result is key.
result = pair[0]
break
}
case 'value': {
// 1. Let idlValue be pairs value.
// 2. Let value be the result of converting idlValue to
// an ECMAScript value.
// 3. result is value.
result = pair[1]
break
}
case 'key+value': {
// 1. Let idlKey be pairs key.
// 2. Let idlValue be pairs value.
// 3. Let key be the result of converting idlKey to an
// ECMAScript value.
// 4. Let value be the result of converting idlValue to
// an ECMAScript value.
// 5. Let array be ! ArrayCreate(2).
// 6. Call ! CreateDataProperty(array, "0", key).
// 7. Call ! CreateDataProperty(array, "1", value).
// 8. result is array.
result = pair
break
}
}
// 2. Return CreateIterResultObject(result, false).
return { value: result, done: false }
}
/** /**
* @see https://fetch.spec.whatwg.org/#body-fully-read * @see https://fetch.spec.whatwg.org/#body-fully-read
*/ */
@@ -715,44 +933,161 @@ async function fullyReadBody (body, processBody, processBodyError) {
// 1. If taskDestination is null, then set taskDestination to // 1. If taskDestination is null, then set taskDestination to
// the result of starting a new parallel queue. // the result of starting a new parallel queue.
// 2. Let promise be the result of fully reading body as promise // 2. Let successSteps given a byte sequence bytes be to queue a
// given body. // fetch task to run processBody given bytes, with taskDestination.
const successSteps = processBody
// 3. Let errorSteps be to queue a fetch task to run processBodyError,
// with taskDestination.
const errorSteps = processBodyError
// 4. Let reader be the result of getting a reader for bodys stream.
// If that threw an exception, then run errorSteps with that
// exception and return.
let reader
try { try {
/** @type {Uint8Array[]} */ reader = body.stream.getReader()
const chunks = [] } catch (e) {
let length = 0 errorSteps(e)
return
const reader = body.stream.getReader()
while (true) {
const { done, value } = await reader.read()
if (done === true) {
break
}
// read-loop chunk steps
assert(isUint8Array(value))
chunks.push(value)
length += value.byteLength
}
// 3. Let fulfilledSteps given a byte sequence bytes be to queue
// a fetch task to run processBody given bytes, with
// taskDestination.
const fulfilledSteps = (bytes) => queueMicrotask(() => {
processBody(bytes)
})
fulfilledSteps(Buffer.concat(chunks, length))
} catch (err) {
// 4. Let rejectedSteps be to queue a fetch task to run
// processBodyError, with taskDestination.
queueMicrotask(() => processBodyError(err))
} }
// 5. React to promise with fulfilledSteps and rejectedSteps. // 5. Read all bytes from reader, given successSteps and errorSteps.
try {
const result = await readAllBytes(reader)
successSteps(result)
} catch (e) {
errorSteps(e)
}
}
/** @type {ReadableStream} */
let ReadableStream = globalThis.ReadableStream
function isReadableStreamLike (stream) {
if (!ReadableStream) {
ReadableStream = require('stream/web').ReadableStream
}
return stream instanceof ReadableStream || (
stream[Symbol.toStringTag] === 'ReadableStream' &&
typeof stream.tee === 'function'
)
}
const MAXIMUM_ARGUMENT_LENGTH = 65535
/**
* @see https://infra.spec.whatwg.org/#isomorphic-decode
* @param {number[]|Uint8Array} input
*/
function isomorphicDecode (input) {
// 1. To isomorphic decode a byte sequence input, return a string whose code point
// length is equal to inputs length and whose code points have the same values
// as the values of inputs bytes, in the same order.
if (input.length < MAXIMUM_ARGUMENT_LENGTH) {
return String.fromCharCode(...input)
}
return input.reduce((previous, current) => previous + String.fromCharCode(current), '')
}
/**
* @param {ReadableStreamController<Uint8Array>} controller
*/
function readableStreamClose (controller) {
try {
controller.close()
} catch (err) {
// TODO: add comment explaining why this error occurs.
if (!err.message.includes('Controller is already closed')) {
throw err
}
}
}
/**
* @see https://infra.spec.whatwg.org/#isomorphic-encode
* @param {string} input
*/
function isomorphicEncode (input) {
// 1. Assert: input contains no code points greater than U+00FF.
for (let i = 0; i < input.length; i++) {
assert(input.charCodeAt(i) <= 0xFF)
}
// 2. Return a byte sequence whose length is equal to inputs code
// point length and whose bytes have the same values as the
// values of inputs code points, in the same order
return input
}
/**
* @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
* @see https://streams.spec.whatwg.org/#read-loop
* @param {ReadableStreamDefaultReader} reader
*/
async function readAllBytes (reader) {
const bytes = []
let byteLength = 0
while (true) {
const { done, value: chunk } = await reader.read()
if (done) {
// 1. Call successSteps with bytes.
return Buffer.concat(bytes, byteLength)
}
// 1. If chunk is not a Uint8Array object, call failureSteps
// with a TypeError and abort these steps.
if (!isUint8Array(chunk)) {
throw new TypeError('Received non-Uint8Array chunk')
}
// 2. Append the bytes represented by chunk to bytes.
bytes.push(chunk)
byteLength += chunk.length
// 3. Read-loop given reader, bytes, successSteps, and failureSteps.
}
}
/**
* @see https://fetch.spec.whatwg.org/#is-local
* @param {URL} url
*/
function urlIsLocal (url) {
assert('protocol' in url) // ensure it's a url object
const protocol = url.protocol
return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
}
/**
* @param {string|URL} url
*/
function urlHasHttpsScheme (url) {
if (typeof url === 'string') {
return url.startsWith('https:')
}
return url.protocol === 'https:'
}
/**
* @see https://fetch.spec.whatwg.org/#http-scheme
* @param {URL} url
*/
function urlIsHttpHttpsScheme (url) {
assert('protocol' in url) // ensure it's a url object
const protocol = url.protocol
return protocol === 'http:' || protocol === 'https:'
} }
/** /**
@@ -795,5 +1130,15 @@ module.exports = {
hasOwn, hasOwn,
isErrorLike, isErrorLike,
fullyReadBody, fullyReadBody,
bytesMatch bytesMatch,
isReadableStreamLike,
readableStreamClose,
isomorphicEncode,
isomorphicDecode,
urlIsLocal,
urlHasHttpsScheme,
urlIsHttpHttpsScheme,
readAllBytes,
normalizeMethodRecord,
parseMetadata
} }

View File

@@ -3,30 +3,16 @@
const { types } = require('util') const { types } = require('util')
const { hasOwn, toUSVString } = require('./util') const { hasOwn, toUSVString } = require('./util')
/** @type {import('../../types/webidl').Webidl} */
const webidl = {} const webidl = {}
webidl.converters = {} webidl.converters = {}
webidl.util = {} webidl.util = {}
webidl.errors = {} webidl.errors = {}
/**
*
* @param {{
* header: string
* message: string
* }} message
*/
webidl.errors.exception = function (message) { webidl.errors.exception = function (message) {
throw new TypeError(`${message.header}: ${message.message}`) return new TypeError(`${message.header}: ${message.message}`)
} }
/**
* Throw an error when conversion from one type to another has failed
* @param {{
* prefix: string
* argument: string
* types: string[]
* }} context
*/
webidl.errors.conversionFailed = function (context) { webidl.errors.conversionFailed = function (context) {
const plural = context.types.length === 1 ? '' : ' one of' const plural = context.types.length === 1 ? '' : ' one of'
const message = const message =
@@ -39,14 +25,6 @@ webidl.errors.conversionFailed = function (context) {
}) })
} }
/**
* Throw an error when an invalid argument is provided
* @param {{
* prefix: string
* value: string
* type: string
* }} context
*/
webidl.errors.invalidArgument = function (context) { webidl.errors.invalidArgument = function (context) {
return webidl.errors.exception({ return webidl.errors.exception({
header: context.prefix, header: context.prefix,
@@ -54,6 +32,32 @@ webidl.errors.invalidArgument = function (context) {
}) })
} }
// https://webidl.spec.whatwg.org/#implements
webidl.brandCheck = function (V, I, opts = undefined) {
if (opts?.strict !== false && !(V instanceof I)) {
throw new TypeError('Illegal invocation')
} else {
return V?.[Symbol.toStringTag] === I.prototype[Symbol.toStringTag]
}
}
webidl.argumentLengthCheck = function ({ length }, min, ctx) {
if (length < min) {
throw webidl.errors.exception({
message: `${min} argument${min !== 1 ? 's' : ''} required, ` +
`but${length ? ' only' : ''} ${length} found.`,
...ctx
})
}
}
webidl.illegalConstructor = function () {
throw webidl.errors.exception({
header: 'TypeError',
message: 'Illegal constructor'
})
}
// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values // https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
webidl.util.Type = function (V) { webidl.util.Type = function (V) {
switch (typeof V) { switch (typeof V) {
@@ -113,7 +117,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) {
let x = Number(V) let x = Number(V)
// 5. If x is 0, then set x to +0. // 5. If x is 0, then set x to +0.
if (Object.is(-0, x)) { if (x === 0) {
x = 0 x = 0
} }
@@ -126,7 +130,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) {
x === Number.POSITIVE_INFINITY || x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY x === Number.NEGATIVE_INFINITY
) { ) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Integer conversion', header: 'Integer conversion',
message: `Could not convert ${V} to an integer.` message: `Could not convert ${V} to an integer.`
}) })
@@ -138,7 +142,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) {
// 3. If x < lowerBound or x > upperBound, then // 3. If x < lowerBound or x > upperBound, then
// throw a TypeError. // throw a TypeError.
if (x < lowerBound || x > upperBound) { if (x < lowerBound || x > upperBound) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Integer conversion', header: 'Integer conversion',
message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.`
}) })
@@ -171,7 +175,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) {
// 8. If x is NaN, +0, +∞, or −∞, then return +0. // 8. If x is NaN, +0, +∞, or −∞, then return +0.
if ( if (
Number.isNaN(x) || Number.isNaN(x) ||
Object.is(0, x) || (x === 0 && Object.is(0, x)) ||
x === Number.POSITIVE_INFINITY || x === Number.POSITIVE_INFINITY ||
x === Number.NEGATIVE_INFINITY x === Number.NEGATIVE_INFINITY
) { ) {
@@ -213,7 +217,7 @@ webidl.sequenceConverter = function (converter) {
return (V) => { return (V) => {
// 1. If Type(V) is not Object, throw a TypeError. // 1. If Type(V) is not Object, throw a TypeError.
if (webidl.util.Type(V) !== 'Object') { if (webidl.util.Type(V) !== 'Object') {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Sequence', header: 'Sequence',
message: `Value of type ${webidl.util.Type(V)} is not an Object.` message: `Value of type ${webidl.util.Type(V)} is not an Object.`
}) })
@@ -229,7 +233,7 @@ webidl.sequenceConverter = function (converter) {
method === undefined || method === undefined ||
typeof method.next !== 'function' typeof method.next !== 'function'
) { ) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Sequence', header: 'Sequence',
message: 'Object is not an iterator.' message: 'Object is not an iterator.'
}) })
@@ -250,37 +254,71 @@ webidl.sequenceConverter = function (converter) {
} }
} }
// https://webidl.spec.whatwg.org/#es-to-record
webidl.recordConverter = function (keyConverter, valueConverter) { webidl.recordConverter = function (keyConverter, valueConverter) {
return (V) => { return (O) => {
const record = {} // 1. If Type(O) is not Object, throw a TypeError.
const type = webidl.util.Type(V) if (webidl.util.Type(O) !== 'Object') {
throw webidl.errors.exception({
if (type === 'Undefined' || type === 'Null') {
return record
}
if (type !== 'Object') {
webidl.errors.exception({
header: 'Record', header: 'Record',
message: `Expected ${V} to be an Object type.` message: `Value of type ${webidl.util.Type(O)} is not an Object.`
}) })
} }
for (let [key, value] of Object.entries(V)) { // 2. Let result be a new empty instance of record<K, V>.
key = keyConverter(key) const result = {}
value = valueConverter(value)
record[key] = value if (!types.isProxy(O)) {
// Object.keys only returns enumerable properties
const keys = Object.keys(O)
for (const key of keys) {
// 1. Let typedKey be key converted to an IDL value of type K.
const typedKey = keyConverter(key)
// 2. Let value be ? Get(O, key).
// 3. Let typedValue be value converted to an IDL value of type V.
const typedValue = valueConverter(O[key])
// 4. Set result[typedKey] to typedValue.
result[typedKey] = typedValue
}
// 5. Return result.
return result
} }
return record // 3. Let keys be ? O.[[OwnPropertyKeys]]().
const keys = Reflect.ownKeys(O)
// 4. For each key of keys.
for (const key of keys) {
// 1. Let desc be ? O.[[GetOwnProperty]](key).
const desc = Reflect.getOwnPropertyDescriptor(O, key)
// 2. If desc is not undefined and desc.[[Enumerable]] is true:
if (desc?.enumerable) {
// 1. Let typedKey be key converted to an IDL value of type K.
const typedKey = keyConverter(key)
// 2. Let value be ? Get(O, key).
// 3. Let typedValue be value converted to an IDL value of type V.
const typedValue = valueConverter(O[key])
// 4. Set result[typedKey] to typedValue.
result[typedKey] = typedValue
}
}
// 5. Return result.
return result
} }
} }
webidl.interfaceConverter = function (i) { webidl.interfaceConverter = function (i) {
return (V, opts = {}) => { return (V, opts = {}) => {
if (opts.strict !== false && !(V instanceof i)) { if (opts.strict !== false && !(V instanceof i)) {
webidl.errors.exception({ throw webidl.errors.exception({
header: i.name, header: i.name,
message: `Expected ${V} to be an instance of ${i.name}.` message: `Expected ${V} to be an instance of ${i.name}.`
}) })
@@ -290,23 +328,15 @@ webidl.interfaceConverter = function (i) {
} }
} }
/**
* @param {{
* key: string,
* defaultValue?: any,
* required?: boolean,
* converter: (...args: unknown[]) => unknown,
* allowedValues?: any[]
* }[]} converters
* @returns
*/
webidl.dictionaryConverter = function (converters) { webidl.dictionaryConverter = function (converters) {
return (dictionary) => { return (dictionary) => {
const type = webidl.util.Type(dictionary) const type = webidl.util.Type(dictionary)
const dict = {} const dict = {}
if (type !== 'Null' && type !== 'Undefined' && type !== 'Object') { if (type === 'Null' || type === 'Undefined') {
webidl.errors.exception({ return dict
} else if (type !== 'Object') {
throw webidl.errors.exception({
header: 'Dictionary', header: 'Dictionary',
message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`
}) })
@@ -317,7 +347,7 @@ webidl.dictionaryConverter = function (converters) {
if (required === true) { if (required === true) {
if (!hasOwn(dictionary, key)) { if (!hasOwn(dictionary, key)) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Dictionary', header: 'Dictionary',
message: `Missing required key "${key}".` message: `Missing required key "${key}".`
}) })
@@ -343,7 +373,7 @@ webidl.dictionaryConverter = function (converters) {
options.allowedValues && options.allowedValues &&
!options.allowedValues.includes(value) !options.allowedValues.includes(value)
) { ) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'Dictionary', header: 'Dictionary',
message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.`
}) })
@@ -397,12 +427,10 @@ webidl.converters.ByteString = function (V) {
// 2. If the value of any element of x is greater than // 2. If the value of any element of x is greater than
// 255, then throw a TypeError. // 255, then throw a TypeError.
for (let index = 0; index < x.length; index++) { for (let index = 0; index < x.length; index++) {
const charCode = x.charCodeAt(index) if (x.charCodeAt(index) > 255) {
if (charCode > 255) {
throw new TypeError( throw new TypeError(
'Cannot convert argument to a ByteString because the character at' + 'Cannot convert argument to a ByteString because the character at ' +
`index ${index} has a value of ${charCode} which is greater than 255.` `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.`
) )
} }
} }
@@ -414,7 +442,6 @@ webidl.converters.ByteString = function (V) {
} }
// https://webidl.spec.whatwg.org/#es-USVString // https://webidl.spec.whatwg.org/#es-USVString
// TODO: ensure that util.toUSVString follows webidl spec
webidl.converters.USVString = toUSVString webidl.converters.USVString = toUSVString
// https://webidl.spec.whatwg.org/#es-boolean // https://webidl.spec.whatwg.org/#es-boolean
@@ -433,19 +460,39 @@ webidl.converters.any = function (V) {
} }
// https://webidl.spec.whatwg.org/#es-long-long // https://webidl.spec.whatwg.org/#es-long-long
webidl.converters['long long'] = function (V, opts) { webidl.converters['long long'] = function (V) {
// 1. Let x be ? ConvertToInt(V, 64, "signed"). // 1. Let x be ? ConvertToInt(V, 64, "signed").
const x = webidl.util.ConvertToInt(V, 64, 'signed', opts) const x = webidl.util.ConvertToInt(V, 64, 'signed')
// 2. Return the IDL long long value that represents // 2. Return the IDL long long value that represents
// the same numeric value as x. // the same numeric value as x.
return x return x
} }
// https://webidl.spec.whatwg.org/#es-unsigned-long-long
webidl.converters['unsigned long long'] = function (V) {
// 1. Let x be ? ConvertToInt(V, 64, "unsigned").
const x = webidl.util.ConvertToInt(V, 64, 'unsigned')
// 2. Return the IDL unsigned long long value that
// represents the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-long
webidl.converters['unsigned long'] = function (V) {
// 1. Let x be ? ConvertToInt(V, 32, "unsigned").
const x = webidl.util.ConvertToInt(V, 32, 'unsigned')
// 2. Return the IDL unsigned long value that
// represents the same numeric value as x.
return x
}
// https://webidl.spec.whatwg.org/#es-unsigned-short // https://webidl.spec.whatwg.org/#es-unsigned-short
webidl.converters['unsigned short'] = function (V) { webidl.converters['unsigned short'] = function (V, opts) {
// 1. Let x be ? ConvertToInt(V, 16, "unsigned"). // 1. Let x be ? ConvertToInt(V, 16, "unsigned").
const x = webidl.util.ConvertToInt(V, 16, 'unsigned') const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts)
// 2. Return the IDL unsigned short value that represents // 2. Return the IDL unsigned short value that represents
// the same numeric value as x. // the same numeric value as x.
@@ -463,7 +510,7 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) {
webidl.util.Type(V) !== 'Object' || webidl.util.Type(V) !== 'Object' ||
!types.isAnyArrayBuffer(V) !types.isAnyArrayBuffer(V)
) { ) {
webidl.errors.conversionFailed({ throw webidl.errors.conversionFailed({
prefix: `${V}`, prefix: `${V}`,
argument: `${V}`, argument: `${V}`,
types: ['ArrayBuffer'] types: ['ArrayBuffer']
@@ -475,7 +522,7 @@ webidl.converters.ArrayBuffer = function (V, opts = {}) {
// IsSharedArrayBuffer(V) is true, then throw a // IsSharedArrayBuffer(V) is true, then throw a
// TypeError. // TypeError.
if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { if (opts.allowShared === false && types.isSharedArrayBuffer(V)) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'ArrayBuffer', header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.' message: 'SharedArrayBuffer is not allowed.'
}) })
@@ -503,7 +550,7 @@ webidl.converters.TypedArray = function (V, T, opts = {}) {
!types.isTypedArray(V) || !types.isTypedArray(V) ||
V.constructor.name !== T.name V.constructor.name !== T.name
) { ) {
webidl.errors.conversionFailed({ throw webidl.errors.conversionFailed({
prefix: `${T.name}`, prefix: `${T.name}`,
argument: `${V}`, argument: `${V}`,
types: [T.name] types: [T.name]
@@ -515,7 +562,7 @@ webidl.converters.TypedArray = function (V, T, opts = {}) {
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is
// true, then throw a TypeError. // true, then throw a TypeError.
if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'ArrayBuffer', header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.' message: 'SharedArrayBuffer is not allowed.'
}) })
@@ -536,7 +583,7 @@ webidl.converters.DataView = function (V, opts = {}) {
// 1. If Type(V) is not Object, or V does not have a // 1. If Type(V) is not Object, or V does not have a
// [[DataView]] internal slot, then throw a TypeError. // [[DataView]] internal slot, then throw a TypeError.
if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'DataView', header: 'DataView',
message: 'Object is not a DataView.' message: 'Object is not a DataView.'
}) })
@@ -547,7 +594,7 @@ webidl.converters.DataView = function (V, opts = {}) {
// IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true,
// then throw a TypeError. // then throw a TypeError.
if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) {
webidl.errors.exception({ throw webidl.errors.exception({
header: 'ArrayBuffer', header: 'ArrayBuffer',
message: 'SharedArrayBuffer is not allowed.' message: 'SharedArrayBuffer is not allowed.'
}) })

View File

@@ -0,0 +1,290 @@
'use strict'
/**
* @see https://encoding.spec.whatwg.org/#concept-encoding-get
* @param {string|undefined} label
*/
function getEncoding (label) {
if (!label) {
return 'failure'
}
// 1. Remove any leading and trailing ASCII whitespace from label.
// 2. If label is an ASCII case-insensitive match for any of the
// labels listed in the table below, then return the
// corresponding encoding; otherwise return failure.
switch (label.trim().toLowerCase()) {
case 'unicode-1-1-utf-8':
case 'unicode11utf8':
case 'unicode20utf8':
case 'utf-8':
case 'utf8':
case 'x-unicode20utf8':
return 'UTF-8'
case '866':
case 'cp866':
case 'csibm866':
case 'ibm866':
return 'IBM866'
case 'csisolatin2':
case 'iso-8859-2':
case 'iso-ir-101':
case 'iso8859-2':
case 'iso88592':
case 'iso_8859-2':
case 'iso_8859-2:1987':
case 'l2':
case 'latin2':
return 'ISO-8859-2'
case 'csisolatin3':
case 'iso-8859-3':
case 'iso-ir-109':
case 'iso8859-3':
case 'iso88593':
case 'iso_8859-3':
case 'iso_8859-3:1988':
case 'l3':
case 'latin3':
return 'ISO-8859-3'
case 'csisolatin4':
case 'iso-8859-4':
case 'iso-ir-110':
case 'iso8859-4':
case 'iso88594':
case 'iso_8859-4':
case 'iso_8859-4:1988':
case 'l4':
case 'latin4':
return 'ISO-8859-4'
case 'csisolatincyrillic':
case 'cyrillic':
case 'iso-8859-5':
case 'iso-ir-144':
case 'iso8859-5':
case 'iso88595':
case 'iso_8859-5':
case 'iso_8859-5:1988':
return 'ISO-8859-5'
case 'arabic':
case 'asmo-708':
case 'csiso88596e':
case 'csiso88596i':
case 'csisolatinarabic':
case 'ecma-114':
case 'iso-8859-6':
case 'iso-8859-6-e':
case 'iso-8859-6-i':
case 'iso-ir-127':
case 'iso8859-6':
case 'iso88596':
case 'iso_8859-6':
case 'iso_8859-6:1987':
return 'ISO-8859-6'
case 'csisolatingreek':
case 'ecma-118':
case 'elot_928':
case 'greek':
case 'greek8':
case 'iso-8859-7':
case 'iso-ir-126':
case 'iso8859-7':
case 'iso88597':
case 'iso_8859-7':
case 'iso_8859-7:1987':
case 'sun_eu_greek':
return 'ISO-8859-7'
case 'csiso88598e':
case 'csisolatinhebrew':
case 'hebrew':
case 'iso-8859-8':
case 'iso-8859-8-e':
case 'iso-ir-138':
case 'iso8859-8':
case 'iso88598':
case 'iso_8859-8':
case 'iso_8859-8:1988':
case 'visual':
return 'ISO-8859-8'
case 'csiso88598i':
case 'iso-8859-8-i':
case 'logical':
return 'ISO-8859-8-I'
case 'csisolatin6':
case 'iso-8859-10':
case 'iso-ir-157':
case 'iso8859-10':
case 'iso885910':
case 'l6':
case 'latin6':
return 'ISO-8859-10'
case 'iso-8859-13':
case 'iso8859-13':
case 'iso885913':
return 'ISO-8859-13'
case 'iso-8859-14':
case 'iso8859-14':
case 'iso885914':
return 'ISO-8859-14'
case 'csisolatin9':
case 'iso-8859-15':
case 'iso8859-15':
case 'iso885915':
case 'iso_8859-15':
case 'l9':
return 'ISO-8859-15'
case 'iso-8859-16':
return 'ISO-8859-16'
case 'cskoi8r':
case 'koi':
case 'koi8':
case 'koi8-r':
case 'koi8_r':
return 'KOI8-R'
case 'koi8-ru':
case 'koi8-u':
return 'KOI8-U'
case 'csmacintosh':
case 'mac':
case 'macintosh':
case 'x-mac-roman':
return 'macintosh'
case 'iso-8859-11':
case 'iso8859-11':
case 'iso885911':
case 'tis-620':
case 'windows-874':
return 'windows-874'
case 'cp1250':
case 'windows-1250':
case 'x-cp1250':
return 'windows-1250'
case 'cp1251':
case 'windows-1251':
case 'x-cp1251':
return 'windows-1251'
case 'ansi_x3.4-1968':
case 'ascii':
case 'cp1252':
case 'cp819':
case 'csisolatin1':
case 'ibm819':
case 'iso-8859-1':
case 'iso-ir-100':
case 'iso8859-1':
case 'iso88591':
case 'iso_8859-1':
case 'iso_8859-1:1987':
case 'l1':
case 'latin1':
case 'us-ascii':
case 'windows-1252':
case 'x-cp1252':
return 'windows-1252'
case 'cp1253':
case 'windows-1253':
case 'x-cp1253':
return 'windows-1253'
case 'cp1254':
case 'csisolatin5':
case 'iso-8859-9':
case 'iso-ir-148':
case 'iso8859-9':
case 'iso88599':
case 'iso_8859-9':
case 'iso_8859-9:1989':
case 'l5':
case 'latin5':
case 'windows-1254':
case 'x-cp1254':
return 'windows-1254'
case 'cp1255':
case 'windows-1255':
case 'x-cp1255':
return 'windows-1255'
case 'cp1256':
case 'windows-1256':
case 'x-cp1256':
return 'windows-1256'
case 'cp1257':
case 'windows-1257':
case 'x-cp1257':
return 'windows-1257'
case 'cp1258':
case 'windows-1258':
case 'x-cp1258':
return 'windows-1258'
case 'x-mac-cyrillic':
case 'x-mac-ukrainian':
return 'x-mac-cyrillic'
case 'chinese':
case 'csgb2312':
case 'csiso58gb231280':
case 'gb2312':
case 'gb_2312':
case 'gb_2312-80':
case 'gbk':
case 'iso-ir-58':
case 'x-gbk':
return 'GBK'
case 'gb18030':
return 'gb18030'
case 'big5':
case 'big5-hkscs':
case 'cn-big5':
case 'csbig5':
case 'x-x-big5':
return 'Big5'
case 'cseucpkdfmtjapanese':
case 'euc-jp':
case 'x-euc-jp':
return 'EUC-JP'
case 'csiso2022jp':
case 'iso-2022-jp':
return 'ISO-2022-JP'
case 'csshiftjis':
case 'ms932':
case 'ms_kanji':
case 'shift-jis':
case 'shift_jis':
case 'sjis':
case 'windows-31j':
case 'x-sjis':
return 'Shift_JIS'
case 'cseuckr':
case 'csksc56011987':
case 'euc-kr':
case 'iso-ir-149':
case 'korean':
case 'ks_c_5601-1987':
case 'ks_c_5601-1989':
case 'ksc5601':
case 'ksc_5601':
case 'windows-949':
return 'EUC-KR'
case 'csiso2022kr':
case 'hz-gb-2312':
case 'iso-2022-cn':
case 'iso-2022-cn-ext':
case 'iso-2022-kr':
case 'replacement':
return 'replacement'
case 'unicodefffe':
case 'utf-16be':
return 'UTF-16BE'
case 'csunicode':
case 'iso-10646-ucs-2':
case 'ucs-2':
case 'unicode':
case 'unicodefeff':
case 'utf-16':
case 'utf-16le':
return 'UTF-16LE'
case 'x-user-defined':
return 'x-user-defined'
default: return 'failure'
}
}
module.exports = {
getEncoding
}

View File

@@ -0,0 +1,344 @@
'use strict'
const {
staticPropertyDescriptors,
readOperation,
fireAProgressEvent
} = require('./util')
const {
kState,
kError,
kResult,
kEvents,
kAborted
} = require('./symbols')
const { webidl } = require('../fetch/webidl')
const { kEnumerableProperty } = require('../core/util')
class FileReader extends EventTarget {
constructor () {
super()
this[kState] = 'empty'
this[kResult] = null
this[kError] = null
this[kEvents] = {
loadend: null,
error: null,
abort: null,
load: null,
progress: null,
loadstart: null
}
}
/**
* @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer
* @param {import('buffer').Blob} blob
*/
readAsArrayBuffer (blob) {
webidl.brandCheck(this, FileReader)
webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsArrayBuffer' })
blob = webidl.converters.Blob(blob, { strict: false })
// The readAsArrayBuffer(blob) method, when invoked,
// must initiate a read operation for blob with ArrayBuffer.
readOperation(this, blob, 'ArrayBuffer')
}
/**
* @see https://w3c.github.io/FileAPI/#readAsBinaryString
* @param {import('buffer').Blob} blob
*/
readAsBinaryString (blob) {
webidl.brandCheck(this, FileReader)
webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsBinaryString' })
blob = webidl.converters.Blob(blob, { strict: false })
// The readAsBinaryString(blob) method, when invoked,
// must initiate a read operation for blob with BinaryString.
readOperation(this, blob, 'BinaryString')
}
/**
* @see https://w3c.github.io/FileAPI/#readAsDataText
* @param {import('buffer').Blob} blob
* @param {string?} encoding
*/
readAsText (blob, encoding = undefined) {
webidl.brandCheck(this, FileReader)
webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsText' })
blob = webidl.converters.Blob(blob, { strict: false })
if (encoding !== undefined) {
encoding = webidl.converters.DOMString(encoding)
}
// The readAsText(blob, encoding) method, when invoked,
// must initiate a read operation for blob with Text and encoding.
readOperation(this, blob, 'Text', encoding)
}
/**
* @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL
* @param {import('buffer').Blob} blob
*/
readAsDataURL (blob) {
webidl.brandCheck(this, FileReader)
webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsDataURL' })
blob = webidl.converters.Blob(blob, { strict: false })
// The readAsDataURL(blob) method, when invoked, must
// initiate a read operation for blob with DataURL.
readOperation(this, blob, 'DataURL')
}
/**
* @see https://w3c.github.io/FileAPI/#dfn-abort
*/
abort () {
// 1. If this's state is "empty" or if this's state is
// "done" set this's result to null and terminate
// this algorithm.
if (this[kState] === 'empty' || this[kState] === 'done') {
this[kResult] = null
return
}
// 2. If this's state is "loading" set this's state to
// "done" and set this's result to null.
if (this[kState] === 'loading') {
this[kState] = 'done'
this[kResult] = null
}
// 3. If there are any tasks from this on the file reading
// task source in an affiliated task queue, then remove
// those tasks from that task queue.
this[kAborted] = true
// 4. Terminate the algorithm for the read method being processed.
// TODO
// 5. Fire a progress event called abort at this.
fireAProgressEvent('abort', this)
// 6. If this's state is not "loading", fire a progress
// event called loadend at this.
if (this[kState] !== 'loading') {
fireAProgressEvent('loadend', this)
}
}
/**
* @see https://w3c.github.io/FileAPI/#dom-filereader-readystate
*/
get readyState () {
webidl.brandCheck(this, FileReader)
switch (this[kState]) {
case 'empty': return this.EMPTY
case 'loading': return this.LOADING
case 'done': return this.DONE
}
}
/**
* @see https://w3c.github.io/FileAPI/#dom-filereader-result
*/
get result () {
webidl.brandCheck(this, FileReader)
// The result attributes getter, when invoked, must return
// this's result.
return this[kResult]
}
/**
* @see https://w3c.github.io/FileAPI/#dom-filereader-error
*/
get error () {
webidl.brandCheck(this, FileReader)
// The error attributes getter, when invoked, must return
// this's error.
return this[kError]
}
get onloadend () {
webidl.brandCheck(this, FileReader)
return this[kEvents].loadend
}
set onloadend (fn) {
webidl.brandCheck(this, FileReader)
if (this[kEvents].loadend) {
this.removeEventListener('loadend', this[kEvents].loadend)
}
if (typeof fn === 'function') {
this[kEvents].loadend = fn
this.addEventListener('loadend', fn)
} else {
this[kEvents].loadend = null
}
}
get onerror () {
webidl.brandCheck(this, FileReader)
return this[kEvents].error
}
set onerror (fn) {
webidl.brandCheck(this, FileReader)
if (this[kEvents].error) {
this.removeEventListener('error', this[kEvents].error)
}
if (typeof fn === 'function') {
this[kEvents].error = fn
this.addEventListener('error', fn)
} else {
this[kEvents].error = null
}
}
get onloadstart () {
webidl.brandCheck(this, FileReader)
return this[kEvents].loadstart
}
set onloadstart (fn) {
webidl.brandCheck(this, FileReader)
if (this[kEvents].loadstart) {
this.removeEventListener('loadstart', this[kEvents].loadstart)
}
if (typeof fn === 'function') {
this[kEvents].loadstart = fn
this.addEventListener('loadstart', fn)
} else {
this[kEvents].loadstart = null
}
}
get onprogress () {
webidl.brandCheck(this, FileReader)
return this[kEvents].progress
}
set onprogress (fn) {
webidl.brandCheck(this, FileReader)
if (this[kEvents].progress) {
this.removeEventListener('progress', this[kEvents].progress)
}
if (typeof fn === 'function') {
this[kEvents].progress = fn
this.addEventListener('progress', fn)
} else {
this[kEvents].progress = null
}
}
get onload () {
webidl.brandCheck(this, FileReader)
return this[kEvents].load
}
set onload (fn) {
webidl.brandCheck(this, FileReader)
if (this[kEvents].load) {
this.removeEventListener('load', this[kEvents].load)
}
if (typeof fn === 'function') {
this[kEvents].load = fn
this.addEventListener('load', fn)
} else {
this[kEvents].load = null
}
}
get onabort () {
webidl.brandCheck(this, FileReader)
return this[kEvents].abort
}
set onabort (fn) {
webidl.brandCheck(this, FileReader)
if (this[kEvents].abort) {
this.removeEventListener('abort', this[kEvents].abort)
}
if (typeof fn === 'function') {
this[kEvents].abort = fn
this.addEventListener('abort', fn)
} else {
this[kEvents].abort = null
}
}
}
// https://w3c.github.io/FileAPI/#dom-filereader-empty
FileReader.EMPTY = FileReader.prototype.EMPTY = 0
// https://w3c.github.io/FileAPI/#dom-filereader-loading
FileReader.LOADING = FileReader.prototype.LOADING = 1
// https://w3c.github.io/FileAPI/#dom-filereader-done
FileReader.DONE = FileReader.prototype.DONE = 2
Object.defineProperties(FileReader.prototype, {
EMPTY: staticPropertyDescriptors,
LOADING: staticPropertyDescriptors,
DONE: staticPropertyDescriptors,
readAsArrayBuffer: kEnumerableProperty,
readAsBinaryString: kEnumerableProperty,
readAsText: kEnumerableProperty,
readAsDataURL: kEnumerableProperty,
abort: kEnumerableProperty,
readyState: kEnumerableProperty,
result: kEnumerableProperty,
error: kEnumerableProperty,
onloadstart: kEnumerableProperty,
onprogress: kEnumerableProperty,
onload: kEnumerableProperty,
onabort: kEnumerableProperty,
onerror: kEnumerableProperty,
onloadend: kEnumerableProperty,
[Symbol.toStringTag]: {
value: 'FileReader',
writable: false,
enumerable: false,
configurable: true
}
})
Object.defineProperties(FileReader, {
EMPTY: staticPropertyDescriptors,
LOADING: staticPropertyDescriptors,
DONE: staticPropertyDescriptors
})
module.exports = {
FileReader
}

View File

@@ -0,0 +1,78 @@
'use strict'
const { webidl } = require('../fetch/webidl')
const kState = Symbol('ProgressEvent state')
/**
* @see https://xhr.spec.whatwg.org/#progressevent
*/
class ProgressEvent extends Event {
constructor (type, eventInitDict = {}) {
type = webidl.converters.DOMString(type)
eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {})
super(type, eventInitDict)
this[kState] = {
lengthComputable: eventInitDict.lengthComputable,
loaded: eventInitDict.loaded,
total: eventInitDict.total
}
}
get lengthComputable () {
webidl.brandCheck(this, ProgressEvent)
return this[kState].lengthComputable
}
get loaded () {
webidl.brandCheck(this, ProgressEvent)
return this[kState].loaded
}
get total () {
webidl.brandCheck(this, ProgressEvent)
return this[kState].total
}
}
webidl.converters.ProgressEventInit = webidl.dictionaryConverter([
{
key: 'lengthComputable',
converter: webidl.converters.boolean,
defaultValue: false
},
{
key: 'loaded',
converter: webidl.converters['unsigned long long'],
defaultValue: 0
},
{
key: 'total',
converter: webidl.converters['unsigned long long'],
defaultValue: 0
},
{
key: 'bubbles',
converter: webidl.converters.boolean,
defaultValue: false
},
{
key: 'cancelable',
converter: webidl.converters.boolean,
defaultValue: false
},
{
key: 'composed',
converter: webidl.converters.boolean,
defaultValue: false
}
])
module.exports = {
ProgressEvent
}

Some files were not shown because too many files have changed in this diff Show More