diff --git a/.github/dependabot.yml b/.github/dependabot.yml index befcde3c066..544e5f81a49 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,8 +28,3 @@ updates: directory: /build schedule: interval: daily - - - package-ecosystem: pip - directory: /test/wpt/tests/resources/test - schedule: - interval: daily diff --git a/.github/workflows/autobahn.yml b/.github/workflows/autobahn.yml index 2542fed5974..758510e7310 100644 --- a/.github/workflows/autobahn.yml +++ b/.github/workflows/autobahn.yml @@ -5,13 +5,14 @@ on: workflow_call: inputs: node-version: - default: '22' + default: '24' type: string pull_request: paths: - '.github/workflows/autobahn.yml' - 'lib/web/websocket/**' - 'test/autobahn/**' + permissions: contents: read @@ -19,7 +20,7 @@ jobs: autobahn: name: Autobahn Test Suite runs-on: ubuntu-latest - container: node:22 + container: node:24 services: fuzzingserver: image: crossbario/autobahn-testsuite:latest @@ -30,8 +31,8 @@ jobs: - ${{ github.workspace }}/test/autobahn/config:/config - ${{ github.workspace }}/test/autobahn/reports:/reports steps: - - name: Checkout Code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false clean: false @@ -42,10 +43,10 @@ jobs: with: args: docker restart --time 0 --signal=SIGKILL fuzzingserver - - name: Setup Node.js@${{ inputs.node-version || '22' }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + - name: Setup Node.js@${{ inputs.node-version }} + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: ${{ inputs.node-version || '22' }} + node-version: ${{ inputs.node-version }} - name: Run Autobahn Test Suite run: npm run test:websocket:autobahn diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 4621cf7e510..1f410ae67d2 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -5,12 +5,9 @@ on: - closed - labeled -permissions: - pull-requests: write - contents: write - jobs: backport: + name: Backport runs-on: ubuntu-latest if: > github.event.pull_request.merged @@ -21,9 +18,12 @@ jobs: && contains(github.event.label.name, 'backport') ) ) - name: Backport + permissions: + pull-requests: write + contents: write steps: - name: Backport - uses: tibdex/backport@v2 + uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2.0.4 + id: backport with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index f9eac1c6f27..45957be9726 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -1,7 +1,13 @@ name: Benchmarks + on: - - push - - pull_request + push: + branches: + - main + - current + - next + - 'v*' + pull_request: permissions: contents: read @@ -11,15 +17,15 @@ jobs: name: benchmark current runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ github.base_ref }} - name: Setup Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: lts/* + node-version: 'lts/*' - name: Install Modules for undici run: npm i --ignore-scripts --omit=dev - name: Install Modules for Benchmarks @@ -33,14 +39,14 @@ jobs: name: benchmark branch runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: lts/* + node-version: 'lts/*' - name: Install Modules for undici run: npm i --ignore-scripts --omit=dev - name: Install Modules for Benchmarks @@ -54,15 +60,15 @@ jobs: name: benchmark (sending data) current runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ github.base_ref }} - name: Setup Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: lts/* + node-version: 'lts/*' - name: Install Modules for undici run: npm i --ignore-scripts --omit=dev - name: Install Modules for Benchmarks @@ -76,14 +82,14 @@ jobs: name: benchmark (sending data) branch runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: lts/* + node-version: 'lts/*' - name: Install Modules for undici run: npm i --ignore-scripts --omit=dev - name: Install Modules for Benchmarks @@ -97,15 +103,15 @@ jobs: name: benchmark current h2 runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ github.base_ref }} - name: Setup Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: lts/* + node-version: 'lts/*' - name: Install Modules for undici run: npm i --ignore-scripts --omit=dev - name: Install Modules for Benchmarks @@ -119,14 +125,14 @@ jobs: name: benchmark branch h2 runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: lts/* + node-version: 'lts/*' - name: Install Modules for undici run: npm i --ignore-scripts --omit=dev - name: Install Modules for Benchmarks diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..db3eb50f76e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,321 @@ +name: CI + +on: + push: + branches: + - main + - current + - next + - 'v*' + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Dependency Review + uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1 + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + # Using `latest` as `lts` could point to previous major version. + # Different versions of Node.js can cause different linting results + # (e.g. dependening on process.getBuiltinModule()) + node-version: 'latest' + + - name: Install dependencies + run: npm install + + - name: Lint + run: npm run lint + + test: + name: Test ${{ matrix.node-version == '24' && matrix.runs-on == 'ubuntu-latest' && 'and Coverage ' || ''}}with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }} + strategy: + fail-fast: false + max-parallel: 0 + matrix: + node-version: ['20', '22', '24', '25', '26'] + runs-on: ['ubuntu-latest', 'windows-latest', 'macos-latest'] + exclude: + - node-version: '20' + runs-on: windows-latest + uses: ./.github/workflows/nodejs.yml + with: + # Disable coverage on Node.js 25 until https://github.com/nodejs/node/issues/61971 is resolved. + codecov: ${{ matrix.node-version == '24' && matrix.runs-on == 'ubuntu-latest' }} + node-version: ${{ matrix.node-version }} + runs-on: ${{ matrix.runs-on }} + secrets: inherit + + test-with-no-wasm-simd: + name: Test with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }} with WASM SIMD disabled + strategy: + fail-fast: false + max-parallel: 0 + matrix: + node-version: ['24', '25', '26'] + runs-on: ['ubuntu-latest'] + uses: ./.github/workflows/nodejs.yml + with: + node-version: ${{ matrix.node-version }} + runs-on: ${{ matrix.runs-on }} + no-wasm-simd: '1' + secrets: inherit + + test-without-intl: + name: Test with Node.js ${{ matrix.node-version }} compiled --without-intl + strategy: + fail-fast: false + max-parallel: 0 + matrix: + node-version: ['20', '22', '24', '25'] + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: recursive + + # Setup node, install deps, and build undici prior to building icu-less node and testing + - name: Setup Node.js@${{ matrix.node-version }} + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Build undici + run: npm run build:node + + - name: Determine latest release + id: release + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + result-encoding: string + script: | + const req = await fetch('https://nodejs.org/download/release/index.json') + const releases = await req.json() + + const latest = releases.find((r) => r.version.startsWith('v${{ matrix.node-version }}')) + return latest.version + + - name: Download and extract source + run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ - + + - name: Install ninja + run: sudo apt-get install ninja-build + + - name: ccache + uses: hendrikmuhs/ccache-action@bfa03e1de4d7f7c3e80ad9109feedd05c4f5a716 #v1.2.19 + with: + key: node${{ matrix.node-version }} + + - name: Build node + working-directory: ./node-${{ steps.release.outputs.result }} + run: | + export CC="ccache gcc" + export CXX="ccache g++" + ./configure --without-intl --ninja --prefix=./final + make + make install + echo "$(pwd)/final/bin" >> $GITHUB_PATH + + - name: Print version information + run: | + echo OS: $(node -p "os.version()") + echo Node.js: $(node --version) + echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")" + echo npm: $(npm --version) + echo git: $(git --version) + echo icu config: $(node -e "console.log(process.config)" | grep icu) + + - name: Configure hosts file for WPT (Windows) + if: runner.os == 'Windows' + run: | + cd ${{ github.workspace }}\test\web-platform-tests\wpt + python wpt make-hosts-file | Out-File $env:SystemRoot\System32\drivers\etc\hosts -Encoding ascii -Append + shell: powershell + + - name: Configure hosts file for WPT (Unix) + if: runner.os != 'Windows' + run: | + cd ${{ github.workspace }}/test/web-platform-tests/wpt + python3 wpt make-hosts-file | sudo tee -a /etc/hosts + + - name: Run tests + run: npm run test:javascript:without-intl + + test-without-ssl: + name: Test with Node.js ${{ matrix.node-version }} compiled --without-ssl + strategy: + fail-fast: false + max-parallel: 0 + matrix: + node-version: ['20', '22', '24', '25'] + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + submodules: recursive + + # Setup node, install deps, and build undici prior to building icu-less node and testing + - name: Setup Node.js@${{ matrix.node-version }} + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Build undici + run: npm run build:node + + - name: Determine latest release + id: release + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + result-encoding: string + script: | + const req = await fetch('https://nodejs.org/download/release/index.json') + const releases = await req.json() + + const latest = releases.find((r) => r.version.startsWith('v${{ matrix.node-version }}')) + return latest.version + + - name: Download and extract source + run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ - + + - name: Install ninja + run: sudo apt-get install ninja-build + + - name: ccache + uses: hendrikmuhs/ccache-action@bfa03e1de4d7f7c3e80ad9109feedd05c4f5a716 #v1.2.19 + with: + key: node${{ matrix.node-version }} + + - name: Build node + working-directory: ./node-${{ steps.release.outputs.result }} + run: | + export CC="ccache gcc" + export CXX="ccache g++" + ./configure --without-ssl --ninja --prefix=./final + make + make install + echo "$(pwd)/final/bin" >> $GITHUB_PATH + + - name: Print version information + run: | + echo OS: $(node -p "os.version()") + echo Node.js: $(node --version) + echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")" + echo npm: $(npm --version) + echo git: $(git --version) + echo icu config: $(node -e "console.log(process.config)" | grep icu) + + - name: Try loading Node.js without Crypto + run: node index.js + + test-fuzzing: + name: Fuzzing + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: lts/* + + - name: Install dependencies + run: npm install + + - name: Run fuzzing tests + run: npm run test:fuzzing + + test-shared-builtin: + name: Test with Node.js ${{ matrix.node-version }} compiled --shared-builtin-undici/undici-path + uses: ./.github/workflows/nodejs-shared.yml + strategy: + fail-fast: false + max-parallel: 0 + matrix: + node-version: ['24', '26'] + runs-on: ['ubuntu-latest'] + with: + node-version: ${{ matrix.node-version }} + + test-types: + name: Test TypeScript types + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: lts/* + + - name: Install dependencies + run: npm install + + - name: Run typings tests + run: npm run test:typescript + + automerge: + if: > + github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' + needs: + - dependency-review + - test + - test-types + - test-with-no-wasm-simd + - test-without-intl + - test-fuzzing + - lint + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Merge Dependabot PR + uses: fastify/github-action-merge-dependabot@1b2ed42db8f9d81a46bac83adedfc03eb5149dff # v3.11.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b8cedf4738f..96169532330 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -35,22 +35,24 @@ jobs: strategy: fail-fast: false matrix: - language: ["javascript", "python", "typescript"] + language: ["javascript", "typescript"] # CodeQL supports [ $supported-codeql-languages ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v2.3.3 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v2.3.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +62,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v2.3.3 + uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v2.3.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +75,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v2.3.3 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v2.3.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index 7da843a9e6e..00000000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Nightly tests - -on: - workflow_dispatch: - schedule: - - cron: "0 10 * * *" - -permissions: - contents: read - -jobs: - test-linux: - if: github.repository == 'nodejs/undici' - uses: ./.github/workflows/test.yml - with: - node-version: 23-nightly - runs-on: ubuntu-latest - secrets: inherit - - test-autobahn: - if: github.repository == 'nodejs/undici' - uses: ./.github/workflows/autobahn.yml - with: - node-version: 23-nightly - secrets: inherit - - test-windows: - if: github.repository == 'nodejs/undici' - uses: ./.github/workflows/test.yml - with: - node-version: 23-nightly - runs-on: windows-latest - secrets: inherit - - test-macos: - if: github.repository == 'nodejs/undici' - uses: ./.github/workflows/test.yml - with: - node-version: 23-nightly - runs-on: macos-latest - secrets: inherit - - report-failure: - if: ${{ always() && (needs.test-linux.result == 'failure' && needs.test-windows.result == 'failure' && needs.test-macos.result == 'failure') }} - needs: - - test-linux - - test-windows - - test-macos - - test-autobahn - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - name: Create or update issue - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const ISSUE_TITLE = "Nightly tests are failing" - - const actionRunUrl = "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - const issueContext = { - owner: context.repo.owner, - repo: context.repo.repo - } - - let issue = (await github.rest.issues.listForRepo({ - state: "open", - creator: "github-actions[bot]", - ...issueContext - })).data.find((issue) => issue.title === ISSUE_TITLE) - - if(!issue) { - issue = (await github.rest.issues.create({ - title: ISSUE_TITLE, - body: `Tests against nightly failed, see: ${actionRunUrl}`, - ...issueContext - })).data - } diff --git a/.github/workflows/nodejs-nightly.yml b/.github/workflows/nodejs-nightly.yml new file mode 100644 index 00000000000..94d46abae87 --- /dev/null +++ b/.github/workflows/nodejs-nightly.yml @@ -0,0 +1,76 @@ +name: Node.js Nightly + +on: + workflow_dispatch: + schedule: + - cron: "0 10 * * *" + +permissions: + contents: read + +jobs: + test: + name: Test with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }} + if: github.repository == 'nodejs/undici' + strategy: + fail-fast: false + max-parallel: 0 + matrix: + node-version: ['25-nightly'] + runs-on: [ubuntu-latest, windows-latest, macos-latest] + uses: ./.github/workflows/nodejs.yml + with: + node-version: ${{ matrix.node-version }} + runs-on: ${{ matrix.runs-on }} + secrets: inherit + + autobahn: + if: github.repository == 'nodejs/undici' + uses: ./.github/workflows/autobahn.yml + with: + node-version: '25-nightly' + secrets: inherit + + test-shared-builtin: + if: github.repository == 'nodejs/undici' + uses: ./.github/workflows/nodejs-shared.yml + with: + node-version: '25' + node-download-server-path: '/nightly' + + report-failure: + if: ${{ always() && (needs.test.result == 'failure' || needs.test-shared-builtin.result == 'failure' || needs.autobahn.result == 'failure') }} + needs: + - test + - test-shared-builtin + - autobahn + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Create or update issue + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const ISSUE_TITLE = "Nightly tests are failing" + + const actionRunUrl = "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + const issueContext = { + owner: context.repo.owner, + repo: context.repo.repo + } + + let issue = (await github.rest.issues.listForRepo({ + state: "open", + creator: "github-actions[bot]", + ...issueContext + })).data.find((issue) => issue.title === ISSUE_TITLE) + + if(!issue) { + issue = (await github.rest.issues.create({ + title: ISSUE_TITLE, + body: `Tests against nightly failed, see: ${actionRunUrl}`, + ...issueContext + })).data + } diff --git a/.github/workflows/nodejs-shared.yml b/.github/workflows/nodejs-shared.yml index 7eb7ff46d88..e8ec93a62bc 100644 --- a/.github/workflows/nodejs-shared.yml +++ b/.github/workflows/nodejs-shared.yml @@ -1,47 +1,44 @@ name: Node.js compiled --shared-builtin-undici/undici-path CI on: - push: - branches: - - main - - current - - next - - 'v*' - pull_request: + workflow_call: + inputs: + node-version: + required: true + type: string + node-download-server-path: + required: false + type: string + default: '/release' permissions: contents: read jobs: test-shared-builtin: - name: Test with Node.js ${{ matrix.version }} compiled --shared-builtin-undici/undici-path - strategy: - fail-fast: false - max-parallel: 0 - matrix: - version: [20, 22, 24] + name: Test with Node.js ${{ inputs.node-version }} compiled --shared-builtin-undici/undici-path runs-on: ubuntu-latest timeout-minutes: 120 steps: # Checkout into a subdirectory otherwise Node.js tests will break due to finding Undici's package.json in a parent directory. - - name: Checkout - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: path: ./undici persist-credentials: false # Setup node, install deps, and build undici prior to building node with `--shared-builtin-undici/undici-path` and testing - - name: Setup Node.js@${{ inputs.version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + - name: Setup Node.js lts/* + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: ${{ inputs.version }} + node-version: 'lts/*' - name: Install dependencies working-directory: ./undici run: npm install - name: Install wasi-libc - run: sudo apt-get install -y wasi-libc + run: sudo apt-get install -y wasi-libc binaryen - name: Build WASM working-directory: ./undici @@ -53,30 +50,30 @@ jobs: export WASM_LDLIBS='-lc' node build/wasm.js - - name: Determine latest release + - name: Determine latest release for Node.js ${{ inputs.node-version }} id: release - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: result-encoding: string script: | - const req = await fetch('https://nodejs.org/download/release/index.json') + const req = await fetch('https://nodejs.org/download${{ inputs.node-download-server-path }}/index.json') const releases = await req.json() - const latest = releases.find((r) => r.version.startsWith('v${{ matrix.version }}')) + const latest = releases.find((r) => r.version.startsWith('v${{ inputs.node-version }}')) return latest.version - - name: Download and extract source - run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ - + - name: Download and extract source for Node.js ${{ steps.release.outputs.result }} + run: curl https://nodejs.org/download${{ inputs.node-download-server-path }}/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ - - name: Install ninja run: sudo apt-get install ninja-build - name: ccache - uses: hendrikmuhs/ccache-action@63069e3931dedbf3b63792097479563182fe70d1 #v1.2.18 + uses: hendrikmuhs/ccache-action@bfa03e1de4d7f7c3e80ad9109feedd05c4f5a716 #v1.2.19 with: - key: node(external_undici)${{ matrix.version }} + key: node(external_undici)${{ inputs.node-version }} - - name: Build node + - name: Build node ${{ steps.release.outputs.result }} with --shared-builtin-undici/undici-path working-directory: ./node-${{ steps.release.outputs.result }} run: | export CC="ccache gcc" @@ -84,6 +81,9 @@ jobs: rm -rf deps/undici ./configure --shared-builtin-undici/undici-path ${{ github.workspace }}/undici/loader.js --ninja --prefix=./final make + if grep -q '^build-ffi-tests:' Makefile; then + make build-ffi-tests + fi make install echo "$(pwd)/final/bin" >> $GITHUB_PATH @@ -91,6 +91,7 @@ jobs: run: | echo OS: $(node -p "os.version()") echo Node.js: $(node --version) + echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")" echo npm: $(npm --version) echo git: $(git --version) echo external config: $(node -e "console.log(process.config)" | grep NODE_SHARED_BUILTIN_UNDICI_UNDICI_PATH) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 38de01b8dcd..c07c16845f3 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,223 +1,203 @@ -name: Node CI +name: Node.js on: - push: - branches: - - main - - current - - next - - 'v*' - pull_request: + workflow_call: + inputs: + node-version: + required: true + type: string + runs-on: + required: true + type: string + codecov: + required: false + type: boolean + default: false + no-wasm-simd: + type: string + required: false + default: '' permissions: contents: read jobs: - dependency-review: - if: ${{ github.event_name == 'pull_request' }} - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - with: - persist-credentials: false - - - name: Dependency Review - uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 - - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - with: - persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: lts/* - - - name: Install dependencies - run: npm install - - - name: Lint - run: npm run lint - test: - strategy: - fail-fast: false - max-parallel: 0 - matrix: - node-version: - - 20 - - 22 - - 24 - runs-on: - - ubuntu-latest - - windows-latest - - macos-latest - uses: ./.github/workflows/test.yml - with: - node-version: ${{ matrix.node-version }} - runs-on: ${{ matrix.runs-on }} - secrets: inherit - - test-without-intl: - name: Test with Node.js ${{ matrix.version }} compiled --without-intl - strategy: - fail-fast: false - max-parallel: 0 - matrix: - version: [20, 22, 24] - runs-on: ubuntu-latest - timeout-minutes: 120 + name: Test ${{ inputs.codecov == true && 'and Coverage ' || '' }}with Node.js ${{ inputs.node-version }} on ${{ inputs.runs-on }} + timeout-minutes: 20 + runs-on: ${{ inputs.runs-on }} steps: - - name: Checkout - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + submodules: recursive - # Setup node, install deps, and build undici prior to building icu-less node and testing - - name: Setup Node.js@${{ inputs.version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + - name: Setup Node.js@${{ inputs.node-version }} + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: ${{ inputs.version }} - - - name: Install dependencies - run: npm install - - - name: Build undici - run: npm run build:node - - - name: Determine latest release - id: release - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - result-encoding: string - script: | - const req = await fetch('https://nodejs.org/download/release/index.json') - const releases = await req.json() - - const latest = releases.find((r) => r.version.startsWith('v${{ matrix.version }}')) - return latest.version - - - name: Download and extract source - run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ - - - - name: Install ninja - run: sudo apt-get install ninja-build - - - name: ccache - uses: hendrikmuhs/ccache-action@63069e3931dedbf3b63792097479563182fe70d1 #v1.2.18 - with: - key: node${{ matrix.version }} - - - name: Build node - working-directory: ./node-${{ steps.release.outputs.result }} - run: | - export CC="ccache gcc" - export CXX="ccache g++" - ./configure --without-intl --ninja --prefix=./final - make - make install - echo "$(pwd)/final/bin" >> $GITHUB_PATH + node-version: ${{ inputs.node-version }} - name: Print version information run: | echo OS: $(node -p "os.version()") echo Node.js: $(node --version) + echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")" echo npm: $(npm --version) echo git: $(git --version) - echo icu config: $(node -e "console.log(process.config)" | grep icu) - - - name: Run tests - run: npm run test:javascript:without-intl - - test-fuzzing: - name: Fuzzing - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - with: - persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: lts/* - - - name: Install dependencies - run: npm install - - - name: Run fuzzing tests - run: npm run test:fuzzing - - test-types: - name: Test TypeScript types - timeout-minutes: 15 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - with: - persist-credentials: false - - - name: Setup Node.js - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: lts/* - name: Install dependencies run: npm install - - name: Run typings tests - run: npm run test:typescript - - test-sqlite: - name: Test with SQLite enabled - timeout-minutes: 15 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - with: - persist-credentials: false + - name: Print installed dependencies + run: npm ls --all + continue-on-error: true - - name: Setup Node.js - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: 22 - - - name: Install dependencies - run: npm install - - - name: Run typings tests - run: npm run test:sqlite + - name: Configure hosts file for WPT (Windows) + if: runner.os == 'Windows' + run: | + cd ${{ github.workspace }}\test\web-platform-tests\wpt + python wpt make-hosts-file | Out-File $env:SystemRoot\System32\drivers\etc\hosts -Encoding ascii -Append + shell: powershell - automerge: - if: > - github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' - needs: - - dependency-review - - test - - test-types - - test-without-intl - - test-fuzzing - - lint - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Merge Dependabot PR - uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 + - name: Configure hosts file for WPT (Unix) + if: runner.os != 'Windows' + run: | + cd ${{ github.workspace }}/test/web-platform-tests/wpt + python3 wpt make-hosts-file | sudo tee -a /etc/hosts + + - name: Generate PEM files + run: npm run generate-pem + id: generate-pem + + - name: Test unit + run: npm run test:unit + id: test-unit + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test node-test + run: npm run test:node-test + id: test-node-test + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test fetch + run: npm run test:fetch + id: test-fetch + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test node-fetch + run: npm run test:node-fetch + id: test-node-fetch + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test cache + run: npm run test:cache + id: test-cache + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test cache-interceptor ${{ inputs.node-version != '20' && 'with' || 'without' }} sqlite + run: npm run test:cache-interceptor + id: test-cache-interceptor + env: + CI: true + NODE_OPTIONS: ${{ inputs.node-version != '20' && '--experimental-sqlite' || '' }} + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test cache-tests + run: npm run test:cache-tests + id: test-cache-tests + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test cookies + run: npm run test:cookies + id: test-cookies + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test interceptors + run: npm run test:interceptors + id: test-interceptors + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test eventsource + run: npm run test:eventsource + id: test-eventsource + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test infra + run: npm run test:infra + id: test-infra + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test subresource-integrity + run: npm run test:subresource-integrity + id: test-subresource-integrity + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test websocket + run: npm run test:websocket + id: test-websocket + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test jest + run: npm run test:jest + id: test-jest + env: + CI: true + NODE_V8_COVERAGE: '' + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Test wpt + run: npm run test:wpt + id: test-wpt + env: + CI: true + NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }} + UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }} + + - name: Coverage summary + if: inputs.codecov == true + run: npm run coverage:report:ci + id: prepare-coverage-report + + - name: Upload coverage report to Codecov + if: inputs.codecov == true + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release-create-pr.yml b/.github/workflows/release-create-pr.yml index bc612917b1e..7d73d66a101 100644 --- a/.github/workflows/release-create-pr.yml +++ b/.github/workflows/release-create-pr.yml @@ -30,14 +30,31 @@ jobs: version: ${{ steps.bump.outputs.version }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + id: checkout with: - node-version: '20' + persist-credentials: true + + - name: Validate release branch + run: | + if [ '${{ github.ref_name }}' != 'v7.x' ]; then + echo 'This workflow must run from v7.x (current ref: ${{ github.ref_name }}).' + exit 1 + fi + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + id: setup-node + with: + node-version: 'lts/*' + - name: Git Config + id: git-config run: | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" + - name: Change version number and push id: bump run: | @@ -48,10 +65,13 @@ jobs: git commit -m "Bumped v$VERSION" git push origin "HEAD:$RELEASE_BRANCH" echo "version=$VERSION" >> $GITHUB_OUTPUT + - name: Create PR - uses: actions/github-script@v7 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: create-pr with: script: | - const defaultBranch = "${{ github.event.repository.default_branch }}" - const versionTag = "v${{ steps.bump.outputs.version }}" - await require('./scripts/release').generatePr({ github, context, defaultBranch, versionTag }) + const releaseBranch = '${{ github.ref_name }}' + const versionTag = 'v${{ steps.bump.outputs.version }}' + const commitHash = '${{ github.sha }}' + await require('./scripts/release').generatePr({ github, context, releaseBranch, versionTag, commitHash }) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f71d450a90..19e40b5345b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,9 @@ -name: Create release +name: Release undici and undici-types on NPM and create GitHub Release on: push: branches: - - main + - v7.x paths: - package.json @@ -11,19 +11,24 @@ permissions: contents: read jobs: - check-release-version: + determine-release-version: runs-on: ubuntu-latest outputs: - release-version: ${{ steps.set-release-version.outputs.result }} + release-version: ${{ steps.determine-release-version.outputs.result }} steps: - - uses: actions/checkout@v4 - - uses: actions/github-script@v7 - id: set-release-version + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + id: checkout + with: + persist-credentials: false + - name: Determine release version + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: determine-release-version with: result-encoding: string script: | const { owner, repo } = context.repo - const version = require("./package.json").version + const version = require('./package.json').version const versionTag = `v${version}` const { data: releases } = await github.rest.repos.listReleases({ @@ -39,8 +44,8 @@ jobs: release: runs-on: ubuntu-latest - needs: check-release-version - if: ${{ startsWith(needs.check-release-version.outputs.release-version, 'v') }} + needs: determine-release-version + if: ${{ github.ref_name == 'v7.x' && startsWith(needs.determine-release-version.outputs.release-version, 'v') }} permissions: contents: write @@ -49,26 +54,45 @@ jobs: environment: release steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + id: checkout + with: + persist-credentials: true + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: - node-version: '20' + node-version: 'lts/*' registry-url: 'https://registry.npmjs.org' - - run: npm install -g npm@latest - - run: npm install - - name: Create NPM release - run: npm publish --provenance --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - run: node scripts/generate-undici-types-package-json.js - - run: npm publish --provenance + + - name: Install globally latest npm + run: npm install -g npm@latest + id: install-globally-latest-npm + + - name: Install dependencies + run: npm install + id: install-dependencies + + - name: Publish undici on NPM + run: npm publish --access public --tag seven + id: npm-publish-undici + + - name: Generate Types Package + run: node scripts/generate-undici-types-package-json.js + id: generate-types-package + + - name: Publish undici-types on NPM + run: npm publish --tag seven + id: npm-publish-undici-types working-directory: './types' - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Create GitHub release - uses: actions/github-script@v7 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: create-gh-release with: script: | - const defaultBranch = "${{ github.event.repository.default_branch }}" - const versionTag = "${{ needs.check-release-version.outputs.release-version }}" - await require('./scripts/release').release({ github, context, defaultBranch, versionTag }) + const releaseBranch = '${{ github.ref_name }}' + const versionTag = '${{ needs.determine-release-version.outputs.release-version }}' + const commitHash = '${{ github.sha }}' + await require('./scripts/release').release({ github, context, releaseBranch, versionTag, commitHash }) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 4e7264a4c47..77bd36c5652 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,12 +29,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif @@ -43,7 +43,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: SARIF file path: results.sarif @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v3.29.5 with: sarif_file: results.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 2c34471cf47..00000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Run tests - -on: - workflow_call: - inputs: - node-version: - required: true - type: string - runs-on: - required: true - type: string - -permissions: - contents: read - -jobs: - test: - name: Test with Node.js ${{ inputs.node-version }} on ${{ inputs.runs-on }} - timeout-minutes: 15 - runs-on: ${{ inputs.runs-on }} - steps: - - name: Checkout - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - with: - persist-credentials: false - - - name: Setup Node.js@${{ inputs.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: ${{ inputs.node-version }} - - - name: Print version information - run: | - echo OS: $(node -p "os.version()") - echo Node.js: $(node --version) - echo npm: $(npm --version) - echo git: $(git --version) - - - name: Install dependencies - run: npm install - - - name: Print installed dependencies - run: npm ls --all - continue-on-error: true - - - name: Run tests with coverage - id: coverage - if: inputs.runs-on == 'ubuntu-latest' && inputs.node-version == 22 - run: npm run coverage:ci - env: - CI: true - NODE_V8_COVERAGE: ./coverage/tmp - - - name: Run tests - if: steps.coverage.outcome == 'skipped' - run: npm run test:javascript - env: - CI: true - - - name: Coverage Report - if: inputs.runs-on == 'ubuntu-latest' && inputs.node-version == 20 - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 - with: - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/triggered-autobahn.yml b/.github/workflows/triggered-autobahn.yml index d8100bb61bf..44d9c8d916f 100644 --- a/.github/workflows/triggered-autobahn.yml +++ b/.github/workflows/triggered-autobahn.yml @@ -5,10 +5,6 @@ on: types: - labeled -permissions: - contents: read - pull-requests: write - jobs: autobahn: if: ${{ github.event.label.name == 'autobahn' }} diff --git a/.github/workflows/update-cache-tests.yml b/.github/workflows/update-cache-tests.yml deleted file mode 100644 index 8082384c453..00000000000 --- a/.github/workflows/update-cache-tests.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Update Cache Tests - -on: - workflow_dispatch: - schedule: - - cron: '0 0 * * *' - -permissions: - contents: write - pull-requests: write - -jobs: - update-cache-tests: - name: Update Cache Tests - runs-on: ubuntu-latest - steps: - - name: Git Config - run: | - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - - - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - - - name: Update Cache Tests - run: | - rm -rf test/fixtures/cache-tests && mkdir test/fixtures/cache-tests && - - git clone https://github.com/http-tests/cache-tests --depth=1 test/fixtures/tmp-cache-tests/ && - - mv test/fixtures/tmp-cache-tests/LICENSE test/fixtures/cache-tests/LICENSE && - mv test/fixtures/tmp-cache-tests/tests test/fixtures/cache-tests/tests && - mv test/fixtures/tmp-cache-tests/test-engine test/fixtures/cache-tests/test-engine && - mv test/fixtures/tmp-cache-tests/results test/fixtures/cache-tests/results && - - rm -rf test/fixtures/tmp-cache-tests/ - - - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 - with: - base: main - branch: cache-tests-update - title: Update Cache Tests - body: Automated update of the Cache Test Suite - commit-message: "chore: update cache tests" diff --git a/.github/workflows/update-submodules.yml b/.github/workflows/update-submodules.yml new file mode 100644 index 00000000000..9ca0015f982 --- /dev/null +++ b/.github/workflows/update-submodules.yml @@ -0,0 +1,62 @@ +name: Update Submodules + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + +jobs: + update-wpt: + name: Update Submodules + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout Repository + id: checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: true + + - name: Git Config + id: git-config + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Update Submodules + id: update-submodules + run: | + git submodule update --init --recursive --remote --merge + + - name: Check for Changes + id: check-for-changes + run: | + if git diff --quiet; then + echo "no changes detected" + echo "change=false" >> "$GITHUB_OUTPUT" + else + echo "changes detected" + echo "change=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create Branch + id: create-branch + if: ${{ steps.check-for-changes.outputs.change == 'true'}} + run: | + git checkout -b submodules-update + git add . + git commit -m "chore: update Submodules" + git push --force --set-upstream origin submodules-update + + - name: Create Pull Request + id: create-pr + if: ${{ steps.check-for-changes.outputs.change == 'true'}} + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + with: + base: main + branch: submodules-update + title: Update Submodules + body: Automated update of the Submodules + commit-message: "chore: update Submodules" diff --git a/.github/workflows/update-wpt.yml b/.github/workflows/update-wpt.yml deleted file mode 100644 index a0b183dc416..00000000000 --- a/.github/workflows/update-wpt.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Update WPT - -on: - workflow_dispatch: - schedule: - - cron: '0 0 * * *' - -permissions: - contents: write - pull-requests: write - -jobs: - update-wpt: - name: Update WPT - runs-on: ubuntu-latest - steps: - - name: Git Config - run: | - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - - - name: Checkout Repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - - name: Update WPT - run: | - rm -rf test/fixtures/wpt && mkdir test/fixtures/wpt && - - git clone https://github.com/web-platform-tests/wpt.git --depth=1 test/fixtures/tmp-wpt && - - mv test/fixtures/tmp-wpt/LICENSE.md test/fixtures/wpt/LICENSE.md && - - mv test/fixtures/tmp-wpt/common test/fixtures/wpt/common && - mv test/fixtures/tmp-wpt/eventsource test/fixtures/wpt/eventsource && - mv test/fixtures/tmp-wpt/fetch test/fixtures/wpt/fetch && - mv test/fixtures/tmp-wpt/interfaces test/fixtures/wpt/interfaces && - mv test/fixtures/tmp-wpt/mimesniff test/fixtures/wpt/mimesniff && - mv test/fixtures/tmp-wpt/resources test/fixtures/wpt/resources && - mv test/fixtures/tmp-wpt/service-workers test/fixtures/wpt/service-workers && - mv test/fixtures/tmp-wpt/storage test/fixtures/wpt/storage && - mv test/fixtures/tmp-wpt/websockets test/fixtures/wpt/websockets && - mv test/fixtures/tmp-wpt/xhr test/fixtures/wpt/xhr && - - rm -rf test/fixtures/tmp-wpt - - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 - with: - base: main - branch: wpt-update - title: Update WPT - body: Automated update of the WPT - commit-message: "chore: update WPT" diff --git a/.gitignore b/.gitignore index 8a85634911a..6791a8c9f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,14 @@ undici-fetch.js # File generated by /test/request-timeout.js test/request-timeout.10mb.bin + +# Claude files +CLAUDE.md +.claude + +# Ignore .pi +.pi +AGENTS.md + +# Ignore .githuman +.githuman diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..ff6357afd2e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "test/web-platform-tests/wpt"] + path = test/web-platform-tests/wpt + url = https://github.com/web-platform-tests/wpt.git +[submodule "test/fixtures/cache-tests"] + path = test/fixtures/cache-tests + url = https://github.com/http-tests/cache-tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce0b68da4e6..c81acfaaba0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,34 +94,54 @@ Create a commit which includes all of the updated files in lib/llhttp. `undici` runs a subset of the [`web-platform-tests`](https://github.com/web-platform-tests/wpt). -### Requirements: -- [Node core utils](https://github.com/nodejs/node-core-utils) setup with credentials. +### Steps: -To update every test, run the following commands. Typically you would only need to update the tests in a specific directory. +`npm run test:wpt` and `node test/web-platform-tests/wpt-runner.mjs setup` will initialize the WPT submodule automatically when it is missing. + +If you want to prepare the checkout explicitly, run: ```bash -git node wpt resources -git node wpt interfaces -git node wpt common -git node wpt fetch -git node wpt xhr -git node wpt websockets -git node wpt mimesniff -git node wpt storage -git node wpt service-workers -git node wpt eventsource +git submodule update --init --recursive ``` -#### Run the tests +### Run the tests Run the tests to ensure that any new failures are marked as such. -You can mark tests as failing in their corresponding [status](./test/wpt/status) file. +Before running the tests for the first time, you must setup the testing environment. +```bash +cd test/web-platform-tests +node wpt-runner.mjs setup +``` + +To run all tests: ```bash npm run test:wpt ``` +To run a subset of tests: +```bash +cd test/web-platform-tests +node wpt-runner.mjs run [filter] [filterb] +``` + +To run a single file: +```bash +cd test/web-platform-tests +node wpt-runner.mjs run /path/to/test +``` + +### Debugging + +Verbose logging can be enabled by setting the [`NODE_DEBUG`](https://nodejs.org/api/cli.html#node_debugmodule) flag: + +```bash +npx cross-env NODE_DEBUG=UNDICI_WPT node --run test:wpt +``` + +(`npx cross-env` can be omitted on Linux and Mac) + ### Lint diff --git a/README.md b/README.md index 966f2be95ec..82930f1053b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # undici -[![Node CI](https://github.com/nodejs/undici/actions/workflows/nodejs.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![neostandard javascript style](https://img.shields.io/badge/neo-standard-7fffff?style=flat\&labelColor=ff80ff)](https://github.com/neostandard/neostandard) [![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) +[![Node CI](https://github.com/nodejs/undici/actions/workflows/ci.yml/badge.svg)](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [![neostandard javascript style](https://img.shields.io/badge/neo-standard-7fffff?style=flat\&labelColor=ff80ff)](https://github.com/neostandard/neostandard) [![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. @@ -103,7 +103,7 @@ const response = await fetch('https://api.example.com/data'); - Superior performance, especially with `undici.request` - HTTP/1.1 pipelining support - Custom interceptors and middleware -- Advanced features like `ProxyAgent`, `MockAgent` +- Advanced features like `ProxyAgent`, `Socks5Agent`, `MockAgent` **Cons:** - Additional dependency to manage @@ -114,13 +114,15 @@ const response = await fetch('https://api.example.com/data'); #### Use Built-in Fetch When: - You want zero dependencies - Building isomorphic code that runs in browsers and Node.js +- Publishing to npm and want to maximize compatibility with JS runtimes - Simple HTTP requests without advanced configuration -- You're okay with the undici version bundled in your Node.js version +- You're publishing to npm and you want to maximize compatiblity +- You don't depend on features from a specific version of undici #### Use Undici Module When: - You need the latest undici features and performance improvements - You require advanced connection pooling configuration -- You need APIs not available in the built-in fetch (`ProxyAgent`, `MockAgent`, etc.) +- You need APIs not available in the built-in fetch (`ProxyAgent`, `Socks5Agent`, `MockAgent`, etc.) - Performance is critical (use `undici.request` for maximum speed) - You want better error handling and debugging capabilities - You need HTTP/1.1 pipelining or advanced interceptors @@ -152,6 +154,57 @@ const { statusCode, body } = await request('https://api.example.com/data'); const data = await body.json(); ``` +### Keep `fetch` and `FormData` together + +When you send a `FormData` body, keep `fetch` and `FormData` from the same +implementation. + +Use one of these patterns: + +```js +// Built-in globals +const body = new FormData() +body.set('name', 'some') +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +```js +// undici module imports +import { fetch, FormData } from 'undici' + +const body = new FormData() +body.set('name', 'some') +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +If you want the installed `undici` package to provide the globals, call +`install()` first: + +```js +import { install } from 'undici' + +install() + +const body = new FormData() +body.set('name', 'some') +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +`install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and +`FormData` implementations with undici's versions, so they all match. + +Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData` +with the built-in global `fetch()`. + ### Version Compatibility You can check which version of undici is bundled with your Node.js version: @@ -164,6 +217,8 @@ Installing undici as a module allows you to use a newer version than what's bund ## Quick Start +### Basic Request + ```js import { request } from 'undici' @@ -182,6 +237,50 @@ for await (const data of body) { console.log('data', data) } console.log('trailers', trailers) ``` +### Using Cache Interceptor + +Undici provides a powerful HTTP caching interceptor that follows HTTP caching best practices. Here's how to use it: + +```js +import { fetch, Agent, interceptors, cacheStores } from 'undici'; + +// Create a client with cache interceptor +const client = new Agent().compose(interceptors.cache({ + // Optional: Configure cache store (defaults to MemoryCacheStore) + store: new cacheStores.MemoryCacheStore({ + maxSize: 100 * 1024 * 1024, // 100MB + maxCount: 1000, + maxEntrySize: 5 * 1024 * 1024 // 5MB + }), + + // Optional: Specify which HTTP methods to cache (default: ['GET', 'HEAD']) + methods: ['GET', 'HEAD'] +})); + +// Set the global dispatcher to use our caching client +setGlobalDispatcher(client); + +// Now all fetch requests will use the cache +async function getData() { + const response = await fetch('https://api.example.com/data'); + // The server should set appropriate Cache-Control headers in the response + // which the cache will respect based on the cache policy + return response.json(); +} + +// First request - fetches from origin +const data1 = await getData(); + +// Second request - served from cache if within max-age +const data2 = await getData(); +``` + +#### Key Features: +- **Automatic Caching**: Respects `Cache-Control` and `Expires` headers +- **Validation**: Supports `ETag` and `Last-Modified` validation +- **Storage Options**: In-memory or persistent SQLite storage +- **Flexible**: Configure cache size, TTL, and more + ## Global Installation Undici provides an `install()` function to add all WHATWG fetch classes to `globalThis`, making them available globally: @@ -209,12 +308,17 @@ The `install()` function adds the following classes to `globalThis`: - `fetch` - The fetch function - `Headers` - HTTP headers management - `Response` - HTTP response representation -- `Request` - HTTP request representation +- `Request` - HTTP request representation - `FormData` - Form data handling - `WebSocket` - WebSocket client - `CloseEvent`, `ErrorEvent`, `MessageEvent` - WebSocket events - `EventSource` - Server-sent events client +When you call `install()`, these globals come from the same undici +implementation. For example, global `fetch` and global `FormData` will both be +undici's versions, which is the recommended setup if you want to use undici +through globals. + This is useful for: - Polyfilling environments that don't have fetch - Ensuring consistent fetch behavior across different Node.js versions @@ -438,13 +542,14 @@ This behavior is intentional for server-side environments where CORS restriction * 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. 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. +Therefore, __it is important to always either consume or cancel the response body anyway__. ```js // Do @@ -457,23 +562,24 @@ for await (const chunk of body) { const { headers } = await fetch(url); ``` -The same applies for `request` too: +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) +``` + +Note that consuming the response body is _mandatory_ for `request`: + ```js // Do const { body, headers } = await request(url); -await res.body.dump(); // force consumption of body +await body.dump(); // force consumption of body // Do not const { headers } = await request(url); ``` -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 @@ -483,6 +589,12 @@ const headers = await fetch(url, { method: 'HEAD' }) 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. +#### Content-Encoding + +* https://www.rfc-editor.org/rfc/rfc9110#field.content-encoding + +Undici limits the number of `Content-Encoding` layers in a response to **5** to prevent resource exhaustion attacks. If a server responds with more than 5 content-encodings (e.g., `Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip`), the fetch will be rejected with an error. This limit matches the approach taken by [curl](https://curl.se/docs/CVE-2022-32206.html) and [urllib3](https://github.com/advisories/GHSA-gm62-xv2j-4rw9). + #### `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. @@ -618,11 +730,11 @@ and `undici.Agent`) which will enable the family autoselection algorithm when es Undici aligns with the Node.js LTS schedule. The following table shows the supported versions: -| Version | Node.js | End of Life | -|---------|-------------|-------------| -| 5.x | v18.x | 2024-04-30 | -| 6.x | v20.x v22.x | 2026-04-30 | -| 7.x | v24.x | 2027-04-30 | +| Undici Version | Bundled in Node.js | Node.js Versions Supported | End of Life | +|----------------|-------------------|----------------------------|-------------| +| 5.x | 18.x | ≥14.0 (tested: 14, 16, 18) | 2024-04-30 | +| 6.x | 20.x, 22.x | ≥18.17 (tested: 18, 20, 21, 22) | 2026-04-30 | +| 7.x | 24.x | ≥20.18.1 (tested: 20, 22, 24) | 2027-04-30 | ## License diff --git a/benchmarks/api/util.mjs b/benchmarks/api/util.mjs deleted file mode 100644 index 34c54015ed8..00000000000 --- a/benchmarks/api/util.mjs +++ /dev/null @@ -1,37 +0,0 @@ -import { bench, group, run } from 'mitata' -import { isContentTypeText, isContentTypeApplicationJson } from '../../lib/api/util.js' - -const html = 'text/html' -const json = 'application/json; charset=UTF-8' - -group('isContentTypeText', () => { - bench(`isContentTypeText('${html}')`, () => { - return isContentTypeText(html) - }) - bench(`isContentTypeText('${json}')`, () => { - return isContentTypeText(json) - }) - bench('html.startsWith(\'text/\')', () => { - return html.startsWith('text/') - }) - bench('json.startsWith(\'text/\')', () => { - return json.startsWith('text/') - }) -}) - -group('isContentTypeApplicationJson', () => { - bench(`isContentTypeApplicationJson('${html}')`, () => { - return isContentTypeApplicationJson(html) - }) - bench(`isContentTypeApplicationJson('${json}')`, () => { - return isContentTypeApplicationJson(json) - }) - bench('html.startsWith(\'application/json\')', () => { - return html.startsWith('application/json') - }) - bench('json.startsWith(\'application/json\')', () => { - return json.startsWith('application/json') - }) -}) - -await run() diff --git a/benchmarks/cookies/Is-ctl-excluding-htab.mjs b/benchmarks/cookies/is-ctl-excluding-htab.mjs similarity index 100% rename from benchmarks/cookies/Is-ctl-excluding-htab.mjs rename to benchmarks/cookies/is-ctl-excluding-htab.mjs diff --git a/benchmarks/core/parse-headers.mjs b/benchmarks/core/parse-headers.mjs index 439986da93d..a005b3b33b2 100644 --- a/benchmarks/core/parse-headers.mjs +++ b/benchmarks/core/parse-headers.mjs @@ -87,7 +87,7 @@ bench('noop', () => {}) bench('noop', () => {}) bench('noop', () => {}) -group('parseHeaders', () => { +group(() => { bench('parseHeaders', () => { for (let i = 0; i < headers.length; ++i) { parseHeaders(headers[i]) diff --git a/benchmarks/package.json b/benchmarks/package.json index 4885423c96a..67e3a9d9aa4 100644 --- a/benchmarks/package.json +++ b/benchmarks/package.json @@ -15,13 +15,14 @@ "dependencies": { "axios": "^1.6.7", "concurrently": "^9.0.0", - "cronometro": "^4.0.1", + "cronometro": "^5.3.0", "got": "^14.2.0", "mitata": "^1.0.4", "node-fetch": "^3.3.2", "request": "^2.88.2", "superagent": "^10.0.0", - "wait-on": "^8.0.0", - "uWebSockets.js": "uNetworking/uWebSockets.js#v20.52.0" + "tinybench": "^5.0.0", + "uWebSockets.js": "uNetworking/uWebSockets.js#v20.58.0", + "wait-on": "^9.0.1" } } diff --git a/benchmarks/utils/date.mjs b/benchmarks/utils/date.mjs new file mode 100644 index 00000000000..216e5d86593 --- /dev/null +++ b/benchmarks/utils/date.mjs @@ -0,0 +1,29 @@ +import { Bench } from 'tinybench' + +import { parseHttpDate } from '../../lib/util/date.js' + +const asctime = 'Sun Nov 6 08:49:37 1994' +const rfc850 = 'Sunday, 06-Nov-94 08:49:37 GMT' +const imf = 'Sun, 06 Nov 1994 08:49:37 GMT' + +console.assert(parseHttpDate(asctime) instanceof Date, 'asctime should return a Date') +console.assert(parseHttpDate(rfc850) instanceof Date, 'rfc850 should return a Date') +console.assert(parseHttpDate(imf) instanceof Date, 'imf should return a Date') + +const bench = new Bench({ name: 'parseHttpDate' }) + +bench + .add('asctime', () => { + parseHttpDate(asctime) + }) + .add('rfc850', () => { + parseHttpDate(rfc850) + }) + .add('imf', () => { + parseHttpDate(imf) + }) + +await bench.run() + +console.log(bench.name) +console.table(bench.table()) diff --git a/benchmarks/wait.js b/benchmarks/wait.js index 95b9377021c..535c568ea06 100644 --- a/benchmarks/wait.js +++ b/benchmarks/wait.js @@ -18,5 +18,5 @@ waitOn({ timeout: 5000 }).catch((err) => { console.error(err) - process.exit(1) + process.exitCode = 1 }) diff --git a/benchmarks/websocket/messageevent.mjs b/benchmarks/websocket/messageevent.mjs index b146cbcf300..2c46433555d 100644 --- a/benchmarks/websocket/messageevent.mjs +++ b/benchmarks/websocket/messageevent.mjs @@ -13,6 +13,7 @@ group('MessageEvent instantiation', () => { }) bench('global - MessageEvent init', () => { + // eslint-disable-next-line no-restricted-globals return new MessageEvent('event', { data: null, ports: [port1, port2] }) }) }) diff --git a/build/wasm.js b/build/wasm.js index 46cd273a3a2..5357177d12e 100644 --- a/build/wasm.js +++ b/build/wasm.js @@ -2,7 +2,7 @@ const WASM_BUILDER_CONTAINER = 'ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970' // v0.0.9 -const { execSync } = require('node:child_process') +const { execSync, execFileSync } = require('node:child_process') const { writeFileSync, readFileSync } = require('node:fs') const { join, resolve } = require('node:path') @@ -15,7 +15,7 @@ const WASM_CC = process.env.WASM_CC || 'clang' let WASM_CFLAGS = process.env.WASM_CFLAGS || '--sysroot=/usr/share/wasi-sysroot -target wasm32-unknown-wasi' let WASM_LDFLAGS = process.env.WASM_LDFLAGS || '' const WASM_LDLIBS = process.env.WASM_LDLIBS || '' -const WASM_OPT = process.env.WASM_OPT || './wasm-opt' +const WASM_OPT = process.env.WASM_OPT || 'wasm-opt' // For compatibility with Node.js' `configure --shared-builtin-undici/undici-path ...` const EXTERNAL_PATH = process.env.EXTERNAL_PATH @@ -65,21 +65,20 @@ if (process.argv[2] === '--docker') { -t ${WASM_BUILDER_CONTAINER} node build/wasm.js` console.log(`> ${cmd}\n\n`) execSync(cmd, { stdio: 'inherit' }) - process.exit(0) + process.exit(0) // eslint-disable-line n/no-process-exit } const hasApk = (function () { - try { execSync('command -v apk'); return true } catch (error) { return false } + try { execSync('command -v apk'); return true } catch { return false } })() const hasOptimizer = (function () { - try { execSync(`${WASM_OPT} --version`); return true } catch (error) { return false } + try { execSync(`${WASM_OPT} --version`); return true } catch { return false } })() if (hasApk) { // Gather information about the tools used for the build const buildInfo = execSync('apk info -v').toString() if (!buildInfo.includes('wasi-sdk')) { - console.log('Failed to generate build environment information') - process.exit(-1) + throw new Error('Failed to generate build environment information') } console.log(buildInfo) } @@ -104,7 +103,19 @@ ${join(WASM_SRC, 'src')}/*.c \ ${WASM_LDLIBS}`, { stdio: 'inherit' }) if (hasOptimizer) { - execSync(`${WASM_OPT} ${WASM_OPT_FLAGS} --enable-simd -o ${join(WASM_OUT, 'llhttp_simd.wasm')} ${join(WASM_OUT, 'llhttp_simd.wasm')}`, { stdio: 'inherit' }) + // Split WASM_OPT_FLAGS into an array, if not empty + const wasmOptFlagsArray = WASM_OPT_FLAGS ? WASM_OPT_FLAGS.split(/\s+/).filter(Boolean) : [] + execFileSync( + WASM_OPT, + [ + ...wasmOptFlagsArray, + '--enable-simd', + '-o', + join(WASM_OUT, 'llhttp_simd.wasm'), + join(WASM_OUT, 'llhttp_simd.wasm') + ], + { stdio: 'inherit' } + ) } writeWasmChunk('llhttp_simd.wasm', 'llhttp_simd-wasm.js') diff --git a/deps/llhttp/include/api.h b/deps/llhttp/include/api.h deleted file mode 100644 index f1b8e506bd7..00000000000 --- a/deps/llhttp/include/api.h +++ /dev/null @@ -1,357 +0,0 @@ -#ifndef INCLUDE_LLHTTP_API_H_ -#define INCLUDE_LLHTTP_API_H_ -#ifdef __cplusplus -extern "C" { -#endif -#include - -#if defined(__wasm__) -#define LLHTTP_EXPORT __attribute__((visibility("default"))) -#elif defined(_WIN32) -#define LLHTTP_EXPORT __declspec(dllexport) -#else -#define LLHTTP_EXPORT -#endif - -typedef llhttp__internal_t llhttp_t; -typedef struct llhttp_settings_s llhttp_settings_t; - -typedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length); -typedef int (*llhttp_cb)(llhttp_t*); - -struct llhttp_settings_s { - /* Possible return values 0, -1, `HPE_PAUSED` */ - llhttp_cb on_message_begin; - - /* Possible return values 0, -1, HPE_USER */ - llhttp_data_cb on_url; - llhttp_data_cb on_status; - llhttp_data_cb on_method; - llhttp_data_cb on_version; - llhttp_data_cb on_header_field; - llhttp_data_cb on_header_value; - llhttp_data_cb on_chunk_extension_name; - llhttp_data_cb on_chunk_extension_value; - - /* Possible return values: - * 0 - Proceed normally - * 1 - Assume that request/response has no body, and proceed to parsing the - * next message - * 2 - Assume absence of body (as above) and make `llhttp_execute()` return - * `HPE_PAUSED_UPGRADE` - * -1 - Error - * `HPE_PAUSED` - */ - llhttp_cb on_headers_complete; - - /* Possible return values 0, -1, HPE_USER */ - llhttp_data_cb on_body; - - /* Possible return values 0, -1, `HPE_PAUSED` */ - llhttp_cb on_message_complete; - llhttp_cb on_url_complete; - llhttp_cb on_status_complete; - llhttp_cb on_method_complete; - llhttp_cb on_version_complete; - llhttp_cb on_header_field_complete; - llhttp_cb on_header_value_complete; - llhttp_cb on_chunk_extension_name_complete; - llhttp_cb on_chunk_extension_value_complete; - - /* When on_chunk_header is called, the current chunk length is stored - * in parser->content_length. - * Possible return values 0, -1, `HPE_PAUSED` - */ - llhttp_cb on_chunk_header; - llhttp_cb on_chunk_complete; - llhttp_cb on_reset; -}; - -/* Initialize the parser with specific type and user settings. - * - * NOTE: lifetime of `settings` has to be at least the same as the lifetime of - * the `parser` here. In practice, `settings` has to be either a static - * variable or be allocated with `malloc`, `new`, etc. - */ -LLHTTP_EXPORT -void llhttp_init(llhttp_t* parser, llhttp_type_t type, - const llhttp_settings_t* settings); - -LLHTTP_EXPORT -llhttp_t* llhttp_alloc(llhttp_type_t type); - -LLHTTP_EXPORT -void llhttp_free(llhttp_t* parser); - -LLHTTP_EXPORT -uint8_t llhttp_get_type(llhttp_t* parser); - -LLHTTP_EXPORT -uint8_t llhttp_get_http_major(llhttp_t* parser); - -LLHTTP_EXPORT -uint8_t llhttp_get_http_minor(llhttp_t* parser); - -LLHTTP_EXPORT -uint8_t llhttp_get_method(llhttp_t* parser); - -LLHTTP_EXPORT -int llhttp_get_status_code(llhttp_t* parser); - -LLHTTP_EXPORT -uint8_t llhttp_get_upgrade(llhttp_t* parser); - -/* Reset an already initialized parser back to the start state, preserving the - * existing parser type, callback settings, user data, and lenient flags. - */ -LLHTTP_EXPORT -void llhttp_reset(llhttp_t* parser); - -/* Initialize the settings object */ -LLHTTP_EXPORT -void llhttp_settings_init(llhttp_settings_t* settings); - -/* Parse full or partial request/response, invoking user callbacks along the - * way. - * - * If any of `llhttp_data_cb` returns errno not equal to `HPE_OK` - the parsing - * interrupts, and such errno is returned from `llhttp_execute()`. If - * `HPE_PAUSED` was used as a errno, the execution can be resumed with - * `llhttp_resume()` call. - * - * In a special case of CONNECT/Upgrade request/response `HPE_PAUSED_UPGRADE` - * is returned after fully parsing the request/response. If the user wishes to - * continue parsing, they need to invoke `llhttp_resume_after_upgrade()`. - * - * NOTE: if this function ever returns a non-pause type error, it will continue - * to return the same error upon each successive call up until `llhttp_init()` - * is called. - */ -LLHTTP_EXPORT -llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len); - -/* This method should be called when the other side has no further bytes to - * send (e.g. shutdown of readable side of the TCP connection.) - * - * Requests without `Content-Length` and other messages might require treating - * all incoming bytes as the part of the body, up to the last byte of the - * connection. This method will invoke `on_message_complete()` callback if the - * request was terminated safely. Otherwise a error code would be returned. - */ -LLHTTP_EXPORT -llhttp_errno_t llhttp_finish(llhttp_t* parser); - -/* Returns `1` if the incoming message is parsed until the last byte, and has - * to be completed by calling `llhttp_finish()` on EOF - */ -LLHTTP_EXPORT -int llhttp_message_needs_eof(const llhttp_t* parser); - -/* Returns `1` if there might be any other messages following the last that was - * successfully parsed. - */ -LLHTTP_EXPORT -int llhttp_should_keep_alive(const llhttp_t* parser); - -/* Make further calls of `llhttp_execute()` return `HPE_PAUSED` and set - * appropriate error reason. - * - * Important: do not call this from user callbacks! User callbacks must return - * `HPE_PAUSED` if pausing is required. - */ -LLHTTP_EXPORT -void llhttp_pause(llhttp_t* parser); - -/* Might be called to resume the execution after the pause in user's callback. - * See `llhttp_execute()` above for details. - * - * Call this only if `llhttp_execute()` returns `HPE_PAUSED`. - */ -LLHTTP_EXPORT -void llhttp_resume(llhttp_t* parser); - -/* Might be called to resume the execution after the pause in user's callback. - * See `llhttp_execute()` above for details. - * - * Call this only if `llhttp_execute()` returns `HPE_PAUSED_UPGRADE` - */ -LLHTTP_EXPORT -void llhttp_resume_after_upgrade(llhttp_t* parser); - -/* Returns the latest return error */ -LLHTTP_EXPORT -llhttp_errno_t llhttp_get_errno(const llhttp_t* parser); - -/* Returns the verbal explanation of the latest returned error. - * - * Note: User callback should set error reason when returning the error. See - * `llhttp_set_error_reason()` for details. - */ -LLHTTP_EXPORT -const char* llhttp_get_error_reason(const llhttp_t* parser); - -/* Assign verbal description to the returned error. Must be called in user - * callbacks right before returning the errno. - * - * Note: `HPE_USER` error code might be useful in user callbacks. - */ -LLHTTP_EXPORT -void llhttp_set_error_reason(llhttp_t* parser, const char* reason); - -/* Returns the pointer to the last parsed byte before the returned error. The - * pointer is relative to the `data` argument of `llhttp_execute()`. - * - * Note: this method might be useful for counting the number of parsed bytes. - */ -LLHTTP_EXPORT -const char* llhttp_get_error_pos(const llhttp_t* parser); - -/* Returns textual name of error code */ -LLHTTP_EXPORT -const char* llhttp_errno_name(llhttp_errno_t err); - -/* Returns textual name of HTTP method */ -LLHTTP_EXPORT -const char* llhttp_method_name(llhttp_method_t method); - -/* Returns textual name of HTTP status */ -LLHTTP_EXPORT -const char* llhttp_status_name(llhttp_status_t status); - -/* Enables/disables lenient header value parsing (disabled by default). - * - * Lenient parsing disables header value token checks, extending llhttp's - * protocol support to highly non-compliant clients/server. No - * `HPE_INVALID_HEADER_TOKEN` will be raised for incorrect header values when - * lenient parsing is "on". - * - * **Enabling this flag can pose a security issue since you will be exposed to - * request smuggling attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_headers(llhttp_t* parser, int enabled); - - -/* Enables/disables lenient handling of conflicting `Transfer-Encoding` and - * `Content-Length` headers (disabled by default). - * - * Normally `llhttp` would error when `Transfer-Encoding` is present in - * conjunction with `Content-Length`. This error is important to prevent HTTP - * request smuggling, but may be less desirable for small number of cases - * involving legacy servers. - * - * **Enabling this flag can pose a security issue since you will be exposed to - * request smuggling attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled); - - -/* Enables/disables lenient handling of `Connection: close` and HTTP/1.0 - * requests responses. - * - * Normally `llhttp` would error on (in strict mode) or discard (in loose mode) - * the HTTP request/response after the request/response with `Connection: close` - * and `Content-Length`. This is important to prevent cache poisoning attacks, - * but might interact badly with outdated and insecure clients. With this flag - * the extra request/response will be parsed normally. - * - * **Enabling this flag can pose a security issue since you will be exposed to - * poisoning attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled); - -/* Enables/disables lenient handling of `Transfer-Encoding` header. - * - * Normally `llhttp` would error when a `Transfer-Encoding` has `chunked` value - * and another value after it (either in a single header or in multiple - * headers whose value are internally joined using `, `). - * This is mandated by the spec to reliably determine request body size and thus - * avoid request smuggling. - * With this flag the extra value will be parsed normally. - * - * **Enabling this flag can pose a security issue since you will be exposed to - * request smuggling attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled); - -/* Enables/disables lenient handling of HTTP version. - * - * Normally `llhttp` would error when the HTTP version in the request or status line - * is not `0.9`, `1.0`, `1.1` or `2.0`. - * With this flag the invalid value will be parsed normally. - * - * **Enabling this flag can pose a security issue since you will allow unsupported - * HTTP versions. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_version(llhttp_t* parser, int enabled); - -/* Enables/disables lenient handling of additional data received after a message ends - * and keep-alive is disabled. - * - * Normally `llhttp` would error when additional unexpected data is received if the message - * contains the `Connection` header with `close` value. - * With this flag the extra data will discarded without throwing an error. - * - * **Enabling this flag can pose a security issue since you will be exposed to - * poisoning attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled); - -/* Enables/disables lenient handling of incomplete CRLF sequences. - * - * Normally `llhttp` would error when a CR is not followed by LF when terminating the - * request line, the status line, the headers or a chunk header. - * With this flag only a CR is required to terminate such sections. - * - * **Enabling this flag can pose a security issue since you will be exposed to - * request smuggling attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled); - -/* - * Enables/disables lenient handling of line separators. - * - * Normally `llhttp` would error when a LF is not preceded by CR when terminating the - * request line, the status line, the headers, a chunk header or a chunk data. - * With this flag only a LF is required to terminate such sections. - * - * **Enabling this flag can pose a security issue since you will be exposed to - * request smuggling attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled); - -/* Enables/disables lenient handling of chunks not separated via CRLF. - * - * Normally `llhttp` would error when after a chunk data a CRLF is missing before - * starting a new chunk. - * With this flag the new chunk can start immediately after the previous one. - * - * **Enabling this flag can pose a security issue since you will be exposed to - * request smuggling attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled); - -/* Enables/disables lenient handling of spaces after chunk size. - * - * Normally `llhttp` would error when after a chunk size is followed by one or more - * spaces are present instead of a CRLF or `;`. - * With this flag this check is disabled. - * - * **Enabling this flag can pose a security issue since you will be exposed to - * request smuggling attacks. USE WITH CAUTION!** - */ -LLHTTP_EXPORT -void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled); - -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* INCLUDE_LLHTTP_API_H_ */ diff --git a/deps/llhttp/include/llhttp.h b/deps/llhttp/include/llhttp.h index 691969367ae..60544596a99 100644 --- a/deps/llhttp/include/llhttp.h +++ b/deps/llhttp/include/llhttp.h @@ -3,7 +3,7 @@ #define INCLUDE_LLHTTP_H_ #define LLHTTP_VERSION_MAJOR 9 -#define LLHTTP_VERSION_MINOR 2 +#define LLHTTP_VERSION_MINOR 3 #define LLHTTP_VERSION_PATCH 0 #ifndef INCLUDE_LLHTTP_ITSELF_H_ @@ -90,7 +90,8 @@ enum llhttp_errno { HPE_CB_HEADER_VALUE_COMPLETE = 29, HPE_CB_CHUNK_EXTENSION_NAME_COMPLETE = 34, HPE_CB_CHUNK_EXTENSION_VALUE_COMPLETE = 35, - HPE_CB_RESET = 31 + HPE_CB_RESET = 31, + HPE_CB_PROTOCOL_COMPLETE = 38 }; typedef enum llhttp_errno llhttp_errno_t; @@ -326,6 +327,7 @@ typedef enum llhttp_status llhttp_status_t; XX(34, CB_CHUNK_EXTENSION_NAME_COMPLETE, CB_CHUNK_EXTENSION_NAME_COMPLETE) \ XX(35, CB_CHUNK_EXTENSION_VALUE_COMPLETE, CB_CHUNK_EXTENSION_VALUE_COMPLETE) \ XX(31, CB_RESET, CB_RESET) \ + XX(38, CB_PROTOCOL_COMPLETE, CB_PROTOCOL_COMPLETE) \ #define HTTP_METHOD_MAP(XX) \ @@ -567,6 +569,7 @@ struct llhttp_settings_s { llhttp_cb on_message_begin; /* Possible return values 0, -1, HPE_USER */ + llhttp_data_cb on_protocol; llhttp_data_cb on_url; llhttp_data_cb on_status; llhttp_data_cb on_method; @@ -592,6 +595,7 @@ struct llhttp_settings_s { /* Possible return values 0, -1, `HPE_PAUSED` */ llhttp_cb on_message_complete; + llhttp_cb on_protocol_complete; llhttp_cb on_url_complete; llhttp_cb on_status_complete; llhttp_cb on_method_complete; diff --git a/deps/llhttp/src/api.c b/deps/llhttp/src/api.c index 8c2ce3dc5c4..0245254177a 100644 --- a/deps/llhttp/src/api.c +++ b/deps/llhttp/src/api.c @@ -57,29 +57,14 @@ static int wasm_on_headers_complete_wrap(llhttp_t* p) { } const llhttp_settings_t wasm_settings = { - wasm_on_message_begin, - wasm_on_url, - wasm_on_status, - NULL, - NULL, - wasm_on_header_field, - wasm_on_header_value, - NULL, - NULL, - wasm_on_headers_complete_wrap, - wasm_on_body, - wasm_on_message_complete, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, + .on_message_begin = wasm_on_message_begin, + .on_url = wasm_on_url, + .on_status = wasm_on_status, + .on_header_field = wasm_on_header_field, + .on_header_value = wasm_on_header_value, + .on_headers_complete = wasm_on_headers_complete_wrap, + .on_body = wasm_on_body, + .on_message_complete = wasm_on_message_complete, }; @@ -341,6 +326,20 @@ int llhttp__on_message_begin(llhttp_t* s, const char* p, const char* endp) { } +int llhttp__on_protocol(llhttp_t* s, const char* p, const char* endp) { + int err; + SPAN_CALLBACK_MAYBE(s, on_protocol, p, endp - p); + return err; +} + + +int llhttp__on_protocol_complete(llhttp_t* s, const char* p, const char* endp) { + int err; + CALLBACK_MAYBE(s, on_protocol_complete); + return err; +} + + int llhttp__on_url(llhttp_t* s, const char* p, const char* endp) { int err; SPAN_CALLBACK_MAYBE(s, on_url, p, endp - p); diff --git a/deps/llhttp/src/llhttp.c b/deps/llhttp/src/llhttp.c index c08de6494f7..aa4c4682097 100644 --- a/deps/llhttp/src/llhttp.c +++ b/deps/llhttp/src/llhttp.c @@ -10,10 +10,20 @@ #endif /* _MSC_VER */ #endif /* __SSE4_2__ */ +#ifdef __ARM_NEON__ + #include +#endif /* __ARM_NEON__ */ + +#ifdef __wasm__ + #include +#endif /* __wasm__ */ + #ifdef _MSC_VER #define ALIGN(n) _declspec(align(n)) + #define UNREACHABLE __assume(0) #else /* !_MSC_VER */ #define ALIGN(n) __attribute__((aligned(n))) + #define UNREACHABLE __builtin_unreachable() #endif /* _MSC_VER */ #include "llhttp.h" @@ -72,16 +82,16 @@ static const unsigned char llparse_blob12[] = { 'p', 'g', 'r', 'a', 'd', 'e' }; static const unsigned char llparse_blob13[] = { - 'T', 'T', 'P', '/' + 'T', 'T', 'P' }; static const unsigned char llparse_blob14[] = { 0xd, 0xa, 0xd, 0xa, 'S', 'M', 0xd, 0xa, 0xd, 0xa }; static const unsigned char llparse_blob15[] = { - 'C', 'E', '/' + 'C', 'E' }; static const unsigned char llparse_blob16[] = { - 'T', 'S', 'P', '/' + 'T', 'S', 'P' }; static const unsigned char llparse_blob17[] = { 'N', 'O', 'U', 'N', 'C', 'E' @@ -207,12 +217,18 @@ static const unsigned char llparse_blob57[] = { 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E' }; static const unsigned char llparse_blob58[] = { - 'H', 'T', 'T', 'P', '/' + 'T', 'T', 'P' }; static const unsigned char llparse_blob59[] = { - 'A', 'D' + 'C', 'E' }; static const unsigned char llparse_blob60[] = { + 'T', 'S', 'P' +}; +static const unsigned char llparse_blob61[] = { + 'A', 'D' +}; +static const unsigned char llparse_blob62[] = { 'T', 'P', '/' }; @@ -379,7 +395,7 @@ enum llparse_state_e { s_n_llhttp__internal__n_header_value_almost_done, s_n_llhttp__internal__n_invoke_test_lenient_flags_17, s_n_llhttp__internal__n_header_value_lenient, - s_n_llhttp__internal__n_error_53, + s_n_llhttp__internal__n_error_54, s_n_llhttp__internal__n_header_value_otherwise, s_n_llhttp__internal__n_header_value_connection_token, s_n_llhttp__internal__n_header_value_connection_ws, @@ -387,12 +403,12 @@ enum llparse_state_e { s_n_llhttp__internal__n_header_value_connection_2, s_n_llhttp__internal__n_header_value_connection_3, s_n_llhttp__internal__n_header_value_connection, - s_n_llhttp__internal__n_error_55, s_n_llhttp__internal__n_error_56, + s_n_llhttp__internal__n_error_57, s_n_llhttp__internal__n_header_value_content_length_ws, s_n_llhttp__internal__n_header_value_content_length, + s_n_llhttp__internal__n_error_59, s_n_llhttp__internal__n_error_58, - s_n_llhttp__internal__n_error_57, s_n_llhttp__internal__n_header_value_te_token_ows, s_n_llhttp__internal__n_header_value, s_n_llhttp__internal__n_header_value_te_token, @@ -425,17 +441,27 @@ enum llparse_state_e { s_n_llhttp__internal__n_req_http_complete, s_n_llhttp__internal__n_invoke_load_method_1, s_n_llhttp__internal__n_invoke_llhttp__on_version_complete, - s_n_llhttp__internal__n_error_65, - s_n_llhttp__internal__n_error_72, + s_n_llhttp__internal__n_error_67, + s_n_llhttp__internal__n_error_74, s_n_llhttp__internal__n_req_http_minor, - s_n_llhttp__internal__n_error_73, + s_n_llhttp__internal__n_error_75, s_n_llhttp__internal__n_req_http_dot, - s_n_llhttp__internal__n_error_74, + s_n_llhttp__internal__n_error_76, s_n_llhttp__internal__n_req_http_major, s_n_llhttp__internal__n_span_start_llhttp__on_version, - s_n_llhttp__internal__n_req_http_start_1, - s_n_llhttp__internal__n_req_http_start_2, - s_n_llhttp__internal__n_req_http_start_3, + s_n_llhttp__internal__n_req_after_protocol, + s_n_llhttp__internal__n_invoke_load_method, + s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete, + s_n_llhttp__internal__n_error_82, + s_n_llhttp__internal__n_req_after_http_start_1, + s_n_llhttp__internal__n_invoke_load_method_2, + s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1, + s_n_llhttp__internal__n_req_after_http_start_2, + s_n_llhttp__internal__n_invoke_load_method_3, + s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2, + s_n_llhttp__internal__n_req_after_http_start_3, + s_n_llhttp__internal__n_req_after_http_start, + s_n_llhttp__internal__n_span_start_llhttp__on_protocol, s_n_llhttp__internal__n_req_http_start, s_n_llhttp__internal__n_url_to_http, s_n_llhttp__internal__n_url_skip_to_http, @@ -534,7 +560,7 @@ enum llparse_state_e { s_n_llhttp__internal__n_after_start_req, s_n_llhttp__internal__n_span_start_llhttp__on_method_1, s_n_llhttp__internal__n_res_line_almost_done, - s_n_llhttp__internal__n_invoke_test_lenient_flags_29, + s_n_llhttp__internal__n_invoke_test_lenient_flags_30, s_n_llhttp__internal__n_res_status, s_n_llhttp__internal__n_span_start_llhttp__on_status, s_n_llhttp__internal__n_res_status_code_otherwise, @@ -543,15 +569,22 @@ enum llparse_state_e { s_n_llhttp__internal__n_res_status_code_digit_1, s_n_llhttp__internal__n_res_after_version, s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1, - s_n_llhttp__internal__n_error_88, - s_n_llhttp__internal__n_error_102, + s_n_llhttp__internal__n_error_93, + s_n_llhttp__internal__n_error_107, s_n_llhttp__internal__n_res_http_minor, - s_n_llhttp__internal__n_error_103, + s_n_llhttp__internal__n_error_108, s_n_llhttp__internal__n_res_http_dot, - s_n_llhttp__internal__n_error_104, + s_n_llhttp__internal__n_error_109, s_n_llhttp__internal__n_res_http_major, s_n_llhttp__internal__n_span_start_llhttp__on_version_1, - s_n_llhttp__internal__n_start_res, + s_n_llhttp__internal__n_res_after_protocol, + s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3, + s_n_llhttp__internal__n_error_115, + s_n_llhttp__internal__n_res_after_start_1, + s_n_llhttp__internal__n_res_after_start_2, + s_n_llhttp__internal__n_res_after_start_3, + s_n_llhttp__internal__n_res_after_start, + s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1, s_n_llhttp__internal__n_invoke_llhttp__on_method_complete, s_n_llhttp__internal__n_req_or_res_method_2, s_n_llhttp__internal__n_invoke_update_type_1, @@ -574,6 +607,10 @@ int llhttp__on_url( llhttp__internal_t* s, const unsigned char* p, const unsigned char* endp); +int llhttp__on_protocol( + llhttp__internal_t* s, const unsigned char* p, + const unsigned char* endp); + int llhttp__on_version( llhttp__internal_t* s, const unsigned char* p, const unsigned char* endp); @@ -887,7 +924,7 @@ int llhttp__internal__c_test_flags_4( return (state->flags & 512) == 512; } -int llhttp__internal__c_test_lenient_flags_21( +int llhttp__internal__c_test_lenient_flags_22( llhttp__internal_t* state, const unsigned char* p, const unsigned char* endp) { @@ -1018,7 +1055,7 @@ int llhttp__internal__c_test_flags_3( return (state->flags & 8) == 8; } -int llhttp__internal__c_test_lenient_flags_19( +int llhttp__internal__c_test_lenient_flags_20( llhttp__internal_t* state, const unsigned char* p, const unsigned char* endp) { @@ -1057,6 +1094,10 @@ int llhttp__internal__c_or_flags_20( return 0; } +int llhttp__on_protocol_complete( + llhttp__internal_t* s, const unsigned char* p, + const unsigned char* endp); + int llhttp__internal__c_load_method( llhttp__internal_t* state, const unsigned char* p, @@ -1082,7 +1123,7 @@ int llhttp__internal__c_store_http_minor( return 0; } -int llhttp__internal__c_test_lenient_flags_23( +int llhttp__internal__c_test_lenient_flags_24( llhttp__internal_t* state, const unsigned char* p, const unsigned char* endp) { @@ -1192,8 +1233,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_test_lenient_flags_3; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__after_message_complete: s_n_llhttp__internal__n_invoke_llhttp__after_message_complete: { @@ -1203,8 +1243,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_update_finish_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_pause_1: s_n_llhttp__internal__n_pause_1: { @@ -1213,8 +1252,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__after_message_complete; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_is_equal_upgrade: s_n_llhttp__internal__n_invoke_is_equal_upgrade: { @@ -1224,8 +1262,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_pause_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2: s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2: { @@ -1237,8 +1274,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_38; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_data_almost_done_1: s_n_llhttp__internal__n_chunk_data_almost_done_1: { @@ -1254,8 +1290,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_test_lenient_flags_7; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_data_almost_done: s_n_llhttp__internal__n_chunk_data_almost_done: { @@ -1275,8 +1310,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_test_lenient_flags_7; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_consume_content_length: s_n_llhttp__internal__n_consume_content_length: { @@ -1293,8 +1327,7 @@ static llparse_state_t llhttp__internal__run( state->content_length -= avail; return s_n_llhttp__internal__n_consume_content_length; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_body: s_n_llhttp__internal__n_span_start_llhttp__on_body: { @@ -1304,8 +1337,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_body; goto s_n_llhttp__internal__n_consume_content_length; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_is_equal_content_length: s_n_llhttp__internal__n_invoke_is_equal_content_length: { @@ -1315,8 +1347,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_or_flags; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_size_almost_done: s_n_llhttp__internal__n_chunk_size_almost_done: { @@ -1332,8 +1363,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_test_lenient_flags_8; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_test_lenient_flags_9: s_n_llhttp__internal__n_invoke_test_lenient_flags_9: { @@ -1343,8 +1373,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_20; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete: s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete: { @@ -1356,8 +1385,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_19; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_1: s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_1: { @@ -1369,8 +1397,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_21; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_2: s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_2: { @@ -1382,8 +1409,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_22; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_test_lenient_flags_10: s_n_llhttp__internal__n_invoke_test_lenient_flags_10: { @@ -1393,8 +1419,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_25; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete: s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete: { @@ -1406,8 +1431,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_24; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_1: s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_1: { @@ -1419,8 +1443,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_26; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_extension_quoted_value_done: s_n_llhttp__internal__n_chunk_extension_quoted_value_done: { @@ -1443,8 +1466,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_error_29; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_2: s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_2: { @@ -1456,8 +1478,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_27; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_error_30: s_n_llhttp__internal__n_error_30: { @@ -1466,8 +1487,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_extension_quoted_value_quoted_pair: s_n_llhttp__internal__n_chunk_extension_quoted_value_quoted_pair: { @@ -1501,8 +1521,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_3; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_error_31: s_n_llhttp__internal__n_error_31: { @@ -1511,8 +1530,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_extension_quoted_value: s_n_llhttp__internal__n_chunk_extension_quoted_value: { @@ -1554,8 +1572,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_4; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_3: s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_3: { @@ -1567,8 +1584,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_32; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_error_33: s_n_llhttp__internal__n_error_33: { @@ -1577,8 +1593,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_extension_value: s_n_llhttp__internal__n_chunk_extension_value: { @@ -1625,8 +1640,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_6; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value: s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value: { @@ -1636,8 +1650,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_chunk_extension_value; goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_3; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_error_34: s_n_llhttp__internal__n_error_34: { @@ -1646,8 +1659,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_extension_name: s_n_llhttp__internal__n_chunk_extension_name: { @@ -1693,8 +1705,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_4; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_name: s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_name: { @@ -1704,8 +1715,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_chunk_extension_name; goto s_n_llhttp__internal__n_chunk_extension_name; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_extensions: s_n_llhttp__internal__n_chunk_extensions: { @@ -1725,8 +1735,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_name; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_size_otherwise: s_n_llhttp__internal__n_chunk_size_otherwise: { @@ -1758,8 +1767,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_error_35; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_size: s_n_llhttp__internal__n_chunk_size: { @@ -1881,8 +1889,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_chunk_size_otherwise; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_chunk_size_digit: s_n_llhttp__internal__n_chunk_size_digit: { @@ -2004,8 +2011,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_error_37; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_update_content_length_1: s_n_llhttp__internal__n_invoke_update_content_length_1: { @@ -2013,8 +2019,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_chunk_size_digit; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_consume_content_length_1: s_n_llhttp__internal__n_consume_content_length_1: { @@ -2031,8 +2036,7 @@ static llparse_state_t llhttp__internal__run( state->content_length -= avail; return s_n_llhttp__internal__n_consume_content_length_1; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_body_1: s_n_llhttp__internal__n_span_start_llhttp__on_body_1: { @@ -2042,8 +2046,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_body; goto s_n_llhttp__internal__n_consume_content_length_1; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_eof: s_n_llhttp__internal__n_eof: { @@ -2052,8 +2055,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_eof; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_body_2: s_n_llhttp__internal__n_span_start_llhttp__on_body_2: { @@ -2063,8 +2065,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_body; goto s_n_llhttp__internal__n_eof; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete: s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete: { @@ -2082,8 +2083,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_llhttp__on_message_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_error_5: s_n_llhttp__internal__n_error_5: { @@ -2092,8 +2092,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_headers_almost_done: s_n_llhttp__internal__n_headers_almost_done: { @@ -2109,8 +2108,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_test_lenient_flags_12; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_colon_discard_ws: s_n_llhttp__internal__n_header_field_colon_discard_ws: { @@ -2126,8 +2124,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_field_colon; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete: s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete: { @@ -2139,8 +2136,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_48; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_header_value: s_n_llhttp__internal__n_span_start_llhttp__on_header_value: { @@ -2150,8 +2146,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_header_value; goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_discard_lws: s_n_llhttp__internal__n_header_value_discard_lws: { @@ -2171,8 +2166,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_load_header_state_1; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_discard_ws_almost_done: s_n_llhttp__internal__n_header_value_discard_ws_almost_done: { @@ -2188,8 +2182,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_test_lenient_flags_16; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_lws: s_n_llhttp__internal__n_header_value_lws: { @@ -2198,17 +2191,16 @@ static llparse_state_t llhttp__internal__run( } switch (*p) { case 9: { - goto s_n_llhttp__internal__n_invoke_load_header_state_4; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_18; } case ' ': { - goto s_n_llhttp__internal__n_invoke_load_header_state_4; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_18; } default: { goto s_n_llhttp__internal__n_invoke_load_header_state_5; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_almost_done: s_n_llhttp__internal__n_header_value_almost_done: { @@ -2221,11 +2213,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_lws; } default: { - goto s_n_llhttp__internal__n_error_52; + goto s_n_llhttp__internal__n_error_53; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_test_lenient_flags_17: s_n_llhttp__internal__n_invoke_test_lenient_flags_17: { @@ -2235,8 +2226,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_51; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_lenient: s_n_llhttp__internal__n_header_value_lenient: { @@ -2255,18 +2245,16 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_lenient; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_53: - s_n_llhttp__internal__n_error_53: { + case s_n_llhttp__internal__n_error_54: + s_n_llhttp__internal__n_error_54: { state->error = 0xa; state->reason = "Invalid header value char"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_otherwise: s_n_llhttp__internal__n_header_value_otherwise: { @@ -2281,11 +2269,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_2; } default: { - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_18; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_19; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_connection_token: s_n_llhttp__internal__n_header_value_connection_token: { @@ -2323,8 +2310,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_otherwise; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_connection_ws: s_n_llhttp__internal__n_header_value_connection_ws: { @@ -2350,8 +2336,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_5; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_connection_1: s_n_llhttp__internal__n_header_value_connection_1: { @@ -2374,8 +2359,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_connection_token; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_connection_2: s_n_llhttp__internal__n_header_value_connection_2: { @@ -2398,8 +2382,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_connection_token; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_connection_3: s_n_llhttp__internal__n_header_value_connection_3: { @@ -2422,8 +2405,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_connection_token; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_connection: s_n_llhttp__internal__n_header_value_connection: { @@ -2455,28 +2437,25 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_connection_token; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_55: - s_n_llhttp__internal__n_error_55: { + case s_n_llhttp__internal__n_error_56: + s_n_llhttp__internal__n_error_56: { state->error = 0xb; state->reason = "Content-Length overflow"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_56: - s_n_llhttp__internal__n_error_56: { + case s_n_llhttp__internal__n_error_57: + s_n_llhttp__internal__n_error_57: { state->error = 0xb; state->reason = "Invalid character in Content-Length"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_content_length_ws: s_n_llhttp__internal__n_header_value_content_length_ws: { @@ -2498,8 +2477,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_7; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_content_length: s_n_llhttp__internal__n_header_value_content_length: { @@ -2561,28 +2539,25 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_content_length_ws; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_58: - s_n_llhttp__internal__n_error_58: { + case s_n_llhttp__internal__n_error_59: + s_n_llhttp__internal__n_error_59: { state->error = 0xf; state->reason = "Invalid `Transfer-Encoding` header value"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_57: - s_n_llhttp__internal__n_error_57: { + case s_n_llhttp__internal__n_error_58: + s_n_llhttp__internal__n_error_58: { state->error = 0xf; state->reason = "Invalid `Transfer-Encoding` header value"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_te_token_ows: s_n_llhttp__internal__n_header_value_te_token_ows: { @@ -2602,8 +2577,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_te_chunked; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value: s_n_llhttp__internal__n_header_value: { @@ -2632,7 +2606,6 @@ static llparse_state_t llhttp__internal__run( if (endp - p >= 16) { __m128i ranges; __m128i input; - int avail; int match_len; /* Load input */ @@ -2652,6 +2625,78 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_otherwise; } #endif /* __SSE4_2__ */ + #ifdef __ARM_NEON__ + while (endp - p >= 16) { + uint8x16_t input; + uint8x16_t single; + uint8x16_t mask; + uint8x8_t narrow; + uint64_t match_mask; + int match_len; + + /* Load input */ + input = vld1q_u8(p); + /* Find first character that does not match `ranges` */ + single = vceqq_u8(input, vdupq_n_u8(0x9)); + mask = single; + single = vandq_u16( + vcgeq_u8(input, vdupq_n_u8(' ')), + vcleq_u8(input, vdupq_n_u8('~')) + ); + mask = vorrq_u16(mask, single); + single = vandq_u16( + vcgeq_u8(input, vdupq_n_u8(0x80)), + vcleq_u8(input, vdupq_n_u8(0xff)) + ); + mask = vorrq_u16(mask, single); + narrow = vshrn_n_u16(mask, 4); + match_mask = ~vget_lane_u64(vreinterpret_u64_u8(narrow), 0); + match_len = __builtin_ctzll(match_mask) >> 2; + if (match_len != 16) { + p += match_len; + goto s_n_llhttp__internal__n_header_value_otherwise; + } + p += 16; + } + if (p == endp) { + return s_n_llhttp__internal__n_header_value; + } + #endif /* __ARM_NEON__ */ + #ifdef __wasm_simd128__ + while (endp - p >= 16) { + v128_t input; + v128_t mask; + v128_t single; + int match_len; + + /* Load input */ + input = wasm_v128_load(p); + /* Find first character that does not match `ranges` */ + single = wasm_i8x16_eq(input, wasm_u8x16_const_splat(0x9)); + mask = single; + single = wasm_v128_and( + wasm_i8x16_ge(input, wasm_u8x16_const_splat(' ')), + wasm_i8x16_le(input, wasm_u8x16_const_splat('~')) + ); + mask = wasm_v128_or(mask, single); + single = wasm_v128_and( + wasm_i8x16_ge(input, wasm_u8x16_const_splat(0x80)), + wasm_i8x16_le(input, wasm_u8x16_const_splat(0xff)) + ); + mask = wasm_v128_or(mask, single); + match_len = __builtin_ctz( + ~wasm_i8x16_bitmask(mask) + ); + if (match_len != 16) { + p += match_len; + goto s_n_llhttp__internal__n_header_value_otherwise; + } + p += 16; + } + if (p == endp) { + return s_n_llhttp__internal__n_header_value; + } + #endif /* __wasm_simd128__ */ switch (lookup_table[(uint8_t) *p]) { case 1: { p++; @@ -2661,8 +2706,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_otherwise; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_te_token: s_n_llhttp__internal__n_header_value_te_token: { @@ -2700,8 +2744,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_9; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_te_chunked_last: s_n_llhttp__internal__n_header_value_te_chunked_last: { @@ -2726,8 +2769,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_te_token; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_te_chunked: s_n_llhttp__internal__n_header_value_te_chunked: { @@ -2750,8 +2792,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_value_te_token; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1: s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1: { @@ -2761,8 +2802,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_header_value; goto s_n_llhttp__internal__n_invoke_load_header_state_3; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_value_discard_ws: s_n_llhttp__internal__n_header_value_discard_ws: { @@ -2790,8 +2830,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_load_header_state: s_n_llhttp__internal__n_invoke_load_header_state: { @@ -2803,8 +2842,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_header_value_discard_ws; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete: s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete: { @@ -2816,8 +2854,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_45; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_general_otherwise: s_n_llhttp__internal__n_header_field_general_otherwise: { @@ -2829,11 +2866,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_header_field_2; } default: { - goto s_n_llhttp__internal__n_error_61; + goto s_n_llhttp__internal__n_error_62; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_general: s_n_llhttp__internal__n_header_field_general: { @@ -2862,7 +2898,6 @@ static llparse_state_t llhttp__internal__run( if (endp - p >= 16) { __m128i ranges; __m128i input; - int avail; int match_len; /* Load input */ @@ -2903,8 +2938,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_field_general_otherwise; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_colon: s_n_llhttp__internal__n_header_field_colon: { @@ -2922,8 +2956,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_10; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_3: s_n_llhttp__internal__n_header_field_3: { @@ -2947,8 +2980,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_11; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_4: s_n_llhttp__internal__n_header_field_4: { @@ -2972,8 +3004,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_11; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_2: s_n_llhttp__internal__n_header_field_2: { @@ -2993,8 +3024,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_11; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_1: s_n_llhttp__internal__n_header_field_1: { @@ -3017,8 +3047,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_11; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_5: s_n_llhttp__internal__n_header_field_5: { @@ -3042,8 +3071,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_11; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_6: s_n_llhttp__internal__n_header_field_6: { @@ -3067,8 +3095,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_11; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_7: s_n_llhttp__internal__n_header_field_7: { @@ -3092,8 +3119,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_11; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field: s_n_llhttp__internal__n_header_field: { @@ -3121,8 +3147,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_header_state_11; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_header_field: s_n_llhttp__internal__n_span_start_llhttp__on_header_field: { @@ -3132,8 +3157,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_header_field; goto s_n_llhttp__internal__n_header_field; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_header_field_start: s_n_llhttp__internal__n_header_field_start: { @@ -3156,8 +3180,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_start_llhttp__on_header_field; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_headers_start: s_n_llhttp__internal__n_headers_start: { @@ -3173,8 +3196,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_header_field_start; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_to_http_09: s_n_llhttp__internal__n_url_to_http_09: { @@ -3194,8 +3216,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_http_major; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_skip_to_http09: s_n_llhttp__internal__n_url_skip_to_http09: { @@ -3216,8 +3237,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_to_http_09; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_skip_lf_to_http09_1: s_n_llhttp__internal__n_url_skip_lf_to_http09_1: { @@ -3230,11 +3250,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_to_http_09; } default: { - goto s_n_llhttp__internal__n_error_62; + goto s_n_llhttp__internal__n_error_63; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_skip_lf_to_http09: s_n_llhttp__internal__n_url_skip_lf_to_http09: { @@ -3255,11 +3274,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_skip_lf_to_http09_1; } default: { - goto s_n_llhttp__internal__n_error_62; + goto s_n_llhttp__internal__n_error_63; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_req_pri_upgrade: s_n_llhttp__internal__n_req_pri_upgrade: { @@ -3273,17 +3291,16 @@ static llparse_state_t llhttp__internal__run( switch (match_seq.status) { case kMatchComplete: { p++; - goto s_n_llhttp__internal__n_error_70; + goto s_n_llhttp__internal__n_error_72; } case kMatchPause: { return s_n_llhttp__internal__n_req_pri_upgrade; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_71; + goto s_n_llhttp__internal__n_error_73; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_req_http_complete_crlf: s_n_llhttp__internal__n_req_http_complete_crlf: { @@ -3296,11 +3313,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_headers_start; } default: { - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_25; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_26; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_req_http_complete: s_n_llhttp__internal__n_req_http_complete: { @@ -3310,18 +3326,17 @@ static llparse_state_t llhttp__internal__run( switch (*p) { case 10: { p++; - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_24; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_25; } case 13: { p++; goto s_n_llhttp__internal__n_req_http_complete_crlf; } default: { - goto s_n_llhttp__internal__n_error_69; + goto s_n_llhttp__internal__n_error_71; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_load_method_1: s_n_llhttp__internal__n_invoke_load_method_1: { @@ -3331,8 +3346,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_req_http_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_version_complete: s_n_llhttp__internal__n_invoke_llhttp__on_version_complete: { @@ -3342,30 +3356,27 @@ static llparse_state_t llhttp__internal__run( case 21: goto s_n_llhttp__internal__n_pause_21; default: - goto s_n_llhttp__internal__n_error_66; + goto s_n_llhttp__internal__n_error_68; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_65: - s_n_llhttp__internal__n_error_65: { + case s_n_llhttp__internal__n_error_67: + s_n_llhttp__internal__n_error_67: { state->error = 0x9; state->reason = "Invalid HTTP version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_72: - s_n_llhttp__internal__n_error_72: { + case s_n_llhttp__internal__n_error_74: + s_n_llhttp__internal__n_error_74: { state->error = 0x9; state->reason = "Invalid minor version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_req_http_minor: s_n_llhttp__internal__n_req_http_minor: { @@ -3427,18 +3438,16 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_version_2; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_73: - s_n_llhttp__internal__n_error_73: { + case s_n_llhttp__internal__n_error_75: + s_n_llhttp__internal__n_error_75: { state->error = 0x9; state->reason = "Expected dot"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_req_http_dot: s_n_llhttp__internal__n_req_http_dot: { @@ -3454,18 +3463,16 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_version_3; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_74: - s_n_llhttp__internal__n_error_74: { + case s_n_llhttp__internal__n_error_76: + s_n_llhttp__internal__n_error_76: { state->error = 0x9; state->reason = "Invalid major version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_req_http_major: s_n_llhttp__internal__n_req_http_major: { @@ -3527,8 +3534,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_version_4; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_version: s_n_llhttp__internal__n_span_start_llhttp__on_version: { @@ -3538,109 +3544,313 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_version; goto s_n_llhttp__internal__n_req_http_major; - /* UNREACHABLE */; - abort(); + UNREACHABLE; + } + case s_n_llhttp__internal__n_req_after_protocol: + s_n_llhttp__internal__n_req_after_protocol: { + if (p == endp) { + return s_n_llhttp__internal__n_req_after_protocol; + } + switch (*p) { + case '/': { + p++; + goto s_n_llhttp__internal__n_span_start_llhttp__on_version; + } + default: { + goto s_n_llhttp__internal__n_error_77; + } + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_load_method: + s_n_llhttp__internal__n_invoke_load_method: { + switch (llhttp__internal__c_load_method(state, p, endp)) { + case 0: + goto s_n_llhttp__internal__n_req_after_protocol; + case 1: + goto s_n_llhttp__internal__n_req_after_protocol; + case 2: + goto s_n_llhttp__internal__n_req_after_protocol; + case 3: + goto s_n_llhttp__internal__n_req_after_protocol; + case 4: + goto s_n_llhttp__internal__n_req_after_protocol; + case 5: + goto s_n_llhttp__internal__n_req_after_protocol; + case 6: + goto s_n_llhttp__internal__n_req_after_protocol; + case 7: + goto s_n_llhttp__internal__n_req_after_protocol; + case 8: + goto s_n_llhttp__internal__n_req_after_protocol; + case 9: + goto s_n_llhttp__internal__n_req_after_protocol; + case 10: + goto s_n_llhttp__internal__n_req_after_protocol; + case 11: + goto s_n_llhttp__internal__n_req_after_protocol; + case 12: + goto s_n_llhttp__internal__n_req_after_protocol; + case 13: + goto s_n_llhttp__internal__n_req_after_protocol; + case 14: + goto s_n_llhttp__internal__n_req_after_protocol; + case 15: + goto s_n_llhttp__internal__n_req_after_protocol; + case 16: + goto s_n_llhttp__internal__n_req_after_protocol; + case 17: + goto s_n_llhttp__internal__n_req_after_protocol; + case 18: + goto s_n_llhttp__internal__n_req_after_protocol; + case 19: + goto s_n_llhttp__internal__n_req_after_protocol; + case 20: + goto s_n_llhttp__internal__n_req_after_protocol; + case 21: + goto s_n_llhttp__internal__n_req_after_protocol; + case 22: + goto s_n_llhttp__internal__n_req_after_protocol; + case 23: + goto s_n_llhttp__internal__n_req_after_protocol; + case 24: + goto s_n_llhttp__internal__n_req_after_protocol; + case 25: + goto s_n_llhttp__internal__n_req_after_protocol; + case 26: + goto s_n_llhttp__internal__n_req_after_protocol; + case 27: + goto s_n_llhttp__internal__n_req_after_protocol; + case 28: + goto s_n_llhttp__internal__n_req_after_protocol; + case 29: + goto s_n_llhttp__internal__n_req_after_protocol; + case 30: + goto s_n_llhttp__internal__n_req_after_protocol; + case 31: + goto s_n_llhttp__internal__n_req_after_protocol; + case 32: + goto s_n_llhttp__internal__n_req_after_protocol; + case 33: + goto s_n_llhttp__internal__n_req_after_protocol; + case 34: + goto s_n_llhttp__internal__n_req_after_protocol; + case 46: + goto s_n_llhttp__internal__n_req_after_protocol; + default: + goto s_n_llhttp__internal__n_error_66; + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete: + s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete: { + switch (llhttp__on_protocol_complete(state, p, endp)) { + case 0: + goto s_n_llhttp__internal__n_invoke_load_method; + case 21: + goto s_n_llhttp__internal__n_pause_22; + default: + goto s_n_llhttp__internal__n_error_65; + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_error_82: + s_n_llhttp__internal__n_error_82: { + state->error = 0x8; + state->reason = "Expected HTTP/, RTSP/ or ICE/"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_error; + return s_error; + UNREACHABLE; } - case s_n_llhttp__internal__n_req_http_start_1: - s_n_llhttp__internal__n_req_http_start_1: { + case s_n_llhttp__internal__n_req_after_http_start_1: + s_n_llhttp__internal__n_req_after_http_start_1: { llparse_match_t match_seq; if (p == endp) { - return s_n_llhttp__internal__n_req_http_start_1; + return s_n_llhttp__internal__n_req_after_http_start_1; } - match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob13, 4); + match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob13, 3); p = match_seq.current; switch (match_seq.status) { case kMatchComplete: { p++; - goto s_n_llhttp__internal__n_invoke_load_method; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol; } case kMatchPause: { - return s_n_llhttp__internal__n_req_http_start_1; + return s_n_llhttp__internal__n_req_after_http_start_1; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_77; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_load_method_2: + s_n_llhttp__internal__n_invoke_load_method_2: { + switch (llhttp__internal__c_load_method(state, p, endp)) { + case 33: + goto s_n_llhttp__internal__n_req_after_protocol; + default: + goto s_n_llhttp__internal__n_error_79; + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1: + s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1: { + switch (llhttp__on_protocol_complete(state, p, endp)) { + case 0: + goto s_n_llhttp__internal__n_invoke_load_method_2; + case 21: + goto s_n_llhttp__internal__n_pause_23; + default: + goto s_n_llhttp__internal__n_error_78; + } + UNREACHABLE; } - case s_n_llhttp__internal__n_req_http_start_2: - s_n_llhttp__internal__n_req_http_start_2: { + case s_n_llhttp__internal__n_req_after_http_start_2: + s_n_llhttp__internal__n_req_after_http_start_2: { llparse_match_t match_seq; if (p == endp) { - return s_n_llhttp__internal__n_req_http_start_2; + return s_n_llhttp__internal__n_req_after_http_start_2; } - match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob15, 3); + match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob15, 2); p = match_seq.current; switch (match_seq.status) { case kMatchComplete: { p++; - goto s_n_llhttp__internal__n_invoke_load_method_2; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_1; } case kMatchPause: { - return s_n_llhttp__internal__n_req_http_start_2; + return s_n_llhttp__internal__n_req_after_http_start_2; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_77; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_load_method_3: + s_n_llhttp__internal__n_invoke_load_method_3: { + switch (llhttp__internal__c_load_method(state, p, endp)) { + case 1: + goto s_n_llhttp__internal__n_req_after_protocol; + case 3: + goto s_n_llhttp__internal__n_req_after_protocol; + case 6: + goto s_n_llhttp__internal__n_req_after_protocol; + case 35: + goto s_n_llhttp__internal__n_req_after_protocol; + case 36: + goto s_n_llhttp__internal__n_req_after_protocol; + case 37: + goto s_n_llhttp__internal__n_req_after_protocol; + case 38: + goto s_n_llhttp__internal__n_req_after_protocol; + case 39: + goto s_n_llhttp__internal__n_req_after_protocol; + case 40: + goto s_n_llhttp__internal__n_req_after_protocol; + case 41: + goto s_n_llhttp__internal__n_req_after_protocol; + case 42: + goto s_n_llhttp__internal__n_req_after_protocol; + case 43: + goto s_n_llhttp__internal__n_req_after_protocol; + case 44: + goto s_n_llhttp__internal__n_req_after_protocol; + case 45: + goto s_n_llhttp__internal__n_req_after_protocol; + default: + goto s_n_llhttp__internal__n_error_81; + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2: + s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2: { + switch (llhttp__on_protocol_complete(state, p, endp)) { + case 0: + goto s_n_llhttp__internal__n_invoke_load_method_3; + case 21: + goto s_n_llhttp__internal__n_pause_24; + default: + goto s_n_llhttp__internal__n_error_80; + } + UNREACHABLE; } - case s_n_llhttp__internal__n_req_http_start_3: - s_n_llhttp__internal__n_req_http_start_3: { + case s_n_llhttp__internal__n_req_after_http_start_3: + s_n_llhttp__internal__n_req_after_http_start_3: { llparse_match_t match_seq; if (p == endp) { - return s_n_llhttp__internal__n_req_http_start_3; + return s_n_llhttp__internal__n_req_after_http_start_3; } - match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob16, 4); + match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob16, 3); p = match_seq.current; switch (match_seq.status) { case kMatchComplete: { p++; - goto s_n_llhttp__internal__n_invoke_load_method_3; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_2; } case kMatchPause: { - return s_n_llhttp__internal__n_req_http_start_3; + return s_n_llhttp__internal__n_req_after_http_start_3; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_77; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_req_http_start: - s_n_llhttp__internal__n_req_http_start: { + case s_n_llhttp__internal__n_req_after_http_start: + s_n_llhttp__internal__n_req_after_http_start: { if (p == endp) { - return s_n_llhttp__internal__n_req_http_start; + return s_n_llhttp__internal__n_req_after_http_start; } switch (*p) { - case ' ': { - p++; - goto s_n_llhttp__internal__n_req_http_start; - } case 'H': { p++; - goto s_n_llhttp__internal__n_req_http_start_1; + goto s_n_llhttp__internal__n_req_after_http_start_1; } case 'I': { p++; - goto s_n_llhttp__internal__n_req_http_start_2; + goto s_n_llhttp__internal__n_req_after_http_start_2; } case 'R': { p++; - goto s_n_llhttp__internal__n_req_http_start_3; + goto s_n_llhttp__internal__n_req_after_http_start_3; } default: { - goto s_n_llhttp__internal__n_error_77; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3; + } + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_span_start_llhttp__on_protocol: + s_n_llhttp__internal__n_span_start_llhttp__on_protocol: { + if (p == endp) { + return s_n_llhttp__internal__n_span_start_llhttp__on_protocol; + } + state->_span_pos0 = (void*) p; + state->_span_cb0 = llhttp__on_protocol; + goto s_n_llhttp__internal__n_req_after_http_start; + UNREACHABLE; + } + case s_n_llhttp__internal__n_req_http_start: + s_n_llhttp__internal__n_req_http_start: { + if (p == endp) { + return s_n_llhttp__internal__n_req_http_start; + } + switch (*p) { + case ' ': { + p++; + goto s_n_llhttp__internal__n_req_http_start; + } + default: { + goto s_n_llhttp__internal__n_span_start_llhttp__on_protocol; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_to_http: s_n_llhttp__internal__n_url_to_http: { @@ -3660,8 +3870,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_llhttp__on_url_complete_1; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_skip_to_http: s_n_llhttp__internal__n_url_skip_to_http: { @@ -3682,8 +3891,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_to_http; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_fragment: s_n_llhttp__internal__n_url_fragment: { @@ -3727,11 +3935,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_fragment; } default: { - goto s_n_llhttp__internal__n_error_78; + goto s_n_llhttp__internal__n_error_83; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_end_stub_query_3: s_n_llhttp__internal__n_span_end_stub_query_3: { @@ -3740,8 +3947,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_url_fragment; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_query: s_n_llhttp__internal__n_url_query: { @@ -3788,11 +3994,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_stub_query_3; } default: { - goto s_n_llhttp__internal__n_error_79; + goto s_n_llhttp__internal__n_error_84; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_query_or_fragment: s_n_llhttp__internal__n_url_query_or_fragment: { @@ -3826,11 +4031,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_query; } default: { - goto s_n_llhttp__internal__n_error_80; + goto s_n_llhttp__internal__n_error_85; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_path: s_n_llhttp__internal__n_url_path: { @@ -3868,8 +4072,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_query_or_fragment; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_stub_path_2: s_n_llhttp__internal__n_span_start_stub_path_2: { @@ -3878,8 +4081,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_url_path; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_stub_path: s_n_llhttp__internal__n_span_start_stub_path: { @@ -3888,8 +4090,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_url_path; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_stub_path_1: s_n_llhttp__internal__n_span_start_stub_path_1: { @@ -3898,8 +4099,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_url_path; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_server_with_at: s_n_llhttp__internal__n_url_server_with_at: { @@ -3951,14 +4151,13 @@ static llparse_state_t llhttp__internal__run( } case 8: { p++; - goto s_n_llhttp__internal__n_error_81; + goto s_n_llhttp__internal__n_error_86; } default: { - goto s_n_llhttp__internal__n_error_82; + goto s_n_llhttp__internal__n_error_87; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_server: s_n_llhttp__internal__n_url_server: { @@ -4013,11 +4212,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_server_with_at; } default: { - goto s_n_llhttp__internal__n_error_83; + goto s_n_llhttp__internal__n_error_88; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_schema_delim_1: s_n_llhttp__internal__n_url_schema_delim_1: { @@ -4030,11 +4228,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_server; } default: { - goto s_n_llhttp__internal__n_error_84; + goto s_n_llhttp__internal__n_error_89; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_schema_delim: s_n_llhttp__internal__n_url_schema_delim: { @@ -4067,11 +4264,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_schema_delim_1; } default: { - goto s_n_llhttp__internal__n_error_84; + goto s_n_llhttp__internal__n_error_89; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_end_stub_schema: s_n_llhttp__internal__n_span_end_stub_schema: { @@ -4080,8 +4276,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_url_schema_delim; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_schema: s_n_llhttp__internal__n_url_schema: { @@ -4119,11 +4314,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_schema; } default: { - goto s_n_llhttp__internal__n_error_85; + goto s_n_llhttp__internal__n_error_90; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_start: s_n_llhttp__internal__n_url_start: { @@ -4160,11 +4354,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_url_schema; } default: { - goto s_n_llhttp__internal__n_error_86; + goto s_n_llhttp__internal__n_error_91; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_url_1: s_n_llhttp__internal__n_span_start_llhttp__on_url_1: { @@ -4174,8 +4367,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_url; goto s_n_llhttp__internal__n_url_start; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_entry_normal: s_n_llhttp__internal__n_url_entry_normal: { @@ -4195,8 +4387,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_start_llhttp__on_url_1; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_url: s_n_llhttp__internal__n_span_start_llhttp__on_url: { @@ -4206,8 +4397,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_url; goto s_n_llhttp__internal__n_url_server; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_url_entry_connect: s_n_llhttp__internal__n_url_entry_connect: { @@ -4227,8 +4417,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_start_llhttp__on_url; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_req_spaces_before_url: s_n_llhttp__internal__n_req_spaces_before_url: { @@ -4244,8 +4433,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_is_equal_method; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_req_first_space_before_url: s_n_llhttp__internal__n_req_first_space_before_url: { @@ -4258,11 +4446,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_req_spaces_before_url; } default: { - goto s_n_llhttp__internal__n_error_87; + goto s_n_llhttp__internal__n_error_92; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_method_complete_1: s_n_llhttp__internal__n_invoke_llhttp__on_method_complete_1: { @@ -4270,12 +4457,11 @@ static llparse_state_t llhttp__internal__run( case 0: goto s_n_llhttp__internal__n_req_first_space_before_url; case 21: - goto s_n_llhttp__internal__n_pause_26; + goto s_n_llhttp__internal__n_pause_29; default: - goto s_n_llhttp__internal__n_error_106; + goto s_n_llhttp__internal__n_error_111; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_2: s_n_llhttp__internal__n_after_start_req_2: { @@ -4289,11 +4475,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_store_method_1; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_3: s_n_llhttp__internal__n_after_start_req_3: { @@ -4314,11 +4499,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_3; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_1: s_n_llhttp__internal__n_after_start_req_1: { @@ -4335,11 +4519,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_3; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_4: s_n_llhttp__internal__n_after_start_req_4: { @@ -4360,11 +4543,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_4; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_6: s_n_llhttp__internal__n_after_start_req_6: { @@ -4385,11 +4567,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_6; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_8: s_n_llhttp__internal__n_after_start_req_8: { @@ -4410,11 +4591,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_8; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_9: s_n_llhttp__internal__n_after_start_req_9: { @@ -4428,11 +4608,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_store_method_1; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_7: s_n_llhttp__internal__n_after_start_req_7: { @@ -4449,11 +4628,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_9; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_5: s_n_llhttp__internal__n_after_start_req_5: { @@ -4470,11 +4648,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_7; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_12: s_n_llhttp__internal__n_after_start_req_12: { @@ -4495,11 +4672,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_12; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_13: s_n_llhttp__internal__n_after_start_req_13: { @@ -4520,11 +4696,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_13; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_11: s_n_llhttp__internal__n_after_start_req_11: { @@ -4541,11 +4716,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_13; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_10: s_n_llhttp__internal__n_after_start_req_10: { @@ -4558,11 +4732,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_11; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_14: s_n_llhttp__internal__n_after_start_req_14: { @@ -4583,11 +4756,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_14; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_17: s_n_llhttp__internal__n_after_start_req_17: { @@ -4608,11 +4780,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_17; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_16: s_n_llhttp__internal__n_after_start_req_16: { @@ -4629,8 +4800,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_store_method_1; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_15: s_n_llhttp__internal__n_after_start_req_15: { @@ -4650,11 +4820,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_15; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_18: s_n_llhttp__internal__n_after_start_req_18: { @@ -4675,11 +4844,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_18; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_20: s_n_llhttp__internal__n_after_start_req_20: { @@ -4700,11 +4868,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_20; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_21: s_n_llhttp__internal__n_after_start_req_21: { @@ -4725,11 +4892,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_21; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_19: s_n_llhttp__internal__n_after_start_req_19: { @@ -4746,11 +4912,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_21; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_23: s_n_llhttp__internal__n_after_start_req_23: { @@ -4771,11 +4936,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_23; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_24: s_n_llhttp__internal__n_after_start_req_24: { @@ -4796,11 +4960,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_24; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_26: s_n_llhttp__internal__n_after_start_req_26: { @@ -4821,11 +4984,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_26; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_28: s_n_llhttp__internal__n_after_start_req_28: { @@ -4846,11 +5008,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_28; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_29: s_n_llhttp__internal__n_after_start_req_29: { @@ -4864,11 +5025,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_store_method_1; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_27: s_n_llhttp__internal__n_after_start_req_27: { @@ -4885,11 +5045,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_29; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_25: s_n_llhttp__internal__n_after_start_req_25: { @@ -4906,11 +5065,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_27; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_30: s_n_llhttp__internal__n_after_start_req_30: { @@ -4931,11 +5089,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_30; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_22: s_n_llhttp__internal__n_after_start_req_22: { @@ -4960,11 +5117,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_30; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_31: s_n_llhttp__internal__n_after_start_req_31: { @@ -4985,11 +5141,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_31; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_32: s_n_llhttp__internal__n_after_start_req_32: { @@ -5010,11 +5165,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_32; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_35: s_n_llhttp__internal__n_after_start_req_35: { @@ -5035,11 +5189,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_35; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_36: s_n_llhttp__internal__n_after_start_req_36: { @@ -5060,11 +5213,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_36; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_34: s_n_llhttp__internal__n_after_start_req_34: { @@ -5081,11 +5233,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_36; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_37: s_n_llhttp__internal__n_after_start_req_37: { @@ -5106,11 +5257,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_37; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_38: s_n_llhttp__internal__n_after_start_req_38: { @@ -5131,11 +5281,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_38; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_42: s_n_llhttp__internal__n_after_start_req_42: { @@ -5156,11 +5305,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_42; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_43: s_n_llhttp__internal__n_after_start_req_43: { @@ -5181,11 +5329,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_43; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_41: s_n_llhttp__internal__n_after_start_req_41: { @@ -5202,11 +5349,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_43; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_40: s_n_llhttp__internal__n_after_start_req_40: { @@ -5219,11 +5365,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_41; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_39: s_n_llhttp__internal__n_after_start_req_39: { @@ -5241,11 +5386,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_40; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_45: s_n_llhttp__internal__n_after_start_req_45: { @@ -5266,11 +5410,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_45; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_44: s_n_llhttp__internal__n_after_start_req_44: { @@ -5288,11 +5431,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_store_method_1; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_33: s_n_llhttp__internal__n_after_start_req_33: { @@ -5321,11 +5463,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_44; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_46: s_n_llhttp__internal__n_after_start_req_46: { @@ -5346,11 +5487,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_46; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_49: s_n_llhttp__internal__n_after_start_req_49: { @@ -5371,11 +5511,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_49; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_50: s_n_llhttp__internal__n_after_start_req_50: { @@ -5396,11 +5535,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_50; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_51: s_n_llhttp__internal__n_after_start_req_51: { @@ -5421,11 +5559,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_51; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_52: s_n_llhttp__internal__n_after_start_req_52: { @@ -5446,11 +5583,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_52; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_48: s_n_llhttp__internal__n_after_start_req_48: { @@ -5475,11 +5611,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_52; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_47: s_n_llhttp__internal__n_after_start_req_47: { @@ -5492,11 +5627,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_48; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_55: s_n_llhttp__internal__n_after_start_req_55: { @@ -5517,11 +5651,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_55; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_57: s_n_llhttp__internal__n_after_start_req_57: { @@ -5535,11 +5668,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_store_method_1; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_58: s_n_llhttp__internal__n_after_start_req_58: { @@ -5560,11 +5692,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_58; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_56: s_n_llhttp__internal__n_after_start_req_56: { @@ -5581,11 +5712,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_58; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_54: s_n_llhttp__internal__n_after_start_req_54: { @@ -5602,11 +5732,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_56; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_59: s_n_llhttp__internal__n_after_start_req_59: { @@ -5627,11 +5756,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_59; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_60: s_n_llhttp__internal__n_after_start_req_60: { @@ -5652,11 +5780,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_60; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_53: s_n_llhttp__internal__n_after_start_req_53: { @@ -5677,11 +5804,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_60; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_62: s_n_llhttp__internal__n_after_start_req_62: { @@ -5702,11 +5828,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_62; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_63: s_n_llhttp__internal__n_after_start_req_63: { @@ -5727,11 +5852,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_63; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_61: s_n_llhttp__internal__n_after_start_req_61: { @@ -5748,11 +5872,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_63; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_66: s_n_llhttp__internal__n_after_start_req_66: { @@ -5773,11 +5896,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_66; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_68: s_n_llhttp__internal__n_after_start_req_68: { @@ -5798,11 +5920,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_68; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_69: s_n_llhttp__internal__n_after_start_req_69: { @@ -5823,11 +5944,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_69; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_67: s_n_llhttp__internal__n_after_start_req_67: { @@ -5844,11 +5964,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_69; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_70: s_n_llhttp__internal__n_after_start_req_70: { @@ -5869,11 +5988,10 @@ static llparse_state_t llhttp__internal__run( return s_n_llhttp__internal__n_after_start_req_70; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_65: s_n_llhttp__internal__n_after_start_req_65: { @@ -5894,11 +6012,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_70; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req_64: s_n_llhttp__internal__n_after_start_req_64: { @@ -5911,11 +6028,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_65; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_after_start_req: s_n_llhttp__internal__n_after_start_req: { @@ -5992,11 +6108,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_after_start_req_64; } default: { - goto s_n_llhttp__internal__n_error_107; + goto s_n_llhttp__internal__n_error_112; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_method_1: s_n_llhttp__internal__n_span_start_llhttp__on_method_1: { @@ -6006,8 +6121,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_method; goto s_n_llhttp__internal__n_after_start_req; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_line_almost_done: s_n_llhttp__internal__n_res_line_almost_done: { @@ -6024,22 +6138,20 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete; } default: { - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_28; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_29; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_invoke_test_lenient_flags_29: - s_n_llhttp__internal__n_invoke_test_lenient_flags_29: { + case s_n_llhttp__internal__n_invoke_test_lenient_flags_30: + s_n_llhttp__internal__n_invoke_test_lenient_flags_30: { switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) { case 1: goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete; default: - goto s_n_llhttp__internal__n_error_93; + goto s_n_llhttp__internal__n_error_98; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_status: s_n_llhttp__internal__n_res_status: { @@ -6058,8 +6170,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_res_status; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_status: s_n_llhttp__internal__n_span_start_llhttp__on_status: { @@ -6069,8 +6180,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_status; goto s_n_llhttp__internal__n_res_status; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_status_code_otherwise: s_n_llhttp__internal__n_res_status_code_otherwise: { @@ -6080,7 +6190,7 @@ static llparse_state_t llhttp__internal__run( switch (*p) { case 10: { p++; - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_27; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_28; } case 13: { p++; @@ -6091,11 +6201,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_start_llhttp__on_status; } default: { - goto s_n_llhttp__internal__n_error_94; + goto s_n_llhttp__internal__n_error_99; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_status_code_digit_3: s_n_llhttp__internal__n_res_status_code_digit_3: { @@ -6154,11 +6263,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_mul_add_status_code_2; } default: { - goto s_n_llhttp__internal__n_error_96; + goto s_n_llhttp__internal__n_error_101; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_status_code_digit_2: s_n_llhttp__internal__n_res_status_code_digit_2: { @@ -6217,11 +6325,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_mul_add_status_code_1; } default: { - goto s_n_llhttp__internal__n_error_98; + goto s_n_llhttp__internal__n_error_103; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_status_code_digit_1: s_n_llhttp__internal__n_res_status_code_digit_1: { @@ -6280,11 +6387,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_mul_add_status_code; } default: { - goto s_n_llhttp__internal__n_error_100; + goto s_n_llhttp__internal__n_error_105; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_after_version: s_n_llhttp__internal__n_res_after_version: { @@ -6297,11 +6403,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_status_code; } default: { - goto s_n_llhttp__internal__n_error_101; + goto s_n_llhttp__internal__n_error_106; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1: s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1: { @@ -6309,32 +6414,29 @@ static llparse_state_t llhttp__internal__run( case 0: goto s_n_llhttp__internal__n_res_after_version; case 21: - goto s_n_llhttp__internal__n_pause_25; + goto s_n_llhttp__internal__n_pause_28; default: - goto s_n_llhttp__internal__n_error_89; + goto s_n_llhttp__internal__n_error_94; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_88: - s_n_llhttp__internal__n_error_88: { + case s_n_llhttp__internal__n_error_93: + s_n_llhttp__internal__n_error_93: { state->error = 0x9; state->reason = "Invalid HTTP version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_102: - s_n_llhttp__internal__n_error_102: { + case s_n_llhttp__internal__n_error_107: + s_n_llhttp__internal__n_error_107: { state->error = 0x9; state->reason = "Invalid minor version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_http_minor: s_n_llhttp__internal__n_res_http_minor: { @@ -6396,18 +6498,16 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_version_7; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_103: - s_n_llhttp__internal__n_error_103: { + case s_n_llhttp__internal__n_error_108: + s_n_llhttp__internal__n_error_108: { state->error = 0x9; state->reason = "Expected dot"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_http_dot: s_n_llhttp__internal__n_res_http_dot: { @@ -6423,18 +6523,16 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_version_8; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_error_104: - s_n_llhttp__internal__n_error_104: { + case s_n_llhttp__internal__n_error_109: + s_n_llhttp__internal__n_error_109: { state->error = 0x9; state->reason = "Invalid major version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_res_http_major: s_n_llhttp__internal__n_res_http_major: { @@ -6496,8 +6594,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_span_end_llhttp__on_version_9; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_version_1: s_n_llhttp__internal__n_span_start_llhttp__on_version_1: { @@ -6507,129 +6604,239 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_version; goto s_n_llhttp__internal__n_res_http_major; - /* UNREACHABLE */; - abort(); + UNREACHABLE; + } + case s_n_llhttp__internal__n_res_after_protocol: + s_n_llhttp__internal__n_res_after_protocol: { + if (p == endp) { + return s_n_llhttp__internal__n_res_after_protocol; + } + switch (*p) { + case '/': { + p++; + goto s_n_llhttp__internal__n_span_start_llhttp__on_version_1; + } + default: { + goto s_n_llhttp__internal__n_error_114; + } + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3: + s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3: { + switch (llhttp__on_protocol_complete(state, p, endp)) { + case 0: + goto s_n_llhttp__internal__n_res_after_protocol; + case 21: + goto s_n_llhttp__internal__n_pause_30; + default: + goto s_n_llhttp__internal__n_error_113; + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_error_115: + s_n_llhttp__internal__n_error_115: { + state->error = 0x8; + state->reason = "Expected HTTP/, RTSP/ or ICE/"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_error; + return s_error; + UNREACHABLE; } - case s_n_llhttp__internal__n_start_res: - s_n_llhttp__internal__n_start_res: { + case s_n_llhttp__internal__n_res_after_start_1: + s_n_llhttp__internal__n_res_after_start_1: { llparse_match_t match_seq; if (p == endp) { - return s_n_llhttp__internal__n_start_res; + return s_n_llhttp__internal__n_res_after_start_1; } - match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob58, 5); + match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob58, 3); p = match_seq.current; switch (match_seq.status) { case kMatchComplete: { p++; - goto s_n_llhttp__internal__n_span_start_llhttp__on_version_1; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_4; } case kMatchPause: { - return s_n_llhttp__internal__n_start_res; + return s_n_llhttp__internal__n_res_after_start_1; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_108; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5; } } - /* UNREACHABLE */; - abort(); - } - case s_n_llhttp__internal__n_invoke_llhttp__on_method_complete: - s_n_llhttp__internal__n_invoke_llhttp__on_method_complete: { - switch (llhttp__on_method_complete(state, p, endp)) { - case 0: - goto s_n_llhttp__internal__n_req_first_space_before_url; - case 21: - goto s_n_llhttp__internal__n_pause_23; - default: - goto s_n_llhttp__internal__n_error_1; - } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_req_or_res_method_2: - s_n_llhttp__internal__n_req_or_res_method_2: { + case s_n_llhttp__internal__n_res_after_start_2: + s_n_llhttp__internal__n_res_after_start_2: { llparse_match_t match_seq; if (p == endp) { - return s_n_llhttp__internal__n_req_or_res_method_2; + return s_n_llhttp__internal__n_res_after_start_2; } match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob59, 2); p = match_seq.current; switch (match_seq.status) { case kMatchComplete: { p++; - match = 2; - goto s_n_llhttp__internal__n_invoke_store_method; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_4; } case kMatchPause: { - return s_n_llhttp__internal__n_req_or_res_method_2; + return s_n_llhttp__internal__n_res_after_start_2; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_105; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5; } } - /* UNREACHABLE */; - abort(); - } - case s_n_llhttp__internal__n_invoke_update_type_1: - s_n_llhttp__internal__n_invoke_update_type_1: { - switch (llhttp__internal__c_update_type_1(state, p, endp)) { - default: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version_1; - } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_req_or_res_method_3: - s_n_llhttp__internal__n_req_or_res_method_3: { + case s_n_llhttp__internal__n_res_after_start_3: + s_n_llhttp__internal__n_res_after_start_3: { llparse_match_t match_seq; if (p == endp) { - return s_n_llhttp__internal__n_req_or_res_method_3; + return s_n_llhttp__internal__n_res_after_start_3; } match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob60, 3); p = match_seq.current; switch (match_seq.status) { case kMatchComplete: { p++; - goto s_n_llhttp__internal__n_span_end_llhttp__on_method_1; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_4; } case kMatchPause: { - return s_n_llhttp__internal__n_req_or_res_method_3; + return s_n_llhttp__internal__n_res_after_start_3; } case kMatchMismatch: { - goto s_n_llhttp__internal__n_error_105; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_req_or_res_method_1: - s_n_llhttp__internal__n_req_or_res_method_1: { + case s_n_llhttp__internal__n_res_after_start: + s_n_llhttp__internal__n_res_after_start: { if (p == endp) { - return s_n_llhttp__internal__n_req_or_res_method_1; + return s_n_llhttp__internal__n_res_after_start; } switch (*p) { - case 'E': { + case 'H': { p++; - goto s_n_llhttp__internal__n_req_or_res_method_2; + goto s_n_llhttp__internal__n_res_after_start_1; } - case 'T': { + case 'I': { p++; - goto s_n_llhttp__internal__n_req_or_res_method_3; + goto s_n_llhttp__internal__n_res_after_start_2; + } + case 'R': { + p++; + goto s_n_llhttp__internal__n_res_after_start_3; } default: { - goto s_n_llhttp__internal__n_error_105; + goto s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - case s_n_llhttp__internal__n_req_or_res_method: - s_n_llhttp__internal__n_req_or_res_method: { + case s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1: + s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1: { if (p == endp) { - return s_n_llhttp__internal__n_req_or_res_method; + return s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1; + } + state->_span_pos0 = (void*) p; + state->_span_cb0 = llhttp__on_protocol; + goto s_n_llhttp__internal__n_res_after_start; + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_llhttp__on_method_complete: + s_n_llhttp__internal__n_invoke_llhttp__on_method_complete: { + switch (llhttp__on_method_complete(state, p, endp)) { + case 0: + goto s_n_llhttp__internal__n_req_first_space_before_url; + case 21: + goto s_n_llhttp__internal__n_pause_26; + default: + goto s_n_llhttp__internal__n_error_1; + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_req_or_res_method_2: + s_n_llhttp__internal__n_req_or_res_method_2: { + llparse_match_t match_seq; + + if (p == endp) { + return s_n_llhttp__internal__n_req_or_res_method_2; + } + match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob61, 2); + p = match_seq.current; + switch (match_seq.status) { + case kMatchComplete: { + p++; + match = 2; + goto s_n_llhttp__internal__n_invoke_store_method; + } + case kMatchPause: { + return s_n_llhttp__internal__n_req_or_res_method_2; + } + case kMatchMismatch: { + goto s_n_llhttp__internal__n_error_110; + } + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_invoke_update_type_1: + s_n_llhttp__internal__n_invoke_update_type_1: { + switch (llhttp__internal__c_update_type_1(state, p, endp)) { + default: + goto s_n_llhttp__internal__n_span_start_llhttp__on_version_1; + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_req_or_res_method_3: + s_n_llhttp__internal__n_req_or_res_method_3: { + llparse_match_t match_seq; + + if (p == endp) { + return s_n_llhttp__internal__n_req_or_res_method_3; + } + match_seq = llparse__match_sequence_id(state, p, endp, llparse_blob62, 3); + p = match_seq.current; + switch (match_seq.status) { + case kMatchComplete: { + p++; + goto s_n_llhttp__internal__n_span_end_llhttp__on_method_1; + } + case kMatchPause: { + return s_n_llhttp__internal__n_req_or_res_method_3; + } + case kMatchMismatch: { + goto s_n_llhttp__internal__n_error_110; + } + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_req_or_res_method_1: + s_n_llhttp__internal__n_req_or_res_method_1: { + if (p == endp) { + return s_n_llhttp__internal__n_req_or_res_method_1; + } + switch (*p) { + case 'E': { + p++; + goto s_n_llhttp__internal__n_req_or_res_method_2; + } + case 'T': { + p++; + goto s_n_llhttp__internal__n_req_or_res_method_3; + } + default: { + goto s_n_llhttp__internal__n_error_110; + } + } + UNREACHABLE; + } + case s_n_llhttp__internal__n_req_or_res_method: + s_n_llhttp__internal__n_req_or_res_method: { + if (p == endp) { + return s_n_llhttp__internal__n_req_or_res_method; } switch (*p) { case 'H': { @@ -6637,11 +6844,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_req_or_res_method_1; } default: { - goto s_n_llhttp__internal__n_error_105; + goto s_n_llhttp__internal__n_error_110; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_span_start_llhttp__on_method: s_n_llhttp__internal__n_span_start_llhttp__on_method: { @@ -6651,8 +6857,7 @@ static llparse_state_t llhttp__internal__run( state->_span_pos0 = (void*) p; state->_span_cb0 = llhttp__on_method; goto s_n_llhttp__internal__n_req_or_res_method; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_start_req_or_res: s_n_llhttp__internal__n_start_req_or_res: { @@ -6667,8 +6872,7 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_update_type_2; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_load_type: s_n_llhttp__internal__n_invoke_load_type: { @@ -6676,12 +6880,11 @@ static llparse_state_t llhttp__internal__run( case 1: goto s_n_llhttp__internal__n_span_start_llhttp__on_method_1; case 2: - goto s_n_llhttp__internal__n_start_res; + goto s_n_llhttp__internal__n_span_start_llhttp__on_protocol_1; default: goto s_n_llhttp__internal__n_start_req_or_res; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_invoke_update_finish: s_n_llhttp__internal__n_invoke_update_finish: { @@ -6689,8 +6892,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_llhttp__on_message_begin; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } case s_n_llhttp__internal__n_start: s_n_llhttp__internal__n_start: { @@ -6710,12 +6912,10 @@ static llparse_state_t llhttp__internal__run( goto s_n_llhttp__internal__n_invoke_load_initial_message_completed; } } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } default: - /* UNREACHABLE */ - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_2: { state->error = 0x7; @@ -6723,32 +6923,28 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_finish_2: { switch (llhttp__internal__c_update_finish_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_start; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_initial_message_completed: { switch (llhttp__internal__c_update_initial_message_completed(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_finish_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_content_length: { switch (llhttp__internal__c_update_content_length(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_initial_message_completed; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_8: { state->error = 0x5; @@ -6756,8 +6952,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_3: { switch (llhttp__internal__c_test_lenient_flags_3(state, p, endp)) { @@ -6766,8 +6961,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_8; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_2: { switch (llhttp__internal__c_test_lenient_flags_2(state, p, endp)) { @@ -6776,16 +6970,14 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_closed; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_finish_1: { switch (llhttp__internal__c_update_finish_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_test_lenient_flags_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_13: { state->error = 0x15; @@ -6793,8 +6985,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_is_equal_upgrade; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_38: { state->error = 0x12; @@ -6802,8 +6993,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_15: { state->error = 0x15; @@ -6811,8 +7001,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_40: { state->error = 0x14; @@ -6820,8 +7009,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete_1: { switch (llhttp__on_chunk_complete(state, p, endp)) { @@ -6832,8 +7020,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_40; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_2: { state->error = 0x15; @@ -6841,8 +7028,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_pause_1; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_9: { state->error = 0x12; @@ -6850,8 +7036,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_1: { switch (llhttp__on_message_complete(state, p, endp)) { @@ -6862,8 +7047,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_9; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_36: { state->error = 0xc; @@ -6871,8 +7055,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_10: { state->error = 0xc; @@ -6880,8 +7063,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_4: { switch (llhttp__internal__c_test_lenient_flags_4(state, p, endp)) { @@ -6890,8 +7072,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_10; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_3: { state->error = 0x15; @@ -6899,8 +7080,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_update_content_length_1; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_14: { state->error = 0x14; @@ -6908,8 +7088,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete: { switch (llhttp__on_chunk_complete(state, p, endp)) { @@ -6920,8 +7099,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_14; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_13: { state->error = 0x19; @@ -6929,8 +7107,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_6: { switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) { @@ -6939,8 +7116,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_13; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_15: { state->error = 0x2; @@ -6948,8 +7124,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_7: { switch (llhttp__internal__c_test_lenient_flags_7(state, p, endp)) { @@ -6958,8 +7133,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_15; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_body: { const unsigned char* start; @@ -6975,16 +7149,14 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_chunk_data_almost_done; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags: { switch (llhttp__internal__c_or_flags(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_field_start; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_4: { state->error = 0x15; @@ -6992,8 +7164,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_is_equal_content_length; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_12: { state->error = 0x13; @@ -7001,8 +7172,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_chunk_header: { switch (llhttp__on_chunk_header(state, p, endp)) { @@ -7013,8 +7183,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_12; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_16: { state->error = 0x2; @@ -7022,8 +7191,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_8: { switch (llhttp__internal__c_test_lenient_flags_8(state, p, endp)) { @@ -7032,8 +7200,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_16; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_11: { state->error = 0x19; @@ -7041,8 +7208,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_5: { switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) { @@ -7051,8 +7217,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_11; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_17: { state->error = 0x2; @@ -7060,8 +7225,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_18: { state->error = 0x2; @@ -7069,8 +7233,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_20: { state->error = 0x19; @@ -7078,8 +7241,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_5: { state->error = 0x15; @@ -7087,8 +7249,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_test_lenient_flags_9; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_19: { state->error = 0x22; @@ -7096,8 +7257,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name: { const unsigned char* start; @@ -7113,8 +7273,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_6: { state->error = 0x15; @@ -7122,8 +7281,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_size_almost_done; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_21: { state->error = 0x22; @@ -7131,8 +7289,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_1: { const unsigned char* start; @@ -7149,8 +7306,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_1; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_7: { state->error = 0x15; @@ -7158,8 +7314,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_extensions; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_22: { state->error = 0x22; @@ -7167,8 +7322,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_2: { const unsigned char* start; @@ -7185,8 +7339,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_2; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_25: { state->error = 0x19; @@ -7194,8 +7347,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_8: { state->error = 0x15; @@ -7203,8 +7355,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_test_lenient_flags_10; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_24: { state->error = 0x23; @@ -7212,8 +7363,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value: { const unsigned char* start; @@ -7229,8 +7379,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_9: { state->error = 0x15; @@ -7238,8 +7387,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_size_almost_done; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_26: { state->error = 0x23; @@ -7247,8 +7395,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_1: { const unsigned char* start; @@ -7265,8 +7412,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_1; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_28: { state->error = 0x19; @@ -7274,8 +7420,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_11: { switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) { @@ -7284,8 +7429,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_28; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_29: { state->error = 0x2; @@ -7293,8 +7437,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_10: { state->error = 0x15; @@ -7302,8 +7445,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_extension_quoted_value_done; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_27: { state->error = 0x23; @@ -7311,8 +7453,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_2: { const unsigned char* start; @@ -7328,8 +7469,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_2; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_3: { const unsigned char* start; @@ -7346,8 +7486,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_error_30; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_4: { const unsigned char* start; @@ -7364,8 +7503,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_error_31; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_11: { state->error = 0x15; @@ -7373,8 +7511,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_extensions; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_32: { state->error = 0x23; @@ -7382,8 +7519,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_5: { const unsigned char* start; @@ -7400,8 +7536,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_value_complete_3; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_value_6: { const unsigned char* start; @@ -7418,8 +7553,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_error_33; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_12: { state->error = 0x15; @@ -7427,8 +7561,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_chunk_extension_value; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_23: { state->error = 0x22; @@ -7436,8 +7569,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_chunk_extension_name_complete_3: { switch (llhttp__on_chunk_extension_name_complete(state, p, endp)) { @@ -7448,8 +7580,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_23; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_3: { const unsigned char* start; @@ -7466,8 +7597,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_span_start_llhttp__on_chunk_extension_value; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_chunk_extension_name_4: { const unsigned char* start; @@ -7484,8 +7614,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_error_34; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_35: { state->error = 0xc; @@ -7493,8 +7622,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_mul_add_content_length: { switch (llhttp__internal__c_mul_add_content_length(state, p, endp, match)) { @@ -7503,8 +7631,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_chunk_size; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_37: { state->error = 0xc; @@ -7512,8 +7639,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_body_1: { const unsigned char* start; @@ -7529,16 +7655,14 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_finish_3: { switch (llhttp__internal__c_update_finish_3(state, p, endp)) { default: goto s_n_llhttp__internal__n_span_start_llhttp__on_body_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_39: { state->error = 0xf; @@ -7546,8 +7670,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause: { state->error = 0x15; @@ -7555,8 +7678,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__after_message_complete; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_7: { state->error = 0x12; @@ -7564,8 +7686,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_message_complete: { switch (llhttp__on_message_complete(state, p, endp)) { @@ -7576,32 +7697,28 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_7; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_1: { switch (llhttp__internal__c_or_flags_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_2: { switch (llhttp__internal__c_or_flags_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_upgrade: { switch (llhttp__internal__c_update_upgrade(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_or_flags_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_14: { state->error = 0x15; @@ -7609,8 +7726,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_6: { state->error = 0x11; @@ -7618,8 +7734,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_headers_complete: { switch (llhttp__on_headers_complete(state, p, endp)) { @@ -7634,16 +7749,14 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_6; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__before_headers_complete: { switch (llhttp__before_headers_complete(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__on_headers_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_flags: { switch (llhttp__internal__c_test_flags(state, p, endp)) { @@ -7652,8 +7765,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_llhttp__before_headers_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_1: { switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) { @@ -7662,8 +7774,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_5; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_17: { state->error = 0x15; @@ -7671,8 +7782,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_message_complete_2; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_42: { state->error = 0x14; @@ -7680,8 +7790,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_chunk_complete_2: { switch (llhttp__on_chunk_complete(state, p, endp)) { @@ -7692,32 +7801,28 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_42; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_3: { switch (llhttp__internal__c_or_flags_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_4: { switch (llhttp__internal__c_or_flags_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_upgrade_1: { switch (llhttp__internal__c_update_upgrade(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_or_flags_4; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_16: { state->error = 0x15; @@ -7725,8 +7830,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__after_headers_complete; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_41: { state->error = 0x11; @@ -7734,8 +7838,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_headers_complete_1: { switch (llhttp__on_headers_complete(state, p, endp)) { @@ -7750,16 +7853,14 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_41; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__before_headers_complete_1: { switch (llhttp__before_headers_complete(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__on_headers_complete_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_flags_1: { switch (llhttp__internal__c_test_flags(state, p, endp)) { @@ -7768,8 +7869,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_llhttp__before_headers_complete_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_43: { state->error = 0x2; @@ -7777,8 +7877,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_12: { switch (llhttp__internal__c_test_lenient_flags_8(state, p, endp)) { @@ -7787,8 +7886,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_43; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_44: { state->error = 0xa; @@ -7796,8 +7894,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_field: { const unsigned char* start; @@ -7814,8 +7911,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_error_5; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_13: { switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) { @@ -7824,17 +7920,15 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_header_field; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_59: { + s_n_llhttp__internal__n_error_60: { state->error = 0xb; state->reason = "Content-Length can't be present with Transfer-Encoding"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_47: { state->error = 0xa; @@ -7842,8 +7936,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_15: { switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) { @@ -7852,8 +7945,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_47; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_49: { state->error = 0xb; @@ -7861,8 +7953,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_18: { state->error = 0x15; @@ -7870,8 +7961,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_header_field_start; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_48: { state->error = 0x1d; @@ -7879,8 +7969,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value: { const unsigned char* start; @@ -7896,48 +7985,42 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state: { switch (llhttp__internal__c_update_header_state(state, p, endp)) { default: goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_5: { switch (llhttp__internal__c_or_flags_5(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_6: { switch (llhttp__internal__c_or_flags_6(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_7: { switch (llhttp__internal__c_or_flags_7(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_8: { switch (llhttp__internal__c_or_flags_8(state, p, endp)) { default: goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_header_state_2: { switch (llhttp__internal__c_load_header_state(state, p, endp)) { @@ -7952,8 +8035,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_header_state_1: { switch (llhttp__internal__c_load_header_state(state, p, endp)) { @@ -7962,8 +8044,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_load_header_state_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_46: { state->error = 0xa; @@ -7971,8 +8052,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_14: { switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) { @@ -7981,8 +8061,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_46; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_50: { state->error = 0x2; @@ -7990,8 +8069,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags_16: { switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) { @@ -8000,16 +8078,14 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_50; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_1: { switch (llhttp__internal__c_update_header_state_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_header_state_4: { switch (llhttp__internal__c_load_header_state(state, p, endp)) { @@ -8018,48 +8094,59 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_start_llhttp__on_header_value_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; + } + s_n_llhttp__internal__n_error_52: { + state->error = 0xa; + state->reason = "Unexpected whitespace after header value"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_error; + return s_error; + UNREACHABLE; + } + s_n_llhttp__internal__n_invoke_test_lenient_flags_18: { + switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) { + case 1: + goto s_n_llhttp__internal__n_invoke_load_header_state_4; + default: + goto s_n_llhttp__internal__n_error_52; + } + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_2: { switch (llhttp__internal__c_update_header_state(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_9: { switch (llhttp__internal__c_or_flags_5(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_10: { switch (llhttp__internal__c_or_flags_6(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_11: { switch (llhttp__internal__c_or_flags_7(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_12: { switch (llhttp__internal__c_or_flags_8(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_header_state_5: { switch (llhttp__internal__c_load_header_state(state, p, endp)) { @@ -8074,17 +8161,15 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_llhttp__on_header_value_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_52: { + s_n_llhttp__internal__n_error_53: { state->error = 0x3; state->reason = "Missing expected LF after header value"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_51: { state->error = 0x19; @@ -8092,8 +8177,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_1: { const unsigned char* start; @@ -8109,8 +8193,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_test_lenient_flags_17; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_2: { const unsigned char* start; @@ -8127,8 +8210,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_header_value_almost_done; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_4: { const unsigned char* start; @@ -8144,8 +8226,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_header_value_almost_done; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_5: { const unsigned char* start; @@ -8162,8 +8243,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_header_value_almost_done; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_3: { const unsigned char* start; @@ -8175,62 +8255,55 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_53; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_54; return s_error; } - goto s_n_llhttp__internal__n_error_53; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_54; + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_18: { + s_n_llhttp__internal__n_invoke_test_lenient_flags_19: { switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) { case 1: goto s_n_llhttp__internal__n_header_value_lenient; default: goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_3; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_4: { switch (llhttp__internal__c_update_header_state(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_connection; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_13: { switch (llhttp__internal__c_or_flags_5(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state_4; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_14: { switch (llhttp__internal__c_or_flags_6(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state_4; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_15: { switch (llhttp__internal__c_or_flags_7(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state_4; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_16: { switch (llhttp__internal__c_or_flags_8(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_connection; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_header_state_6: { switch (llhttp__internal__c_load_header_state(state, p, endp)) { @@ -8245,40 +8318,35 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_header_value_connection; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_5: { switch (llhttp__internal__c_update_header_state_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_connection_token; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_3: { switch (llhttp__internal__c_update_header_state_3(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_connection_ws; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_6: { switch (llhttp__internal__c_update_header_state_6(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_connection_ws; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_7: { switch (llhttp__internal__c_update_header_state_7(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_connection_ws; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_6: { const unsigned char* start; @@ -8290,12 +8358,11 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_55; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_56; return s_error; } - goto s_n_llhttp__internal__n_error_55; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_56; + UNREACHABLE; } s_n_llhttp__internal__n_invoke_mul_add_content_length_1: { switch (llhttp__internal__c_mul_add_content_length_1(state, p, endp, match)) { @@ -8304,16 +8371,14 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_header_value_content_length; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_17: { switch (llhttp__internal__c_or_flags_17(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_otherwise; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_7: { const unsigned char* start; @@ -8325,31 +8390,28 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_56; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_57; return s_error; } - goto s_n_llhttp__internal__n_error_56; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_57; + UNREACHABLE; } - s_n_llhttp__internal__n_error_54: { + s_n_llhttp__internal__n_error_55: { state->error = 0x4; state->reason = "Duplicate Content-Length"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_flags_2: { switch (llhttp__internal__c_test_flags_2(state, p, endp)) { case 0: goto s_n_llhttp__internal__n_header_value_content_length; default: - goto s_n_llhttp__internal__n_error_54; + goto s_n_llhttp__internal__n_error_55; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_9: { const unsigned char* start; @@ -8361,21 +8423,19 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) (p + 1); - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_58; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_59; return s_error; } p++; - goto s_n_llhttp__internal__n_error_58; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_59; + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_8: { switch (llhttp__internal__c_update_header_state_8(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_otherwise; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_value_8: { const unsigned char* start; @@ -8387,85 +8447,76 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) (p + 1); - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_57; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_58; return s_error; } p++; - goto s_n_llhttp__internal__n_error_57; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_58; + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_19: { - switch (llhttp__internal__c_test_lenient_flags_19(state, p, endp)) { + s_n_llhttp__internal__n_invoke_test_lenient_flags_20: { + switch (llhttp__internal__c_test_lenient_flags_20(state, p, endp)) { case 0: goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_8; default: goto s_n_llhttp__internal__n_header_value_te_chunked; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_type_1: { switch (llhttp__internal__c_load_type(state, p, endp)) { case 1: - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_19; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_20; default: goto s_n_llhttp__internal__n_header_value_te_chunked; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_9: { switch (llhttp__internal__c_update_header_state_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_and_flags: { switch (llhttp__internal__c_and_flags(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_value_te_chunked; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_19: { switch (llhttp__internal__c_or_flags_18(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_and_flags; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_20: { - switch (llhttp__internal__c_test_lenient_flags_19(state, p, endp)) { + s_n_llhttp__internal__n_invoke_test_lenient_flags_21: { + switch (llhttp__internal__c_test_lenient_flags_20(state, p, endp)) { case 0: goto s_n_llhttp__internal__n_span_end_llhttp__on_header_value_9; default: goto s_n_llhttp__internal__n_invoke_or_flags_19; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_type_2: { switch (llhttp__internal__c_load_type(state, p, endp)) { case 1: - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_20; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_21; default: goto s_n_llhttp__internal__n_invoke_or_flags_19; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_18: { switch (llhttp__internal__c_or_flags_18(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_and_flags; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_flags_3: { switch (llhttp__internal__c_test_flags_3(state, p, endp)) { @@ -8474,16 +8525,14 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_or_flags_18; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_or_flags_20: { switch (llhttp__internal__c_or_flags_20(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_header_state_9; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_header_state_3: { switch (llhttp__internal__c_load_header_state(state, p, endp)) { @@ -8498,57 +8547,51 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_header_value; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_21: { - switch (llhttp__internal__c_test_lenient_flags_21(state, p, endp)) { + s_n_llhttp__internal__n_invoke_test_lenient_flags_22: { + switch (llhttp__internal__c_test_lenient_flags_22(state, p, endp)) { case 0: - goto s_n_llhttp__internal__n_error_59; + goto s_n_llhttp__internal__n_error_60; default: goto s_n_llhttp__internal__n_header_value_discard_ws; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_flags_4: { switch (llhttp__internal__c_test_flags_4(state, p, endp)) { case 1: - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_21; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_22; default: goto s_n_llhttp__internal__n_header_value_discard_ws; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_60: { + s_n_llhttp__internal__n_error_61: { state->error = 0xf; state->reason = "Transfer-Encoding can't be present with Content-Length"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_22: { - switch (llhttp__internal__c_test_lenient_flags_21(state, p, endp)) { + s_n_llhttp__internal__n_invoke_test_lenient_flags_23: { + switch (llhttp__internal__c_test_lenient_flags_22(state, p, endp)) { case 0: - goto s_n_llhttp__internal__n_error_60; + goto s_n_llhttp__internal__n_error_61; default: goto s_n_llhttp__internal__n_header_value_discard_ws; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_flags_5: { switch (llhttp__internal__c_test_flags_2(state, p, endp)) { case 1: - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_22; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_23; default: goto s_n_llhttp__internal__n_header_value_discard_ws; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_19: { state->error = 0x15; @@ -8556,8 +8599,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_header_state; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_45: { state->error = 0x1c; @@ -8565,8 +8607,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_field_1: { const unsigned char* start; @@ -8583,8 +8624,7 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_header_field_2: { const unsigned char* start; @@ -8601,41 +8641,36 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_invoke_llhttp__on_header_field_complete; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_61: { + s_n_llhttp__internal__n_error_62: { state->error = 0xa; state->reason = "Invalid header token"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_10: { switch (llhttp__internal__c_update_header_state_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_field_general; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_store_header_state: { switch (llhttp__internal__c_store_header_state(state, p, endp, match)) { default: goto s_n_llhttp__internal__n_header_field_colon; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_header_state_11: { switch (llhttp__internal__c_update_header_state_1(state, p, endp)) { default: goto s_n_llhttp__internal__n_header_field_general; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_4: { state->error = 0x1e; @@ -8643,8 +8678,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_test_lenient_flags: { switch (llhttp__internal__c_test_lenient_flags(state, p, endp)) { @@ -8653,8 +8687,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_4; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_20: { state->error = 0x15; @@ -8662,8 +8695,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_headers_start; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_3: { state->error = 0x1a; @@ -8671,8 +8703,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_url_complete: { switch (llhttp__on_url_complete(state, p, endp)) { @@ -8683,24 +8714,21 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_error_3; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_http_minor: { switch (llhttp__internal__c_update_http_minor(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_llhttp__on_url_complete; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_http_major: { switch (llhttp__internal__c_update_http_major(state, p, endp)) { default: goto s_n_llhttp__internal__n_invoke_update_http_minor; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_3: { const unsigned char* start; @@ -8716,17 +8744,15 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_62: { + s_n_llhttp__internal__n_error_63: { state->error = 0x7; state->reason = "Expected CRLF"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_4: { const unsigned char* start; @@ -8742,73 +8768,65 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_lf_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_70: { + s_n_llhttp__internal__n_error_72: { state->error = 0x17; state->reason = "Pause on PRI/Upgrade"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_71: { + s_n_llhttp__internal__n_error_73: { state->error = 0x9; state->reason = "Expected HTTP/2 Connection Preface"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_68: { + s_n_llhttp__internal__n_error_70: { state->error = 0x2; state->reason = "Expected CRLF after version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_25: { + s_n_llhttp__internal__n_invoke_test_lenient_flags_26: { switch (llhttp__internal__c_test_lenient_flags_8(state, p, endp)) { case 1: goto s_n_llhttp__internal__n_headers_start; default: - goto s_n_llhttp__internal__n_error_68; + goto s_n_llhttp__internal__n_error_70; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_67: { + s_n_llhttp__internal__n_error_69: { state->error = 0x9; state->reason = "Expected CRLF after version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_24: { + s_n_llhttp__internal__n_invoke_test_lenient_flags_25: { switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) { case 1: goto s_n_llhttp__internal__n_req_http_complete_crlf; default: - goto s_n_llhttp__internal__n_error_67; + goto s_n_llhttp__internal__n_error_69; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_69: { + s_n_llhttp__internal__n_error_71: { state->error = 0x9; state->reason = "Expected CRLF after version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_pause_21: { state->error = 0x15; @@ -8816,17 +8834,15 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_method_1; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_66: { + s_n_llhttp__internal__n_error_68: { state->error = 0x21; state->reason = "`on_version_complete` callback error"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_1: { const unsigned char* start; @@ -8842,8 +8858,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_version_complete; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version: { const unsigned char* start; @@ -8855,12 +8870,11 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_65; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_67; return s_error; } - goto s_n_llhttp__internal__n_error_65; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_67; + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_http_minor: { switch (llhttp__internal__c_load_http_minor(state, p, endp)) { @@ -8869,8 +8883,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_version; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_http_minor_1: { switch (llhttp__internal__c_load_http_minor(state, p, endp)) { @@ -8881,8 +8894,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_version; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_http_minor_2: { switch (llhttp__internal__c_load_http_minor(state, p, endp)) { @@ -8891,8 +8903,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_version; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_http_major: { switch (llhttp__internal__c_load_http_major(state, p, endp)) { @@ -8905,26 +8916,23 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_version; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_23: { - switch (llhttp__internal__c_test_lenient_flags_23(state, p, endp)) { + s_n_llhttp__internal__n_invoke_test_lenient_flags_24: { + switch (llhttp__internal__c_test_lenient_flags_24(state, p, endp)) { case 1: goto s_n_llhttp__internal__n_span_end_llhttp__on_version_1; default: goto s_n_llhttp__internal__n_invoke_load_http_major; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_store_http_minor: { switch (llhttp__internal__c_store_http_minor(state, p, endp, match)) { default: - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_23; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_24; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_2: { const unsigned char* start; @@ -8936,12 +8944,11 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_72; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_74; return s_error; } - goto s_n_llhttp__internal__n_error_72; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_74; + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_3: { const unsigned char* start; @@ -8953,20 +8960,18 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_73; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_75; return s_error; } - goto s_n_llhttp__internal__n_error_73; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_75; + UNREACHABLE; } s_n_llhttp__internal__n_invoke_store_http_major: { switch (llhttp__internal__c_store_http_major(state, p, endp, match)) { default: goto s_n_llhttp__internal__n_req_http_dot; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_4: { const unsigned char* start; @@ -8978,204 +8983,182 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_74; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_76; return s_error; } - goto s_n_llhttp__internal__n_error_74; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_76; + UNREACHABLE; } - s_n_llhttp__internal__n_error_64: { + s_n_llhttp__internal__n_error_77: { + state->error = 0x8; + state->reason = "Expected HTTP/, RTSP/ or ICE/"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_error; + return s_error; + UNREACHABLE; + } + s_n_llhttp__internal__n_error_66: { state->error = 0x8; state->reason = "Invalid method for HTTP/x.x request"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_load_method: { - switch (llhttp__internal__c_load_method(state, p, endp)) { - case 0: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 1: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 2: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 3: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 4: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 5: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 6: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 7: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 8: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 9: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 10: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 11: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 12: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 13: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 14: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 15: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 16: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 17: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 18: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 19: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 20: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 21: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 22: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 23: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 24: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 25: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 26: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 27: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 28: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 29: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 30: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 31: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 32: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 33: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 34: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 46: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - default: - goto s_n_llhttp__internal__n_error_64; - } - /* UNREACHABLE */; - abort(); + s_n_llhttp__internal__n_pause_22: { + state->error = 0x15; + state->reason = "on_protocol_complete pause"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_method; + return s_error; + UNREACHABLE; } - s_n_llhttp__internal__n_error_77: { - state->error = 0x8; - state->reason = "Expected HTTP/"; + s_n_llhttp__internal__n_error_65: { + state->error = 0x26; + state->reason = "`on_protocol_complete` callback error"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; + } + s_n_llhttp__internal__n_span_end_llhttp__on_protocol: { + const unsigned char* start; + int err; + + start = state->_span_pos0; + state->_span_pos0 = NULL; + err = llhttp__on_protocol(state, start, p); + if (err != 0) { + state->error = err; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete; + return s_error; + } + goto s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete; + UNREACHABLE; + } + s_n_llhttp__internal__n_span_end_llhttp__on_protocol_3: { + const unsigned char* start; + int err; + + start = state->_span_pos0; + state->_span_pos0 = NULL; + err = llhttp__on_protocol(state, start, p); + if (err != 0) { + state->error = err; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_82; + return s_error; + } + goto s_n_llhttp__internal__n_error_82; + UNREACHABLE; } - s_n_llhttp__internal__n_error_75: { + s_n_llhttp__internal__n_error_79: { state->error = 0x8; state->reason = "Expected SOURCE method for ICE/x.x request"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_load_method_2: { - switch (llhttp__internal__c_load_method(state, p, endp)) { - case 33: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - default: - goto s_n_llhttp__internal__n_error_75; + s_n_llhttp__internal__n_pause_23: { + state->error = 0x15; + state->reason = "on_protocol_complete pause"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_method_2; + return s_error; + UNREACHABLE; + } + s_n_llhttp__internal__n_error_78: { + state->error = 0x26; + state->reason = "`on_protocol_complete` callback error"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_error; + return s_error; + UNREACHABLE; + } + s_n_llhttp__internal__n_span_end_llhttp__on_protocol_1: { + const unsigned char* start; + int err; + + start = state->_span_pos0; + state->_span_pos0 = NULL; + err = llhttp__on_protocol(state, start, p); + if (err != 0) { + state->error = err; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1; + return s_error; } - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_1; + UNREACHABLE; } - s_n_llhttp__internal__n_error_76: { + s_n_llhttp__internal__n_error_81: { state->error = 0x8; state->reason = "Invalid method for RTSP/x.x request"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_load_method_3: { - switch (llhttp__internal__c_load_method(state, p, endp)) { - case 1: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 3: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 6: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 35: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 36: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 37: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 38: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 39: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 40: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 41: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 42: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 43: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 44: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - case 45: - goto s_n_llhttp__internal__n_span_start_llhttp__on_version; - default: - goto s_n_llhttp__internal__n_error_76; + s_n_llhttp__internal__n_pause_24: { + state->error = 0x15; + state->reason = "on_protocol_complete pause"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_method_3; + return s_error; + UNREACHABLE; + } + s_n_llhttp__internal__n_error_80: { + state->error = 0x26; + state->reason = "`on_protocol_complete` callback error"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_error; + return s_error; + UNREACHABLE; + } + s_n_llhttp__internal__n_span_end_llhttp__on_protocol_2: { + const unsigned char* start; + int err; + + start = state->_span_pos0; + state->_span_pos0 = NULL; + err = llhttp__on_protocol(state, start, p); + if (err != 0) { + state->error = err; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2; + return s_error; } - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_2; + UNREACHABLE; } - s_n_llhttp__internal__n_pause_22: { + s_n_llhttp__internal__n_pause_25: { state->error = 0x15; state->reason = "on_url_complete pause"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_req_http_start; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_63: { + s_n_llhttp__internal__n_error_64: { state->error = 0x1a; state->reason = "`on_url_complete` callback error"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_url_complete_1: { switch (llhttp__on_url_complete(state, p, endp)) { case 0: goto s_n_llhttp__internal__n_req_http_start; case 21: - goto s_n_llhttp__internal__n_pause_22; + goto s_n_llhttp__internal__n_pause_25; default: - goto s_n_llhttp__internal__n_error_63; + goto s_n_llhttp__internal__n_error_64; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_5: { const unsigned char* start; @@ -9191,8 +9174,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_6: { const unsigned char* start; @@ -9208,8 +9190,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_7: { const unsigned char* start; @@ -9225,8 +9206,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_lf_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_8: { const unsigned char* start; @@ -9242,17 +9222,15 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_78: { + s_n_llhttp__internal__n_error_83: { state->error = 0x7; state->reason = "Invalid char in url fragment start"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_9: { const unsigned char* start; @@ -9268,8 +9246,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_10: { const unsigned char* start; @@ -9285,8 +9262,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_lf_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_11: { const unsigned char* start; @@ -9302,26 +9278,23 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_79: { + s_n_llhttp__internal__n_error_84: { state->error = 0x7; state->reason = "Invalid char in url query"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_80: { + s_n_llhttp__internal__n_error_85: { state->error = 0x7; state->reason = "Invalid char in url path"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url: { const unsigned char* start; @@ -9337,8 +9310,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_1: { const unsigned char* start; @@ -9354,8 +9326,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_lf_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_2: { const unsigned char* start; @@ -9371,8 +9342,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_12: { const unsigned char* start; @@ -9388,8 +9358,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_13: { const unsigned char* start; @@ -9405,8 +9374,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_lf_to_http09; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_url_14: { const unsigned char* start; @@ -9422,62 +9390,55 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_url_skip_to_http; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_81: { + s_n_llhttp__internal__n_error_86: { state->error = 0x7; state->reason = "Double @ in url"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_82: { + s_n_llhttp__internal__n_error_87: { state->error = 0x7; state->reason = "Unexpected char in url server"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_83: { + s_n_llhttp__internal__n_error_88: { state->error = 0x7; state->reason = "Unexpected char in url server"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_84: { + s_n_llhttp__internal__n_error_89: { state->error = 0x7; state->reason = "Unexpected char in url schema"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_85: { + s_n_llhttp__internal__n_error_90: { state->error = 0x7; state->reason = "Unexpected char in url schema"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_86: { + s_n_llhttp__internal__n_error_91: { state->error = 0x7; state->reason = "Unexpected start char in url"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_is_equal_method: { switch (llhttp__internal__c_is_equal_method(state, p, endp)) { @@ -9486,35 +9447,31 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_url_entry_connect; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_87: { + s_n_llhttp__internal__n_error_92: { state->error = 0x6; state->reason = "Expected space after method"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_pause_26: { + s_n_llhttp__internal__n_pause_29: { state->error = 0x15; state->reason = "on_method_complete pause"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_req_first_space_before_url; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_106: { + s_n_llhttp__internal__n_error_111: { state->error = 0x20; state->reason = "`on_method_complete` callback error"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_method_2: { const unsigned char* start; @@ -9530,129 +9487,115 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_method_complete_1; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_store_method_1: { switch (llhttp__internal__c_store_method(state, p, endp, match)) { default: goto s_n_llhttp__internal__n_span_end_llhttp__on_method_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_107: { + s_n_llhttp__internal__n_error_112: { state->error = 0x6; state->reason = "Invalid method encountered"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_99: { + s_n_llhttp__internal__n_error_104: { state->error = 0xd; state->reason = "Invalid status code"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_97: { + s_n_llhttp__internal__n_error_102: { state->error = 0xd; state->reason = "Invalid status code"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_95: { + s_n_llhttp__internal__n_error_100: { state->error = 0xd; state->reason = "Invalid status code"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_pause_24: { + s_n_llhttp__internal__n_pause_27: { state->error = 0x15; state->reason = "on_status_complete pause"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_headers_start; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_91: { + s_n_llhttp__internal__n_error_96: { state->error = 0x1b; state->reason = "`on_status_complete` callback error"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_status_complete: { switch (llhttp__on_status_complete(state, p, endp)) { case 0: goto s_n_llhttp__internal__n_headers_start; case 21: - goto s_n_llhttp__internal__n_pause_24; + goto s_n_llhttp__internal__n_pause_27; default: - goto s_n_llhttp__internal__n_error_91; + goto s_n_llhttp__internal__n_error_96; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_90: { + s_n_llhttp__internal__n_error_95: { state->error = 0xd; state->reason = "Invalid response status"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_27: { + s_n_llhttp__internal__n_invoke_test_lenient_flags_28: { switch (llhttp__internal__c_test_lenient_flags_1(state, p, endp)) { case 1: goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete; default: - goto s_n_llhttp__internal__n_error_90; + goto s_n_llhttp__internal__n_error_95; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_92: { + s_n_llhttp__internal__n_error_97: { state->error = 0x2; state->reason = "Expected LF after CR"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_28: { + s_n_llhttp__internal__n_invoke_test_lenient_flags_29: { switch (llhttp__internal__c_test_lenient_flags_8(state, p, endp)) { case 1: goto s_n_llhttp__internal__n_invoke_llhttp__on_status_complete; default: - goto s_n_llhttp__internal__n_error_92; + goto s_n_llhttp__internal__n_error_97; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_93: { + s_n_llhttp__internal__n_error_98: { state->error = 0x19; state->reason = "Missing expected CR after response line"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_status: { const unsigned char* start; @@ -9664,13 +9607,12 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) (p + 1); - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_test_lenient_flags_29; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_test_lenient_flags_30; return s_error; } p++; - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_29; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_30; + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_status_1: { const unsigned char* start; @@ -9687,109 +9629,97 @@ static llparse_state_t llhttp__internal__run( } p++; goto s_n_llhttp__internal__n_res_line_almost_done; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_94: { + s_n_llhttp__internal__n_error_99: { state->error = 0xd; state->reason = "Invalid response status"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_mul_add_status_code_2: { switch (llhttp__internal__c_mul_add_status_code(state, p, endp, match)) { case 1: - goto s_n_llhttp__internal__n_error_95; + goto s_n_llhttp__internal__n_error_100; default: goto s_n_llhttp__internal__n_res_status_code_otherwise; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_96: { + s_n_llhttp__internal__n_error_101: { state->error = 0xd; state->reason = "Invalid status code"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_mul_add_status_code_1: { switch (llhttp__internal__c_mul_add_status_code(state, p, endp, match)) { case 1: - goto s_n_llhttp__internal__n_error_97; + goto s_n_llhttp__internal__n_error_102; default: goto s_n_llhttp__internal__n_res_status_code_digit_3; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_98: { + s_n_llhttp__internal__n_error_103: { state->error = 0xd; state->reason = "Invalid status code"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_mul_add_status_code: { switch (llhttp__internal__c_mul_add_status_code(state, p, endp, match)) { case 1: - goto s_n_llhttp__internal__n_error_99; + goto s_n_llhttp__internal__n_error_104; default: goto s_n_llhttp__internal__n_res_status_code_digit_2; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_100: { + s_n_llhttp__internal__n_error_105: { state->error = 0xd; state->reason = "Invalid status code"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_status_code: { switch (llhttp__internal__c_update_status_code(state, p, endp)) { default: goto s_n_llhttp__internal__n_res_status_code_digit_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_101: { + s_n_llhttp__internal__n_error_106: { state->error = 0x9; state->reason = "Expected space after version"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_pause_25: { + s_n_llhttp__internal__n_pause_28: { state->error = 0x15; state->reason = "on_version_complete pause"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_res_after_version; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_89: { + s_n_llhttp__internal__n_error_94: { state->error = 0x21; state->reason = "`on_version_complete` callback error"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_6: { const unsigned char* start; @@ -9805,8 +9735,7 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_version_complete_1; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_5: { const unsigned char* start; @@ -9818,12 +9747,11 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_88; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_93; return s_error; } - goto s_n_llhttp__internal__n_error_88; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_93; + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_http_minor_3: { switch (llhttp__internal__c_load_http_minor(state, p, endp)) { @@ -9832,8 +9760,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_version_5; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_http_minor_4: { switch (llhttp__internal__c_load_http_minor(state, p, endp)) { @@ -9844,8 +9771,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_version_5; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_http_minor_5: { switch (llhttp__internal__c_load_http_minor(state, p, endp)) { @@ -9854,8 +9780,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_version_5; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_http_major_1: { switch (llhttp__internal__c_load_http_major(state, p, endp)) { @@ -9868,26 +9793,23 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_span_end_llhttp__on_version_5; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_invoke_test_lenient_flags_26: { - switch (llhttp__internal__c_test_lenient_flags_23(state, p, endp)) { + s_n_llhttp__internal__n_invoke_test_lenient_flags_27: { + switch (llhttp__internal__c_test_lenient_flags_24(state, p, endp)) { case 1: goto s_n_llhttp__internal__n_span_end_llhttp__on_version_6; default: goto s_n_llhttp__internal__n_invoke_load_http_major_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_store_http_minor_1: { switch (llhttp__internal__c_store_http_minor(state, p, endp, match)) { default: - goto s_n_llhttp__internal__n_invoke_test_lenient_flags_26; + goto s_n_llhttp__internal__n_invoke_test_lenient_flags_27; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_7: { const unsigned char* start; @@ -9899,12 +9821,11 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_102; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_107; return s_error; } - goto s_n_llhttp__internal__n_error_102; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_107; + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_8: { const unsigned char* start; @@ -9916,20 +9837,18 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_103; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_108; return s_error; } - goto s_n_llhttp__internal__n_error_103; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_108; + UNREACHABLE; } s_n_llhttp__internal__n_invoke_store_http_major_1: { switch (llhttp__internal__c_store_http_major(state, p, endp, match)) { default: goto s_n_llhttp__internal__n_res_http_dot; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_version_9: { const unsigned char* start; @@ -9941,30 +9860,75 @@ static llparse_state_t llhttp__internal__run( if (err != 0) { state->error = err; state->error_pos = (const char*) p; - state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_104; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_109; return s_error; } - goto s_n_llhttp__internal__n_error_104; - /* UNREACHABLE */; - abort(); + goto s_n_llhttp__internal__n_error_109; + UNREACHABLE; } - s_n_llhttp__internal__n_error_108: { + s_n_llhttp__internal__n_error_114: { state->error = 0x8; - state->reason = "Expected HTTP/"; + state->reason = "Expected HTTP/, RTSP/ or ICE/"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_pause_23: { + s_n_llhttp__internal__n_pause_30: { + state->error = 0x15; + state->reason = "on_protocol_complete pause"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_res_after_protocol; + return s_error; + UNREACHABLE; + } + s_n_llhttp__internal__n_error_113: { + state->error = 0x26; + state->reason = "`on_protocol_complete` callback error"; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_error; + return s_error; + UNREACHABLE; + } + s_n_llhttp__internal__n_span_end_llhttp__on_protocol_4: { + const unsigned char* start; + int err; + + start = state->_span_pos0; + state->_span_pos0 = NULL; + err = llhttp__on_protocol(state, start, p); + if (err != 0) { + state->error = err; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3; + return s_error; + } + goto s_n_llhttp__internal__n_invoke_llhttp__on_protocol_complete_3; + UNREACHABLE; + } + s_n_llhttp__internal__n_span_end_llhttp__on_protocol_5: { + const unsigned char* start; + int err; + + start = state->_span_pos0; + state->_span_pos0 = NULL; + err = llhttp__on_protocol(state, start, p); + if (err != 0) { + state->error = err; + state->error_pos = (const char*) p; + state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_error_115; + return s_error; + } + goto s_n_llhttp__internal__n_error_115; + UNREACHABLE; + } + s_n_llhttp__internal__n_pause_26: { state->error = 0x15; state->reason = "on_method_complete pause"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_req_first_space_before_url; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error_1: { state->error = 0x20; @@ -9972,8 +9936,7 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_method: { const unsigned char* start; @@ -9989,33 +9952,29 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_llhttp__on_method_complete; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_type: { switch (llhttp__internal__c_update_type(state, p, endp)) { default: goto s_n_llhttp__internal__n_span_end_llhttp__on_method; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_store_method: { switch (llhttp__internal__c_store_method(state, p, endp, match)) { default: goto s_n_llhttp__internal__n_invoke_update_type; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_105: { + s_n_llhttp__internal__n_error_110: { state->error = 0x8; state->reason = "Invalid word encountered"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_span_end_llhttp__on_method_1: { const unsigned char* start; @@ -10031,25 +9990,22 @@ static llparse_state_t llhttp__internal__run( return s_error; } goto s_n_llhttp__internal__n_invoke_update_type_1; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_update_type_2: { switch (llhttp__internal__c_update_type(state, p, endp)) { default: goto s_n_llhttp__internal__n_span_start_llhttp__on_method_1; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_pause_27: { + s_n_llhttp__internal__n_pause_31: { state->error = 0x15; state->reason = "on_message_begin pause"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_load_type; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_error: { state->error = 0x10; @@ -10057,50 +10013,45 @@ static llparse_state_t llhttp__internal__run( state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_message_begin: { switch (llhttp__on_message_begin(state, p, endp)) { case 0: goto s_n_llhttp__internal__n_invoke_load_type; case 21: - goto s_n_llhttp__internal__n_pause_27; + goto s_n_llhttp__internal__n_pause_31; default: goto s_n_llhttp__internal__n_error; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_pause_28: { + s_n_llhttp__internal__n_pause_32: { state->error = 0x15; state->reason = "on_reset pause"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_n_llhttp__internal__n_invoke_update_finish; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } - s_n_llhttp__internal__n_error_109: { + s_n_llhttp__internal__n_error_116: { state->error = 0x1f; state->reason = "`on_reset` callback error"; state->error_pos = (const char*) p; state->_current = (void*) (intptr_t) s_error; return s_error; - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_llhttp__on_reset: { switch (llhttp__on_reset(state, p, endp)) { case 0: goto s_n_llhttp__internal__n_invoke_update_finish; case 21: - goto s_n_llhttp__internal__n_pause_28; + goto s_n_llhttp__internal__n_pause_32; default: - goto s_n_llhttp__internal__n_error_109; + goto s_n_llhttp__internal__n_error_116; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } s_n_llhttp__internal__n_invoke_load_initial_message_completed: { switch (llhttp__internal__c_load_initial_message_completed(state, p, endp)) { @@ -10109,8 +10060,7 @@ static llparse_state_t llhttp__internal__run( default: goto s_n_llhttp__internal__n_invoke_update_finish; } - /* UNREACHABLE */; - abort(); + UNREACHABLE; } } diff --git a/docs/docs/api/Agent.md b/docs/docs/api/Agent.md index 2a8e30bac14..0131b103975 100644 --- a/docs/docs/api/Agent.md +++ b/docs/docs/api/Agent.md @@ -19,6 +19,7 @@ Returns: `Agent` Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions) * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)` +* **maxOrigins** `number` (optional) - Default: `Infinity` - Limits the total number of origins that can receive requests at a time, throwing an `MaxOriginsReachedError` error when attempting to dispatch when the max is reached. If `Infinity`, no limit is enforced. ## Instance Properties diff --git a/docs/docs/api/CacheStore.md b/docs/docs/api/CacheStore.md index 00ceb960641..15524748ad5 100644 --- a/docs/docs/api/CacheStore.md +++ b/docs/docs/api/CacheStore.md @@ -66,10 +66,12 @@ Parameters: Returns: `GetResult | Promise | undefined` - If the request is cached, the cached response is returned. If the request's method is anything other than HEAD, the response is also returned. If the request isn't cached, `undefined` is returned. +The `get` method may return a `Promise` for async cache stores (e.g. Redis-backed or remote stores). The cache interceptor handles both synchronous and asynchronous return values, including in revalidation paths (304 Not Modified handling and stale-while-revalidate background revalidation). + Response properties: * **response** `CacheValue` - The cached response data. -* **body** `Readable | undefined` - The response's body. +* **body** `Readable | Iterable | undefined` - The response's body. This can be an array of `Buffer` chunks (with a `.values()` method) or a `Readable` stream. Both formats are supported in all code paths, including 304 revalidation. ### Function: `createWriteStream` @@ -98,8 +100,11 @@ This is an interface containing the majority of a response's data (minus the bod ### Property `vary` -`Record | undefined` - The headers defined by the response's `Vary` header -and their respective values for later comparison +`Record | undefined` - The headers defined by the response's `Vary` header +and their respective values for later comparison. Values are `null` when the +header specified in `Vary` was not present in the original request. These `null` +values are automatically filtered out during revalidation so they are not sent +as request headers. For example, for a response like ``` @@ -116,6 +121,14 @@ This would be } ``` +If the original request did not include the `accepts` header: +```js +{ + 'content-encoding': 'utf8', + accepts: null +} +``` + ### Property `cachedAt` `number` - Time in millis that this value was cached. diff --git a/docs/docs/api/Client.md b/docs/docs/api/Client.md index eab6ddc45ac..d3d115fdef6 100644 --- a/docs/docs/api/Client.md +++ b/docs/docs/api/Client.md @@ -24,13 +24,20 @@ Returns: `Client` * **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - 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 2 seconds. * **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. +* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options. + * **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit. + * **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit. * **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`. -* **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. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source. * **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. +* **useH2c**: `boolean` - Default: `false`. Enforces h2c for non-https connections. * **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. +* **initialWindowSize**: `number` (optional) - Default: `262144` (256KB). Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE). Must be a positive integer greater than 0. This default is higher than Node.js core's default (65535 bytes) to improve throughput, Node's choice is very conservative for current high-bandwith networks. See [RFC 7540 Section 6.9.2](https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.2) for more details. +* **connectionWindowSize**: `number` (optional) - Default `524288` (512KB). Sets the HTTP/2 connection-level flow-control window size using `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer greater than 0. This provides better flow control for the entire connection across multiple streams. See [Node.js HTTP/2 documentation](https://nodejs.org/api/http2.html#clienthttp2sessionsetlocalwindowsize) for more details. +* **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections. This will emit a `ping` event on the client with the duration of the ping in milliseconds. > **Notes about HTTP/2** > - It only works under TLS connections. h2c is not supported. diff --git a/docs/docs/api/Cookies.md b/docs/docs/api/Cookies.md index 0cad37914d6..2368db43a79 100644 --- a/docs/docs/api/Cookies.md +++ b/docs/docs/api/Cookies.md @@ -80,6 +80,33 @@ Arguments: Returns: `Cookie[]` +## `parseCookie(cookie)` + +Parses a single `Set-Cookie` header value into a `Cookie` object. + +```js +import { parseCookie } from 'undici' + +console.log(parseCookie('undici=getSetCookies; Secure; SameSite=Lax')) +// { +// name: 'undici', +// value: 'getSetCookies', +// secure: true, +// sameSite: 'Lax' +// } +``` + +Notes: + +* The cookie value is returned as it appears in the header. Percent-encoded sequences such as `%20` or `%0D%0A` are **not** decoded. +* `sameSite` is only set for exact case-insensitive matches of `Strict`, `Lax`, or `None`. + +Arguments: + +* **cookie** `string` + +Returns: `Cookie | null` + ## `setCookie(headers, cookie)` Appends a cookie to the `Set-Cookie` header. diff --git a/docs/docs/api/DiagnosticsChannel.md b/docs/docs/api/DiagnosticsChannel.md index 9a21c7433cc..cff8546f6f0 100644 --- a/docs/docs/api/DiagnosticsChannel.md +++ b/docs/docs/api/DiagnosticsChannel.md @@ -169,13 +169,40 @@ This message is published after the client has successfully connected to a serve ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:open').subscribe(({ address, protocol, extensions }) => { +diagnosticsChannel.channel('undici:websocket:open').subscribe(({ + address, // { address: string, family: string, port: number } + protocol, // string - negotiated subprotocol + extensions, // string - negotiated extensions + websocket, // WebSocket - the WebSocket instance + handshakeResponse // object - HTTP response that upgraded the connection +}) => { console.log(address) // address, family, and port console.log(protocol) // negotiated subprotocols console.log(extensions) // negotiated extensions + console.log(websocket) // the WebSocket instance + + // Handshake response details + console.log(handshakeResponse.status) // 101 for HTTP/1.1, 200 for HTTP/2 extended CONNECT + console.log(handshakeResponse.statusText) // 'Switching Protocols' for HTTP/1.1, commonly 'OK' for HTTP/2 in Node.js + console.log(handshakeResponse.headers) // Object containing response headers }) ``` +### Handshake Response Object + +The `handshakeResponse` object contains the HTTP response that established the WebSocket connection: + +- `status` (number): The HTTP status code (`101` for HTTP/1.1 upgrade, `200` for HTTP/2 extended CONNECT) +- `statusText` (string): The HTTP status message (`'Switching Protocols'` for HTTP/1.1, commonly `'OK'` for HTTP/2 in Node.js) +- `headers` (object): The HTTP response headers from the server, including: + - `sec-websocket-accept` and other WebSocket-related headers + - `upgrade: 'websocket'` + - `connection: 'upgrade'` + + The `upgrade` and `connection` headers are only present for HTTP/1.1 handshakes. + +This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection. + ## `undici:websocket:close` This message is published after the connection has closed. @@ -184,7 +211,7 @@ This message is published after the connection has closed. import diagnosticsChannel from 'diagnostics_channel' diagnosticsChannel.channel('undici:websocket:close').subscribe(({ websocket, code, reason }) => { - console.log(websocket) // the WebSocket object + console.log(websocket) // the WebSocket instance console.log(code) // the closing status code console.log(reason) // the closing reason }) @@ -209,9 +236,10 @@ This message is published after the client receives a ping frame, if the connect ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:ping').subscribe(({ payload }) => { +diagnosticsChannel.channel('undici:websocket:ping').subscribe(({ payload, websocket }) => { // a Buffer or undefined, containing the optional application data of the frame console.log(payload) + console.log(websocket) // the WebSocket instance }) ``` @@ -222,8 +250,66 @@ This message is published after the client receives a pong frame. ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:pong').subscribe(({ payload }) => { +diagnosticsChannel.channel('undici:websocket:pong').subscribe(({ payload, websocket }) => { // a Buffer or undefined, containing the optional application data of the frame console.log(payload) + console.log(websocket) // the WebSocket instance }) ``` + +## `undici:proxy:connected` + +This message is published after the `ProxyAgent` establishes a connection to the proxy server. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:proxy:connected').subscribe(({ socket, connectParams }) => { + console.log(socket) + console.log(connectParams) + // const { origin, port, path, signal, headers, servername } = connectParams +}) +``` + +## `undici:request:pending-requests` + +This message is published when the deduplicate interceptor's pending request map changes. This is useful for monitoring and debugging request deduplication behavior. + +The deduplicate interceptor automatically deduplicates concurrent requests for the same resource. When multiple identical requests are made while one is already in-flight, only one request is sent to the origin server, and all waiting handlers receive the same response. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:request:pending-requests').subscribe(({ type, size, key }) => { + console.log(type) // 'added' or 'removed' + console.log(size) // current number of pending requests + console.log(key) // the deduplication key for this request +}) +``` + +### Event Properties + +- `type` (`string`): Either `'added'` when a new pending request is registered, or `'removed'` when a pending request completes (successfully or with an error). +- `size` (`number`): The current number of pending requests after the change. +- `key` (`string`): The deduplication key for the request, composed of the origin, method, path, and request headers. + +### Example: Monitoring Request Deduplication + +```js +import diagnosticsChannel from 'diagnostics_channel' + +const channel = diagnosticsChannel.channel('undici:request:pending-requests') + +channel.subscribe(({ type, size, key }) => { + if (type === 'added') { + console.log(`New pending request: ${key} (${size} total pending)`) + } else { + console.log(`Request completed: ${key} (${size} remaining)`) + } +}) +``` + +This can be useful for: +- Verifying that request deduplication is working as expected +- Monitoring the number of concurrent in-flight requests +- Debugging deduplication behavior in production environments diff --git a/docs/docs/api/Dispatcher.md b/docs/docs/api/Dispatcher.md index 6def7872dbf..c9df4c63255 100644 --- a/docs/docs/api/Dispatcher.md +++ b/docs/docs/api/Dispatcher.md @@ -207,7 +207,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **onRequestStart** `(controller: DispatchController, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. * **onRequestUpgrade** `(controller: DispatchController, statusCode: number, headers: Record, socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`. -* **onResponseStart** `(controller: DispatchController, statusCode: number, headers: Record, statusMessage?: string) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests. +* **onResponseStart** `(controller: DispatchController, statusCode: number, headers: Record, statusMessage?: string) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests. Any return value is ignored. * **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests. * **onResponseEnd** `(controller: DispatchController, trailers: Record) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests. * **onResponseError** `(controller: DispatchController, error: Error) => void` - Invoked when an error has occurred. May not throw. @@ -364,7 +364,7 @@ client.dispatch({ ### `Dispatcher.pipeline(options, handler)` -For easy use with [stream.pipeline](https://nodejs.org/api/stream.html#stream_stream_pipeline_source_transforms_destination_callback). The `handler` argument should return a `Readable` from which the result will be read. Usually it should just return the `body` argument unless some kind of transformation needs to be performed based on e.g. `headers` or `statusCode`. The `handler` should validate the response and save any required state. If there is an error, it should be thrown. The function returns a `Duplex` which writes to the request and reads from the response. +For easy use with [stream.pipeline](https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-options). The `handler` argument should return a `Readable` from which the result will be read. Usually it should just return the `body` argument unless some kind of transformation needs to be performed based on e.g. `headers` or `statusCode`. The `handler` should validate the response and save any required state. If there is an error, it should be thrown. The function returns a `Duplex` which writes to the request and reads from the response. Arguments: @@ -476,6 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`. #### Parameter: `ResponseData` * **statusCode** `number` +* **statusText** `string` - The status message from the response (e.g., "OK", "Not Found"). * **headers** `Record` - 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). * **trailers** `Record` - This object starts out @@ -517,7 +518,7 @@ await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`) try { - const { body, headers, statusCode, trailers } = await client.request({ + const { body, headers, statusCode, statusText, trailers } = await client.request({ path: '/', method: 'GET' }) @@ -961,8 +962,8 @@ It accepts the same arguments as the [`RedirectHandler` constructor](/docs/docs/ const { Client, interceptors } = require("undici"); const { redirect } = interceptors; -const client = new Client("http://example.com").compose( - redirect({ maxRedirections: 3, throwOnMaxRedirects: true }) +const client = new Client("http://service.example").compose( + redirect({ maxRedirections: 3, throwOnMaxRedirect: true }) ); client.request({ path: "/" }) ``` @@ -979,7 +980,7 @@ It accepts the same arguments as the [`RetryHandler` constructor](/docs/docs/api const { Client, interceptors } = require("undici"); const { retry } = interceptors; -const client = new Client("http://example.com").compose( +const client = new Client("http://service.example").compose( retry({ maxRetries: 3, minTimeout: 1000, @@ -1005,7 +1006,7 @@ The `dump` interceptor enables you to dump the response body from a request upon const { Client, interceptors } = require("undici"); const { dump } = interceptors; -const client = new Client("http://example.com").compose( +const client = new Client("http://service.example").compose( dump({ maxSize: 1024, }) @@ -1035,14 +1036,15 @@ The `dns` interceptor enables you to cache DNS lookups for a given duration, per - `dualStack` - Whether to resolve both IPv4 and IPv6 addresses. Default: `true`. - It will also attempt a happy-eyeballs-like approach to connect to the available addresses in case of a connection failure. - `affinity` - Whether to use IPv4 or IPv6 addresses. Default: `4`. - - It can be either `'4` or `6`. + - It can be either `4` or `6`. - It will only take effect if `dualStack` is `false`. - `lookup: (hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void) => void` - Custom lookup function. Default: `dns.lookup`. - - For more info see [dns.lookup](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback). + - For more info see [dns.lookup](https://nodejs.org/api/dns.html#dnslookuphostname-options-callback). - `pick: (origin: URL, records: DNSInterceptorRecords, affinity: 4 | 6) => DNSInterceptorRecord` - Custom pick function. Default: `RoundRobin`. - The function should return a single record from the records array. - By default a simplified version of Round Robin is used. - The `records` property can be mutated to store the state of the balancing algorithm. +- `storage: DNSStorage` - Custom storage for resolved DNS records > The `Dispatcher#options` also gets extended with the options `dns.affinity`, `dns.dualStack`, `dns.lookup` and `dns.pick` which can be used to configure the interceptor at a request-per-request basis. @@ -1057,6 +1059,14 @@ It represents a map of DNS IP addresses records for a single origin. - `4.ips` - (`DNSInterceptorRecord[] | null`) The IPv4 addresses. - `6.ips` - (`DNSInterceptorRecord[] | null`) The IPv6 addresses. +**DNSStorage** +It represents a storage object for resolved DNS records. +- `size` - (`number`) current size of the storage. +- `get` - (`(origin: string) => DNSInterceptorOriginRecords | null`) method to get the records for a given origin. +- `set` - (`(origin: string, records: DNSInterceptorOriginRecords | null, options: { ttl: number }) => void`) method to set the records for a given origin. +- `delete` - (`(origin: string) => void`) method to delete records for a given origin. +- `full` - (`() => boolean`) method to check if the storage is full, if returns `true`, DNS lookup will be skipped in this interceptor and new records will not be stored. + **Example - Basic DNS Interceptor** ```js @@ -1073,6 +1083,45 @@ const response = await client.request({ }) ``` +**Example - DNS Interceptor and LRU cache as a storage** + +```js +const { Client, interceptors } = require("undici"); +const QuickLRU = require("quick-lru"); +const { dns } = interceptors; + +const lru = new QuickLRU({ maxSize: 100 }); + +const lruAdapter = { + get size() { + return lru.size; + }, + get(origin) { + return lru.get(origin); + }, + set(origin, records, { ttl }) { + lru.set(origin, records, { maxAge: ttl }); + }, + delete(origin) { + lru.delete(origin); + }, + full() { + // For LRU cache, we can always store new records, + // old records will be evicted automatically + return false; + } +} + +const client = new Agent().compose([ + dns({ storage: lruAdapter }) +]) + +const response = await client.request({ + origin: `http://localhost:3030`, + ...requestOpts +}) +``` + ##### `responseError` The `responseError` interceptor throws an error for responses with status code errors (>= 400). @@ -1083,7 +1132,7 @@ The `responseError` interceptor throws an error for responses with status code e const { Client, interceptors } = require("undici"); const { responseError } = interceptors; -const client = new Client("http://example.com").compose( +const client = new Client("http://service.example").compose( responseError() ); @@ -1094,6 +1143,65 @@ await client.request({ }); ``` +##### `decompress` + +⚠️ The decompress interceptor is experimental and subject to change. + +The `decompress` interceptor automatically decompresses response bodies that are compressed with gzip, deflate, brotli, or zstd compression. It removes the `content-encoding` and `content-length` headers from decompressed responses and supports RFC-9110 compliant multiple encodings. + +**Options** + +- `skipErrorResponses` - Whether to skip decompression for error responses (status codes >= 400). Default: `true`. +- `skipStatusCodes` - Array of status codes to skip decompression for. Default: `[204, 304]`. + +**Example - Basic Decompress Interceptor** + +```js +const { Client, interceptors } = require("undici"); +const { decompress } = interceptors; + +const client = new Client("http://service.example").compose( + decompress() +); + +// Automatically decompresses gzip/deflate/brotli/zstd responses +const response = await client.request({ + method: "GET", + path: "/" +}); +``` + +**Example - Custom Options** + +```js +const { Client, interceptors } = require("undici"); +const { decompress } = interceptors; + +const client = new Client("http://service.example").compose( + decompress({ + skipErrorResponses: false, // Decompress 5xx responses + skipStatusCodes: [204, 304, 201] // Skip these status codes + }) +); +``` + +**Supported Encodings** + +- `gzip` / `x-gzip` - GZIP compression +- `deflate` / `x-compress` - DEFLATE compression +- `br` - Brotli compression +- `zstd` - Zstandard compression +- Multiple encodings (e.g., `gzip, deflate`) are supported per RFC-9110 + +**Behavior** + +- Skips decompression for status codes < 200 or >= 400 (configurable) +- Skips decompression for 204 No Content and 304 Not Modified by default +- Removes `content-encoding` and `content-length` headers when decompressing +- Passes through unsupported encodings unchanged +- Handles case-insensitive encoding names +- Supports streaming decompression without buffering + ##### `Cache Interceptor` The `cache` interceptor implements client-side response caching as described in @@ -1103,8 +1211,69 @@ The `cache` interceptor implements client-side response caching as described in - `store` - The [`CacheStore`](/docs/docs/api/CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](/docs/docs/api/CacheStore.md#memorycachestore). - `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of. -- `cacheByDefault` - The default expiration time to cache responses by if they don't have an explicit expiration. If this isn't present, responses without explicit expiration will not be cached. Default `undefined`. -- `type` - The type of cache for Undici to act as. Can be `shared` or `private`. Default `shared`. +- `cacheByDefault` - The default expiration time to cache responses by if they don't have an explicit expiration and cannot have an heuristic expiry computed. If this isn't present, responses neither with an explicit expiration nor heuristically cacheable will not be cached. Default `undefined`. +- `type` - The [type of cache](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching#types_of_caches) for Undici to act as. Can be `shared` or `private`. Default `shared`. `private` implies privately cacheable responses will be cached and potentially shared with other users of your application. + +**Usage with `fetch`** + +```js +const { Agent, cacheStores, interceptors, setGlobalDispatcher } = require('undici') + +const client = new Agent().compose(interceptors.cache({ + store: new cacheStores.MemoryCacheStore({ + maxSize: 100 * 1024 * 1024, // 100MB + maxCount: 1000, + maxEntrySize: 5 * 1024 * 1024 // 5MB + }) +})) + +setGlobalDispatcher(client) + +// First request goes to the network and is cached when cache headers allow it. +const first = await fetch('https://example.com/data') + +// Second request can be served from cache according to RFC9111 rules. +const second = await fetch('https://example.com/data') +``` + +##### `Deduplicate Interceptor` + +The `deduplicate` interceptor deduplicates concurrent identical requests. When multiple identical requests are made while one is already in-flight, only one request is sent to the origin server, and all waiting handlers receive the same response. This reduces server load and improves performance. + +**Options** + +- `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to deduplicate. Default `['GET']`. +- `skipHeaderNames` - Header names that, if present in a request, will cause the request to skip deduplication entirely. Useful for headers like `idempotency-key` where presence indicates unique processing. Header name matching is case-insensitive. Default `[]`. +- `excludeHeaderNames` - Header names to exclude from the deduplication key. Requests with different values for these headers will still be deduplicated together. Useful for headers like `x-request-id` that vary per request but shouldn't affect deduplication. Header name matching is case-insensitive. Default `[]`. +- `maxBufferSize` - Maximum bytes buffered per paused waiting deduplicated handler. If a waiting handler remains paused and exceeds this threshold, it is failed with an abort error to prevent unbounded memory growth. Default `5 * 1024 * 1024`. + +**Usage** + +```js +const { Client, interceptors } = require("undici"); +const { deduplicate, cache } = interceptors; + +// Deduplicate only +const client = new Client("http://service.example").compose( + deduplicate() +); + +// Deduplicate with caching +const clientWithCache = new Client("http://service.example").compose( + deduplicate(), + cache() +); +``` + +Requests are considered identical if they have the same: +- Origin +- HTTP method +- Path +- Request headers (excluding any headers specified in `excludeHeaderNames`) + +All deduplicated requests receive the complete response including status code, headers, and body. + +For observability, request deduplication events are published to the `undici:request:pending-requests` [diagnostic channel](/docs/docs/api/DiagnosticsChannel.md#undicirequestpending-requests). ## Instance Events @@ -1158,6 +1327,10 @@ Header arguments such as `options.headers` in [`Client.dispatch`](/docs/docs/api * As an iterable that can encompass `Headers`, `Map`, or a custom iterator returning key-value pairs. Keys are lowercase and values are not modified. +Undici validates header syntax at the protocol level (for example, invalid header names and invalid control characters in string values), but it does not sanitize untrusted application input. Validate and sanitize any user-provided header names and values before passing them to Undici to prevent header/body injection vulnerabilities. + +When using the array header format (`string[]`), Undici processes only indexed elements. Additional properties assigned to the array object are ignored. + Response headers will derive a `host` from the `url` of the [Client](/docs/docs/api/Client.md#class-client) instance if no `host` header was previously specified. ### Example 1 - Object diff --git a/docs/docs/api/Errors.md b/docs/docs/api/Errors.md index dfba3b39ce0..4402db47593 100644 --- a/docs/docs/api/Errors.md +++ b/docs/docs/api/Errors.md @@ -14,7 +14,6 @@ import { errors } from 'undici' | `HeadersTimeoutError` | `UND_ERR_HEADERS_TIMEOUT` | socket is destroyed due to headers timeout. | | `HeadersOverflowError` | `UND_ERR_HEADERS_OVERFLOW` | socket is destroyed due to headers' max size being exceeded. | | `BodyTimeoutError` | `UND_ERR_BODY_TIMEOUT` | socket is destroyed due to body timeout. | -| `ResponseStatusCodeError` | `UND_ERR_RESPONSE_STATUS_CODE` | an error is thrown when `throwOnError` is `true` for status codes >= 400. | | `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. | | `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. | | `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user | @@ -27,6 +26,7 @@ import { errors } from 'undici' | `InformationalError` | `UND_ERR_INFO` | expected error with reason | | `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed | | `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed | +| `MessageSizeExceededError` | `UND_ERR_WS_MESSAGE_SIZE_EXCEEDED` | WebSocket decompressed message exceeded the maximum allowed size | Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === ''` instead to avoid inconsistencies. ### `SocketError` diff --git a/docs/docs/api/Fetch.md b/docs/docs/api/Fetch.md index 00c349847dc..a256824891c 100644 --- a/docs/docs/api/Fetch.md +++ b/docs/docs/api/Fetch.md @@ -10,6 +10,14 @@ This API is implemented as per the standard, you can find documentation on [MDN] If any parameters are passed to the FormData constructor other than `undefined`, an error will be thrown. Other parameters are ignored. +When you use `FormData` as a request body, keep `fetch` and `FormData` from the +same implementation. Use the built-in global `FormData` with the built-in +global `fetch()`, and use `undici`'s `FormData` with `undici.fetch()`. + +If you want the installed `undici` package to provide the globals, call +[`install()`](/docs/api/GlobalInstallation.md) so `fetch`, `Headers`, +`Response`, `Request`, and `FormData` are installed together as a matching set. + ## 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) diff --git a/docs/docs/api/GlobalInstallation.md b/docs/docs/api/GlobalInstallation.md index 7e4529d8f19..2fc5bc2fd86 100644 --- a/docs/docs/api/GlobalInstallation.md +++ b/docs/docs/api/GlobalInstallation.md @@ -43,6 +43,54 @@ The `install()` function adds the following classes to `globalThis`: | `MessageEvent` | WebSocket message event | | `EventSource` | Server-sent events client | +## Using `FormData` with `fetch` + +If you send a `FormData` body, use matching implementations for `fetch` and +`FormData`. + +These two patterns are safe: + +```js +// Built-in globals from Node.js +const body = new FormData() +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +```js +// Globals installed from the undici package +import { install } from 'undici' + +install() + +const body = new FormData() +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +After `install()`, `fetch`, `Headers`, `Response`, `Request`, and `FormData` +all come from the installed `undici` package, so they work as a matching set. + +If you do not want to install globals, import both from `undici` instead: + +```js +import { fetch, FormData } from 'undici' + +const body = new FormData() +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData` +with the built-in global `fetch()`. Keeping them paired avoids surprising +multipart behavior across Node.js and undici versions. + ## Use Cases Global installation is useful for: diff --git a/docs/docs/api/H2CClient.md b/docs/docs/api/H2CClient.md index c9bbb3f17e4..6558ad5f4a4 100644 --- a/docs/docs/api/H2CClient.md +++ b/docs/docs/api/H2CClient.md @@ -48,8 +48,9 @@ Returns: `H2CClient` - **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. - **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. - **pipelining** `number | null` (optional) - Default to `maxConcurrentStreams` - The amount of concurrent requests sent over a single HTTP/2 session in accordance with [RFC-7540](https://httpwg.org/specs/rfc7540.html#StreamsLayer) Stream specification. Streams can be closed up by remote server at any time. +- **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections. - **connect** `ConnectOptions | 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. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source. - **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. diff --git a/docs/docs/api/MockPool.md b/docs/docs/api/MockPool.md index 6656b95d834..64a7044b805 100644 --- a/docs/docs/api/MockPool.md +++ b/docs/docs/api/MockPool.md @@ -323,7 +323,8 @@ try { method: 'GET' }) } catch (error) { - console.error(error) // Error: kaboom + console.error(error) // TypeError: fetch failed + console.error(error.cause) // Error: kaboom } ``` diff --git a/docs/docs/api/ProxyAgent.md b/docs/docs/api/ProxyAgent.md index 0fced821cd8..8db7221a362 100644 --- a/docs/docs/api/ProxyAgent.md +++ b/docs/docs/api/ProxyAgent.md @@ -27,7 +27,7 @@ For detailed information on the parsing process and potential validation errors, * **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. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions). * **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions). -* **proxyTunnel** `boolean` (optional) - By default, ProxyAgent will request that the Proxy facilitate a tunnel between the endpoint and the agent. Setting `proxyTunnel` to false avoids issuing a CONNECT extension, and includes the endpoint domain and path in each request. +* **proxyTunnel** `boolean` (optional) - For connections involving secure protocols, Undici will always establish a tunnel via the HTTP2 CONNECT extension. If proxyTunnel is set to true, this will occur for unsecured proxy/endpoint connections as well. Currently, there is no way to facilitate HTTP1 IP tunneling as described in https://www.rfc-editor.org/rfc/rfc9484.html#name-http-11-request. If proxyTunnel is set to false (the default), ProxyAgent connections where both the Proxy and Endpoint are unsecured will issue all requests to the Proxy, and prefix the endpoint request path with the endpoint origin address. Examples: diff --git a/docs/docs/api/RedirectHandler.md b/docs/docs/api/RedirectHandler.md index bb16284fff4..ca24c6917b8 100644 --- a/docs/docs/api/RedirectHandler.md +++ b/docs/docs/api/RedirectHandler.md @@ -2,7 +2,7 @@ A class that handles redirection logic for HTTP requests. -## `new RedirectHandler(dispatch, maxRedirections, opts, handler, redirectionLimitReached)` +## `new RedirectHandler(dispatch, maxRedirections, opts, handler)` Arguments: @@ -10,7 +10,6 @@ Arguments: - **maxRedirections** `number` - Maximum number of redirections allowed. - **opts** `object` - Options for handling redirection. - **handler** `object` - An object containing handlers for different stages of the request lifecycle. -- **redirectionLimitReached** `boolean` (default: `false`) - A flag that the implementer can provide to enable or disable the feature. If set to `false`, it indicates that the caller doesn't want to use the feature and prefers the old behavior. Returns: `RedirectHandler` @@ -20,7 +19,6 @@ Returns: `RedirectHandler` - **maxRedirections** `number` (required) - Maximum number of redirections allowed. - **opts** `object` (required) - Options for handling redirection. - **handler** `object` (required) - Handlers for different stages of the request lifecycle. -- **redirectionLimitReached** `boolean` (default: `false`) - A flag that the implementer can provide to enable or disable the feature. If set to `false`, it indicates that the caller doesn't want to use the feature and prefers the old behavior. ### Properties @@ -30,7 +28,6 @@ Returns: `RedirectHandler` - **maxRedirections** `number` - Maximum number of redirections allowed. - **handler** `object` - Handlers for different stages of the request lifecycle. - **history** `Array` - An array representing the history of URLs during redirection. -- **redirectionLimitReached** `boolean` - Indicates whether the redirection limit has been reached. ### Methods diff --git a/docs/docs/api/RoundRobinPool.md b/docs/docs/api/RoundRobinPool.md new file mode 100644 index 00000000000..7221122ae6d --- /dev/null +++ b/docs/docs/api/RoundRobinPool.md @@ -0,0 +1,145 @@ +# Class: RoundRobinPool + +Extends: `undici.Dispatcher` + +A pool of [Client](/docs/docs/api/Client.md) instances connected to the same upstream target with round-robin client selection. + +Unlike [`Pool`](/docs/docs/api/Pool.md), which always selects the first available client, `RoundRobinPool` cycles through clients in a round-robin fashion. This ensures even distribution of requests across all connections, which is particularly useful when the upstream target is behind a load balancer that round-robins TCP connections across multiple backend servers (e.g., Kubernetes Services). + +Requests are not guaranteed to be dispatched in order of invocation. + +## `new RoundRobinPool(url[, options])` + +Arguments: + +* **url** `URL | string` - It should only include the **protocol, hostname, and port**. +* **options** `RoundRobinPoolOptions` (optional) + +### Parameter: `RoundRobinPoolOptions` + +Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions) + +* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)` +* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `RoundRobinPool` instance will create an unlimited amount of `Client` instances. +* **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `RoundRobinPool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age. + +## Use Case + +`RoundRobinPool` is designed for scenarios where: + +1. You connect to a single origin (e.g., `http://my-service.namespace.svc`) +2. That origin is backed by a load balancer distributing TCP connections across multiple servers +3. You want requests evenly distributed across all backend servers + +**Example**: In Kubernetes, when using a Service DNS name with multiple Pod replicas, kube-proxy load balances TCP connections. `RoundRobinPool` ensures each connection (and thus each Pod) receives an equal share of requests. + +### Important: Backend Distribution Considerations + +`RoundRobinPool` distributes **HTTP requests** evenly across **TCP connections**. Whether this translates to even backend server distribution depends on the load balancer's behavior: + +**✓ Works when the load balancer**: +- Assigns different backends to different TCP connections from the same client +- Uses algorithms like: round-robin, random, least-connections (without client affinity) +- Example: Default Kubernetes Services without `sessionAffinity` + +**✗ Does NOT work when**: +- Load balancer has client/source IP affinity (all connections from one IP → same backend) +- Load balancer uses source-IP-hash or sticky sessions + +**How it works:** +1. `RoundRobinPool` creates N TCP connections to the load balancer endpoint +2. Load balancer assigns each TCP connection to a backend (per its algorithm) +3. `RoundRobinPool` cycles HTTP requests across those N connections +4. Result: Requests distributed proportionally to how the LB distributed the connections + +If the load balancer assigns all connections to the same backend (e.g., due to session affinity), `RoundRobinPool` cannot overcome this. In such cases, consider using [`BalancedPool`](/docs/docs/api/BalancedPool.md) with direct backend addresses (e.g., individual pod IPs) instead of a load-balanced endpoint. + +## Instance Properties + +### `RoundRobinPool.closed` + +Implements [Client.closed](/docs/docs/api/Client.md#clientclosed) + +### `RoundRobinPool.destroyed` + +Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed) + +### `RoundRobinPool.stats` + +Returns [`PoolStats`](PoolStats.md) instance for this pool. + +## Instance Methods + +### `RoundRobinPool.close([callback])` + +Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise). + +### `RoundRobinPool.destroy([error, callback])` + +Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). + +### `RoundRobinPool.connect(options[, callback])` + +See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback). + +### `RoundRobinPool.dispatch(options, handler)` + +Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). + +### `RoundRobinPool.pipeline(options, handler)` + +See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler). + +### `RoundRobinPool.request(options[, callback])` + +See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). + +### `RoundRobinPool.stream(options, factory[, callback])` + +See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback). + +### `RoundRobinPool.upgrade(options[, callback])` + +See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback). + +## Instance Events + +### Event: `'connect'` + +See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect). + +### Event: `'disconnect'` + +See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect). + +### Event: `'drain'` + +See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain). + +## Example + +```javascript +import { RoundRobinPool } from 'undici' + +const pool = new RoundRobinPool('http://my-service.default.svc.cluster.local', { + connections: 10 +}) + +// Requests will be distributed evenly across all 10 connections +for (let i = 0; i < 100; i++) { + const { body } = await pool.request({ + path: '/api/data', + method: 'GET' + }) + console.log(await body.json()) +} + +await pool.close() +``` + +## See Also + +- [Pool](/docs/docs/api/Pool.md) - Connection pool without round-robin +- [BalancedPool](/docs/docs/api/BalancedPool.md) - Load balancing across multiple origins +- [Issue #3648](https://github.com/nodejs/undici/issues/3648) - Original issue describing uneven distribution + diff --git a/docs/docs/api/SnapshotAgent.md b/docs/docs/api/SnapshotAgent.md new file mode 100644 index 00000000000..e4c8f2484a5 --- /dev/null +++ b/docs/docs/api/SnapshotAgent.md @@ -0,0 +1,616 @@ +# SnapshotAgent + +The `SnapshotAgent` provides a powerful way to record and replay HTTP requests for testing purposes. It extends `MockAgent` to enable automatic snapshot testing, eliminating the need to manually define mock responses. + +## Use Cases + +- **Integration Testing**: Record real API interactions and replay them in tests +- **Offline Development**: Work with APIs without network connectivity +- **Consistent Test Data**: Ensure tests use the same responses across runs +- **API Contract Testing**: Capture and validate API behavior over time + +## Constructor + +```javascript +new SnapshotAgent([options]) +``` + +### Parameters + +- **options** `Object` (optional) + - **mode** `String` - The snapshot mode: `'record'`, `'playback'`, or `'update'`. Default: `'record'` + - **snapshotPath** `String` - Path to the snapshot file for loading/saving + - **maxSnapshots** `Number` - Maximum number of snapshots to keep in memory. Default: `Infinity` + - **autoFlush** `Boolean` - Whether to automatically save snapshots to disk. Default: `false` + - **flushInterval** `Number` - Interval in milliseconds for auto-flush. Default: `30000` + - **matchHeaders** `Array` - Specific headers to include in request matching. Default: all headers + - **ignoreHeaders** `Array` - Headers to ignore during request matching + - **excludeHeaders** `Array` - Headers to exclude from snapshots (for security) + - **matchBody** `Boolean` - Whether to include request body in matching. Default: `true` + - **matchQuery** `Boolean` - Whether to include query parameters in matching. Default: `true` + - **caseSensitive** `Boolean` - Whether header matching is case-sensitive. Default: `false` + - **shouldRecord** `Function` - Callback to determine if a request should be recorded + - **shouldPlayback** `Function` - Callback to determine if a request should be played back + - **excludeUrls** `Array` - URL patterns (strings or RegExp) to exclude from recording/playback + - All other options from `MockAgent` are supported + +### Modes + +#### Record Mode (`'record'`) +Makes real HTTP requests and saves the responses to snapshots. + +```javascript +import { SnapshotAgent, setGlobalDispatcher } from 'undici' + +const agent = new SnapshotAgent({ + mode: 'record', + snapshotPath: './test/snapshots/api-calls.json' +}) +setGlobalDispatcher(agent) + +// Makes real requests and records them +const response = await fetch('https://api.example.com/users') +const users = await response.json() + +// Save recorded snapshots +await agent.saveSnapshots() +``` + +#### Playback Mode (`'playback'`) +Replays recorded responses without making real HTTP requests. + +```javascript +import { SnapshotAgent, setGlobalDispatcher } from 'undici' + +const agent = new SnapshotAgent({ + mode: 'playback', + snapshotPath: './test/snapshots/api-calls.json' +}) +setGlobalDispatcher(agent) + +// Uses recorded response instead of real request +const response = await fetch('https://api.example.com/users') +``` + +#### Update Mode (`'update'`) +Uses existing snapshots when available, but records new ones for missing requests. + +```javascript +import { SnapshotAgent, setGlobalDispatcher } from 'undici' + +const agent = new SnapshotAgent({ + mode: 'update', + snapshotPath: './test/snapshots/api-calls.json' +}) +setGlobalDispatcher(agent) + +// Uses snapshot if exists, otherwise makes real request and records it +const response = await fetch('https://api.example.com/new-endpoint') +``` + +## Instance Methods + +### `agent.saveSnapshots([filePath])` + +Saves all recorded snapshots to a file. + +#### Parameters + +- **filePath** `String` (optional) - Path to save snapshots. Uses constructor `snapshotPath` if not provided. + +#### Returns + +`Promise` + +```javascript +await agent.saveSnapshots('./custom-snapshots.json') +``` + +## Advanced Configuration + +### Header Filtering + +Control which headers are used for request matching and what gets stored in snapshots: + +```javascript +const agent = new SnapshotAgent({ + mode: 'record', + snapshotPath: './snapshots.json', + + // Only match these specific headers + matchHeaders: ['content-type', 'accept'], + + // Ignore these headers during matching (but still store them) + ignoreHeaders: ['user-agent', 'date'], + + // Exclude sensitive headers from snapshots entirely + excludeHeaders: ['authorization', 'x-api-key', 'cookie'] +}) +``` + +### Custom Request/Response Filtering + +Use callback functions to determine what gets recorded or played back: + +```javascript +const agent = new SnapshotAgent({ + mode: 'record', + snapshotPath: './snapshots.json', + + // Only record GET requests to specific endpoints + shouldRecord: (requestOpts) => { + const url = new URL(requestOpts.path, requestOpts.origin) + return requestOpts.method === 'GET' && url.pathname.startsWith('/api/v1/') + }, + + // Skip authentication endpoints during playback + shouldPlayback: (requestOpts) => { + const url = new URL(requestOpts.path, requestOpts.origin) + return !url.pathname.includes('/auth/') + } +}) +``` + +### URL Pattern Exclusion + +Exclude specific URLs from recording/playback using patterns: + +```javascript +const agent = new SnapshotAgent({ + mode: 'record', + snapshotPath: './snapshots.json', + + excludeUrls: [ + 'https://analytics.example.com', // String match + /\/api\/v\d+\/health/, // Regex pattern + 'telemetry' // Substring match + ] +}) +``` + +### Memory Management + +Configure automatic memory and disk management: + +```javascript +const agent = new SnapshotAgent({ + mode: 'record', + snapshotPath: './snapshots.json', + + // Keep only 1000 snapshots in memory + maxSnapshots: 1000, + + // Automatically save to disk every 30 seconds + autoFlush: true, + flushInterval: 30000 +}) +``` + +### Sequential Response Handling + +Handle multiple responses for the same request (similar to nock): + +```javascript +// In record mode, multiple identical requests get recorded as separate responses +const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './sequential.json' }) + +// First call returns response A +await fetch('https://api.example.com/random') + +// Second call returns response B +await fetch('https://api.example.com/random') + +await agent.saveSnapshots() + +// In playback mode, calls return responses in sequence +const playbackAgent = new SnapshotAgent({ mode: 'playback', snapshotPath: './sequential.json' }) + +// Returns response A +const first = await fetch('https://api.example.com/random') + +// Returns response B +const second = await fetch('https://api.example.com/random') + +// Third call repeats the last response (B) +const third = await fetch('https://api.example.com/random') +``` + +## Managing Snapshots + +### Replacing Existing Snapshots + +```javascript +// Load existing snapshots +await agent.loadSnapshots('./old-snapshots.json') + +// Get snapshot data +const recorder = agent.getRecorder() +const snapshots = recorder.getSnapshots() + +// Modify or filter snapshots +const filteredSnapshots = snapshots.filter(s => + !s.request.url.includes('deprecated') +) + +// Replace all snapshots +agent.replaceSnapshots(filteredSnapshots.map((snapshot, index) => ({ + hash: `new-hash-${index}`, + snapshot +}))) + +// Save updated snapshots +await agent.saveSnapshots('./updated-snapshots.json') +``` + +### `agent.loadSnapshots([filePath])` + +Loads snapshots from a file. + +#### Parameters + +- **filePath** `String` (optional) - Path to load snapshots from. Uses constructor `snapshotPath` if not provided. + +#### Returns + +`Promise` + +```javascript +await agent.loadSnapshots('./existing-snapshots.json') +``` + +### `agent.getRecorder()` + +Gets the underlying `SnapshotRecorder` instance. + +#### Returns + +`SnapshotRecorder` + +```javascript +const recorder = agent.getRecorder() +console.log(`Recorded ${recorder.size()} interactions`) +``` + +### `agent.getMode()` + +Gets the current snapshot mode. + +#### Returns + +`String` - The current mode (`'record'`, `'playback'`, or `'update'`) + +### `agent.clearSnapshots()` + +Clears all recorded snapshots from memory. + +```javascript +agent.clearSnapshots() +``` + +## Working with Different Request Types + +### GET Requests + +```javascript +// Record mode +const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './get-snapshots.json' }) +setGlobalDispatcher(agent) + +const response = await fetch('https://jsonplaceholder.typicode.com/posts/1') +const post = await response.json() + +await agent.saveSnapshots() +``` + +### POST Requests with Body + +```javascript +// Record mode +const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './post-snapshots.json' }) +setGlobalDispatcher(agent) + +const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: 'Test Post', body: 'Content' }) +}) + +await agent.saveSnapshots() +``` + +### Using with `undici.request` + +SnapshotAgent works with all undici APIs, not just fetch: + +```javascript +import { SnapshotAgent, request, setGlobalDispatcher } from 'undici' + +const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './request-snapshots.json' }) +setGlobalDispatcher(agent) + +const { statusCode, headers, body } = await request('https://api.example.com/data') +const data = await body.json() + +await agent.saveSnapshots() +``` + +## Test Integration + +### Basic Test Setup + +```javascript +import { test } from 'node:test' +import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici' + +test('API integration test', async (t) => { + const originalDispatcher = getGlobalDispatcher() + + const agent = new SnapshotAgent({ + mode: 'playback', + snapshotPath: './test/snapshots/api-test.json' + }) + setGlobalDispatcher(agent) + + t.after(() => setGlobalDispatcher(originalDispatcher)) + + // This will use recorded data + const response = await fetch('https://api.example.com/users') + const users = await response.json() + + assert(Array.isArray(users)) + assert(users.length > 0) +}) +``` + +### Environment-Based Mode Selection + +```javascript +const mode = process.env.SNAPSHOT_MODE || 'playback' + +const agent = new SnapshotAgent({ + mode, + snapshotPath: './test/snapshots/integration.json' +}) + +// Run with: SNAPSHOT_MODE=record npm test (to record) +// Run with: npm test (to playback) +``` + +### Test Helper Function + +```javascript +function createSnapshotAgent(testName, mode = 'playback') { + return new SnapshotAgent({ + mode, + snapshotPath: `./test/snapshots/${testName}.json` + }) +} + +test('user API test', async (t) => { + const agent = createSnapshotAgent('user-api') + setGlobalDispatcher(agent) + + // Test implementation... +}) +``` + +## Snapshot File Format + +Snapshots are stored as JSON with the following structure: + +```json +[ + { + "hash": "dGVzdC1oYXNo...", + "snapshot": { + "request": { + "method": "GET", + "url": "https://api.example.com/users", + "headers": { + "authorization": "Bearer token" + }, + "body": undefined + }, + "response": { + "statusCode": 200, + "headers": { + "content-type": "application/json" + }, + "body": "eyJkYXRhIjoidGVzdCJ9", // base64 encoded + "trailers": {} + }, + "timestamp": "2024-01-01T00:00:00.000Z" + } + } +] +``` + +## Security Considerations + +### Sensitive Data in Snapshots + +By default, SnapshotAgent records all headers and request/response data. For production use, always exclude sensitive information: + +```javascript +const agent = new SnapshotAgent({ + mode: 'record', + snapshotPath: './snapshots.json', + + // Exclude sensitive headers from snapshots + excludeHeaders: [ + 'authorization', + 'x-api-key', + 'cookie', + 'set-cookie', + 'x-auth-token', + 'x-csrf-token' + ], + + // Filter out requests with sensitive data + shouldRecord: (requestOpts) => { + const url = new URL(requestOpts.path, requestOpts.origin) + + // Don't record authentication endpoints + if (url.pathname.includes('/auth/') || url.pathname.includes('/login')) { + return false + } + + // Don't record if request contains sensitive body data + if (requestOpts.body && typeof requestOpts.body === 'string') { + const body = requestOpts.body.toLowerCase() + if (body.includes('password') || body.includes('secret')) { + return false + } + } + + return true + } +}) +``` + +### Snapshot File Security + +**Important**: Snapshot files may contain sensitive data. Handle them securely: + +- ✅ Add snapshot files to `.gitignore` if they contain real API data +- ✅ Use environment-specific snapshots (dev/staging/prod) +- ✅ Regularly review snapshot contents for sensitive information +- ✅ Use the `excludeHeaders` option for production snapshots +- ❌ Never commit snapshots with real authentication tokens +- ❌ Don't share snapshot files containing personal data + +```gitignore +# Exclude snapshots with real data +/test/snapshots/production-*.json +/test/snapshots/*-real-data.json + +# Include sanitized test snapshots +!/test/snapshots/mock-*.json +``` + +## Error Handling + +### Missing Snapshots in Playback Mode + +```javascript +try { + const response = await fetch('https://api.example.com/nonexistent') +} catch (error) { + if (error.message.includes('No snapshot found')) { + // Handle missing snapshot + console.log('Snapshot not found for this request') + } +} +``` + +### Handling Network Errors in Record Mode + +```javascript +const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' }) + +try { + const response = await fetch('https://nonexistent-api.example.com/data') +} catch (error) { + // Network errors are not recorded as snapshots + console.log('Network error:', error.message) +} +``` + +## Best Practices + +### 1. Organize Snapshots by Test Suite + +```javascript +// Use descriptive snapshot file names +const agent = new SnapshotAgent({ + mode: 'playback', + snapshotPath: `./test/snapshots/${testSuiteName}-${testName}.json` +}) +``` + +### 2. Version Control Snapshots + +Add snapshot files to version control to ensure consistent test behavior across environments: + +```gitignore +# Include snapshots in version control +!/test/snapshots/*.json +``` + +### 3. Clean Up Test Data + +```javascript +test('API test', async (t) => { + const agent = new SnapshotAgent({ + mode: 'playback', + snapshotPath: './test/snapshots/temp-test.json' + }) + + // Clean up after test + t.after(() => { + agent.clearSnapshots() + }) +}) +``` + +### 4. Snapshot Validation + +```javascript +test('validate snapshot contents', async (t) => { + const agent = new SnapshotAgent({ + mode: 'playback', + snapshotPath: './test/snapshots/validation.json' + }) + + const recorder = agent.getRecorder() + const snapshots = recorder.getSnapshots() + + // Validate snapshot structure + assert(snapshots.length > 0, 'Should have recorded snapshots') + assert(snapshots[0].request.url.startsWith('https://'), 'Should use HTTPS') +}) +``` + +## Comparison with Other Tools + +### vs Manual MockAgent Setup + +**Manual MockAgent:** +```javascript +const mockAgent = new MockAgent() +const mockPool = mockAgent.get('https://api.example.com') + +mockPool.intercept({ + path: '/users', + method: 'GET' +}).reply(200, [ + { id: 1, name: 'User 1' }, + { id: 2, name: 'User 2' } +]) +``` + +**SnapshotAgent:** +```javascript +// Record once +const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' }) +// Real API call gets recorded automatically + +// Use in tests +const agent = new SnapshotAgent({ mode: 'playback', snapshotPath: './snapshots.json' }) +// Automatically replays recorded response +``` + +### vs nock + +SnapshotAgent provides similar functionality to nock but is specifically designed for undici: + +- ✅ Works with all undici APIs (`request`, `stream`, `pipeline`, etc.) +- ✅ Supports undici-specific features (RetryAgent, connection pooling) +- ✅ Better TypeScript integration +- ✅ More efficient for high-performance scenarios + +## See Also + +- [MockAgent](./MockAgent.md) - Manual mocking for more control +- [MockCallHistory](./MockCallHistory.md) - Inspecting request history +- [Testing Best Practices](../best-practices/writing-tests.md) - General testing guidance \ No newline at end of file diff --git a/docs/docs/api/Socks5ProxyAgent.md b/docs/docs/api/Socks5ProxyAgent.md new file mode 100644 index 00000000000..3ef0f852f27 --- /dev/null +++ b/docs/docs/api/Socks5ProxyAgent.md @@ -0,0 +1,275 @@ +# Class: Socks5ProxyAgent + +Extends: `undici.Dispatcher` + +A SOCKS5 proxy wrapper class that implements the Dispatcher API. It enables HTTP requests to be routed through a SOCKS5 proxy server, providing connection tunneling and authentication support. + +## `new Socks5ProxyAgent(proxyUrl[, options])` + +Arguments: + +* **proxyUrl** `string | URL` (required) - The SOCKS5 proxy server URL. Must use `socks5://` or `socks://` protocol. +* **options** `Socks5ProxyAgent.Options` (optional) - Additional configuration options. + +Returns: `Socks5ProxyAgent` + +### Parameter: `Socks5ProxyAgent.Options` + +Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions) + +* **headers** `IncomingHttpHeaders` (optional) - Additional headers to send with proxy connections. +* **username** `string` (optional) - SOCKS5 proxy username for authentication. Can also be provided in the proxy URL. +* **password** `string` (optional) - SOCKS5 proxy password for authentication. Can also be provided in the proxy URL. +* **connect** `Function` (optional) - Custom connector function for the proxy connection. +* **proxyTls** `BuildOptions` (optional) - TLS options for the proxy connection (when using SOCKS5 over TLS). +* **requestTls** `BuildOptions` (optional) - TLS options applied to the HTTPS connection to the target server through the SOCKS5 tunnel. Use this to configure `ca`, `cert`, `key`, `rejectUnauthorized`, `servername`, etc. for the target HTTPS endpoint. + +Examples: + +```js +import { Socks5ProxyAgent } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080') +// or with authentication +const socks5ProxyWithAuth = new Socks5ProxyAgent('socks5://user:pass@localhost:1080') +// or with options +const socks5ProxyWithOptions = new Socks5ProxyAgent('socks5://localhost:1080', { + username: 'user', + password: 'pass', + connections: 10 +}) +``` + +#### Example - Basic SOCKS5 Proxy instantiation + +This will instantiate the Socks5ProxyAgent. It will not do anything until registered as the dispatcher to use with requests. + +```js +import { Socks5ProxyAgent } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080') +``` + +#### Example - Basic SOCKS5 Proxy Request with global dispatcher + +```js +import { setGlobalDispatcher, request, Socks5ProxyAgent } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080') +setGlobalDispatcher(socks5Proxy) + +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 +} +``` + +#### Example - Basic SOCKS5 Proxy Request with local dispatcher + +```js +import { Socks5ProxyAgent, request } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080') + +const { + statusCode, + body +} = await request('http://localhost:3000/foo', { dispatcher: socks5Proxy }) + +console.log('response received', statusCode) // response received 200 + +for await (const data of body) { + console.log('data', data.toString('utf8')) // data foo +} +``` + +#### Example - SOCKS5 Proxy Request with authentication + +```js +import { setGlobalDispatcher, request, Socks5ProxyAgent } from 'undici' + +// Authentication via URL +const socks5Proxy = new Socks5ProxyAgent('socks5://username:password@localhost:1080') + +// Or authentication via options +// const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080', { +// username: 'username', +// password: 'password' +// }) + +setGlobalDispatcher(socks5Proxy) + +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 +} +``` + +#### Example - SOCKS5 Proxy with HTTPS requests + +SOCKS5 proxy supports both HTTP and HTTPS requests through tunneling: + +```js +import { Socks5ProxyAgent, request } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080') + +const response = await request('https://api.example.com/data', { + dispatcher: socks5Proxy, + method: 'GET' +}) + +console.log('Response status:', response.statusCode) +console.log('Response data:', await response.body.json()) +``` + +#### Example - SOCKS5 Proxy with Fetch + +```js +import { Socks5ProxyAgent, fetch } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080') + +const response = await fetch('http://localhost:3000/api/users', { + dispatcher: socks5Proxy, + method: 'GET' +}) + +console.log('Response status:', response.status) +console.log('Response data:', await response.text()) +``` + +#### Example - Connection Pooling + +SOCKS5ProxyWrapper automatically manages connection pooling for better performance: + +```js +import { Socks5ProxyAgent, request } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080', { + connections: 10, // Allow up to 10 concurrent connections + pipelining: 1 // Enable HTTP/1.1 pipelining +}) + +// Multiple requests will reuse connections through the SOCKS5 tunnel +const responses = await Promise.all([ + request('http://api.example.com/endpoint1', { dispatcher: socks5Proxy }), + request('http://api.example.com/endpoint2', { dispatcher: socks5Proxy }), + request('http://api.example.com/endpoint3', { dispatcher: socks5Proxy }) +]) + +console.log('All requests completed through the same SOCKS5 proxy') +``` + +### `Socks5ProxyAgent.close()` + +Closes the SOCKS5 proxy wrapper and waits for all underlying pools and connections to close before resolving. + +Returns: `Promise` + +#### Example - clean up after tests are complete + +```js +import { Socks5ProxyAgent, setGlobalDispatcher } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080') +setGlobalDispatcher(socks5Proxy) + +// ... make requests + +await socks5Proxy.close() +``` + +### `Socks5ProxyAgent.destroy([err])` + +Destroys the SOCKS5 proxy wrapper and all underlying connections immediately. + +Arguments: +* **err** `Error` (optional) - The error that caused the destruction. + +Returns: `Promise` + +#### Example - force close all connections + +```js +import { Socks5ProxyAgent } from 'undici' + +const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080') + +// Force close all connections +await socks5Proxy.destroy() +``` + +### `Socks5ProxyAgent.dispatch(options, handlers)` + +Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handlers). + +### `Socks5ProxyAgent.request(options[, callback])` + +See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). + +## Debugging + +SOCKS5 proxy connections can be debugged using Node.js diagnostics: + +```sh +NODE_DEBUG=undici:socks5 node script.js +``` + +This will output detailed information about the SOCKS5 handshake, authentication, and connection establishment. + +## SOCKS5 Protocol Support + +The Socks5ProxyAgent supports the following SOCKS5 features: + +### Authentication Methods + +- **No Authentication** (`0x00`) - For public or internal proxies +- **Username/Password** (`0x02`) - RFC 1929 authentication + +### Address Types + +- **IPv4** (`0x01`) - Standard IPv4 addresses +- **Domain Name** (`0x03`) - Domain names (recommended for flexibility) +- **IPv6** (`0x04`) - IPv6 addresses (full support for standard and compressed notation) + +### Commands + +- **CONNECT** (`0x01`) - Establish TCP connection (primary use case for HTTP) + +### Error Handling + +The wrapper handles various SOCKS5 error conditions: + +- Connection refused by proxy +- Authentication failures +- Network unreachable +- Host unreachable +- Unsupported address types or commands + +## Performance Considerations + +- **Connection Pooling**: Automatically pools connections through the SOCKS5 tunnel for better performance +- **HTTP/1.1 Pipelining**: Supports pipelining when enabled +- **DNS Resolution**: Domain names are resolved by the SOCKS5 proxy, reducing local DNS queries +- **TLS Termination**: HTTPS connections are encrypted end-to-end, with the SOCKS5 proxy only handling the TCP tunnel + +## Security Notes + +1. **Authentication**: Credentials are sent to the SOCKS5 proxy in plaintext unless using SOCKS5 over TLS +2. **DNS Leaks**: All DNS resolution happens on the proxy server, preventing DNS leaks +3. **End-to-end Encryption**: HTTPS traffic remains encrypted between client and final destination +4. **Connection Security**: Consider using authenticated proxies and secure networks + +## Compatibility + +- **Protocol**: SOCKS5 (RFC 1928) with Username/Password Authentication (RFC 1929) +- **Transport**: TCP only (UDP support not implemented) +- **Node.js**: Compatible with all supported Node.js versions +- **HTTP Versions**: Works with HTTP/1.1 and HTTP/2 over the tunnel \ No newline at end of file diff --git a/docs/docs/api/WebSocket.md b/docs/docs/api/WebSocket.md index 6f6836fd6ee..55a76aae64e 100644 --- a/docs/docs/api/WebSocket.md +++ b/docs/docs/api/WebSocket.md @@ -11,11 +11,19 @@ Arguments: * **url** `URL | string` * **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](/docs/docs/api/Dispatcher.md). +### WebSocketInit + +When passing an object as the second argument, the following options are available: + +* **protocols** `string | string[]` (optional) - Subprotocol(s) to request the server use. +* **dispatcher** `Dispatcher` (optional) - A custom [`Dispatcher`](/docs/docs/api/Dispatcher.md) to use for the connection. +* **headers** `HeadersInit` (optional) - Custom headers to include in the WebSocket handshake request. + ### Example: This example will not work in browsers or other platforms that don't allow passing an object. -```mjs +```js import { WebSocket, ProxyAgent } from 'undici' const proxyAgent = new ProxyAgent('my.proxy.server') @@ -28,12 +36,33 @@ const ws = new WebSocket('wss://echo.websocket.events', { If you do not need a custom Dispatcher, it's recommended to use the following pattern: -```mjs +```js import { WebSocket } from 'undici' const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat']) ``` +### Example with HTTP/2: + +> ⚠️ Warning: WebSocket over HTTP/2 is experimental, it is likely to change in the future. + +> 🗒️ Note: WebSocket over HTTP/2 may be enabled by default in a future version, +> this will happen by enabling HTTP/2 connections as the default behavior of Undici's Agent as well the global dispatcher. +> Stay tuned to the changelog for more information. + +This example will not work in browsers or other platforms that don't allow passing an object. + +```js +import { Agent } from 'undici' + +const agent = new Agent({ allowH2: true }) + +const ws = new WebSocket('wss://echo.websocket.events', { + dispatcher: agent, + protocols: ['echo', 'chat'] +}) +``` + # Class: WebSocketStream > ⚠️ Warning: the WebSocketStream API has not been finalized and is likely to change. @@ -78,6 +107,33 @@ setInterval(() => write(), 5000) ``` +## ping(websocket, payload) +Arguments: + +* **websocket** `WebSocket` - The WebSocket instance to send the ping frame on +* **payload** `Buffer|undefined` (optional) - Optional payload data to include with the ping frame. Must not exceed 125 bytes. + +Sends a ping frame to the WebSocket server. The server must respond with a pong frame containing the same payload data. This can be used for keepalive purposes or to verify that the connection is still active. + +### Example: + +```js +import { WebSocket, ping } from 'undici' + +const ws = new WebSocket('wss://echo.websocket.events') + +ws.addEventListener('open', () => { + // Send ping with no payload + ping(ws) + + // Send ping with payload + const payload = Buffer.from('hello') + ping(ws, payload) +}) +``` + +**Note**: A ping frame cannot have a payload larger than 125 bytes. The ping will only be sent if the WebSocket connection is in the OPEN state. + ## Read More - [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) diff --git a/docs/docs/best-practices/crawling.md b/docs/docs/best-practices/crawling.md new file mode 100644 index 00000000000..96d67b2f0cc --- /dev/null +++ b/docs/docs/best-practices/crawling.md @@ -0,0 +1,58 @@ +# Crawling + +[RFC 9309](https://datatracker.ietf.org/doc/html/rfc9309) defines crawlers as automated clients. + +Some web servers may reject requests that omit the `User-Agent` header or that use common defaults such as `'curl/7.79.1'`. + +In **undici**, the default user agent is `'undici'`. Since undici is integrated into Node.js core as the implementation of `fetch()`, requests made via `fetch()` use `'node'` as the default user agent. + +It is recommended to specify a **custom `User-Agent` header** when implementing crawlers. Providing a descriptive user agent allows servers to correctly identify the client and reduces the likelihood of requests being denied. + +A user agent string should include sufficient detail to identify the crawler and provide contact information. For example: + +``` +AcmeCo Crawler - acme.co - contact@acme.co +``` + +When adding contact details, avoid using personal identifiers such as your own name or a private email address—especially in a professional or employment context. Instead, use a role-based or organizational contact (e.g., crawler-team@company.com) to protect individual privacy while still enabling communication. + +If a crawler behaves unexpectedly—for example, due to misconfiguration or implementation errors—server administrators can use the information in the user agent to contact the operator and coordinate an appropriate resolution. + +The `User-Agent` header can be set on individual requests or applied globally by configuring a custom dispatcher. + +**Example: setting a `User-Agent` per request** + +```js +import { fetch } from 'undici' + +const headers = { + 'User-Agent': 'AcmeCo Crawler - acme.co - contact@acme.co' +} + +const res = await fetch('https://example.com', { headers }) +``` + +## Best Practices for Crawlers + +When developing a crawler, the following practices are recommended in addition to setting a descriptive `User-Agent` header: + +* **Respect `robots.txt`** + Follow the directives defined in the target site’s `robots.txt` file, including disallowed paths and optional crawl-delay settings (see [W3C guidelines](https://www.w3.org/wiki/Write_Web_Crawler)). + +* **Rate limiting** + Regulate request frequency to avoid imposing excessive load on servers. Introduce delays between requests or limit the number of concurrent requests. The W3C suggests at least one second between requests. + +* **Error handling** + Implement retry logic with exponential backoff for transient failures, and stop requests when persistent errors occur (e.g., HTTP 403 or 429). + +* **Monitoring and logging** + Track request volume, response codes, and error rates to detect misbehavior and address issues proactively. + +* **Contact information** + Always include valid and current contact details in the `User-Agent` string so that administrators can reach the crawler operator if necessary. + +## References and Further Reading + +* [RFC 9309: The Robots Exclusion Protocol](https://datatracker.ietf.org/doc/html/rfc9309) +* [W3C Wiki: Write Web Crawler](https://www.w3.org/wiki/Write_Web_Crawler) +* [Ethical Web Crawling (WWW 2010 Conference Paper)](https://archives.iw3c2.org/www2010/proceedings/www/p1101.pdf) diff --git a/docs/docs/best-practices/undici-vs-builtin-fetch.md b/docs/docs/best-practices/undici-vs-builtin-fetch.md new file mode 100644 index 00000000000..979801ca817 --- /dev/null +++ b/docs/docs/best-practices/undici-vs-builtin-fetch.md @@ -0,0 +1,224 @@ +# Undici Module vs. Node.js Built-in Fetch + +Node.js has shipped a built-in `fetch()` implementation powered by undici since +Node.js v18. This guide explains the relationship between the `undici` npm +package and the built-in `fetch`, and when you should install one versus relying +on the other. + +## Background + +The `fetch()`, `Request`, `Response`, `Headers`, and `FormData` globals in +Node.js v18+ are provided by a version of undici that is bundled into Node.js +itself. You can check which version is bundled with: + +```js +console.log(process.versions.undici); // e.g., "7.5.0" +``` + +When you install undici from npm, you get the full library with all of its +additional APIs, and potentially a newer release than what your Node.js version +bundles. + +## Keep `fetch` and `FormData` from the same implementation + +When you send a `FormData` body, keep `fetch` and `FormData` together from the +same implementation. + +Use one of these patterns: + +### Built-in globals + +```js +const body = new FormData() +body.set('name', 'some') +body.set('someOtherProperty', '8000') + +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +### `undici` module imports + +```js +import { fetch, FormData } from 'undici' + +const body = new FormData() +body.set('name', 'some') +body.set('someOtherProperty', '8000') + +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +### `undici.install()` globals + +If you want the installed `undici` package to provide the globals, call +[`install()`](/docs/api/GlobalInstallation.md): + +```js +import { install } from 'undici' + +install() + +const body = new FormData() +body.set('name', 'some') +body.set('someOtherProperty', '8000') + +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +`install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and +`FormData` implementations with undici's versions, and also installs undici's +`WebSocket`, `CloseEvent`, `ErrorEvent`, `MessageEvent`, and `EventSource` +globals. + +Avoid mixing implementations in the same request, for example: + +```js +import { fetch } from 'undici' + +const body = new FormData() + +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +```js +import { FormData } from 'undici' + +const body = new FormData() + +await fetch('https://example.com', { + method: 'POST', + body +}) +``` + +Those combinations may behave differently across Node.js and undici versions. +Using matching pairs keeps multipart handling predictable. + +## When you do NOT need to install undici + +If all of the following are true, you can rely on the built-in globals and skip +adding undici to your dependencies: + +- You only need the standard Fetch API (`fetch`, `Request`, `Response`, + `Headers`, `FormData`). +- You are running Node.js v18 or later. +- You do not depend on features or bug fixes introduced in a version of undici + newer than the one bundled with your Node.js release. +- You want zero additional runtime dependencies. +- You want cross-platform interoperability with browsers and other runtimes + (Deno, Bun, Cloudflare Workers, etc.) using the same Fetch API surface. + +This is common in applications that make straightforward HTTP requests or in +libraries that target multiple JavaScript runtimes. + +## When you SHOULD install undici + +Install undici from npm when you need capabilities beyond the standard Fetch API: + +### Advanced HTTP APIs + +undici exposes `request`, `stream`, `pipeline`, and `connect` methods that +provide lower-level control and significantly better performance than `fetch`: + +```js +import { request } from 'undici'; + +const { statusCode, headers, body } = await request('https://example.com'); +const data = await body.json(); +``` + +### Connection pooling and dispatchers + +`Client`, `Pool`, `BalancedPool`, `Agent`, and their configuration options +let you manage connection lifecycle, keep-alive behavior, pipelining depth, +and concurrency limits: + +```js +import { Pool } from 'undici'; + +const pool = new Pool('https://example.com', { connections: 10 }); +const { body } = await pool.request({ path: '/', method: 'GET' }); +``` + +### Proxy support + +`ProxyAgent` and `EnvHttpProxyAgent` handle HTTP(S) proxying. Note that +Node.js v22.21.0+ and v24.0.0+ support environment-variable-based proxy +configuration for the built-in `fetch` via the `--use-env-proxy` flag (or +`NODE_USE_ENV_PROXY=1`). However, undici's `ProxyAgent` still provides +programmatic control through the dispatcher API: + +```js +import { ProxyAgent, fetch } from 'undici'; + +const proxyAgent = new ProxyAgent('https://my-proxy.example.com:8080'); +const response = await fetch('https://example.com', { dispatcher: proxyAgent }); +``` + +### Testing and mocking + +`MockAgent`, `MockClient`, and `MockPool` let you intercept and mock HTTP +requests without patching globals or depending on external libraries: + +```js +import { MockAgent, setGlobalDispatcher, fetch } from 'undici'; + +const mockAgent = new MockAgent(); +setGlobalDispatcher(mockAgent); + +const pool = mockAgent.get('https://example.com'); +pool.intercept({ path: '/api' }).reply(200, { message: 'mocked' }); +``` + +### Interceptors and middleware + +Custom dispatchers and interceptors (retry, redirect, cache, DNS) give you +fine-grained control over how requests are processed. + +### Newer version than what Node.js bundles + +The npm package often includes features, performance improvements, and bug fixes +that have not yet landed in a Node.js release. If you need a specific fix or +feature, you can install a newer version directly. + +## Version compatibility + +| Node.js version | Bundled undici version | Notes | +|---|---|---| +| v18.x | ~5.x | `fetch` is experimental (behind `--experimental-fetch` in early v18) | +| v20.x | ~6.x | `fetch` is stable | +| v22.x | ~6.x / ~7.x | `fetch` is stable | +| v24.x | ~7.x | `fetch` is stable; env-proxy support via `--use-env-proxy` | + +You can always check the exact bundled version at runtime with +`process.versions.undici`. + +Installing undici from npm does not replace the built-in globals. If you want +your installed version to replace the global `fetch` and related classes, use +[`install()`](/docs/api/GlobalInstallation.md). Otherwise, import `fetch` +directly from `'undici'`: + +```js +import { fetch } from 'undici' // uses your installed version, not the built-in +``` + +## Further reading + +- [API Reference: Fetch](/docs/api/Fetch.md) +- [API Reference: Client](/docs/api/Client.md) +- [API Reference: Pool](/docs/api/Pool.md) +- [API Reference: ProxyAgent](/docs/api/ProxyAgent.md) +- [API Reference: MockAgent](/docs/api/MockAgent.md) +- [API Reference: Global Installation](/docs/api/GlobalInstallation.md) diff --git a/docs/docs/best-practices/writing-tests.md b/docs/docs/best-practices/writing-tests.md index 57549de6357..40939542e41 100644 --- a/docs/docs/best-practices/writing-tests.md +++ b/docs/docs/best-practices/writing-tests.md @@ -18,3 +18,46 @@ const agent = new Agent({ setGlobalDispatcher(agent) ``` + +## Guarding against unexpected disconnects + +Undici's `Client` automatically reconnects after a socket error. This means +a test can silently disconnect, reconnect, and still pass. Unfortunately, +this could mask bugs like unexpected parser errors or protocol violations. +To catch these silent reconnections, add a disconnect guard after creating +a `Client`: + +```js +const { Client } = require('undici') +const { test, after } = require('node:test') +const { tspl } = require('@matteo.collina/tspl') + +test('example with disconnect guard', async (t) => { + t = tspl(t, { plan: 1 }) + + const client = new Client('http://localhost:3000') + after(() => client.close()) + + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + + // ... test logic ... +}) +``` + +`client.close()` and `client.destroy()` both emit `'disconnect'` events, but +those are expected. The guard only fails when a disconnect happens during the +active test (i.e., `!client.closed && !client.destroyed` is true). + +Skip the guard for tests where a disconnect is expected behavior, such as: + +- Signal aborts (`signal.emit('abort')`, `ac.abort()`) +- Server-side destruction (`res.destroy()`, `req.socket.destroy()`) +- Client-side body destruction mid-stream (`data.body.destroy()`) +- Timeout errors (`HeadersTimeoutError`, `BodyTimeoutError`) +- Successful upgrades (the socket is detached from the `Client`) +- Retry/reconnect tests where the disconnect triggers the retry +- HTTP parser errors from malformed responses (`HTTPParserError`) diff --git a/docs/docsify/sidebar.md b/docs/docsify/sidebar.md index 6179a95c87b..7950cdb570a 100644 --- a/docs/docsify/sidebar.md +++ b/docs/docsify/sidebar.md @@ -7,8 +7,10 @@ * [H2CClient](/docs/api/H2CClient.md "Undici H2C API - Client") * [Pool](/docs/api/Pool.md "Undici API - Pool") * [BalancedPool](/docs/api/BalancedPool.md "Undici API - BalancedPool") + * [RoundRobinPool](/docs/api/RoundRobinPool.md "Undici API - RoundRobinPool") * [Agent](/docs/api/Agent.md "Undici API - Agent") * [ProxyAgent](/docs/api/ProxyAgent.md "Undici API - ProxyAgent") + * [Socks5Agent](/docs/api/Socks5Agent.md "Undici API - SOCKS5 Agent") * [RetryAgent](/docs/api/RetryAgent.md "Undici API - RetryAgent") * [Connector](/docs/api/Connector.md "Custom connector") * [Errors](/docs/api/Errors.md "Undici API - Errors") @@ -19,6 +21,7 @@ * [MockClient](/docs/api/MockClient.md "Undici API - MockClient") * [MockPool](/docs/api/MockPool.md "Undici API - MockPool") * [MockAgent](/docs/api/MockAgent.md "Undici API - MockAgent") + * [SnapshotAgent](/docs/api/SnapshotAgent.md "Undici API - SnapshotAgent") * [MockCallHistory](/docs/api/MockCallHistory.md "Undici API - MockCallHistory") * [MockCallHistoryLog](/docs/api/MockCallHistoryLog.md "Undici API - MockCallHistoryLog") * [MockErrors](/docs/api/MockErrors.md "Undici API - MockErrors") @@ -37,7 +40,9 @@ * Examples * [Undici Examples](/examples/ "Undici Examples") * Best Practices + * [Undici vs. Built-in Fetch](/docs/best-practices/undici-vs-builtin-fetch.md "When to install undici vs using Node.js built-in fetch") * [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy") * [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate") * [Writing Tests](/docs/best-practices/writing-tests.md "Using Undici inside tests") * [Mocking Request](/docs/best-practices/mocking-request.md "Using Undici inside tests") + * [Crawling](/docs/best-practices/crawling.md "Crawling") diff --git a/docs/examples/README.md b/docs/examples/README.md index 37ba748d58e..a9a669745e5 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -147,3 +147,75 @@ const defaultDispatcher = new Agent({ setGlobalDispatcher(defaultDispatcher) // Add these interceptors to all `fetch` and Undici `request` calls ``` + +### Cache interceptor with `fetch` + +```js +import { Agent, interceptors, setGlobalDispatcher, fetch } from 'undici' +import { createServer } from 'node:http' + +const { cache } = interceptors + +const server = createServer((req, res) => { + // Cache this response for 60 seconds + res.setHeader('cache-control', 'public, max-age=60') + res.end(JSON.stringify({ now: Date.now() })) +}) + +await new Promise((resolve) => server.listen(0, resolve)) +const { port } = server.address() + +const dispatcher = new Agent().compose(cache()) +setGlobalDispatcher(dispatcher) + +// First request goes to the origin server +const first = await fetch(`http://localhost:${port}`) +const firstBody = await first.json() + +// Second request is served from cache if still fresh +const second = await fetch(`http://localhost:${port}`) +const secondBody = await second.json() + +console.log(firstBody.now === secondBody.now) // true + +await dispatcher.close() +await new Promise((resolve) => server.close(resolve)) +``` + +## Connecting via Unix domain sockets (UDS) + +### request() over UDS (per-call dispatcher) +```js +const { Agent, request } = require('undici') + +async function requestOverUds () { + const uds = new Agent({ connect: { socketPath: '/var/run/docker.sock' } }) + try { + const { statusCode, headers, body } = await request('http://localhost/_ping', { + dispatcher: uds + }) + console.log(statusCode, headers, await body.text()) + } finally { + await uds.close() + } +} +``` + +### fetch() over UDS (per-call dispatcher) +```js +const { Agent, fetch } = require('undici') + +async function fetchOverUds () { + const uds = new Agent({ connect: { socketPath: '/var/run/docker.sock' } }) + try { + const res = await fetch('http://localhost/containers/json', { dispatcher: uds }) + console.log(res.status, await res.text()) + } finally { + await uds.close() + } +} +``` + +> Note +> - `connect.socketPath` must be the exact filesystem path your server listens on (e.g., Docker: `/var/run/docker.sock`).. +> - Not supported on Windows (uses named pipes instead). \ No newline at end of file diff --git a/docs/examples/ca-fingerprint/index.js b/docs/examples/ca-fingerprint/index.js index b4dfc41124c..ac6b408f390 100644 --- a/docs/examples/ca-fingerprint/index.js +++ b/docs/examples/ca-fingerprint/index.js @@ -3,7 +3,7 @@ const crypto = require('node:crypto') const https = require('node:https') const { Client, buildConnector } = require('../../../') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const caFingerprint = getFingerprint(pem.cert.toString() .split('\n') diff --git a/docs/examples/proxy/fetch.mjs b/docs/examples/proxy/fetch.mjs index 56e37728a73..ddc50de2af9 100644 --- a/docs/examples/proxy/fetch.mjs +++ b/docs/examples/proxy/fetch.mjs @@ -1,7 +1,7 @@ import * as http from 'node:http' import { once } from 'node:events' import { createProxy } from 'proxy' -import { ProxyAgent } from '../../../index.js' +import { fetch, ProxyAgent } from '../../../' const proxyServer = createProxy(http.createServer()) const server = http.createServer((req, res) => { diff --git a/docs/examples/snapshot-testing.js b/docs/examples/snapshot-testing.js new file mode 100644 index 00000000000..681ddda7dd1 --- /dev/null +++ b/docs/examples/snapshot-testing.js @@ -0,0 +1,109 @@ +const { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher, request } = require('../../index.js') +const { createServer } = require('node:http') +const { promisify } = require('node:util') +const { tmpdir } = require('node:os') +const { join } = require('node:path') + +/** + * Example: Basic Snapshot Testing + * + * This example demonstrates how to use SnapshotAgent to record API + * interactions and replay them in tests for consistent, offline testing. + */ + +async function basicSnapshotExample () { + console.log('🚀 Basic Snapshot Testing Example\n') + + // Create a temporary snapshot file path + const snapshotPath = join(tmpdir(), `snapshot-example-${Date.now()}.json`) + console.log(`📁 Using temporary snapshot file: ${snapshotPath}\n`) + + // Create a local test server + const server = createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ + message: 'Hello from test server!', + timestamp: new Date().toISOString(), + path: req.url + })) + }) + + await promisify(server.listen.bind(server))(0) + const { port } = server.address() + const origin = `http://localhost:${port}` + + try { + // Step 1: Record mode - capture API responses + console.log('📹 Step 1: Recording API response...') + + const recordingAgent = new SnapshotAgent({ + mode: 'record', + snapshotPath + }) + + const originalDispatcher = getGlobalDispatcher() + setGlobalDispatcher(recordingAgent) + + try { + // Make an API call that will be recorded + const response = await request(`${origin}/api/test`) + const data = await response.body.json() + + console.log(`✅ Recorded response: ${data.message}`) + + // Save the recorded snapshots + await recordingAgent.saveSnapshots() + console.log('💾 Snapshot saved to temporary file\n') + } finally { + setGlobalDispatcher(originalDispatcher) + recordingAgent.close() + } + + // Step 2: Playback mode - use recorded responses (server can be down) + console.log('🎬 Step 2: Playing back recorded response...') + server.close() // Close server to prove we're using snapshots + + const playbackAgent = new SnapshotAgent({ + mode: 'playback', + snapshotPath + }) + + setGlobalDispatcher(playbackAgent) + + try { + // This will use the recorded response instead of making a real request + const response = await request(`${origin}/api/test`) + const data = await response.body.json() + + console.log(`✅ Playback response: ${data.message}`) + console.log('🎉 Successfully used recorded data instead of live server!') + } finally { + setGlobalDispatcher(originalDispatcher) + playbackAgent.close() + } + } finally { + // Ensure server is closed + if (server.listening) { + server.close() + } + + // Clean up temporary file + try { + const { unlink } = require('node:fs/promises') + await unlink(snapshotPath) + console.log('\n🗑️ Cleaned up temporary snapshot file') + } catch { + // File might not exist or already be deleted + } + } +} + +// Main execution +async function main () { + await basicSnapshotExample() +} + +// Run if called directly +if (require.main === module) { + main().catch(console.error) +} diff --git a/docs/examples/socks5-proxy.js b/docs/examples/socks5-proxy.js new file mode 100644 index 00000000000..15add067933 --- /dev/null +++ b/docs/examples/socks5-proxy.js @@ -0,0 +1,212 @@ +'use strict' + +const { Socks5Agent, request, fetch } = require('undici') + +// Basic example demonstrating SOCKS5 proxy usage +async function basicSocks5Example () { + console.log('=== Basic SOCKS5 Proxy Example ===') + + try { + // Create SOCKS5 proxy wrapper + const socks5Proxy = new Socks5Agent('socks5://localhost:1080') + + // Make request through SOCKS5 proxy + const response = await request('http://httpbin.org/ip', { + dispatcher: socks5Proxy + }) + + console.log('Status:', response.statusCode) + const body = await response.body.json() + console.log('Response:', body) + + await socks5Proxy.close() + } catch (error) { + console.error('Error:', error.message) + } +} + +// Example with authentication +async function authenticatedSocks5Example () { + console.log('\n=== Authenticated SOCKS5 Proxy Example ===') + + try { + // Using credentials in URL + const socks5Proxy = new Socks5Agent('socks5://username:password@localhost:1080') + + // Alternative: using options + // const socks5Proxy = new Socks5Agent('socks5://localhost:1080', { + // username: 'username', + // password: 'password' + // }) + + const response = await request('http://httpbin.org/headers', { + dispatcher: socks5Proxy + }) + + console.log('Status:', response.statusCode) + const body = await response.body.json() + console.log('Headers seen by server:', body.headers) + + await socks5Proxy.close() + } catch (error) { + console.error('Error:', error.message) + } +} + +// Example with fetch API +async function fetchWithSocks5Example () { + console.log('\n=== Fetch with SOCKS5 Proxy Example ===') + + try { + const socks5Proxy = new Socks5Agent('socks5://localhost:1080') + + const response = await fetch('http://httpbin.org/json', { + dispatcher: socks5Proxy + }) + + console.log('Status:', response.status) + const data = await response.json() + console.log('JSON data:', data) + + await socks5Proxy.close() + } catch (error) { + console.error('Error:', error.message) + } +} + +// Example with HTTPS +async function httpsWithSocks5Example () { + console.log('\n=== HTTPS with SOCKS5 Proxy Example ===') + + try { + const socks5Proxy = new Socks5Agent('socks5://localhost:1080') + + const response = await request('https://httpbin.org/ip', { + dispatcher: socks5Proxy + }) + + console.log('Status:', response.statusCode) + const body = await response.body.json() + console.log('HTTPS Response:', body) + + await socks5Proxy.close() + } catch (error) { + console.error('Error:', error.message) + } +} + +// Example with connection pooling +async function connectionPoolingExample () { + console.log('\n=== Connection Pooling Example ===') + + try { + const socks5Proxy = new Socks5Agent('socks5://localhost:1080', { + connections: 5, // Allow up to 5 concurrent connections + pipelining: 1 // Enable HTTP/1.1 pipelining + }) + + // Make multiple concurrent requests + const requests = [] + for (let i = 0; i < 3; i++) { + requests.push( + request(`http://httpbin.org/delay/${i}`, { + dispatcher: socks5Proxy + }) + ) + } + + console.log('Making 3 concurrent requests...') + const responses = await Promise.all(requests) + + for (let i = 0; i < responses.length; i++) { + console.log(`Request ${i + 1} status:`, responses[i].statusCode) + // Consume body to avoid warnings + await responses[i].body.dump() + } + + await socks5Proxy.close() + } catch (error) { + console.error('Error:', error.message) + } +} + +// Example with error handling +async function errorHandlingExample () { + console.log('\n=== Error Handling Example ===') + + try { + // Intentionally use a non-existent proxy + const socks5Proxy = new Socks5Agent('socks5://localhost:9999') + + await request('http://httpbin.org/ip', { + dispatcher: socks5Proxy + }) + } catch (error) { + console.log('Caught expected error:', error.message) + console.log('Error code:', error.code) + } +} + +// Global dispatcher example +async function globalDispatcherExample () { + console.log('\n=== Global Dispatcher Example ===') + + const { setGlobalDispatcher, getGlobalDispatcher } = require('undici') + + try { + const socks5Proxy = new Socks5Agent('socks5://localhost:1080') + + // Save original dispatcher + const originalDispatcher = getGlobalDispatcher() + + // Set SOCKS5 proxy as global dispatcher + setGlobalDispatcher(socks5Proxy) + + // All requests now go through SOCKS5 proxy automatically + const response = await request('http://httpbin.org/ip') + + console.log('Status:', response.statusCode) + const body = await response.body.json() + console.log('Response through global SOCKS5 proxy:', body) + + // Restore original dispatcher + setGlobalDispatcher(originalDispatcher) + + await socks5Proxy.close() + } catch (error) { + console.error('Error:', error.message) + } +} + +// Run examples +async function runExamples () { + console.log('SOCKS5 Proxy Examples for Undici') + console.log('================================') + console.log('Note: These examples require a SOCKS5 proxy running on localhost:1080') + console.log('You can use tools like dante-server, shadowsocks, or SSH tunneling.\n') + + await basicSocks5Example() + await authenticatedSocks5Example() + await fetchWithSocks5Example() + await httpsWithSocks5Example() + await connectionPoolingExample() + await errorHandlingExample() + await globalDispatcherExample() + + console.log('\n=== All examples completed ===') +} + +// Only run if this file is executed directly +if (require.main === module) { + runExamples().catch(console.error) +} + +module.exports = { + basicSocks5Example, + authenticatedSocks5Example, + fetchWithSocks5Example, + httpsWithSocks5Example, + connectionPoolingExample, + errorHandlingExample, + globalDispatcherExample +} diff --git a/eslint.config.js b/eslint.config.js index 3c0cbc1c927..d768afe6b9b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,20 +1,23 @@ 'use strict' const neo = require('neostandard') +const { installedExports } = require('./lib/global') module.exports = [ ...neo({ ignores: [ 'lib/llhttp', - 'test/fixtures/wpt', 'test/fixtures/cache-tests', - 'undici-fetch.js' + 'undici-fetch.js', + 'test/web-platform-tests/wpt' ], noJsx: true, ts: true }), { rules: { + 'n/prefer-node-protocol': ['error'], + 'n/no-process-exit': 'error', '@stylistic/comma-dangle': ['error', { arrays: 'never', objects: 'never', @@ -22,7 +25,15 @@ module.exports = [ exports: 'never', functions: 'never' }], - '@typescript-eslint/no-redeclare': 'off' + '@typescript-eslint/no-redeclare': 'off', + 'no-restricted-globals': ['error', + ...installedExports.map(name => { + return { + name, + message: `Use undici-own ${name} instead of the global.` + } + }) + ] } } ] diff --git a/index-fetch.js b/index-fetch.js index 711d7e8a1e4..3d1b811daa4 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -4,10 +4,40 @@ const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent') const fetchImpl = require('./lib/web/fetch').fetch -module.exports.fetch = function fetch (resource, init = undefined) { - return fetchImpl(resource, init).catch((err) => { - if (err && typeof err === 'object') { - Error.captureStackTrace(err) +// Capture __filename at module load time for stack trace augmentation. +// This may be undefined when bundled in environments like Node.js internals. +const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined + +function appendFetchStackTrace (err, filename) { + if (!err || typeof err !== 'object') { + return + } + + const stack = typeof err.stack === 'string' ? err.stack : '' + const normalizedFilename = filename.replace(/\\/g, '/') + + if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) { + return + } + + const capture = {} + Error.captureStackTrace(capture, appendFetchStackTrace) + + if (!capture.stack) { + return + } + + const captureLines = capture.stack.split('\n').slice(1).join('\n') + + err.stack = stack ? `${stack}\n${captureLines}` : capture.stack +} + +module.exports.fetch = function fetch (init, options = undefined) { + return fetchImpl(init, options).catch(err => { + if (currentFilename) { + appendFetchStackTrace(err, currentFilename) + } else if (err && typeof err === 'object') { + Error.captureStackTrace(err, module.exports.fetch) } throw err }) diff --git a/index.js b/index.js index 8f2ed39c14e..708a8ee80c5 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,10 @@ const Client = require('./lib/dispatcher/client') const Dispatcher = require('./lib/dispatcher/dispatcher') const Pool = require('./lib/dispatcher/pool') const BalancedPool = require('./lib/dispatcher/balanced-pool') +const RoundRobinPool = require('./lib/dispatcher/round-robin-pool') const Agent = require('./lib/dispatcher/agent') const ProxyAgent = require('./lib/dispatcher/proxy-agent') +const Socks5ProxyAgent = require('./lib/dispatcher/socks5-proxy-agent') const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent') const RetryAgent = require('./lib/dispatcher/retry-agent') const H2CClient = require('./lib/dispatcher/h2c-client') @@ -18,6 +20,7 @@ const MockClient = require('./lib/mock/mock-client') const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history') const MockAgent = require('./lib/mock/mock-agent') const MockPool = require('./lib/mock/mock-pool') +const SnapshotAgent = require('./lib/mock/snapshot-agent') const mockErrors = require('./lib/mock/mock-errors') const RetryHandler = require('./lib/handler/retry-handler') const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') @@ -30,8 +33,10 @@ module.exports.Dispatcher = Dispatcher module.exports.Client = Client module.exports.Pool = Pool module.exports.BalancedPool = BalancedPool +module.exports.RoundRobinPool = RoundRobinPool module.exports.Agent = Agent module.exports.ProxyAgent = ProxyAgent +module.exports.Socks5ProxyAgent = Socks5ProxyAgent module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent module.exports.RetryAgent = RetryAgent module.exports.H2CClient = H2CClient @@ -45,7 +50,9 @@ module.exports.interceptors = { retry: require('./lib/interceptor/retry'), dump: require('./lib/interceptor/dump'), dns: require('./lib/interceptor/dns'), - cache: require('./lib/interceptor/cache') + cache: require('./lib/interceptor/cache'), + decompress: require('./lib/interceptor/decompress'), + deduplicate: require('./lib/interceptor/deduplicate') } module.exports.cacheStores = { @@ -115,16 +122,44 @@ module.exports.setGlobalDispatcher = setGlobalDispatcher module.exports.getGlobalDispatcher = getGlobalDispatcher const fetchImpl = require('./lib/web/fetch').fetch -module.exports.fetch = async function fetch (init, options = undefined) { - try { - return await fetchImpl(init, options) - } catch (err) { - if (err && typeof err === 'object') { - Error.captureStackTrace(err) - } - throw err +// Capture __filename at module load time for stack trace augmentation. +// This may be undefined when bundled in environments like Node.js internals. +const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined + +function appendFetchStackTrace (err, filename) { + if (!err || typeof err !== 'object') { + return + } + + const stack = typeof err.stack === 'string' ? err.stack : '' + const normalizedFilename = filename.replace(/\\/g, '/') + + if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) { + return } + + const capture = {} + Error.captureStackTrace(capture, appendFetchStackTrace) + + if (!capture.stack) { + return + } + + const captureLines = capture.stack.split('\n').slice(1).join('\n') + + err.stack = stack ? `${stack}\n${captureLines}` : capture.stack +} + +module.exports.fetch = function fetch (init, options = undefined) { + return fetchImpl(init, options).catch(err => { + if (currentFilename) { + appendFetchStackTrace(err, currentFilename) + } else if (err && typeof err === 'object') { + Error.captureStackTrace(err, module.exports.fetch) + } + throw err + }) } module.exports.Headers = require('./lib/web/fetch/headers').Headers module.exports.Response = require('./lib/web/fetch/response').Response @@ -139,8 +174,6 @@ module.exports.getGlobalOrigin = getGlobalOrigin const { CacheStorage } = require('./lib/web/cache/cachestorage') const { kConstruct } = require('./lib/core/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) const { deleteCookie, getCookies, getSetCookies, setCookie, parseCookie } = require('./lib/web/cookies') @@ -157,10 +190,12 @@ module.exports.parseMIMEType = parseMIMEType module.exports.serializeAMimeType = serializeAMimeType const { CloseEvent, ErrorEvent, MessageEvent } = require('./lib/web/websocket/events') -module.exports.WebSocket = require('./lib/web/websocket/websocket').WebSocket +const { WebSocket, ping } = require('./lib/web/websocket/websocket') +module.exports.WebSocket = WebSocket module.exports.CloseEvent = CloseEvent module.exports.ErrorEvent = ErrorEvent module.exports.MessageEvent = MessageEvent +module.exports.ping = ping module.exports.WebSocketStream = require('./lib/web/websocket/stream/websocketstream').WebSocketStream module.exports.WebSocketError = require('./lib/web/websocket/stream/websocketerror').WebSocketError @@ -176,6 +211,7 @@ module.exports.MockCallHistory = MockCallHistory module.exports.MockCallHistoryLog = MockCallHistoryLog module.exports.MockPool = MockPool module.exports.MockAgent = MockAgent +module.exports.SnapshotAgent = SnapshotAgent module.exports.mockErrors = mockErrors const { EventSource } = require('./lib/web/eventsource/eventsource') diff --git a/lib/api/api-request.js b/lib/api/api-request.js index 9ae7ed6c740..a6341af531b 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -21,7 +21,7 @@ class RequestHandler extends AsyncResource { throw new InvalidArgumentError('invalid callback') } - if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) { + if (highWaterMark != null && (!Number.isFinite(highWaterMark) || highWaterMark < 0)) { throw new InvalidArgumentError('invalid highWaterMark') } @@ -118,14 +118,29 @@ class RequestHandler extends AsyncResource { this.callback = null this.res = res if (callback !== null) { - this.runInAsyncScope(callback, null, null, { - statusCode, - headers, - trailers: this.trailers, - opaque, - body: res, - context - }) + try { + this.runInAsyncScope(callback, null, null, { + statusCode, + statusText: statusMessage, + headers, + trailers: this.trailers, + opaque, + body: res, + context + }) + } catch (err) { + // If the callback throws synchronously, we need to handle it + // Remove reference to res to allow res being garbage collected + this.res = null + + // Destroy the response stream + util.destroy(res.on('error', noop), err) + + // Use queueMicrotask to re-throw the error so it reaches uncaughtException + queueMicrotask(() => { + throw err + }) + } } } diff --git a/lib/api/api-upgrade.js b/lib/api/api-upgrade.js index f6efdc98626..2b03f207562 100644 --- a/lib/api/api-upgrade.js +++ b/lib/api/api-upgrade.js @@ -4,6 +4,7 @@ const { InvalidArgumentError, SocketError } = require('../core/errors') const { AsyncResource } = require('node:async_hooks') const assert = require('node:assert') const util = require('../core/util') +const { kHTTP2Stream } = require('../core/symbols') const { addSignal, removeSignal } = require('./abort-signal') class UpgradeHandler extends AsyncResource { @@ -50,7 +51,7 @@ class UpgradeHandler extends AsyncResource { } onUpgrade (statusCode, rawHeaders, socket) { - assert(statusCode === 101) + assert(socket[kHTTP2Stream] === true ? statusCode === 200 : statusCode === 101) const { callback, opaque, context } = this diff --git a/lib/api/readable.js b/lib/api/readable.js index 31a31ace925..cdede959980 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -1,5 +1,3 @@ -// Ported from https://github.com/nodejs/undici/pull/907 - 'use strict' const assert = require('node:assert') @@ -50,23 +48,32 @@ class BodyReadable extends Readable { this[kAbort] = abort - /** - * @type {Consume | null} - */ + /** @type {Consume | null} */ this[kConsume] = null + + /** @type {number} */ this[kBytesRead] = 0 - /** - * @type {ReadableStream|null} - */ + + /** @type {ReadableStream|null} */ this[kBody] = null + + /** @type {boolean} */ this[kUsed] = false + + /** @type {string} */ this[kContentType] = contentType + + /** @type {number|null} */ this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null - // 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(). + /** + * 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(). + * + * @type {boolean} + */ this[kReading] = false } @@ -89,16 +96,14 @@ class BodyReadable extends Readable { // promise (i.e micro tick) for installing an 'error' listener will // never get a chance and will always encounter an unhandled exception. if (!this[kUsed]) { - setImmediate(() => { - callback(err) - }) + setImmediate(callback, err) } else { callback(err) } } /** - * @param {string} event + * @param {string|symbol} event * @param {(...args: any[]) => void} listener * @returns {this} */ @@ -111,7 +116,7 @@ class BodyReadable extends Readable { } /** - * @param {string} event + * @param {string|symbol} event * @param {(...args: any[]) => void} listener * @returns {this} */ @@ -149,12 +154,14 @@ class BodyReadable extends Readable { * @returns {boolean} */ push (chunk) { - this[kBytesRead] += chunk ? chunk.length : 0 - - if (this[kConsume] && chunk !== null) { - consumePush(this[kConsume], chunk) - return this[kReading] ? super.push(chunk) : true + if (chunk) { + this[kBytesRead] += chunk.length + if (this[kConsume]) { + consumePush(this[kConsume], chunk) + return this[kReading] ? super.push(chunk) : true + } } + return super.push(chunk) } @@ -255,24 +262,26 @@ class BodyReadable extends Readable { * @param {AbortSignal} [opts.signal] An AbortSignal to cancel the dump. * @returns {Promise} */ - async dump (opts) { + dump (opts) { const signal = opts?.signal if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) { - throw new InvalidArgumentError('signal must be an AbortSignal') + return Promise.reject(new InvalidArgumentError('signal must be an AbortSignal')) } const limit = opts?.limit && Number.isFinite(opts.limit) ? opts.limit : 128 * 1024 - signal?.throwIfAborted() + if (signal?.aborted) { + return Promise.reject(signal.reason ?? new AbortError()) + } if (this._readableState.closeEmitted) { - return null + return Promise.resolve(null) } - return await new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if ( (this[kContentLength] && (this[kContentLength] > limit)) || this[kBytesRead] > limit @@ -340,9 +349,23 @@ function isUnusable (bodyReadable) { return util.isDisturbed(bodyReadable) || isLocked(bodyReadable) } +/** + * @typedef {'text' | 'json' | 'blob' | 'bytes' | 'arrayBuffer'} ConsumeType + */ + +/** + * @template {ConsumeType} T + * @typedef {T extends 'text' ? string : + * T extends 'json' ? unknown : + * T extends 'blob' ? Blob : + * T extends 'arrayBuffer' ? ArrayBuffer : + * T extends 'bytes' ? Uint8Array : + * never + * } ConsumeReturnType + */ /** * @typedef {object} Consume - * @property {string} type + * @property {ConsumeType} type * @property {BodyReadable} stream * @property {((value?: any) => void)} resolve * @property {((err: Error) => void)} reject @@ -351,9 +374,10 @@ function isUnusable (bodyReadable) { */ /** + * @template {ConsumeType} T * @param {BodyReadable} stream - * @param {string} type - * @returns {Promise} + * @param {T} type + * @returns {Promise>} */ function consume (stream, type) { assert(!stream[kConsume]) @@ -363,9 +387,7 @@ function consume (stream, type) { const rState = stream._readableState if (rState.destroyed && rState.closeEmitted === false) { stream - .on('error', err => { - reject(err) - }) + .on('error', reject) .on('close', () => { reject(new TypeError('unusable')) }) @@ -440,7 +462,7 @@ function consumeStart (consume) { /** * @param {Buffer[]} chunks * @param {number} length - * @param {BufferEncoding} encoding + * @param {BufferEncoding} [encoding='utf8'] * @returns {string} */ function chunksDecode (chunks, length, encoding) { diff --git a/lib/api/util.js b/lib/api/util.js deleted file mode 100644 index 5512636f339..00000000000 --- a/lib/api/util.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict' - -const assert = require('node:assert') -const { - ResponseStatusCodeError -} = require('../core/errors') - -const { chunksDecode } = require('./readable') -const CHUNK_LIMIT = 128 * 1024 - -async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { - assert(body) - - let chunks = [] - let length = 0 - - try { - for await (const chunk of body) { - chunks.push(chunk) - length += chunk.length - if (length > CHUNK_LIMIT) { - chunks = [] - length = 0 - break - } - } - } catch { - chunks = [] - length = 0 - // Do nothing.... - } - - const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}` - - if (statusCode === 204 || !contentType || !length) { - queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers))) - return - } - - const stackTraceLimit = Error.stackTraceLimit - Error.stackTraceLimit = 0 - let payload - - try { - if (isContentTypeApplicationJson(contentType)) { - payload = JSON.parse(chunksDecode(chunks, length)) - } else if (isContentTypeText(contentType)) { - payload = chunksDecode(chunks, length) - } - } catch { - // process in a callback to avoid throwing in the microtask queue - } finally { - Error.stackTraceLimit = stackTraceLimit - } - queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers, payload))) -} - -const isContentTypeApplicationJson = (contentType) => { - return ( - contentType.length > 15 && - contentType[11] === '/' && - contentType[0] === 'a' && - contentType[1] === 'p' && - contentType[2] === 'p' && - contentType[3] === 'l' && - contentType[4] === 'i' && - contentType[5] === 'c' && - contentType[6] === 'a' && - contentType[7] === 't' && - contentType[8] === 'i' && - contentType[9] === 'o' && - contentType[10] === 'n' && - contentType[12] === 'j' && - contentType[13] === 's' && - contentType[14] === 'o' && - contentType[15] === 'n' - ) -} - -const isContentTypeText = (contentType) => { - return ( - contentType.length > 4 && - contentType[4] === '/' && - contentType[0] === 't' && - contentType[1] === 'e' && - contentType[2] === 'x' && - contentType[3] === 't' - ) -} - -module.exports = { - getResolveErrorBodyCallback, - isContentTypeApplicationJson, - isContentTypeText -} diff --git a/lib/cache/sqlite-cache-store.js b/lib/cache/sqlite-cache-store.js index 7cb4aa7e246..7a5fc999f47 100644 --- a/lib/cache/sqlite-cache-store.js +++ b/lib/cache/sqlite-cache-store.js @@ -216,7 +216,7 @@ module.exports = class SqliteCacheStore { SELECT id FROM cacheInterceptorV${VERSION} - ORDER BY cachedAt DESC + ORDER BY cachedAt ASC LIMIT ? ) `) @@ -283,7 +283,6 @@ module.exports = class SqliteCacheStore { existingValue.id ) } else { - this.#prune() // New response, let's insert it this.#insertValueQuery.run( url, @@ -299,6 +298,7 @@ module.exports = class SqliteCacheStore { value.cachedAt, value.staleAt ) + this.#prune() } } @@ -409,7 +409,7 @@ module.exports = class SqliteCacheStore { const now = Date.now() for (const value of values) { if (now >= value.deleteAt && !canBeExpired) { - return undefined + continue } let matches = true diff --git a/lib/core/connect.js b/lib/core/connect.js index 4e11deee37f..89a20c740a7 100644 --- a/lib/core/connect.js +++ b/lib/core/connect.js @@ -38,12 +38,28 @@ const SessionCache = class WeakSessionCache { return } + if (this._sessionCache.has(sessionKey)) { + this._sessionCache.delete(sessionKey) + } else if (this._sessionCache.size >= this._maxCachedSessions) { + for (const [key, ref] of this._sessionCache) { + if (ref.deref() === undefined) { + this._sessionCache.delete(key) + return + } + } + + const oldest = this._sessionCache.keys().next() + if (!oldest.done) { + this._sessionCache.delete(oldest.value) + } + } + this._sessionCache.set(sessionKey, new WeakRef(session)) this._sessionRegistry.register(session, sessionKey) } } -function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) { +function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) { if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') } @@ -96,6 +112,9 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess port, host: hostname }) + if (useH2c === true) { + socket.alpnProtocol = 'h2' + } } // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket diff --git a/lib/core/diagnostics.js b/lib/core/diagnostics.js index 224a5c49f5d..454ab379be3 100644 --- a/lib/core/diagnostics.js +++ b/lib/core/diagnostics.js @@ -26,7 +26,9 @@ const channels = { close: diagnosticsChannel.channel('undici:websocket:close'), socketError: diagnosticsChannel.channel('undici:websocket:socket_error'), ping: diagnosticsChannel.channel('undici:websocket:ping'), - pong: diagnosticsChannel.channel('undici:websocket:pong') + pong: diagnosticsChannel.channel('undici:websocket:pong'), + // ProxyAgent + proxyConnected: diagnosticsChannel.channel('undici:proxy:connected') } let isTrackingClientEvents = false @@ -36,6 +38,14 @@ function trackClientEvents (debugLog = undiciDebugLog) { return } + // Check if any of the channels already have subscribers to prevent duplicate subscriptions + // This can happen when both Node.js built-in undici and undici as a dependency are present + if (channels.beforeConnect.hasSubscribers || channels.connected.hasSubscribers || + channels.connectError.hasSubscribers || channels.sendHeaders.hasSubscribers) { + isTrackingClientEvents = true + return + } + isTrackingClientEvents = true diagnosticsChannel.subscribe('undici:client:beforeConnect', @@ -98,6 +108,14 @@ function trackRequestEvents (debugLog = undiciDebugLog) { return } + // Check if any of the channels already have subscribers to prevent duplicate subscriptions + // This can happen when both Node.js built-in undici and undici as a dependency are present + if (channels.headers.hasSubscribers || channels.trailers.hasSubscribers || + channels.error.hasSubscribers) { + isTrackingRequestEvents = true + return + } + isTrackingRequestEvents = true diagnosticsChannel.subscribe('undici:request:headers', @@ -146,14 +164,25 @@ function trackWebSocketEvents (debugLog = websocketDebuglog) { return } + // Check if any of the channels already have subscribers to prevent duplicate subscriptions + // This can happen when both Node.js built-in undici and undici as a dependency are present + if (channels.open.hasSubscribers || channels.close.hasSubscribers || + channels.socketError.hasSubscribers || channels.ping.hasSubscribers || + channels.pong.hasSubscribers) { + isTrackingWebSocketEvents = true + return + } + isTrackingWebSocketEvents = true diagnosticsChannel.subscribe('undici:websocket:open', evt => { - const { - address: { address, port } - } = evt - debugLog('connection opened %s%s', address, port ? `:${port}` : '') + if (evt.address != null) { + const { address, port } = evt.address + debugLog('connection opened %s%s', address, port ? `:${port}` : '') + } else { + debugLog('connection opened') + } }) diagnosticsChannel.subscribe('undici:websocket:close', diff --git a/lib/core/errors.js b/lib/core/errors.js index b2b3f326bc4..5a7fcbff3ad 100644 --- a/lib/core/errors.js +++ b/lib/core/errors.js @@ -1,13 +1,23 @@ 'use strict' +const kUndiciError = Symbol.for('undici.error.UND_ERR') class UndiciError extends Error { constructor (message, options) { super(message, options) this.name = 'UndiciError' this.code = 'UND_ERR' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kUndiciError] === true + } + + get [kUndiciError] () { + return true + } } +const kConnectTimeoutError = Symbol.for('undici.error.UND_ERR_CONNECT_TIMEOUT') class ConnectTimeoutError extends UndiciError { constructor (message) { super(message) @@ -15,8 +25,17 @@ class ConnectTimeoutError extends UndiciError { this.message = message || 'Connect Timeout Error' this.code = 'UND_ERR_CONNECT_TIMEOUT' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kConnectTimeoutError] === true + } + + get [kConnectTimeoutError] () { + return true + } } +const kHeadersTimeoutError = Symbol.for('undici.error.UND_ERR_HEADERS_TIMEOUT') class HeadersTimeoutError extends UndiciError { constructor (message) { super(message) @@ -24,8 +43,17 @@ class HeadersTimeoutError extends UndiciError { this.message = message || 'Headers Timeout Error' this.code = 'UND_ERR_HEADERS_TIMEOUT' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kHeadersTimeoutError] === true + } + + get [kHeadersTimeoutError] () { + return true + } } +const kHeadersOverflowError = Symbol.for('undici.error.UND_ERR_HEADERS_OVERFLOW') class HeadersOverflowError extends UndiciError { constructor (message) { super(message) @@ -33,8 +61,17 @@ class HeadersOverflowError extends UndiciError { this.message = message || 'Headers Overflow Error' this.code = 'UND_ERR_HEADERS_OVERFLOW' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kHeadersOverflowError] === true + } + + get [kHeadersOverflowError] () { + return true + } } +const kBodyTimeoutError = Symbol.for('undici.error.UND_ERR_BODY_TIMEOUT') class BodyTimeoutError extends UndiciError { constructor (message) { super(message) @@ -42,21 +79,17 @@ class BodyTimeoutError extends UndiciError { this.message = message || 'Body Timeout Error' this.code = 'UND_ERR_BODY_TIMEOUT' } -} -class ResponseStatusCodeError extends UndiciError { - constructor (message, statusCode, headers, body) { - super(message) - 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 + static [Symbol.hasInstance] (instance) { + return instance && instance[kBodyTimeoutError] === true + } + + get [kBodyTimeoutError] () { + return true } } +const kInvalidArgumentError = Symbol.for('undici.error.UND_ERR_INVALID_ARG') class InvalidArgumentError extends UndiciError { constructor (message) { super(message) @@ -64,8 +97,17 @@ class InvalidArgumentError extends UndiciError { this.message = message || 'Invalid Argument Error' this.code = 'UND_ERR_INVALID_ARG' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kInvalidArgumentError] === true + } + + get [kInvalidArgumentError] () { + return true + } } +const kInvalidReturnValueError = Symbol.for('undici.error.UND_ERR_INVALID_RETURN_VALUE') class InvalidReturnValueError extends UndiciError { constructor (message) { super(message) @@ -73,16 +115,35 @@ class InvalidReturnValueError extends UndiciError { this.message = message || 'Invalid Return Value Error' this.code = 'UND_ERR_INVALID_RETURN_VALUE' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kInvalidReturnValueError] === true + } + + get [kInvalidReturnValueError] () { + return true + } } +const kAbortError = Symbol.for('undici.error.UND_ERR_ABORT') class AbortError extends UndiciError { constructor (message) { super(message) this.name = 'AbortError' this.message = message || 'The operation was aborted' + this.code = 'UND_ERR_ABORT' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kAbortError] === true + } + + get [kAbortError] () { + return true } } +const kRequestAbortedError = Symbol.for('undici.error.UND_ERR_ABORTED') class RequestAbortedError extends AbortError { constructor (message) { super(message) @@ -90,8 +151,17 @@ class RequestAbortedError extends AbortError { this.message = message || 'Request aborted' this.code = 'UND_ERR_ABORTED' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kRequestAbortedError] === true + } + + get [kRequestAbortedError] () { + return true + } } +const kInformationalError = Symbol.for('undici.error.UND_ERR_INFO') class InformationalError extends UndiciError { constructor (message) { super(message) @@ -99,8 +169,17 @@ class InformationalError extends UndiciError { this.message = message || 'Request information' this.code = 'UND_ERR_INFO' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kInformationalError] === true + } + + get [kInformationalError] () { + return true + } } +const kRequestContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_REQ_CONTENT_LENGTH_MISMATCH') class RequestContentLengthMismatchError extends UndiciError { constructor (message) { super(message) @@ -108,8 +187,17 @@ class RequestContentLengthMismatchError extends UndiciError { this.message = message || 'Request body length does not match content-length header' this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kRequestContentLengthMismatchError] === true + } + + get [kRequestContentLengthMismatchError] () { + return true + } } +const kResponseContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_RES_CONTENT_LENGTH_MISMATCH') class ResponseContentLengthMismatchError extends UndiciError { constructor (message) { super(message) @@ -117,8 +205,17 @@ class ResponseContentLengthMismatchError extends UndiciError { this.message = message || 'Response body length does not match content-length header' this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kResponseContentLengthMismatchError] === true + } + + get [kResponseContentLengthMismatchError] () { + return true + } } +const kClientDestroyedError = Symbol.for('undici.error.UND_ERR_DESTROYED') class ClientDestroyedError extends UndiciError { constructor (message) { super(message) @@ -126,8 +223,17 @@ class ClientDestroyedError extends UndiciError { this.message = message || 'The client is destroyed' this.code = 'UND_ERR_DESTROYED' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kClientDestroyedError] === true + } + + get [kClientDestroyedError] () { + return true + } } +const kClientClosedError = Symbol.for('undici.error.UND_ERR_CLOSED') class ClientClosedError extends UndiciError { constructor (message) { super(message) @@ -135,8 +241,17 @@ class ClientClosedError extends UndiciError { this.message = message || 'The client is closed' this.code = 'UND_ERR_CLOSED' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kClientClosedError] === true + } + + get [kClientClosedError] () { + return true + } } +const kSocketError = Symbol.for('undici.error.UND_ERR_SOCKET') class SocketError extends UndiciError { constructor (message, socket) { super(message) @@ -145,8 +260,17 @@ class SocketError extends UndiciError { this.code = 'UND_ERR_SOCKET' this.socket = socket } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kSocketError] === true + } + + get [kSocketError] () { + return true + } } +const kNotSupportedError = Symbol.for('undici.error.UND_ERR_NOT_SUPPORTED') class NotSupportedError extends UndiciError { constructor (message) { super(message) @@ -154,8 +278,17 @@ class NotSupportedError extends UndiciError { this.message = message || 'Not supported error' this.code = 'UND_ERR_NOT_SUPPORTED' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kNotSupportedError] === true + } + + get [kNotSupportedError] () { + return true + } } +const kBalancedPoolMissingUpstreamError = Symbol.for('undici.error.UND_ERR_BPL_MISSING_UPSTREAM') class BalancedPoolMissingUpstreamError extends UndiciError { constructor (message) { super(message) @@ -163,8 +296,17 @@ class BalancedPoolMissingUpstreamError extends UndiciError { this.message = message || 'No upstream has been added to the BalancedPool' this.code = 'UND_ERR_BPL_MISSING_UPSTREAM' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kBalancedPoolMissingUpstreamError] === true + } + + get [kBalancedPoolMissingUpstreamError] () { + return true + } } +const kHTTPParserError = Symbol.for('undici.error.UND_ERR_HTTP_PARSER') class HTTPParserError extends Error { constructor (message, code, data) { super(message) @@ -172,8 +314,17 @@ class HTTPParserError extends Error { this.code = code ? `HPE_${code}` : undefined this.data = data ? data.toString() : undefined } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kHTTPParserError] === true + } + + get [kHTTPParserError] () { + return true + } } +const kResponseExceededMaxSizeError = Symbol.for('undici.error.UND_ERR_RES_EXCEEDED_MAX_SIZE') class ResponseExceededMaxSizeError extends UndiciError { constructor (message) { super(message) @@ -181,8 +332,17 @@ class ResponseExceededMaxSizeError extends UndiciError { this.message = message || 'Response content exceeded max size' this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kResponseExceededMaxSizeError] === true + } + + get [kResponseExceededMaxSizeError] () { + return true + } } +const kRequestRetryError = Symbol.for('undici.error.UND_ERR_REQ_RETRY') class RequestRetryError extends UndiciError { constructor (message, code, { headers, data }) { super(message) @@ -193,8 +353,17 @@ class RequestRetryError extends UndiciError { this.data = data this.headers = headers } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kRequestRetryError] === true + } + + get [kRequestRetryError] () { + return true + } } +const kResponseError = Symbol.for('undici.error.UND_ERR_RESPONSE') class ResponseError extends UndiciError { constructor (message, code, { headers, body }) { super(message) @@ -205,8 +374,17 @@ class ResponseError extends UndiciError { this.body = body this.headers = headers } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kResponseError] === true + } + + get [kResponseError] () { + return true + } } +const kSecureProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_TLS') class SecureProxyConnectionError extends UndiciError { constructor (cause, message, options = {}) { super(message, { cause, ...options }) @@ -215,6 +393,59 @@ class SecureProxyConnectionError extends UndiciError { this.code = 'UND_ERR_PRX_TLS' this.cause = cause } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kSecureProxyConnectionError] === true + } + + get [kSecureProxyConnectionError] () { + return true + } +} + +const kMaxOriginsReachedError = Symbol.for('undici.error.UND_ERR_MAX_ORIGINS_REACHED') +class MaxOriginsReachedError extends UndiciError { + constructor (message) { + super(message) + this.name = 'MaxOriginsReachedError' + this.message = message || 'Maximum allowed origins reached' + this.code = 'UND_ERR_MAX_ORIGINS_REACHED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kMaxOriginsReachedError] === true + } + + get [kMaxOriginsReachedError] () { + return true + } +} + +class Socks5ProxyError extends UndiciError { + constructor (message, code) { + super(message) + this.name = 'Socks5ProxyError' + this.message = message || 'SOCKS5 proxy error' + this.code = code || 'UND_ERR_SOCKS5' + } +} + +const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED') +class MessageSizeExceededError extends UndiciError { + constructor (message) { + super(message) + this.name = 'MessageSizeExceededError' + this.message = message || 'Max decompressed message size exceeded' + this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kMessageSizeExceededError] === true + } + + get [kMessageSizeExceededError] () { + return true + } } module.exports = { @@ -226,7 +457,6 @@ module.exports = { BodyTimeoutError, RequestContentLengthMismatchError, ConnectTimeoutError, - ResponseStatusCodeError, InvalidArgumentError, InvalidReturnValueError, RequestAbortedError, @@ -240,5 +470,8 @@ module.exports = { ResponseExceededMaxSizeError, RequestRetryError, ResponseError, - SecureProxyConnectionError + SecureProxyConnectionError, + MaxOriginsReachedError, + Socks5ProxyError, + MessageSizeExceededError } diff --git a/lib/core/request.js b/lib/core/request.js index ef216af5952..326d58fb43f 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -13,11 +13,13 @@ const { isBuffer, isFormDataLike, isIterable, + hasSafeIterator, isBlobLike, serializePathWithQuery, assertRequestHandler, getServerName, - normalizedMethodRecords + normalizedMethodRecords, + getProtocolFromUrlString } = require('./util') const { channels } = require('./diagnostics.js') const { headerNameLowerCasedRecord } = require('./constants') @@ -25,6 +27,21 @@ const { headerNameLowerCasedRecord } = require('./constants') // Verifies that a given path is valid does not contain control chars \x00 to \x20 const invalidPathRegex = /[^\u0021-\u00ff]/ +function isValidContentLengthHeaderValue (val) { + if (typeof val !== 'string' || val.length === 0) { + return false + } + + for (let i = 0; i < val.length; i++) { + const charCode = val.charCodeAt(i) + if (charCode < 48 || charCode > 57) { + return false + } + } + + return true +} + const kHandler = Symbol('handler') class Request { @@ -42,7 +59,9 @@ class Request { reset, expectContinue, servername, - throwOnError + throwOnError, + maxRedirections, + typeOfService }, handler) { if (typeof path !== 'string') { throw new InvalidArgumentError('path must be a string') @@ -66,6 +85,10 @@ class Request { throw new InvalidArgumentError('upgrade must be a string') } + if (upgrade && !isValidHeaderValue(upgrade)) { + throw new InvalidArgumentError('invalid upgrade header') + } + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { throw new InvalidArgumentError('invalid headersTimeout') } @@ -86,12 +109,22 @@ class Request { throw new InvalidArgumentError('invalid throwOnError') } + if (maxRedirections != null && maxRedirections !== 0) { + throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor') + } + + if (typeOfService != null && (!Number.isInteger(typeOfService) || typeOfService < 0 || typeOfService > 255)) { + throw new InvalidArgumentError('typeOfService must be an integer between 0 and 255') + } + this.headersTimeout = headersTimeout this.bodyTimeout = bodyTimeout this.method = method + this.typeOfService = typeOfService ?? 0 + this.abort = null if (body == null) { @@ -136,8 +169,11 @@ class Request { this.path = query ? serializePathWithQuery(path, query) : path + // TODO: shall we maybe standardize it to an URL object? this.origin = origin + this.protocol = getProtocolFromUrlString(origin) + this.idempotent = idempotent == null ? method === 'HEAD' || method === 'GET' : idempotent @@ -165,7 +201,7 @@ class Request { processHeader(this, headers[i], headers[i + 1]) } } else if (headers && typeof headers === 'object') { - if (headers[Symbol.iterator]) { + if (hasSafeIterator(headers)) { for (const header of headers) { if (!Array.isArray(header) || header.length !== 2) { throw new InvalidArgumentError('headers must be in key-value pair format') @@ -368,30 +404,44 @@ function processHeader (request, key, val) { val = `${val}` } - if (request.host === null && headerName === 'host') { + if (headerName === 'host') { + if (request.host !== null) { + throw new InvalidArgumentError('duplicate host header') + } if (typeof val !== 'string') { throw new InvalidArgumentError('invalid host header') } // Consumed by Client request.host = val - } else if (request.contentLength === null && headerName === 'content-length') { - request.contentLength = parseInt(val, 10) - if (!Number.isFinite(request.contentLength)) { + } else if (headerName === 'content-length') { + if (request.contentLength !== null) { + throw new InvalidArgumentError('duplicate content-length header') + } + if (!isValidContentLengthHeaderValue(val)) { throw new InvalidArgumentError('invalid content-length header') } + request.contentLength = parseInt(val, 10) } else if (request.contentType === null && headerName === 'content-type') { request.contentType = val request.headers.push(key, val) } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') { throw new InvalidArgumentError(`invalid ${headerName} header`) } else if (headerName === 'connection') { - const value = typeof val === 'string' ? val.toLowerCase() : null - if (value !== 'close' && value !== 'keep-alive') { + // Per RFC 7230 Section 6.1, Connection header can contain + // a comma-separated list of connection option tokens (header names) + const value = typeof val === 'string' ? val : null + if (value === null) { throw new InvalidArgumentError('invalid connection header') } - if (value === 'close') { - request.reset = true + for (const token of value.toLowerCase().split(',')) { + const trimmed = token.trim() + if (!isValidHTTPToken(trimmed)) { + throw new InvalidArgumentError('invalid connection header') + } + if (trimmed === 'close') { + request.reset = true + } } } else if (headerName === 'expect') { throw new NotSupportedError('expect header not supported') diff --git a/lib/core/socks5-client.js b/lib/core/socks5-client.js new file mode 100644 index 00000000000..90f6d0a680f --- /dev/null +++ b/lib/core/socks5-client.js @@ -0,0 +1,422 @@ +'use strict' + +const { EventEmitter } = require('node:events') +const { Buffer } = require('node:buffer') +const { InvalidArgumentError, Socks5ProxyError } = require('./errors') +const { debuglog } = require('node:util') +const { parseAddress } = require('./socks5-utils') + +const debug = debuglog('undici:socks5') +const EMPTY_BUFFER = Buffer.alloc(0) + +// SOCKS5 constants +const SOCKS_VERSION = 0x05 + +// Authentication methods +const AUTH_METHODS = { + NO_AUTH: 0x00, + GSSAPI: 0x01, + USERNAME_PASSWORD: 0x02, + NO_ACCEPTABLE: 0xFF +} + +// SOCKS5 commands +const COMMANDS = { + CONNECT: 0x01, + BIND: 0x02, + UDP_ASSOCIATE: 0x03 +} + +// Address types +const ADDRESS_TYPES = { + IPV4: 0x01, + DOMAIN: 0x03, + IPV6: 0x04 +} + +// Reply codes +const REPLY_CODES = { + SUCCEEDED: 0x00, + GENERAL_FAILURE: 0x01, + CONNECTION_NOT_ALLOWED: 0x02, + NETWORK_UNREACHABLE: 0x03, + HOST_UNREACHABLE: 0x04, + CONNECTION_REFUSED: 0x05, + TTL_EXPIRED: 0x06, + COMMAND_NOT_SUPPORTED: 0x07, + ADDRESS_TYPE_NOT_SUPPORTED: 0x08 +} + +// State machine states +const STATES = { + INITIAL: 'initial', + HANDSHAKING: 'handshaking', + AUTHENTICATING: 'authenticating', + AUTHENTICATED: 'authenticated', + CONNECTING: 'connecting', + CONNECTED: 'connected', + ERROR: 'error', + CLOSED: 'closed' +} + +/** + * SOCKS5 client implementation + * Handles SOCKS5 protocol negotiation and connection establishment + */ +class Socks5Client extends EventEmitter { + constructor (socket, options = {}) { + super() + + if (!socket) { + throw new InvalidArgumentError('socket is required') + } + + this.socket = socket + this.options = options + this.state = STATES.INITIAL + this.buffer = EMPTY_BUFFER + this.onSocketData = this.onData.bind(this) + this.onSocketError = this.onError.bind(this) + this.onSocketClose = this.onClose.bind(this) + + // Authentication settings + this.authMethods = [] + if (options.username && options.password) { + this.authMethods.push(AUTH_METHODS.USERNAME_PASSWORD) + } + this.authMethods.push(AUTH_METHODS.NO_AUTH) + + // Socket event handlers + this.socket.on('data', this.onSocketData) + this.socket.on('error', this.onSocketError) + this.socket.on('close', this.onSocketClose) + } + + /** + * Handle incoming data from the socket + */ + onData (data) { + debug('received data', data.length, 'bytes in state', this.state) + this.buffer = Buffer.concat([this.buffer, data]) + + try { + switch (this.state) { + case STATES.HANDSHAKING: + this.handleHandshakeResponse() + break + case STATES.AUTHENTICATING: + this.handleAuthResponse() + break + case STATES.CONNECTING: + this.handleConnectResponse() + break + } + } catch (err) { + this.onError(err) + } + } + + /** + * Handle socket errors + */ + onError (err) { + debug('socket error', err) + this.state = STATES.ERROR + this.emit('error', err) + this.destroy() + } + + /** + * Handle socket close + */ + onClose () { + debug('socket closed') + this.state = STATES.CLOSED + this.emit('close') + } + + /** + * Destroy the client and underlying socket + */ + destroy () { + if (this.socket && !this.socket.destroyed) { + this.socket.destroy() + } + } + + markAuthenticated () { + this.state = STATES.AUTHENTICATED + this.emit('authenticated') + } + + /** + * Start the SOCKS5 handshake + */ + handshake () { + if (this.state !== STATES.INITIAL) { + throw new InvalidArgumentError('Handshake already started') + } + + debug('starting handshake with', this.authMethods.length, 'auth methods') + this.state = STATES.HANDSHAKING + + // Build handshake request + // +----+----------+----------+ + // |VER | NMETHODS | METHODS | + // +----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +----+----------+----------+ + const request = Buffer.alloc(2 + this.authMethods.length) + request[0] = SOCKS_VERSION + request[1] = this.authMethods.length + this.authMethods.forEach((method, i) => { + request[2 + i] = method + }) + + this.socket.write(request) + } + + /** + * Handle handshake response from server + */ + handleHandshakeResponse () { + if (this.buffer.length < 2) { + return // Not enough data yet + } + + const version = this.buffer[0] + const method = this.buffer[1] + + if (version !== SOCKS_VERSION) { + throw new Socks5ProxyError(`Invalid SOCKS version: ${version}`, 'UND_ERR_SOCKS5_VERSION') + } + + if (method === AUTH_METHODS.NO_ACCEPTABLE) { + throw new Socks5ProxyError('No acceptable authentication method', 'UND_ERR_SOCKS5_AUTH_REJECTED') + } + + this.buffer = this.buffer.subarray(2) + debug('server selected auth method', method) + + if (method === AUTH_METHODS.NO_AUTH) { + this.markAuthenticated() + } else if (method === AUTH_METHODS.USERNAME_PASSWORD) { + this.state = STATES.AUTHENTICATING + this.sendAuthRequest() + } else { + throw new Socks5ProxyError(`Unsupported authentication method: ${method}`, 'UND_ERR_SOCKS5_AUTH_METHOD') + } + } + + /** + * Send username/password authentication request + */ + sendAuthRequest () { + const { username, password } = this.options + + if (!username || !password) { + throw new InvalidArgumentError('Username and password required for authentication') + } + + debug('sending username/password auth') + + // Username/Password authentication request (RFC 1929) + // +----+------+----------+------+----------+ + // |VER | ULEN | UNAME | PLEN | PASSWD | + // +----+------+----------+------+----------+ + // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + // +----+------+----------+------+----------+ + const usernameBuffer = Buffer.from(username) + const passwordBuffer = Buffer.from(password) + + if (usernameBuffer.length > 255 || passwordBuffer.length > 255) { + throw new InvalidArgumentError('Username or password too long') + } + + const request = Buffer.alloc(3 + usernameBuffer.length + passwordBuffer.length) + request[0] = 0x01 // Sub-negotiation version + request[1] = usernameBuffer.length + usernameBuffer.copy(request, 2) + request[2 + usernameBuffer.length] = passwordBuffer.length + passwordBuffer.copy(request, 3 + usernameBuffer.length) + + this.socket.write(request) + } + + /** + * Handle authentication response + */ + handleAuthResponse () { + if (this.buffer.length < 2) { + return // Not enough data yet + } + + const version = this.buffer[0] + const status = this.buffer[1] + + if (version !== 0x01) { + throw new Socks5ProxyError(`Invalid auth sub-negotiation version: ${version}`, 'UND_ERR_SOCKS5_AUTH_VERSION') + } + + if (status !== 0x00) { + throw new Socks5ProxyError('Authentication failed', 'UND_ERR_SOCKS5_AUTH_FAILED') + } + + this.buffer = this.buffer.subarray(2) + debug('authentication successful') + this.markAuthenticated() + } + + /** + * Send CONNECT command + * @param {string} address - Target address (IP or domain) + * @param {number} port - Target port + */ + connect (address, port) { + if (this.state === STATES.CONNECTING || this.state === STATES.CONNECTED) { + throw new InvalidArgumentError('Connection already in progress') + } + + if (this.state !== STATES.AUTHENTICATED) { + throw new InvalidArgumentError('Client must be authenticated before CONNECT') + } + + debug('connecting to', address, port) + this.state = STATES.CONNECTING + + const request = this.buildConnectRequest(COMMANDS.CONNECT, address, port) + this.socket.write(request) + } + + /** + * Build a SOCKS5 request + */ + buildConnectRequest (command, address, port) { + // Parse address to determine type and buffer + const { type: addressType, buffer: addressBuffer } = parseAddress(address) + + // Build request + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + const request = Buffer.alloc(4 + addressBuffer.length + 2) + request[0] = SOCKS_VERSION + request[1] = command + request[2] = 0x00 // Reserved + request[3] = addressType + addressBuffer.copy(request, 4) + request.writeUInt16BE(port, 4 + addressBuffer.length) + + return request + } + + /** + * Handle CONNECT response + */ + handleConnectResponse () { + if (this.buffer.length < 4) { + return // Not enough data for header + } + + const version = this.buffer[0] + const reply = this.buffer[1] + const addressType = this.buffer[3] + + if (version !== SOCKS_VERSION) { + throw new Socks5ProxyError(`Invalid SOCKS version in reply: ${version}`, 'UND_ERR_SOCKS5_REPLY_VERSION') + } + + // Calculate the expected response length + let responseLength = 4 // VER + REP + RSV + ATYP + if (addressType === ADDRESS_TYPES.IPV4) { + responseLength += 4 + 2 // IPv4 + port + } else if (addressType === ADDRESS_TYPES.DOMAIN) { + if (this.buffer.length < 5) { + return // Need domain length byte + } + responseLength += 1 + this.buffer[4] + 2 // length byte + domain + port + } else if (addressType === ADDRESS_TYPES.IPV6) { + responseLength += 16 + 2 // IPv6 + port + } else { + throw new Socks5ProxyError(`Invalid address type in reply: ${addressType}`, 'UND_ERR_SOCKS5_ADDR_TYPE') + } + + if (this.buffer.length < responseLength) { + return // Not enough data for full response + } + + if (reply !== REPLY_CODES.SUCCEEDED) { + const errorMessage = this.getReplyErrorMessage(reply) + throw new Socks5ProxyError(`SOCKS5 connection failed: ${errorMessage}`, `UND_ERR_SOCKS5_REPLY_${reply}`) + } + + // Parse bound address and port + let boundAddress + let offset = 4 + + if (addressType === ADDRESS_TYPES.IPV4) { + boundAddress = Array.from(this.buffer.subarray(offset, offset + 4)).join('.') + offset += 4 + } else if (addressType === ADDRESS_TYPES.DOMAIN) { + const domainLength = this.buffer[offset] + offset += 1 + boundAddress = this.buffer.subarray(offset, offset + domainLength).toString() + offset += domainLength + } else if (addressType === ADDRESS_TYPES.IPV6) { + // Parse IPv6 address from 16-byte buffer + const parts = [] + for (let i = 0; i < 8; i++) { + const value = this.buffer.readUInt16BE(offset + i * 2) + parts.push(value.toString(16)) + } + boundAddress = parts.join(':') + offset += 16 + } + + const boundPort = this.buffer.readUInt16BE(offset) + + this.buffer = EMPTY_BUFFER + this.state = STATES.CONNECTED + this.socket.removeListener('data', this.onSocketData) + + debug('connected, bound address:', boundAddress, 'port:', boundPort) + this.emit('connected', { address: boundAddress, port: boundPort }) + } + + /** + * Get human-readable error message for reply code + */ + getReplyErrorMessage (reply) { + switch (reply) { + case REPLY_CODES.GENERAL_FAILURE: + return 'General SOCKS server failure' + case REPLY_CODES.CONNECTION_NOT_ALLOWED: + return 'Connection not allowed by ruleset' + case REPLY_CODES.NETWORK_UNREACHABLE: + return 'Network unreachable' + case REPLY_CODES.HOST_UNREACHABLE: + return 'Host unreachable' + case REPLY_CODES.CONNECTION_REFUSED: + return 'Connection refused' + case REPLY_CODES.TTL_EXPIRED: + return 'TTL expired' + case REPLY_CODES.COMMAND_NOT_SUPPORTED: + return 'Command not supported' + case REPLY_CODES.ADDRESS_TYPE_NOT_SUPPORTED: + return 'Address type not supported' + default: + return `Unknown error code: ${reply}` + } + } +} + +module.exports = { + Socks5Client, + AUTH_METHODS, + COMMANDS, + ADDRESS_TYPES, + REPLY_CODES, + STATES +} diff --git a/lib/core/socks5-utils.js b/lib/core/socks5-utils.js new file mode 100644 index 00000000000..e6098bef05f --- /dev/null +++ b/lib/core/socks5-utils.js @@ -0,0 +1,212 @@ +'use strict' + +const { Buffer } = require('node:buffer') +const net = require('node:net') +const { InvalidArgumentError } = require('./errors') + +/** + * Parse an address and determine its type + * @param {string} address - The address to parse + * @returns {{type: number, buffer: Buffer}} Address type and buffer + */ +function parseAddress (address) { + // Check if it's an IPv4 address + if (net.isIPv4(address)) { + const parts = address.split('.').map(Number) + return { + type: 0x01, // IPv4 + buffer: Buffer.from(parts) + } + } + + // Check if it's an IPv6 address + if (net.isIPv6(address)) { + return { + type: 0x04, // IPv6 + buffer: parseIPv6(address) + } + } + + // Otherwise, treat as domain name + const domainBuffer = Buffer.from(address, 'utf8') + if (domainBuffer.length > 255) { + throw new InvalidArgumentError('Domain name too long (max 255 bytes)') + } + + return { + type: 0x03, // Domain + buffer: Buffer.concat([Buffer.from([domainBuffer.length]), domainBuffer]) + } +} + +/** + * Parse IPv6 address to buffer + * @param {string} address - IPv6 address string + * @returns {Buffer} 16-byte buffer + */ +function parseIPv6 (address) { + const buffer = Buffer.alloc(16) + let normalizedAddress = address + + // Expand an embedded IPv4 tail into the last two IPv6 groups. + if (address.includes('.')) { + const lastColonIndex = address.lastIndexOf(':') + const ipv4Part = address.slice(lastColonIndex + 1) + + if (net.isIPv4(ipv4Part)) { + const octets = ipv4Part.split('.').map(Number) + const high = ((octets[0] << 8) | octets[1]).toString(16) + const low = ((octets[2] << 8) | octets[3]).toString(16) + normalizedAddress = `${address.slice(0, lastColonIndex)}:${high}:${low}` + } + } + + // Handle compressed notation (::) + const doubleColonIndex = normalizedAddress.indexOf('::') + if (doubleColonIndex !== -1) { + const before = normalizedAddress.slice(0, doubleColonIndex) + const after = normalizedAddress.slice(doubleColonIndex + 2) + const beforeParts = before === '' ? [] : before.split(':') + const afterParts = after === '' ? [] : after.split(':') + + let bufferIndex = 0 + for (const part of beforeParts) { + buffer.writeUInt16BE(parseInt(part, 16), bufferIndex) + bufferIndex += 2 + } + bufferIndex = 16 - afterParts.length * 2 + for (const part of afterParts) { + buffer.writeUInt16BE(parseInt(part, 16), bufferIndex) + bufferIndex += 2 + } + } else { + const parts = normalizedAddress.split(':') + for (let i = 0; i < parts.length; i++) { + buffer.writeUInt16BE(parseInt(parts[i], 16), i * 2) + } + } + + return buffer +} + +/** + * Build a SOCKS5 address buffer + * @param {number} type - Address type (1=IPv4, 3=Domain, 4=IPv6) + * @param {Buffer} addressBuffer - The address data + * @param {number} port - Port number + * @returns {Buffer} Complete address buffer including type, address, and port + */ +function buildAddressBuffer (type, addressBuffer, port) { + const portBuffer = Buffer.allocUnsafe(2) + portBuffer.writeUInt16BE(port, 0) + + return Buffer.concat([ + Buffer.from([type]), + addressBuffer, + portBuffer + ]) +} + +/** + * Parse address from SOCKS5 response + * @param {Buffer} buffer - Buffer containing the address + * @param {number} offset - Starting offset in buffer + * @returns {{address: string, port: number, bytesRead: number}} + */ +function parseResponseAddress (buffer, offset = 0) { + if (buffer.length < offset + 1) { + throw new InvalidArgumentError('Buffer too small to contain address type') + } + + const addressType = buffer[offset] + let address + let currentOffset = offset + 1 + + switch (addressType) { + case 0x01: { // IPv4 + if (buffer.length < currentOffset + 6) { + throw new InvalidArgumentError('Buffer too small for IPv4 address') + } + address = Array.from(buffer.subarray(currentOffset, currentOffset + 4)).join('.') + currentOffset += 4 + break + } + + case 0x03: { // Domain + if (buffer.length < currentOffset + 1) { + throw new InvalidArgumentError('Buffer too small for domain length') + } + const domainLength = buffer[currentOffset] + currentOffset += 1 + + if (buffer.length < currentOffset + domainLength + 2) { + throw new InvalidArgumentError('Buffer too small for domain address') + } + address = buffer.subarray(currentOffset, currentOffset + domainLength).toString('utf8') + currentOffset += domainLength + break + } + + case 0x04: { // IPv6 + if (buffer.length < currentOffset + 18) { + throw new InvalidArgumentError('Buffer too small for IPv6 address') + } + // Convert buffer to IPv6 string + const parts = [] + for (let i = 0; i < 8; i++) { + const value = buffer.readUInt16BE(currentOffset + i * 2) + parts.push(value.toString(16)) + } + address = parts.join(':') + currentOffset += 16 + break + } + + default: + throw new InvalidArgumentError(`Invalid address type: ${addressType}`) + } + + // Parse port + if (buffer.length < currentOffset + 2) { + throw new InvalidArgumentError('Buffer too small for port') + } + const port = buffer.readUInt16BE(currentOffset) + currentOffset += 2 + + return { + address, + port, + bytesRead: currentOffset - offset + } +} + +/** + * Create error for SOCKS5 reply code + * @param {number} replyCode - SOCKS5 reply code + * @returns {Error} Appropriate error object + */ +function createReplyError (replyCode) { + const messages = { + 0x01: 'General SOCKS server failure', + 0x02: 'Connection not allowed by ruleset', + 0x03: 'Network unreachable', + 0x04: 'Host unreachable', + 0x05: 'Connection refused', + 0x06: 'TTL expired', + 0x07: 'Command not supported', + 0x08: 'Address type not supported' + } + + const message = messages[replyCode] || `Unknown SOCKS5 error code: ${replyCode}` + const error = new Error(message) + error.code = `SOCKS5_${replyCode}` + return error +} + +module.exports = { + parseAddress, + parseIPv6, + buildAddressBuffer, + parseResponseAddress, + createReplyError +} diff --git a/lib/core/symbols.js b/lib/core/symbols.js index f3b563a5419..ff37adc0448 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -62,7 +62,14 @@ module.exports = { kListeners: Symbol('listeners'), kHTTPContext: Symbol('http context'), kMaxConcurrentStreams: Symbol('max concurrent streams'), + kHTTP2InitialWindowSize: Symbol('http2 initial window size'), + kHTTP2ConnectionWindowSize: Symbol('http2 connection window size'), + kEnableConnectProtocol: Symbol('http2session connect protocol'), + kRemoteSettings: Symbol('http2session remote settings'), + kHTTP2Stream: Symbol('http2session client stream'), + kPingInterval: Symbol('ping interval'), kNoProxyAgent: Symbol('no proxy agent'), kHttpProxyAgent: Symbol('http proxy agent'), - kHttpsProxyAgent: Symbol('https proxy agent') + kHttpsProxyAgent: Symbol('https proxy agent'), + kSocks5ProxyAgent: Symbol('socks5 proxy agent') } diff --git a/lib/core/tree.js b/lib/core/tree.js index e7b960cb2b3..6eed58aad69 100644 --- a/lib/core/tree.js +++ b/lib/core/tree.js @@ -86,7 +86,7 @@ class TstNode { /** * @param {Uint8Array} key - * @return {TstNode | null} + * @returns {TstNode | null} */ search (key) { const keylength = key.length diff --git a/lib/core/util.js b/lib/core/util.js index aa44edfa97f..767d586b93a 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -5,7 +5,6 @@ const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols') const { IncomingMessage } = require('node:http') const stream = require('node:stream') const net = require('node:net') -const { Blob } = require('node:buffer') const { stringify } = require('node:querystring') const { EventEmitter: EE } = require('node:events') const timers = require('../util/timers') @@ -59,6 +58,8 @@ function wrapRequestBody (body) { // to determine whether or not it has been disturbed. This is just // a workaround. return new BodyAsyncIterable(body) + } else if (body && isFormDataLike(body)) { + return body } else if ( body && typeof body !== 'string' && @@ -103,13 +104,24 @@ function isBlobLike (object) { } } +/** + * @param {string} url The path to check for query strings or fragments. + * @returns {boolean} Returns true if the path contains a query string or fragment. + */ +function pathHasQueryOrFragment (url) { + return ( + url.includes('?') || + url.includes('#') + ) +} + /** * @param {string} url The URL to add the query params to * @param {import('node:querystring').ParsedUrlQueryInput} queryParams The object to serialize into a URL query string * @returns {string} The URL with the query params added */ function serializePathWithQuery (url, queryParams) { - if (url.includes('?') || url.includes('#')) { + if (pathHasQueryOrFragment(url)) { throw new Error('Query params cannot be passed when url already contains "?" or "#".') } @@ -315,6 +327,20 @@ function isIterable (obj) { return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) } +/** + * Checks whether an object has a safe Symbol.iterator — i.e. one that is + * either own or inherited from a non-Object.prototype chain. This prevents + * prototype-pollution attacks from injecting a fake iterator on + * Object.prototype. + * @param {object} obj + * @returns {boolean} + */ +function hasSafeIterator (obj) { + const prototype = Object.getPrototypeOf(obj) + const ownIterator = Object.prototype.hasOwnProperty.call(obj, Symbol.iterator) + return ownIterator || (prototype != null && prototype !== Object.prototype && typeof obj[Symbol.iterator] === 'function') +} + /** * @param {Blob|Buffer|import ('stream').Stream} body * @returns {number|null} @@ -414,25 +440,40 @@ function parseHeaders (headers, obj) { const key = headerNameToString(headers[i]) let val = obj[key] - if (val) { - if (typeof val === 'string') { - val = [val] - obj[key] = val - } - val.push(headers[i + 1].toString('utf8')) - } else { - const headersValue = headers[i + 1] - if (typeof headersValue === 'string') { - obj[key] = headersValue + if (val !== undefined) { + if (!Object.hasOwn(obj, key)) { + const headersValue = typeof headers[i + 1] === 'string' + ? headers[i + 1] + : Array.isArray(headers[i + 1]) + ? headers[i + 1].map(x => x.toString('latin1')) + : headers[i + 1].toString('latin1') + + if (key === '__proto__') { + Object.defineProperty(obj, key, { + value: headersValue, + enumerable: true, + configurable: true, + writable: true + }) + } else { + obj[key] = headersValue + } } else { - obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8') + if (typeof val === 'string') { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('latin1')) } - } - } + } else { + const headersValue = typeof headers[i + 1] === 'string' + ? headers[i + 1] + : Array.isArray(headers[i + 1]) + ? headers[i + 1].map(x => x.toString('latin1')) + : headers[i + 1].toString('latin1') - // 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') + obj[key] = headersValue + } } return obj @@ -449,34 +490,20 @@ function parseRawHeaders (headers) { */ const ret = new Array(headersLength) - let hasContentLength = false - let contentDispositionIdx = -1 let key let val - let kLen = 0 for (let n = 0; n < headersLength; n += 2) { key = headers[n] val = headers[n + 1] typeof key !== 'string' && (key = key.toString()) - typeof val !== 'string' && (val = val.toString('utf8')) + typeof val !== 'string' && (val = val.toString('latin1')) - kLen = key.length - if (kLen === 14 && key[7] === '-' && (key === 'content-length' || key.toLowerCase() === 'content-length')) { - hasContentLength = true - } else if (kLen === 19 && key[7] === '-' && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { - contentDispositionIdx = n + 1 - } ret[n] = key ret[n + 1] = val } - // See https://github.com/nodejs/node/pull/46528 - if (hasContentLength && contentDispositionIdx !== -1) { - ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1') - } - return ret } @@ -599,31 +626,28 @@ function ReadableStreamFrom (iterable) { let iterator return new ReadableStream( { - async start () { + start () { iterator = iterable[Symbol.asyncIterator]() }, pull (controller) { - async function pull () { - const { done, value } = await iterator.next() + return iterator.next().then(({ done, value }) => { if (done) { - queueMicrotask(() => { + return queueMicrotask(() => { controller.close() controller.byobRequest?.respond(0) }) } else { const buf = Buffer.isBuffer(value) ? value : Buffer.from(value) if (buf.byteLength) { - controller.enqueue(new Uint8Array(buf)) + return controller.enqueue(new Uint8Array(buf)) } else { - return await pull() + return this.pull(controller) } } - } - - return pull() + }) }, - async cancel () { - await iterator.return() + cancel () { + return iterator.return() }, type: 'bytes' } @@ -659,48 +683,46 @@ function addAbortListener (signal, listener) { return () => signal.removeListener('abort', listener) } +const validTokenChars = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 (!"#$%&'()*+,-./) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 (0-9:;<=>?) + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 (@A-O) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 (P-Z[\]^_) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 (`a-o) + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 (p-z{|}~) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-159 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-175 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-191 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-207 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-223 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-239 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255 +]) + /** * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 * @param {number} c * @returns {boolean} */ function isTokenCharCode (c) { - switch (c) { - case 0x22: - case 0x28: - case 0x29: - case 0x2c: - case 0x2f: - case 0x3a: - case 0x3b: - case 0x3c: - case 0x3d: - case 0x3e: - case 0x3f: - case 0x40: - case 0x5b: - case 0x5c: - case 0x5d: - case 0x7b: - case 0x7d: - // DQUOTE and "(),/:;<=>?@[\]{}" - return false - default: - // VCHAR %x21-7E - return c >= 0x21 && c <= 0x7e - } + return (validTokenChars[c] === 1) } +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/ + /** * @param {string} characters * @returns {boolean} */ function isValidHTTPToken (characters) { - if (characters.length === 0) { - return false - } - for (let i = 0; i < characters.length; ++i) { - if (!isTokenCharCode(characters.charCodeAt(i))) { + if (characters.length >= 12) return tokenRegExp.test(characters) + if (characters.length === 0) return false + + for (let i = 0; i < characters.length; i++) { + if (validTokenChars[characters.charCodeAt(i)] !== 1) { return false } } @@ -869,6 +891,30 @@ function onConnectTimeout (socket, opts) { destroy(socket, new ConnectTimeoutError(message)) } +/** + * @param {string} urlString + * @returns {string} + */ +function getProtocolFromUrlString (urlString) { + if ( + urlString[0] === 'h' && + urlString[1] === 't' && + urlString[2] === 't' && + urlString[3] === 'p' + ) { + switch (urlString[4]) { + case ':': + return 'http:' + case 's': + if (urlString[5] === ':') { + return 'https:' + } + } + } + // fallback if none of the usual suspects + return urlString.slice(0, urlString.indexOf(':') + 1) +} + const kEnumerableProperty = Object.create(null) kEnumerableProperty.enumerable = true @@ -906,6 +952,7 @@ module.exports = { getServerName, isStream, isIterable, + hasSafeIterator, isAsyncIterable, isDestroyed, headerNameToString, @@ -925,6 +972,7 @@ module.exports = { assertRequestHandler, getSocketInfo, isFormDataLike, + pathHasQueryOrFragment, serializePathWithQuery, addAbortListener, isValidHTTPToken, @@ -939,5 +987,6 @@ module.exports = { nodeMinor, safeHTTPMethods: Object.freeze(['GET', 'HEAD', 'OPTIONS', 'TRACE']), wrapRequestBody, - setupConnectTimeout + setupConnectTimeout, + getProtocolFromUrlString } diff --git a/lib/dispatcher/agent.js b/lib/dispatcher/agent.js index 7c413701a6d..09fdd4be3d5 100644 --- a/lib/dispatcher/agent.js +++ b/lib/dispatcher/agent.js @@ -1,6 +1,6 @@ 'use strict' -const { InvalidArgumentError } = require('../core/errors') +const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors') const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols') const DispatcherBase = require('./dispatcher-base') const Pool = require('./pool') @@ -13,6 +13,7 @@ const kOnConnectionError = Symbol('onConnectionError') const kOnDrain = Symbol('onDrain') const kFactory = Symbol('factory') const kOptions = Symbol('options') +const kOrigins = Symbol('origins') function defaultFactory (origin, opts) { return opts && opts.connections === 1 @@ -21,7 +22,7 @@ function defaultFactory (origin, opts) { } class Agent extends DispatcherBase { - constructor ({ factory = defaultFactory, connect, ...options } = {}) { + constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) { if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } @@ -30,42 +31,34 @@ class Agent extends DispatcherBase { throw new InvalidArgumentError('connect must be a function or an object') } - super() + if (typeof maxOrigins !== 'number' || Number.isNaN(maxOrigins) || maxOrigins <= 0) { + throw new InvalidArgumentError('maxOrigins must be a number greater than 0') + } + + super(options) if (connect && typeof connect !== 'function') { connect = { ...connect } } - this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions] = { ...util.deepClone(options), maxOrigins, connect } this[kFactory] = factory this[kClients] = new Map() + this[kOrigins] = new Set() this[kOnDrain] = (origin, targets) => { this.emit('drain', origin, [this, ...targets]) } this[kOnConnect] = (origin, targets) => { - const result = this[kClients].get(origin) - if (result) { - result.count += 1 - } this.emit('connect', origin, [this, ...targets]) } this[kOnDisconnect] = (origin, targets, err) => { - const result = this[kClients].get(origin) - if (result) { - result.count -= 1 - if (result.count <= 0) { - this[kClients].delete(origin) - result.dispatcher.destroy() - } - } this.emit('disconnect', origin, [this, ...targets], err) } this[kOnConnectionError] = (origin, targets, err) => { - // TODO: should this decrement result.count here? this.emit('connectionError', origin, [this, ...targets], err) } } @@ -86,39 +79,69 @@ class Agent extends DispatcherBase { throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') } + if (this[kOrigins].size >= this[kOptions].maxOrigins && !this[kOrigins].has(key)) { + throw new MaxOriginsReachedError() + } + const result = this[kClients].get(key) let dispatcher = result && result.dispatcher if (!dispatcher) { + const closeClientIfUnused = (connected) => { + const result = this[kClients].get(key) + if (result) { + if (connected) result.count -= 1 + if (result.count <= 0) { + this[kClients].delete(key) + if (!result.dispatcher.destroyed) { + result.dispatcher.close() + } + } + this[kOrigins].delete(key) + } + } dispatcher = this[kFactory](opts.origin, this[kOptions]) .on('drain', this[kOnDrain]) - .on('connect', this[kOnConnect]) - .on('disconnect', this[kOnDisconnect]) - .on('connectionError', this[kOnConnectionError]) + .on('connect', (origin, targets) => { + const result = this[kClients].get(key) + if (result) { + result.count += 1 + } + this[kOnConnect](origin, targets) + }) + .on('disconnect', (origin, targets, err) => { + closeClientIfUnused(true) + this[kOnDisconnect](origin, targets, err) + }) + .on('connectionError', (origin, targets, err) => { + closeClientIfUnused(false) + this[kOnConnectionError](origin, targets, err) + }) this[kClients].set(key, { count: 0, dispatcher }) + this[kOrigins].add(key) } return dispatcher.dispatch(opts, handler) } - async [kClose] () { + [kClose] () { const closePromises = [] for (const { dispatcher } of this[kClients].values()) { closePromises.push(dispatcher.close()) } this[kClients].clear() - await Promise.all(closePromises) + return Promise.all(closePromises) } - async [kDestroy] (err) { + [kDestroy] (err) { const destroyPromises = [] for (const { dispatcher } of this[kClients].values()) { destroyPromises.push(dispatcher.destroy(err)) } this[kClients].clear() - await Promise.all(destroyPromises) + return Promise.all(destroyPromises) } get stats () { diff --git a/lib/dispatcher/balanced-pool.js b/lib/dispatcher/balanced-pool.js index 5bbec0e618d..c21c081c45c 100644 --- a/lib/dispatcher/balanced-pool.js +++ b/lib/dispatcher/balanced-pool.js @@ -14,7 +14,7 @@ const { } = require('./pool-base') const Pool = require('./pool') const { kUrl } = require('../core/symbols') -const { parseOrigin } = require('../core/util') +const util = require('../core/util') const kFactory = Symbol('factory') const kOptions = Symbol('options') @@ -54,9 +54,12 @@ class BalancedPool extends PoolBase { throw new InvalidArgumentError('factory must be a function.') } - super() + super(opts) - this[kOptions] = opts + this[kOptions] = { ...util.deepClone(opts) } + this[kOptions].interceptors = opts.interceptors + ? { ...opts.interceptors } + : undefined this[kIndex] = -1 this[kCurrentWeight] = 0 @@ -76,7 +79,7 @@ class BalancedPool extends PoolBase { } addUpstream (upstream) { - const upstreamOrigin = parseOrigin(upstream).origin + const upstreamOrigin = util.parseOrigin(upstream).origin if (this[kClients].find((pool) => ( pool[kUrl].origin === upstreamOrigin && @@ -85,7 +88,7 @@ class BalancedPool extends PoolBase { ))) { return this } - const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions])) + const pool = this[kFactory](upstreamOrigin, this[kOptions]) this[kAddClient](pool) pool.on('connect', () => { @@ -125,7 +128,7 @@ class BalancedPool extends PoolBase { } removeUpstream (upstream) { - const upstreamOrigin = parseOrigin(upstream).origin + const upstreamOrigin = util.parseOrigin(upstream).origin const pool = this[kClients].find((pool) => ( pool[kUrl].origin === upstreamOrigin && @@ -140,6 +143,16 @@ class BalancedPool extends PoolBase { return this } + getUpstream (upstream) { + const upstreamOrigin = util.parseOrigin(upstream).origin + + return this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + )) + } + get upstreams () { return this[kClients] .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 63aee63760d..7d179967110 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -57,26 +57,42 @@ const constants = require('../llhttp/constants.js') const EMPTY_BUF = Buffer.alloc(0) const FastBuffer = Buffer[Symbol.species] const removeAllListeners = util.removeAllListeners +const kIdleSocketValidation = Symbol('kIdleSocketValidation') +const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout') +const kSocketUsed = Symbol('kSocketUsed') let extractBody -async function lazyllhttp () { +function lazyllhttp () { const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined let mod - try { - mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js')) - } catch (e) { - /* istanbul ignore next */ + // We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures. + let useWasmSIMD = process.arch !== 'ppc64' + // The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior + if (process.env.UNDICI_NO_WASM_SIMD === '1') { + useWasmSIMD = false + } else if (process.env.UNDICI_NO_WASM_SIMD === '0') { + useWasmSIMD = true + } + + if (useWasmSIMD) { + try { + mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js')) + } catch { + } + } + + if (!mod) { // We could check if the error was caused by the simd option not // being enabled, but the occurring of this other error // * https://github.com/emscripten-core/emscripten/issues/11495 // got me to remove that check to avoid breaking Node 12. - mod = await WebAssembly.compile(llhttpWasmData || require('../llhttp/llhttp-wasm.js')) + mod = new WebAssembly.Module(llhttpWasmData || require('../llhttp/llhttp-wasm.js')) } - return await WebAssembly.instantiate(mod, { + return new WebAssembly.Instance(mod, { env: { /** * @param {number} p @@ -85,7 +101,6 @@ async function lazyllhttp () { * @returns {number} */ wasm_on_url: (p, at, len) => { - /* istanbul ignore next */ return 0 }, /** @@ -165,11 +180,6 @@ async function lazyllhttp () { } let llhttpInstance = null -/** - * @type {Promise|null} - */ -let llhttpPromise = lazyllhttp() -llhttpPromise.catch() /** * @type {Parser|null} @@ -209,6 +219,7 @@ class Parser { */ this.socket = socket this.timeout = null + this.timeoutWeakRef = new WeakRef(this) this.timeoutValue = null this.timeoutType = null this.statusCode = 0 @@ -246,16 +257,15 @@ class Parser { if (delay) { if (type & USE_FAST_TIMER) { - this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this)) + this.timeout = timers.setFastTimeout(onParserTimeout, delay, this.timeoutWeakRef) } else { - this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this)) + this.timeout = setTimeout(onParserTimeout, delay, this.timeoutWeakRef) this.timeout?.unref() } } this.timeoutValue = delay } else if (this.timeout) { - // istanbul ignore else: only for jest if (this.timeout.refresh) { this.timeout.refresh() } @@ -276,7 +286,6 @@ class Parser { assert(this.timeoutType === TIMEOUT_BODY) if (this.timeout) { - // istanbul ignore else: only for jest if (this.timeout.refresh) { this.timeout.refresh() } @@ -330,10 +339,6 @@ class Parser { currentBufferRef = chunk currentParser = this ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, chunk.length) - /* eslint-disable-next-line no-useless-catch */ - } catch (err) { - /* istanbul ignore next: difficult to make a test case for */ - throw err } finally { currentParser = null currentBufferRef = null @@ -348,17 +353,7 @@ class Parser { this.paused = true socket.unshift(data) } else { - const ptr = llhttp.llhttp_get_error_reason(this.ptr) - let message = '' - /* istanbul ignore else: difficult to make a test case for */ - if (ptr) { - const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) - message = - 'Response does not match the HTTP/1.1 protocol (' + - Buffer.from(llhttp.memory.buffer, ptr, len).toString() + - ')' - } - throw new HTTPParserError(message, constants.ERROR[ret], data) + throw this.createError(ret, data) } } } catch (err) { @@ -366,6 +361,54 @@ class Parser { } } + finish () { + assert(currentParser === null) + assert(this.ptr != null) + assert(!this.paused) + + const { llhttp } = this + + let ret + + try { + currentParser = this + ret = llhttp.llhttp_finish(this.ptr) + } finally { + currentParser = null + } + + if (ret === constants.ERROR.OK) { + return null + } + + if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) { + this.paused = true + return null + } + + return this.createError(ret, EMPTY_BUF) + } + + createError (ret, data) { + const { llhttp, contentLength, bytesRead } = this + + if (contentLength && bytesRead !== parseInt(contentLength, 10)) { + return new ResponseContentLengthMismatchError() + } + + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + + return new HTTPParserError(message, constants.ERROR[ret], data) + } + destroy () { assert(currentParser === null) assert(this.ptr != null) @@ -396,11 +439,15 @@ class Parser { onMessageBegin () { const { socket, client } = this - /* istanbul ignore next: difficult to make a test case for */ if (socket.destroyed) { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] if (!request) { return -1 @@ -525,14 +572,17 @@ class Parser { onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { const { client, socket, headers, statusText } = this - /* istanbul ignore next: difficult to make a test case for */ if (socket.destroyed) { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] - /* istanbul ignore next: difficult to make a test case for */ if (!request) { return -1 } @@ -566,7 +616,6 @@ class Parser { : client[kBodyTimeout] this.setTimeout(bodyTimeout, TIMEOUT_BODY) } else if (this.timeout) { - // istanbul ignore else: only for jest if (this.timeout.refresh) { this.timeout.refresh() } @@ -647,7 +696,6 @@ class Parser { assert(this.timeoutType === TIMEOUT_BODY) if (this.timeout) { - // istanbul ignore else: only for jest if (this.timeout.refresh) { this.timeout.refresh() } @@ -703,7 +751,6 @@ class Parser { return 0 } - /* istanbul ignore next: should be handled by llhttp? */ if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { util.destroy(socket, new ResponseContentLengthMismatchError()) return -1 @@ -712,6 +759,7 @@ class Parser { request.onComplete(headers) client[kQueue][client[kRunningIdx]++] = null + socket[kSocketUsed] = client[kPending] === 0 if (socket[kWriting]) { assert(client[kRunning] === 0) @@ -732,7 +780,7 @@ class Parser { // We must wait a full event loop cycle to reuse this socket to make sure // that non-spec compliant servers are not closing the connection even if they // said they won't. - setImmediate(() => client[kResume]()) + setImmediate(client[kResume]) } else { client[kResume]() } @@ -741,10 +789,14 @@ class Parser { } } -function onParserTimeout (parser) { - const { socket, timeoutType, client, paused } = parser.deref() +function onParserTimeout (parserWeakRef) { + const parser = parserWeakRef.deref() + if (!parser) { + return + } + + const { socket, timeoutType, client, paused } = parser - /* istanbul ignore else */ if (timeoutType === TIMEOUT_HEADERS) { if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { assert(!paused, 'cannot be paused while waiting for headers') @@ -765,15 +817,11 @@ function onParserTimeout (parser) { * @param {import('net').Socket} socket * @returns */ -async function connectH1 (client, socket) { +function connectH1 (client, socket) { client[kSocket] = socket if (!llhttpInstance) { - const noop = () => {} - socket.on('error', noop) - llhttpInstance = await llhttpPromise - llhttpPromise = null - socket.off('error', noop) + llhttpInstance = lazyllhttp() } if (socket.errored) { @@ -788,6 +836,9 @@ async function connectH1 (client, socket) { socket[kWriting] = false socket[kReset] = false socket[kBlocking] = false + socket[kIdleSocketValidation] = 0 + socket[kIdleSocketValidationTimeout] = null + socket[kSocketUsed] = false socket[kParser] = new Parser(client, socket, llhttpInstance) util.addListener(socket, 'error', onHttpSocketError) @@ -830,7 +881,7 @@ async function connectH1 (client, socket) { * @returns {boolean} */ busy (request) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true } @@ -876,8 +927,11 @@ function onHttpSocketError (err) { // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // to the user. if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so for as a valid response. - parser.onMessageComplete() + const parserErr = parser.finish() + if (parserErr) { + this[kError] = parserErr + this[kClient][kOnError](parserErr) + } return } @@ -894,8 +948,10 @@ function onHttpSocketEnd () { const parser = this[kParser] if (parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() + const parserErr = parser.finish() + if (parserErr) { + util.destroy(this, parserErr) + } return } @@ -905,10 +961,11 @@ function onHttpSocketEnd () { function onHttpSocketClose () { const parser = this[kParser] + clearIdleSocketValidation(this) + if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() + this[kError] = parser.finish() || this[kError] } this[kParser].destroy() @@ -952,6 +1009,28 @@ function onSocketClose () { this[kClosed] = true } +function clearIdleSocketValidation (socket) { + if (socket[kIdleSocketValidationTimeout]) { + clearTimeout(socket[kIdleSocketValidationTimeout]) + socket[kIdleSocketValidationTimeout] = null + } + + socket[kIdleSocketValidation] = 0 +} + +function scheduleIdleSocketValidation (client, socket) { + socket[kIdleSocketValidation] = 1 + socket[kIdleSocketValidationTimeout] = setTimeout(() => { + socket[kIdleSocketValidationTimeout] = null + socket[kIdleSocketValidation] = 2 + + if (client[kSocket] === socket && !socket.destroyed) { + client[kResume]() + } + }, 0) + socket[kIdleSocketValidationTimeout].unref?.() +} + /** * @param {import('./client.js')} client */ @@ -969,6 +1048,32 @@ function resumeH1 (client) { socket[kNoRef] = false } + if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) { + if (socket[kIdleSocketValidation] === 0) { + scheduleIdleSocketValidation(client, socket) + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + + if (socket[kIdleSocketValidation] === 1) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + } + + if (client[kRunning] === 0) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + } + if (client[kSize] === 0) { if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE) @@ -1067,6 +1172,7 @@ function writeH1 (client, request) { } const socket = client[kSocket] + clearIdleSocketValidation(socket) /** * @param {Error} [err] @@ -1120,6 +1226,10 @@ function writeH1 (client, request) { socket[kBlocking] = true } + if (socket.setTypeOfService) { + socket.setTypeOfService(request.typeOfService) + } + let header = `${method} ${path} HTTP/1.1\r\n` if (typeof host === 'string') { @@ -1155,7 +1265,6 @@ function writeH1 (client, request) { channels.sendHeaders.publish({ request, headers: header, socket }) } - /* istanbul ignore else: assertion */ if (!body || bodyLength === 0) { writeBuffer(abort, null, client, request, socket, contentLength, header, expectsPayload) } else if (util.isBuffer(body)) { @@ -1297,9 +1406,9 @@ function writeStream (abort, body, client, request, socket, contentLength, heade .on('error', onFinished) if (body.errorEmitted ?? body.errored) { - setImmediate(() => onFinished(body.errored)) + setImmediate(onFinished, body.errored) } else if (body.endEmitted ?? body.readableEnded) { - setImmediate(() => onFinished(null)) + setImmediate(onFinished, null) } if (body.closeEmitted ?? body.closed) { @@ -1536,7 +1645,6 @@ class AsyncWriter { if (!ret) { if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { - // istanbul ignore else: only for jest if (socket[kParser].timeout.refresh) { socket[kParser].timeout.refresh() } @@ -1587,7 +1695,6 @@ class AsyncWriter { } if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { - // istanbul ignore else: only for jest if (socket[kParser].timeout.refresh) { socket[kParser].timeout.refresh() } diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 661d857bee1..0585e7cd925 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -7,7 +7,8 @@ const { RequestContentLengthMismatchError, RequestAbortedError, SocketError, - InformationalError + InformationalError, + InvalidArgumentError } = require('../core/errors.js') const { kUrl, @@ -23,12 +24,19 @@ const { kStrictContentLength, kOnError, kMaxConcurrentStreams, + kPingInterval, kHTTP2Session, + kHTTP2InitialWindowSize, + kHTTP2ConnectionWindowSize, kResume, kSize, kHTTPContext, kClosed, - kBodyTimeout + kBodyTimeout, + kEnableConnectProtocol, + kRemoteSettings, + kHTTP2Stream, + kHTTP2SessionState } = require('../core/symbols.js') const { channels } = require('../core/diagnostics.js') @@ -53,7 +61,10 @@ const { HTTP2_HEADER_SCHEME, HTTP2_HEADER_CONTENT_LENGTH, HTTP2_HEADER_EXPECT, - HTTP2_HEADER_STATUS + HTTP2_HEADER_STATUS, + HTTP2_HEADER_PROTOCOL, + NGHTTP2_REFUSED_STREAM, + NGHTTP2_CANCEL } } = http2 @@ -77,28 +88,51 @@ function parseH2Headers (headers) { return result } -async function connectH2 (client, socket) { +function connectH2 (client, socket) { client[kSocket] = socket + const http2InitialWindowSize = client[kHTTP2InitialWindowSize] + const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize] + const session = http2.connect(client[kUrl], { createConnection: () => socket, peerMaxConcurrentStreams: client[kMaxConcurrentStreams], settings: { // TODO(metcoder95): add support for PUSH - enablePush: false + enablePush: false, + ...(http2InitialWindowSize != null ? { initialWindowSize: http2InitialWindowSize } : null) } }) + client[kSocket] = socket session[kOpenStreams] = 0 session[kClient] = client session[kSocket] = socket - session[kHTTP2Session] = null + session[kHTTP2SessionState] = { + ping: { + interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref() + } + } + // We set it to true by default in a best-effort; however once connected to an H2 server + // we will check if extended CONNECT protocol is supported or not + // and set this value accordingly. + session[kEnableConnectProtocol] = false + // States whether or not we have received the remote settings from the server + session[kRemoteSettings] = false + + // Apply connection-level flow control once connected (if supported). + if (http2ConnectionWindowSize) { + util.addListener(session, 'connect', applyConnectionWindowSize.bind(session, http2ConnectionWindowSize)) + } util.addListener(session, 'error', onHttp2SessionError) util.addListener(session, 'frameError', onHttp2FrameError) util.addListener(session, 'end', onHttp2SessionEnd) util.addListener(session, 'goaway', onHttp2SessionGoAway) util.addListener(session, 'close', onHttp2SessionClose) + util.addListener(session, 'remoteSettings', onHttp2RemoteSettings) + // TODO (@metcoder95): implement SETTINGS support + // util.addListener(session, 'localSettings', onHttp2RemoteSettings) session.unref() @@ -115,12 +149,23 @@ async function connectH2 (client, socket) { return { version: 'h2', defaultPipelining: Infinity, + /** + * @param {import('../core/request.js')} request + * @returns {boolean} + */ write (request) { return writeH2(client, request) }, + /** + * @returns {void} + */ resume () { resumeH2(client) }, + /** + * @param {Error | null} err + * @param {() => void} callback + */ destroy (err, callback) { if (socket[kClosed]) { queueMicrotask(callback) @@ -128,10 +173,43 @@ async function connectH2 (client, socket) { socket.destroy(err).on('close', callback) } }, + /** + * @type {boolean} + */ get destroyed () { return socket.destroyed }, - busy () { + /** + * @param {import('../core/request.js')} request + * @returns {boolean} + */ + busy (request) { + if (request != null) { + if (client[kRunning] > 0) { + // We are already processing requests + + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + if (request.idempotent === false) return true + // Don't dispatch an upgrade until all preceding requests have completed. + // Possibly, we do not have remote settings confirmed yet. + if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + if (util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true + } else { + return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false + } + } + return false } } @@ -151,6 +229,62 @@ function resumeH2 (client) { } } +function applyConnectionWindowSize (connectionWindowSize) { + try { + if (typeof this.setLocalWindowSize === 'function') { + this.setLocalWindowSize(connectionWindowSize) + } + } catch { + // Best-effort only. + } +} + +function onHttp2RemoteSettings (settings) { + // Fallbacks are a safe bet, remote setting will always override + this[kClient][kMaxConcurrentStreams] = settings.maxConcurrentStreams ?? this[kClient][kMaxConcurrentStreams] + /** + * From RFC-8441 + * A sender MUST NOT send a SETTINGS_ENABLE_CONNECT_PROTOCOL parameter + * with the value of 0 after previously sending a value of 1. + */ + // Note: Cannot be tested in Node, it does not supports disabling the extended CONNECT protocol once enabled + if (this[kRemoteSettings] === true && this[kEnableConnectProtocol] === true && settings.enableConnectProtocol === false) { + const err = new InformationalError('HTTP/2: Server disabled extended CONNECT protocol against RFC-8441') + this[kSocket][kError] = err + this[kClient][kOnError](err) + return + } + + this[kEnableConnectProtocol] = settings.enableConnectProtocol ?? this[kEnableConnectProtocol] + this[kRemoteSettings] = true + this[kClient][kResume]() +} + +function onHttp2SendPing (session) { + const state = session[kHTTP2SessionState] + if ((session.closed || session.destroyed) && state.ping.interval != null) { + clearInterval(state.ping.interval) + state.ping.interval = null + return + } + + // If no ping sent, do nothing + session.ping(onPing.bind(session)) + + function onPing (err, duration) { + const client = this[kClient] + const socket = this[kClient] + + if (err != null) { + const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`) + socket[kError] = error + client[kOnError](error) + } else { + client.emit('ping', duration) + } + } +} + function onHttp2SessionError (err) { assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') @@ -214,7 +348,7 @@ function onHttp2SessionGoAway (errorCode) { } function onHttp2SessionClose () { - const { [kClient]: client } = this + const { [kClient]: client, [kHTTP2SessionState]: state } = this const { [kSocket]: socket } = client const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket)) @@ -222,6 +356,11 @@ function onHttp2SessionClose () { client[kSocket] = null client[kHTTPContext] = null + if (state.ping.interval != null) { + clearInterval(state.ping.interval) + state.ping.interval = null + } + if (client.destroyed) { assert(client[kPending] === 0) @@ -279,11 +418,11 @@ function shouldSendContentLength (method) { function writeH2 (client, request) { const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout] const session = client[kHTTP2Session] - const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request let { body } = request - if (upgrade) { - util.errorRequest(client, request, new Error('Upgrade not supported for H2')) + if (upgrade != null && upgrade !== 'websocket') { + util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`)) return false } @@ -292,6 +431,16 @@ function writeH2 (client, request) { const key = reqHeaders[n + 0] const val = reqHeaders[n + 1] + if (key === 'cookie') { + if (headers[key] != null) { + headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val] + } else { + headers[key] = val + } + + continue + } + if (Array.isArray(val)) { for (let i = 0; i < val.length; i++) { if (headers[key]) { @@ -354,26 +503,75 @@ function writeH2 (client, request) { return false } - if (method === 'CONNECT') { + if (upgrade || method === 'CONNECT') { session.ref() - // We are already connected, streams are pending, first request + + if (upgrade === 'websocket') { + // We cannot upgrade to websocket if extended CONNECT protocol is not supported + if (session[kEnableConnectProtocol] === false) { + util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server')) + session.unref() + return false + } + + // We force the method to CONNECT + // as per RFC-8441 + // https://datatracker.ietf.org/doc/html/rfc8441#section-4 + headers[HTTP2_HEADER_METHOD] = 'CONNECT' + headers[HTTP2_HEADER_PROTOCOL] = 'websocket' + // :path and :scheme headers must be omitted when sending CONNECT but set if extended-CONNECT + headers[HTTP2_HEADER_PATH] = path + + if (protocol === 'ws:' || protocol === 'wss:') { + headers[HTTP2_HEADER_SCHEME] = protocol === 'ws:' ? 'http' : 'https' + } else { + headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https' + } + + stream = session.request(headers, { endStream: false, signal }) + stream[kHTTP2Stream] = true + + stream.once('response', (headers, _flags) => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + + request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream) + + ++session[kOpenStreams] + client[kQueue][client[kRunningIdx]++] = null + }) + + stream.on('error', () => { + if (stream.rstCode === NGHTTP2_REFUSED_STREAM || stream.rstCode === NGHTTP2_CANCEL) { + // NGHTTP2_REFUSED_STREAM (7) or NGHTTP2_CANCEL (8) + // We do not treat those as errors as the server might + // not support websockets and refuse the stream + abort(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`)) + } + }) + + stream.once('close', () => { + session[kOpenStreams] -= 1 + if (session[kOpenStreams] === 0) session.unref() + }) + + stream.setTimeout(requestTimeout) + return true + } + + // TODO: consolidate once we support CONNECT properly + // NOTE: We are already connected, streams are pending, first request // will create a new stream. We trigger a request to create the stream and wait until // `ready` event is triggered // We disabled endStream to allow the user to write to the stream stream = session.request(headers, { endStream: false, signal }) + stream[kHTTP2Stream] = true + stream.on('response', headers => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers - if (!stream.pending) { - request.onUpgrade(null, null, stream) + request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream) ++session[kOpenStreams] client[kQueue][client[kRunningIdx]++] = null - } else { - stream.once('ready', () => { - request.onUpgrade(null, null, stream) - ++session[kOpenStreams] - client[kQueue][client[kRunningIdx]++] = null - }) - } - + }) stream.once('close', () => { session[kOpenStreams] -= 1 if (session[kOpenStreams] === 0) session.unref() @@ -385,9 +583,8 @@ function writeH2 (client, request) { // https://tools.ietf.org/html/rfc7540#section-8.3 // :path and :scheme headers must be omitted when sending CONNECT - headers[HTTP2_HEADER_PATH] = path - headers[HTTP2_HEADER_SCHEME] = 'https' + headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https' // https://tools.ietf.org/html/rfc7231#section-4.3.1 // https://tools.ietf.org/html/rfc7231#section-4.3.2 @@ -425,12 +622,12 @@ function writeH2 (client, request) { contentLength = request.contentLength } - if (contentLength === 0 || !expectsPayload) { + if (!expectsPayload) { // https://tools.ietf.org/html/rfc7230#section-3.3.2 // A user agent SHOULD NOT send a Content-Length header field when // the request message does not contain a payload body and the method // semantics do not anticipate such a body. - + // And for methods that don't expect a payload, omit Content-Length. contentLength = null } @@ -446,7 +643,7 @@ function writeH2 (client, request) { } if (contentLength != null) { - assert(body, 'no body must not have content length') + assert(body || contentLength === 0, 'no body must not have content length') headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` } @@ -465,6 +662,7 @@ function writeH2 (client, request) { if (expectContinue) { headers[HTTP2_HEADER_EXPECT] = '100-continue' stream = session.request(headers, { endStream: shouldEndStream, signal }) + stream[kHTTP2Stream] = true stream.once('continue', writeBodyH2) } else { @@ -472,6 +670,7 @@ function writeH2 (client, request) { endStream: shouldEndStream, signal }) + stream[kHTTP2Stream] = true writeBodyH2() } @@ -480,9 +679,13 @@ function writeH2 (client, request) { ++session[kOpenStreams] stream.setTimeout(requestTimeout) + // Track whether we received a response (headers) + let responseReceived = false + stream.once('response', headers => { const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers request.onResponseStarted() + responseReceived = true // Due to the stream nature, it is possible we face a race condition // where the stream has been assigned, but the request has been aborted @@ -497,22 +700,22 @@ function writeH2 (client, request) { if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) { stream.pause() } - }) - stream.on('data', (chunk) => { - if (request.onData(chunk) === false) { - stream.pause() - } + stream.on('data', (chunk) => { + if (request.aborted || request.completed) { + return + } + + if (request.onData(chunk) === false) { + stream.pause() + } + }) }) - stream.once('end', (err) => { + stream.once('end', () => { stream.removeAllListeners('data') - // When state is null, it means we haven't consumed body and the stream still do not have - // a state. - // Present specially when using pipeline or stream - if (stream.state?.state == null || stream.state.state < 6) { - // Do not complete the request if it was aborted - // Not prone to happen for as safety net to avoid race conditions with 'trailers' + // If we received a response, this is a normal completion + if (responseReceived) { if (!request.aborted && !request.completed) { request.onComplete({}) } @@ -520,15 +723,9 @@ function writeH2 (client, request) { client[kQueue][client[kRunningIdx]++] = null client[kResume]() } else { - // Stream is closed or half-closed-remote (6), decrement counter and cleanup - // It does not have sense to continue working with the stream as we do not - // have yet RST_STREAM support on client-side - --session[kOpenStreams] - if (session[kOpenStreams] === 0) { - session.unref() - } - - abort(err ?? new InformationalError('HTTP/2: stream half-closed (remote)')) + // Stream ended without receiving a response - this is an error + // (e.g., server destroyed the stream before sending headers) + abort(new InformationalError('HTTP/2: stream half-closed (remote)')) client[kQueue][client[kRunningIdx]++] = null client[kPendingIdx] = client[kRunningIdx] client[kResume]() @@ -574,13 +771,13 @@ function writeH2 (client, request) { return } + stream.removeAllListeners('data') request.onComplete(trailers) }) return true function writeBodyH2 () { - /* istanbul ignore else: assertion */ if (!body || contentLength === 0) { writeBuffer( abort, diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 0b0990206e7..6ef97ba0a36 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -52,7 +52,10 @@ const { kOnError, kHTTPContext, kMaxConcurrentStreams, - kResume + kHTTP2InitialWindowSize, + kHTTP2ConnectionWindowSize, + kResume, + kPingInterval } = require('../core/symbols.js') const connectH1 = require('./client-h1.js') const connectH2 = require('./client-h2.js') @@ -66,7 +69,7 @@ const getDefaultNodeMaxHeaderSize = http && ? () => http.maxHeaderSize : () => { throw new InvalidArgumentError('http module not available or http.maxHeaderSize invalid') } -const noop = () => {} +const noop = () => { } function getPipelining (client) { return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1 @@ -107,7 +110,12 @@ class Client extends DispatcherBase { autoSelectFamilyAttemptTimeout, // h2 maxConcurrentStreams, - allowH2 + allowH2, + useH2c, + initialWindowSize, + connectionWindowSize, + pingInterval, + webSocket } = {}) { if (keepAlive !== undefined) { throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') @@ -199,18 +207,42 @@ class Client extends DispatcherBase { throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0') } - super() + if (useH2c != null && typeof useH2c !== 'boolean') { + throw new InvalidArgumentError('useH2c must be a valid boolean value') + } + + if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) { + throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0') + } + + if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) { + throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0') + } + + if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) { + throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') + } + + super({ webSocket }) if (typeof connect !== 'function') { connect = buildConnector({ ...tls, maxCachedSessions, allowH2, + useH2c, socketPath, timeout: connectTimeout, ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect }) + } else { + const customConnect = connect + connect = (opts, callback) => customConnect({ + ...opts, + ...(socketPath != null ? { socketPath } : null), + ...(allowH2 != null ? { allowH2 } : null) + }, callback) } this[kUrl] = util.parseOrigin(url) @@ -232,8 +264,18 @@ class Client extends DispatcherBase { this[kMaxRequests] = maxRequestsPerClient this[kClosedResolve] = null this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 - this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server this[kHTTPContext] = null + // h2 + this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server + // HTTP/2 window sizes are set to higher defaults than Node.js core for better performance: + // - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1) + // Allows more data to be sent before requiring acknowledgment, improving throughput + // especially on high-latency networks. This matches common production HTTP/2 servers. + // - connectionWindowSize: 524288 (512KB) vs Node.js default (none set) + // Provides better flow control for the entire connection across multiple streams. + this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144 + this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288 + this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute // kQueue is built up of 3 sections separated by // the kRunningIdx and kPendingIdx indices. @@ -289,15 +331,13 @@ class Client extends DispatcherBase { ) } - /* istanbul ignore: only used for test */ [kConnect] (cb) { connect(this) this.once('connect', cb) } [kDispatch] (opts, handler) { - const origin = opts.origin || this[kUrl].origin - const request = new Request(origin, opts, handler) + const request = new Request(this[kUrl].origin, opts, handler) this[kQueue].push(request) if (this[kResuming]) { @@ -317,7 +357,7 @@ class Client extends DispatcherBase { return this[kNeedDrain] < 2 } - async [kClose] () { + [kClose] () { // TODO: for H2 we need to gracefully flush the remaining enqueued // request and close each stream. return new Promise((resolve) => { @@ -329,7 +369,7 @@ class Client extends DispatcherBase { }) } - async [kDestroy] (err) { + [kDestroy] (err) { return new Promise((resolve) => { const requests = this[kQueue].splice(this[kPendingIdx]) for (let i = 0; i < requests.length; i++) { @@ -381,9 +421,9 @@ function onError (client, err) { /** * @param {Client} client - * @returns + * @returns {void} */ -async function connect (client) { +function connect (client) { assert(!client[kConnecting]) assert(!client[kHTTPContext]) @@ -418,99 +458,105 @@ async function connect (client) { } try { - const socket = await new Promise((resolve, reject) => { - client[kConnector]({ + client[kConnector]({ + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, (err, socket) => { + if (err) { + handleConnectError(client, err, { host, hostname, protocol, port }) + client[kResume]() + return + } + + if (client.destroyed) { + util.destroy(socket.on('error', noop), new ClientDestroyedError()) + client[kResume]() + return + } + + assert(socket) + + try { + client[kHTTPContext] = socket.alpnProtocol === 'h2' + ? connectH2(client, socket) + : connectH1(client, socket) + } catch (err) { + socket.destroy().on('error', noop) + handleConnectError(client, err, { host, hostname, protocol, port }) + client[kResume]() + return + } + + client[kConnecting] = false + + socket[kCounter] = 0 + socket[kMaxRequests] = client[kMaxRequests] + socket[kClient] = client + socket[kError] = null + + if (channels.connected.hasSubscribers) { + channels.connected.publish({ + connectParams: { + host, + hostname, + protocol, + port, + version: client[kHTTPContext]?.version, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + socket + }) + } + + client.emit('connect', client[kUrl], [client]) + client[kResume]() + }) + } catch (err) { + handleConnectError(client, err, { host, hostname, protocol, port }) + client[kResume]() + } +} + +function handleConnectError (client, err, { host, hostname, protocol, port }) { + if (client.destroyed) { + return + } + + client[kConnecting] = false + + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ + connectParams: { host, hostname, protocol, port, + version: client[kHTTPContext]?.version, servername: client[kServerName], localAddress: client[kLocalAddress] - }, (err, socket) => { - if (err) { - reject(err) - } else { - resolve(socket) - } - }) + }, + connector: client[kConnector], + error: err }) + } - if (client.destroyed) { - util.destroy(socket.on('error', noop), new ClientDestroyedError()) - return - } - - assert(socket) - - try { - client[kHTTPContext] = socket.alpnProtocol === 'h2' - ? await connectH2(client, socket) - : await connectH1(client, socket) - } catch (err) { - socket.destroy().on('error', noop) - throw err - } - - client[kConnecting] = false - - socket[kCounter] = 0 - socket[kMaxRequests] = client[kMaxRequests] - socket[kClient] = client - socket[kError] = null - - if (channels.connected.hasSubscribers) { - channels.connected.publish({ - connectParams: { - host, - hostname, - protocol, - port, - version: client[kHTTPContext]?.version, - servername: client[kServerName], - localAddress: client[kLocalAddress] - }, - connector: client[kConnector], - socket - }) - } - client.emit('connect', client[kUrl], [client]) - } catch (err) { - if (client.destroyed) { - return - } - - client[kConnecting] = false - - if (channels.connectError.hasSubscribers) { - channels.connectError.publish({ - connectParams: { - host, - hostname, - protocol, - port, - version: client[kHTTPContext]?.version, - servername: client[kServerName], - localAddress: client[kLocalAddress] - }, - connector: client[kConnector], - error: err - }) - } - - if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { - assert(client[kRunning] === 0) - while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { - const request = client[kQueue][client[kPendingIdx]++] - util.errorRequest(client, request, err) - } - } else { - onError(client, err) + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + assert(client[kRunning] === 0) + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { + const request = client[kQueue][client[kPendingIdx]++] + util.errorRequest(client, request, err) } - - client.emit('connectionError', client[kUrl], [client], err) + } else { + onError(client, err) } - client[kResume]() + client.emit('connectionError', client[kUrl], [client], err) } function emitDrain (client) { @@ -574,6 +620,10 @@ function _resume (client, sync) { const request = client[kQueue][client[kPendingIdx]] + if (request === null) { + return + } + if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) { if (client[kRunning] > 0) { return diff --git a/lib/dispatcher/dispatcher-base.js b/lib/dispatcher/dispatcher-base.js index 615754d0fb5..feb2571abc2 100644 --- a/lib/dispatcher/dispatcher-base.js +++ b/lib/dispatcher/dispatcher-base.js @@ -11,21 +11,45 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy const kOnDestroyed = Symbol('onDestroyed') const kOnClosed = Symbol('onClosed') +const kWebSocketOptions = Symbol('webSocketOptions') class DispatcherBase extends Dispatcher { - constructor () { + /** @type {boolean} */ + [kDestroyed] = false; + + /** @type {Array|null} */ + [kOnClosed] = null + + /** + * @param {import('../../types/dispatcher').DispatcherOptions} [opts] + */ + constructor (opts) { super() + this[kWebSocketOptions] = opts?.webSocket ?? {} + } - this[kDestroyed] = false - this[kOnDestroyed] = null - this[kClosed] = false - this[kOnClosed] = [] + /** + * @returns {import('../../types/dispatcher').WebSocketOptions} + */ + get webSocketOptions () { + return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default + } } + /** @returns {boolean} */ get destroyed () { return this[kDestroyed] } + /** @returns {boolean} */ get closed () { return this[kClosed] } @@ -44,7 +68,8 @@ class DispatcherBase extends Dispatcher { } if (this[kDestroyed]) { - queueMicrotask(() => callback(new ClientDestroyedError(), null)) + const err = new ClientDestroyedError() + queueMicrotask(() => callback(err, null)) return } @@ -58,6 +83,7 @@ class DispatcherBase extends Dispatcher { } this[kClosed] = true + this[kOnClosed] ??= [] this[kOnClosed].push(callback) const onClosed = () => { @@ -71,9 +97,7 @@ class DispatcherBase extends Dispatcher { // Should not error. this[kClose]() .then(() => this.destroy()) - .then(() => { - queueMicrotask(onClosed) - }) + .then(() => queueMicrotask(onClosed)) } destroy (err, callback) { @@ -85,7 +109,7 @@ class DispatcherBase extends Dispatcher { if (callback === undefined) { return new Promise((resolve, reject) => { this.destroy(err, (err, data) => { - return err ? /* istanbul ignore next: should never error */ reject(err) : resolve(data) + return err ? reject(err) : resolve(data) }) }) } @@ -108,7 +132,7 @@ class DispatcherBase extends Dispatcher { } this[kDestroyed] = true - this[kOnDestroyed] = this[kOnDestroyed] || [] + this[kOnDestroyed] ??= [] this[kOnDestroyed].push(callback) const onDestroyed = () => { @@ -120,9 +144,8 @@ class DispatcherBase extends Dispatcher { } // Should not error. - this[kDestroy](err).then(() => { - queueMicrotask(onDestroyed) - }) + this[kDestroy](err) + .then(() => queueMicrotask(onDestroyed)) } dispatch (opts, handler) { diff --git a/lib/dispatcher/env-http-proxy-agent.js b/lib/dispatcher/env-http-proxy-agent.js index 48cc3f88e7f..f88437f1936 100644 --- a/lib/dispatcher/env-http-proxy-agent.js +++ b/lib/dispatcher/env-http-proxy-agent.js @@ -46,24 +46,20 @@ class EnvHttpProxyAgent extends DispatcherBase { return agent.dispatch(opts, handler) } - async [kClose] () { - await this[kNoProxyAgent].close() - if (!this[kHttpProxyAgent][kClosed]) { - await this[kHttpProxyAgent].close() - } - if (!this[kHttpsProxyAgent][kClosed]) { - await this[kHttpsProxyAgent].close() - } + [kClose] () { + return Promise.all([ + this[kNoProxyAgent].close(), + !this[kHttpProxyAgent][kClosed] && this[kHttpProxyAgent].close(), + !this[kHttpsProxyAgent][kClosed] && this[kHttpsProxyAgent].close() + ]) } - async [kDestroy] (err) { - await this[kNoProxyAgent].destroy(err) - if (!this[kHttpProxyAgent][kDestroyed]) { - await this[kHttpProxyAgent].destroy(err) - } - if (!this[kHttpsProxyAgent][kDestroyed]) { - await this[kHttpsProxyAgent].destroy(err) - } + [kDestroy] (err) { + return Promise.all([ + this[kNoProxyAgent].destroy(err), + !this[kHttpProxyAgent][kDestroyed] && this[kHttpProxyAgent].destroy(err), + !this[kHttpsProxyAgent][kDestroyed] && this[kHttpsProxyAgent].destroy(err) + ]) } #getProxyAgentForUrl (url) { @@ -99,16 +95,14 @@ class EnvHttpProxyAgent extends DispatcherBase { if (entry.port && entry.port !== port) { continue // Skip if ports don't match. } - if (!/^[.*]/.test(entry.hostname)) { - // No wildcards, so don't proxy only if there is not an exact match. - if (hostname === entry.hostname) { - return false - } - } else { - // Don't proxy if the hostname ends with the no_proxy host. - if (hostname.endsWith(entry.hostname.replace(/^\*/, ''))) { - return false - } + // Don't proxy if the hostname is equal with the no_proxy host. + if (hostname === entry.hostname) { + return false + } + // Don't proxy if the hostname is the subdomain of the no_proxy host. + // Reference - https://github.com/denoland/deno/blob/6fbce91e40cc07fc6da74068e5cc56fdd40f7b4c/ext/fetch/proxy.rs#L485 + if (hostname.slice(-(entry.hostname.length + 1)) === `.${entry.hostname}`) { + return false } } @@ -127,7 +121,8 @@ class EnvHttpProxyAgent extends DispatcherBase { } const parsed = entry.match(/^(.+):(\d+)$/) noProxyEntries.push({ - hostname: (parsed ? parsed[1] : entry).toLowerCase(), + // strip leading dot or asterisk with dot + hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, '').toLowerCase(), port: parsed ? Number.parseInt(parsed[2], 10) : 0 }) } diff --git a/lib/dispatcher/fixed-queue.js b/lib/dispatcher/fixed-queue.js index 5f7a08bc47f..f918849c44c 100644 --- a/lib/dispatcher/fixed-queue.js +++ b/lib/dispatcher/fixed-queue.js @@ -59,35 +59,21 @@ const kMask = kSize - 1 * @template T */ class FixedCircularBuffer { - constructor () { - /** - * @type {number} - */ - this.bottom = 0 - /** - * @type {number} - */ - this.top = 0 - /** - * @type {Array} - */ - this.list = new Array(kSize).fill(undefined) - /** - * @type {T|null} - */ - this.next = null - } + /** @type {number} */ + bottom = 0 + /** @type {number} */ + top = 0 + /** @type {Array} */ + list = new Array(kSize).fill(undefined) + /** @type {T|null} */ + next = null - /** - * @returns {boolean} - */ + /** @returns {boolean} */ isEmpty () { return this.top === this.bottom } - /** - * @returns {boolean} - */ + /** @returns {boolean} */ isFull () { return ((this.top + 1) & kMask) === this.bottom } @@ -101,9 +87,7 @@ class FixedCircularBuffer { this.top = (this.top + 1) & kMask } - /** - * @returns {T|null} - */ + /** @returns {T|null} */ shift () { const nextItem = this.list[this.bottom] if (nextItem === undefined) { return null } @@ -118,22 +102,16 @@ class FixedCircularBuffer { */ module.exports = class FixedQueue { constructor () { - /** - * @type {FixedCircularBuffer} - */ + /** @type {FixedCircularBuffer} */ this.head = this.tail = new FixedCircularBuffer() } - /** - * @returns {boolean} - */ + /** @returns {boolean} */ isEmpty () { return this.head.isEmpty() } - /** - * @param {T} data - */ + /** @param {T} data */ push (data) { if (this.head.isFull()) { // Head is full: Creates a new queue, sets the old queue's `.next` to it, @@ -143,9 +121,7 @@ module.exports = class FixedQueue { this.head.push(data) } - /** - * @returns {T|null} - */ + /** @returns {T|null} */ shift () { const tail = this.tail const next = tail.shift() diff --git a/lib/dispatcher/h2c-client.js b/lib/dispatcher/h2c-client.js index 3a876fd4553..5fe7e778ba4 100644 --- a/lib/dispatcher/h2c-client.js +++ b/lib/dispatcher/h2c-client.js @@ -1,19 +1,10 @@ 'use strict' -const { connect } = require('node:net') -const { kClose, kDestroy } = require('../core/symbols') const { InvalidArgumentError } = require('../core/errors') -const util = require('../core/util') - const Client = require('./client') -const DispatcherBase = require('./dispatcher-base') - -class H2CClient extends DispatcherBase { - #client = null +class H2CClient extends Client { constructor (origin, clientOpts) { - super() - if (typeof origin === 'string') { origin = new URL(origin) } @@ -24,15 +15,15 @@ class H2CClient extends DispatcherBase { ) } - const { connect, maxConcurrentStreams, pipelining, ...opts } = - clientOpts ?? {} + const { maxConcurrentStreams, pipelining, ...opts } = + clientOpts ?? {} let defaultMaxConcurrentStreams = 100 let defaultPipelining = 100 if ( maxConcurrentStreams != null && - Number.isInteger(maxConcurrentStreams) && - maxConcurrentStreams > 0 + Number.isInteger(maxConcurrentStreams) && + maxConcurrentStreams > 0 ) { defaultMaxConcurrentStreams = maxConcurrentStreams } @@ -47,76 +38,14 @@ class H2CClient extends DispatcherBase { ) } - this.#client = new Client(origin, { + super(origin, { ...opts, - connect: this.#buildConnector(connect), maxConcurrentStreams: defaultMaxConcurrentStreams, pipelining: defaultPipelining, - allowH2: true + allowH2: true, + useH2c: true }) } - - #buildConnector (connectOpts) { - return (opts, callback) => { - const timeout = connectOpts?.connectOpts ?? 10e3 - const { hostname, port, pathname } = opts - const socket = connect({ - ...opts, - host: hostname, - port, - pathname - }) - - // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket - if (opts.keepAlive == null || opts.keepAlive) { - const keepAliveInitialDelay = - opts.keepAliveInitialDelay == null ? 60e3 : opts.keepAliveInitialDelay - socket.setKeepAlive(true, keepAliveInitialDelay) - } - - socket.alpnProtocol = 'h2' - - const clearConnectTimeout = util.setupConnectTimeout( - new WeakRef(socket), - { timeout, hostname, port } - ) - - socket - .setNoDelay(true) - .once('connect', function () { - queueMicrotask(clearConnectTimeout) - - if (callback) { - const cb = callback - callback = null - cb(null, this) - } - }) - .on('error', function (err) { - queueMicrotask(clearConnectTimeout) - - if (callback) { - const cb = callback - callback = null - cb(err) - } - }) - - return socket - } - } - - dispatch (opts, handler) { - return this.#client.dispatch(opts, handler) - } - - async [kClose] () { - await this.#client.close() - } - - async [kDestroy] () { - await this.#client.destroy() - } } module.exports = H2CClient diff --git a/lib/dispatcher/pool-base.js b/lib/dispatcher/pool-base.js index 4b7b6a26f1d..6c1f2388766 100644 --- a/lib/dispatcher/pool-base.js +++ b/lib/dispatcher/pool-base.js @@ -18,54 +18,58 @@ const kAddClient = Symbol('add client') const kRemoveClient = Symbol('remove client') class PoolBase extends DispatcherBase { - constructor () { - super() + [kQueue] = new FixedQueue(); - this[kQueue] = new FixedQueue() - this[kClients] = [] - this[kQueued] = 0 + [kQueued] = 0; - const pool = this + [kClients] = []; - this[kOnDrain] = function onDrain (origin, targets) { - const queue = pool[kQueue] + [kNeedDrain] = false; - let needDrain = false + [kOnDrain] (client, origin, targets) { + const queue = this[kQueue] - while (!needDrain) { - const item = queue.shift() - if (!item) { - break - } - pool[kQueued]-- - needDrain = !this.dispatch(item.opts, item.handler) + let needDrain = false + + while (!needDrain) { + const item = queue.shift() + if (!item) { + break } + this[kQueued]-- + needDrain = !client.dispatch(item.opts, item.handler) + } - this[kNeedDrain] = needDrain + client[kNeedDrain] = needDrain - if (!this[kNeedDrain] && pool[kNeedDrain]) { - pool[kNeedDrain] = false - pool.emit('drain', origin, [pool, ...targets]) - } + if (!needDrain && this[kNeedDrain]) { + this[kNeedDrain] = false + this.emit('drain', origin, [this, ...targets]) + } - if (pool[kClosedResolve] && queue.isEmpty()) { - Promise - .all(pool[kClients].map(c => c.close())) - .then(pool[kClosedResolve]) + if (this[kClosedResolve] && queue.isEmpty()) { + const closeAll = [] + for (let i = 0; i < this[kClients].length; i++) { + const client = this[kClients][i] + if (!client.destroyed) { + closeAll.push(client.close()) + } } + return Promise.all(closeAll) + .then(this[kClosedResolve]) } + } - this[kOnConnect] = (origin, targets) => { - pool.emit('connect', origin, [pool, ...targets]) - } + [kOnConnect] = (origin, targets) => { + this.emit('connect', origin, [this, ...targets]) + }; - this[kOnDisconnect] = (origin, targets, err) => { - pool.emit('disconnect', origin, [pool, ...targets], err) - } + [kOnDisconnect] = (origin, targets, err) => { + this.emit('disconnect', origin, [this, ...targets], err) + }; - this[kOnConnectionError] = (origin, targets, err) => { - pool.emit('connectionError', origin, [pool, ...targets], err) - } + [kOnConnectionError] = (origin, targets, err) => { + this.emit('connectionError', origin, [this, ...targets], err) } get [kBusy] () { @@ -73,11 +77,19 @@ class PoolBase extends DispatcherBase { } get [kConnected] () { - return this[kClients].filter(client => client[kConnected]).length + let ret = 0 + for (const { [kConnected]: connected } of this[kClients]) { + ret += connected + } + return ret } get [kFree] () { - return this[kClients].filter(client => client[kConnected] && !client[kNeedDrain]).length + let ret = 0 + for (const { [kConnected]: connected, [kNeedDrain]: needDrain } of this[kClients]) { + ret += connected && !needDrain + } + return ret } get [kPending] () { @@ -108,17 +120,24 @@ class PoolBase extends DispatcherBase { return new PoolStats(this) } - async [kClose] () { + [kClose] () { if (this[kQueue].isEmpty()) { - await Promise.all(this[kClients].map(c => c.close())) + const closeAll = [] + for (let i = 0; i < this[kClients].length; i++) { + const client = this[kClients][i] + if (!client.destroyed) { + closeAll.push(client.close()) + } + } + return Promise.all(closeAll) } else { - await new Promise((resolve) => { + return new Promise((resolve) => { this[kClosedResolve] = resolve }) } } - async [kDestroy] (err) { + [kDestroy] (err) { while (true) { const item = this[kQueue].shift() if (!item) { @@ -127,7 +146,11 @@ class PoolBase extends DispatcherBase { item.handler.onError(err) } - await Promise.all(this[kClients].map(c => c.destroy(err))) + const destroyAll = new Array(this[kClients].length) + for (let i = 0; i < this[kClients].length; i++) { + destroyAll[i] = this[kClients][i].destroy(err) + } + return Promise.all(destroyAll) } [kDispatch] (opts, handler) { @@ -147,7 +170,7 @@ class PoolBase extends DispatcherBase { [kAddClient] (client) { client - .on('drain', this[kOnDrain]) + .on('drain', this[kOnDrain].bind(this, client)) .on('connect', this[kOnConnect]) .on('disconnect', this[kOnDisconnect]) .on('connectionError', this[kOnConnectionError]) @@ -157,7 +180,7 @@ class PoolBase extends DispatcherBase { if (this[kNeedDrain]) { queueMicrotask(() => { if (this[kNeedDrain]) { - this[kOnDrain](client[kUrl], [this, client]) + this[kOnDrain](client, client[kUrl], [client, this]) } }) } diff --git a/lib/dispatcher/pool.js b/lib/dispatcher/pool.js index 00cf50c3012..0c5dbe44da7 100644 --- a/lib/dispatcher/pool.js +++ b/lib/dispatcher/pool.js @@ -51,8 +51,6 @@ class Pool extends PoolBase { throw new InvalidArgumentError('connect must be a function or an object') } - super() - if (typeof connect !== 'function') { connect = buildConnector({ ...tls, @@ -65,9 +63,11 @@ class Pool extends PoolBase { }) } + super(options) + this[kConnections] = connections || null this[kUrl] = util.parseOrigin(origin) - this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl } + this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath } this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : undefined diff --git a/lib/dispatcher/proxy-agent.js b/lib/dispatcher/proxy-agent.js index cd9bc45ffad..d2a2f23cbff 100644 --- a/lib/dispatcher/proxy-agent.js +++ b/lib/dispatcher/proxy-agent.js @@ -1,13 +1,14 @@ 'use strict' -const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols') -const { URL } = require('node:url') +const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols') const Agent = require('./agent') const Pool = require('./pool') const DispatcherBase = require('./dispatcher-base') const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors') const buildConnector = require('../core/connect') const Client = require('./client') +const { channels } = require('../core/diagnostics') +const Socks5ProxyAgent = require('./socks5-proxy-agent') const kAgent = Symbol('proxy agent') const kClient = Symbol('proxy client') @@ -27,61 +28,70 @@ function defaultFactory (origin, opts) { const noop = () => {} -class ProxyClient extends DispatcherBase { - #client = null - constructor (origin, opts) { - if (typeof origin === 'string') { - origin = new URL(origin) - } +function defaultAgentFactory (origin, opts) { + if (opts.connections === 1) { + return new Client(origin, opts) + } + return new Pool(origin, opts) +} - if (origin.protocol !== 'http:' && origin.protocol !== 'https:') { - throw new InvalidArgumentError('ProxyClient only supports http and https protocols') +class Http1ProxyWrapper extends DispatcherBase { + #client + + constructor (proxyUrl, { headers = {}, connect, factory }) { + if (!proxyUrl) { + throw new InvalidArgumentError('Proxy URL is mandatory') } super() - this.#client = new Client(origin, opts) - } - - async [kClose] () { - await this.#client.close() - } - - async [kDestroy] () { - await this.#client.destroy() + this[kProxyHeaders] = headers + if (factory) { + this.#client = factory(proxyUrl, { connect }) + } else { + this.#client = new Client(proxyUrl, { connect }) + } } - async [kDispatch] (opts, handler) { - const { method, origin } = opts - if (method === 'CONNECT') { - this.#client[kConnector]({ - origin, - port: opts.port || defaultProtocolPort(opts.protocol), - path: opts.host, - signal: opts.signal, - headers: { - ...this[kProxyHeaders], - host: opts.host - }, - servername: this[kProxyTls]?.servername || opts.servername - }, - (err, socket) => { - if (err) { - handler.callback(err) - } else { - handler.callback(null, { socket, statusCode: 200 }) + [kDispatch] (opts, handler) { + const onHeaders = handler.onHeaders + handler.onHeaders = function (statusCode, data, resume) { + if (statusCode === 407) { + if (typeof handler.onError === 'function') { + handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)')) } + return } - ) - return + if (onHeaders) onHeaders.call(this, statusCode, data, resume) } - if (typeof origin === 'string') { - opts.origin = new URL(origin) + + // Rewrite request as an HTTP1 Proxy request, without tunneling. + const { + origin, + path = '/', + headers = {} + } = opts + + opts.path = origin + path + + if (!('host' in headers) && !('Host' in headers)) { + const { host } = new URL(origin) + headers.host = host } + opts.headers = { ...this[kProxyHeaders], ...headers } + + return this.#client[kDispatch](opts, handler) + } + + [kClose] () { + return this.#client.close() + } - return this.#client.dispatch(opts, handler) + [kDestroy] (err) { + return this.#client.destroy(err) } } + class ProxyAgent extends DispatcherBase { constructor (opts) { if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) { @@ -104,6 +114,7 @@ class ProxyAgent extends DispatcherBase { this[kRequestTls] = opts.requestTls this[kProxyTls] = opts.proxyTls this[kProxyHeaders] = opts.headers || {} + this[kTunnelProxy] = proxyTunnel if (opts.auth && opts.token) { throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') @@ -116,28 +127,61 @@ class ProxyAgent extends DispatcherBase { this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}` } - const factory = (!proxyTunnel && protocol === 'http:') - ? (origin, options) => { - if (origin.protocol === 'http:') { - return new ProxyClient(origin, options) - } - return new Client(origin, options) - } - : undefined - const connect = buildConnector({ ...opts.proxyTls }) this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) - this[kClient] = clientFactory(url, { connect, factory }) - this[kTunnelProxy] = proxyTunnel + + const agentFactory = opts.factory || defaultAgentFactory + const factory = (origin, options) => { + const { protocol } = new URL(origin) + + // Handle SOCKS5 proxy + if (this[kProxy].protocol === 'socks5:' || this[kProxy].protocol === 'socks:') { + return new Socks5ProxyAgent(this[kProxy].uri, { + headers: this[kProxyHeaders], + connect, + factory: agentFactory, + username: opts.username || username, + password: opts.password || password, + proxyTls: opts.proxyTls, + requestTls: opts.requestTls + }) + } + + if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') { + return new Http1ProxyWrapper(this[kProxy].uri, { + headers: this[kProxyHeaders], + connect, + factory: agentFactory + }) + } + return agentFactory(origin, options) + } + + // For SOCKS5 proxies, we don't need a client to the proxy itself + // The SOCKS5 connection is handled within Socks5ProxyAgent + if (protocol === 'socks5:' || protocol === 'socks:') { + this[kClient] = null + } else { + this[kClient] = clientFactory(url, { connect }) + } + this[kAgent] = new Agent({ ...opts, + factory, connect: async (opts, callback) => { + // SOCKS5 proxies handle their own connections via Socks5ProxyAgent, + // so this connect function should never be called for them. + if (!this[kClient]) { + callback(new InvalidArgumentError('Cannot establish tunnel connection without a proxy client')) + return + } + let requestedPath = opts.host if (!opts.port) { requestedPath += `:${defaultProtocolPort(opts.protocol)}` } try { - const { socket, statusCode } = await this[kClient].connect({ + const connectParams = { origin, port, path: requestedPath, @@ -148,11 +192,21 @@ class ProxyAgent extends DispatcherBase { ...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {}) }, servername: this[kProxyTls]?.servername || proxyHostname - }) + } + const { socket, statusCode } = await this[kClient].connect(connectParams) if (statusCode !== 200) { socket.on('error', noop).destroy() callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) + return + } + + if (channels.proxyConnected.hasSubscribers) { + channels.proxyConnected.publish({ + socket, + connectParams + }) } + if (opts.protocol !== 'https:') { callback(null, socket) return @@ -185,10 +239,6 @@ class ProxyAgent extends DispatcherBase { headers.host = host } - if (!this.#shouldConnect(new URL(opts.origin))) { - opts.path = opts.origin + opts.path - } - return this[kAgent].dispatch( { ...opts, @@ -199,7 +249,7 @@ class ProxyAgent extends DispatcherBase { } /** - * @param {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts + * @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts * @returns {URL} */ #getUrl (opts) { @@ -212,27 +262,20 @@ class ProxyAgent extends DispatcherBase { } } - async [kClose] () { - await this[kAgent].close() - await this[kClient].close() - } - - async [kDestroy] () { - await this[kAgent].destroy() - await this[kClient].destroy() + [kClose] () { + const promises = [this[kAgent].close()] + if (this[kClient]) { + promises.push(this[kClient].close()) + } + return Promise.all(promises) } - #shouldConnect (uri) { - if (typeof uri === 'string') { - uri = new URL(uri) - } - if (this[kTunnelProxy]) { - return true - } - if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') { - return true + [kDestroy] () { + const promises = [this[kAgent].destroy()] + if (this[kClient]) { + promises.push(this[kClient].destroy()) } - return false + return Promise.all(promises) } } diff --git a/lib/dispatcher/round-robin-pool.js b/lib/dispatcher/round-robin-pool.js new file mode 100644 index 00000000000..dd065351050 --- /dev/null +++ b/lib/dispatcher/round-robin-pool.js @@ -0,0 +1,137 @@ +'use strict' + +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher, + kRemoveClient +} = require('./pool-base') +const Client = require('./client') +const { + InvalidArgumentError +} = require('../core/errors') +const util = require('../core/util') +const { kUrl } = require('../core/symbols') +const buildConnector = require('../core/connect') + +const kOptions = Symbol('options') +const kConnections = Symbol('connections') +const kFactory = Symbol('factory') +const kIndex = Symbol('index') + +function defaultFactory (origin, opts) { + return new Client(origin, opts) +} + +class RoundRobinPool extends PoolBase { + constructor (origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + allowH2, + clientTtl, + ...options + } = {}) { + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError('invalid connections') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + super() + + this[kConnections] = connections || null + this[kUrl] = util.parseOrigin(origin) + this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl, socketPath } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kFactory] = factory + this[kIndex] = -1 + + this.on('connect', (origin, targets) => { + if (clientTtl != null && clientTtl > 0) { + for (const target of targets) { + Object.assign(target, { ttl: Date.now() }) + } + } + }) + + this.on('connectionError', (origin, targets, error) => { + for (const target of targets) { + const idx = this[kClients].indexOf(target) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + } + }) + } + + [kGetDispatcher] () { + const clientTtlOption = this[kOptions].clientTtl + const clientsLength = this[kClients].length + + // If we have no clients yet, create one + if (clientsLength === 0) { + const dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + return dispatcher + } + + // Round-robin through existing clients + let checked = 0 + while (checked < clientsLength) { + this[kIndex] = (this[kIndex] + 1) % clientsLength + const client = this[kClients][this[kIndex]] + + // Check if client is stale (TTL expired) + if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) { + this[kRemoveClient](client) + checked++ + continue + } + + // Return client if it's not draining + if (!client[kNeedDrain]) { + return client + } + + checked++ + } + + // All clients are busy, create a new one if we haven't reached the limit + if (!this[kConnections] || clientsLength < this[kConnections]) { + const dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + return dispatcher + } + } +} + +module.exports = RoundRobinPool diff --git a/lib/dispatcher/socks5-proxy-agent.js b/lib/dispatcher/socks5-proxy-agent.js new file mode 100644 index 00000000000..544d3025ea9 --- /dev/null +++ b/lib/dispatcher/socks5-proxy-agent.js @@ -0,0 +1,260 @@ +'use strict' + +const { URL } = require('node:url') + +let tls // include tls conditionally since it is not always available +const DispatcherBase = require('./dispatcher-base') +const { InvalidArgumentError } = require('../core/errors') +const { Socks5Client, STATES } = require('../core/socks5-client') +const { kDispatch, kClose, kDestroy } = require('../core/symbols') +const Pool = require('./pool') +const buildConnector = require('../core/connect') +const { debuglog } = require('node:util') + +const debug = debuglog('undici:socks5-proxy') + +const kProxyUrl = Symbol('proxy url') +const kProxyHeaders = Symbol('proxy headers') +const kProxyAuth = Symbol('proxy auth') +const kProxyProtocol = Symbol('proxy protocol') +const kPools = Symbol('pools') +const kConnector = Symbol('connector') +const kRequestTls = Symbol('request tls settings') + +// Static flag to ensure warning is only emitted once per process +let experimentalWarningEmitted = false + +/** + * SOCKS5 proxy agent for dispatching requests through a SOCKS5 proxy + */ +class Socks5ProxyAgent extends DispatcherBase { + constructor (proxyUrl, options = {}) { + super() + + // Emit experimental warning only once + if (!experimentalWarningEmitted) { + process.emitWarning( + 'SOCKS5 proxy support is experimental and subject to change', + 'ExperimentalWarning' + ) + experimentalWarningEmitted = true + } + + if (!proxyUrl) { + throw new InvalidArgumentError('Proxy URL is mandatory') + } + + // Parse proxy URL + const url = typeof proxyUrl === 'string' ? new URL(proxyUrl) : proxyUrl + + if (url.protocol !== 'socks5:' && url.protocol !== 'socks:') { + throw new InvalidArgumentError('Proxy URL must use socks5:// or socks:// protocol') + } + + this[kProxyUrl] = url + this[kProxyHeaders] = options.headers || {} + this[kProxyProtocol] = options.proxyTls ? 'https:' : 'http:' + this[kRequestTls] = options.requestTls + + // Extract auth from URL or options + this[kProxyAuth] = { + username: options.username || (url.username ? decodeURIComponent(url.username) : null), + password: options.password || (url.password ? decodeURIComponent(url.password) : null) + } + + // Create connector for proxy connection + this[kConnector] = options.connect || buildConnector({ + ...options.proxyTls, + servername: options.proxyTls?.servername || url.hostname + }) + + // Pools for the actual HTTP connections (with SOCKS5 tunnel connect function), keyed by origin + this[kPools] = new Map() + } + + /** + * Create a SOCKS5 connection to the proxy + */ + async createSocks5Connection (targetHost, targetPort) { + const proxyHost = this[kProxyUrl].hostname + const proxyPort = parseInt(this[kProxyUrl].port) || 1080 + + debug('creating SOCKS5 connection to', proxyHost, proxyPort) + + // Connect to the SOCKS5 proxy + const socket = await new Promise((resolve, reject) => { + this[kConnector]({ + hostname: proxyHost, + host: proxyHost, + port: proxyPort, + protocol: this[kProxyProtocol] + }, (err, socket) => { + if (err) { + reject(err) + } else { + resolve(socket) + } + }) + }) + + // Create SOCKS5 client + const socks5Client = new Socks5Client(socket, this[kProxyAuth]) + + // Handle SOCKS5 errors + socks5Client.on('error', (err) => { + debug('SOCKS5 error:', err) + socket.destroy() + }) + + // Perform SOCKS5 handshake + await socks5Client.handshake() + + // Wait for authentication (if required) + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('SOCKS5 authentication timeout')) + }, 5000) + + const onAuthenticated = () => { + clearTimeout(timeout) + socks5Client.removeListener('error', onError) + resolve() + } + + const onError = (err) => { + clearTimeout(timeout) + socks5Client.removeListener('authenticated', onAuthenticated) + reject(err) + } + + // Check if already authenticated (for NO_AUTH method) + if (socks5Client.state === STATES.AUTHENTICATED) { + clearTimeout(timeout) + resolve() + } else { + socks5Client.once('authenticated', onAuthenticated) + socks5Client.once('error', onError) + } + }) + + // Send CONNECT command + await socks5Client.connect(targetHost, targetPort) + + // Wait for connection + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('SOCKS5 connection timeout')) + }, 5000) + + const onConnected = (info) => { + debug('SOCKS5 tunnel established to', targetHost, targetPort, 'via', info) + clearTimeout(timeout) + socks5Client.removeListener('error', onError) + resolve() + } + + const onError = (err) => { + clearTimeout(timeout) + socks5Client.removeListener('connected', onConnected) + reject(err) + } + + socks5Client.once('connected', onConnected) + socks5Client.once('error', onError) + }) + + return socket + } + + /** + * Dispatch a request through the SOCKS5 proxy + */ + [kDispatch] (opts, handler) { + const { origin } = opts + + debug('dispatching request to', origin, 'via SOCKS5') + + try { + const originKey = String(origin) + let pool = this[kPools].get(originKey) + // Create a Pool per origin so requests are not routed to the wrong host + if (!pool || pool.destroyed || pool.closed) { + pool = new Pool(origin, { + pipelining: opts.pipelining, + connections: opts.connections, + connect: async (connectOpts, callback) => { + try { + const url = new URL(origin) + const targetHost = url.hostname + const targetPort = parseInt(url.port) || (url.protocol === 'https:' ? 443 : 80) + + debug('establishing SOCKS5 connection to', targetHost, targetPort) + + // Create SOCKS5 tunnel + const socket = await this.createSocks5Connection(targetHost, targetPort) + + // Handle TLS if needed + let finalSocket = socket + if (url.protocol === 'https:') { + if (!tls) { + tls = require('node:tls') + } + debug('upgrading to TLS') + finalSocket = tls.connect({ + ...this[kRequestTls], + socket, + servername: this[kRequestTls]?.servername || targetHost + }) + + await new Promise((resolve, reject) => { + finalSocket.once('secureConnect', resolve) + finalSocket.once('error', reject) + }) + } + + callback(null, finalSocket) + } catch (err) { + debug('SOCKS5 connection error:', err) + callback(err) + } + } + }) + this[kPools].set(originKey, pool) + } + + // Dispatch the request through the per-origin pool + return pool[kDispatch](opts, handler) + } catch (err) { + debug('dispatch error:', err) + if (typeof handler.onResponseError === 'function') { + handler.onResponseError(null, err) + return false + } else if (typeof handler.onError === 'function') { + handler.onError(err) + return false + } else { + throw err + } + } + } + + async [kClose] () { + const closePromises = [] + for (const pool of this[kPools].values()) { + closePromises.push(pool.close()) + } + this[kPools].clear() + await Promise.all(closePromises) + } + + async [kDestroy] (err) { + const destroyPromises = [] + for (const pool of this[kPools].values()) { + destroyPromises.push(pool.destroy(err)) + } + this[kPools].clear() + await Promise.all(destroyPromises) + } +} + +module.exports = Socks5ProxyAgent diff --git a/lib/encoding/index.js b/lib/encoding/index.js new file mode 100644 index 00000000000..41f7794d1be --- /dev/null +++ b/lib/encoding/index.js @@ -0,0 +1,33 @@ +'use strict' + +const textDecoder = new TextDecoder() + +/** + * @see https://encoding.spec.whatwg.org/#utf-8-decode + * @param {Uint8Array} 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-8’s + // decoder, ioQueue, output, and "replacement". + const output = textDecoder.decode(buffer) + + // 4. Return output. + return output +} + +module.exports = { + utf8DecodeBytes +} diff --git a/lib/global.js b/lib/global.js index 0c7528fa653..565aceb4a03 100644 --- a/lib/global.js +++ b/lib/global.js @@ -2,7 +2,8 @@ // We include a version number for the Dispatcher API. In case of breaking changes, // this version number must be increased to avoid conflicts. -const globalDispatcher = Symbol.for('undici.globalDispatcher.1') +const globalDispatcher = Symbol.for('undici.globalDispatcher.2') +const legacyGlobalDispatcher = Symbol.for('undici.globalDispatcher.1') const { InvalidArgumentError } = require('./core/errors') const Agent = require('./dispatcher/agent') @@ -14,19 +15,45 @@ function setGlobalDispatcher (agent) { if (!agent || typeof agent.dispatch !== 'function') { throw new InvalidArgumentError('Argument agent must implement Agent') } + Object.defineProperty(globalThis, globalDispatcher, { value: agent, writable: true, enumerable: false, configurable: false }) + + Object.defineProperty(globalThis, legacyGlobalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) } function getGlobalDispatcher () { - return globalThis[globalDispatcher] + return globalThis[legacyGlobalDispatcher] } +// These are the globals that can be installed by undici.install(). +// Not exported by index.js to avoid use outside of this module. +const installedExports = /** @type {const} */ ( + [ + 'fetch', + 'Headers', + 'Response', + 'Request', + 'FormData', + 'WebSocket', + 'CloseEvent', + 'ErrorEvent', + 'MessageEvent', + 'EventSource' + ] +) + module.exports = { setGlobalDispatcher, - getGlobalDispatcher + getGlobalDispatcher, + installedExports } diff --git a/lib/handler/cache-handler.js b/lib/handler/cache-handler.js index e02ff9c9d72..8cfe073503a 100644 --- a/lib/handler/cache-handler.js +++ b/lib/handler/cache-handler.js @@ -15,6 +15,15 @@ const HEURISTICALLY_CACHEABLE_STATUS_CODES = [ 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501 ] +// Status codes which semantic is not handled by the cache +// https://datatracker.ietf.org/doc/html/rfc9111#section-3 +// This list should not grow beyond 206 unless the RFC is updated +// by a newer one including more. Please introduce another list if +// implementing caching of responses with the 'must-understand' directive. +const NOT_UNDERSTOOD_STATUS_CODES = [ + 206 +] + const MAX_RESPONSE_AGE = 2147483647000 /** @@ -95,6 +104,7 @@ class CacheHandler { resHeaders, statusMessage ) + const handler = this if ( !util.safeHTTPMethods.includes(this.#cacheKey.method) && @@ -125,7 +135,7 @@ class CacheHandler { } const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {} - if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) { + if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) { return downstreamOnHeaders() } @@ -180,36 +190,127 @@ class CacheHandler { deleteAt } - if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) { - value.etag = resHeaders.etag - } + // Not modified, re-use the cached value + // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified + if (statusCode === 304) { + const handle304 = (cachedValue) => { + if (!cachedValue) { + // Do not create a new cache entry, as a 304 won't have a body - so cannot be cached. + return downstreamOnHeaders() + } - this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value) - if (!this.#writeStream) { - return downstreamOnHeaders() - } + // Re-use the cached value: statuscode, statusmessage, headers and body + value.statusCode = cachedValue.statusCode + value.statusMessage = cachedValue.statusMessage + value.etag = cachedValue.etag + value.headers = { ...cachedValue.headers, ...strippedHeaders } - const handler = this - this.#writeStream - .on('drain', () => controller.resume()) - .on('error', function () { - // TODO (fix): Make error somehow observable? - handler.#writeStream = undefined - - // Delete the value in case the cache store is holding onto state from - // the call to createWriteStream - handler.#store.delete(handler.#cacheKey) - }) - .on('close', function () { - if (handler.#writeStream === this) { - handler.#writeStream = undefined + downstreamOnHeaders() + + this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value) + + if (!this.#writeStream || !cachedValue?.body) { + return } - // TODO (fix): Should we resume even if was paused downstream? - controller.resume() - }) + if (typeof cachedValue.body.values === 'function') { + const bodyIterator = cachedValue.body.values() + + const streamCachedBody = () => { + for (const chunk of bodyIterator) { + const full = this.#writeStream.write(chunk) === false + this.#handler.onResponseData?.(controller, chunk) + // when stream is full stop writing until we get a 'drain' event + if (full) { + break + } + } + } + + this.#writeStream + .on('error', function () { + handler.#writeStream = undefined + handler.#store.delete(handler.#cacheKey) + }) + .on('drain', () => { + streamCachedBody() + }) + .on('close', function () { + if (handler.#writeStream === this) { + handler.#writeStream = undefined + } + }) + + streamCachedBody() + } else if (typeof cachedValue.body.on === 'function') { + // Readable stream body (e.g. from async/remote cache stores) + cachedValue.body + .on('data', (chunk) => { + this.#writeStream.write(chunk) + this.#handler.onResponseData?.(controller, chunk) + }) + .on('end', () => { + this.#writeStream.end() + }) + .on('error', () => { + this.#writeStream = undefined + this.#store.delete(this.#cacheKey) + }) + + this.#writeStream + .on('error', function () { + handler.#writeStream = undefined + handler.#store.delete(handler.#cacheKey) + }) + .on('close', function () { + if (handler.#writeStream === this) { + handler.#writeStream = undefined + } + }) + } + } + + /** + * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} + */ + const result = this.#store.get(this.#cacheKey) + if (result && typeof result.then === 'function') { + result.then(handle304) + } else { + handle304(result) + } + } else { + if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) { + value.etag = resHeaders.etag + } + + this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value) - return downstreamOnHeaders() + if (!this.#writeStream) { + return downstreamOnHeaders() + } + + this.#writeStream + .on('drain', () => controller.resume()) + .on('error', function () { + // TODO (fix): Make error somehow observable? + handler.#writeStream = undefined + + // Delete the value in case the cache store is holding onto state from + // the call to createWriteStream + handler.#store.delete(handler.#cacheKey) + }) + .on('close', function () { + if (handler.#writeStream === this) { + handler.#writeStream = undefined + } + + // TODO (fix): Should we resume even if was paused downstream? + controller.resume() + }) + + downstreamOnHeaders() + } } onResponseData (controller, chunk) { @@ -239,9 +340,22 @@ class CacheHandler { * @param {number} statusCode * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders] */ -function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) { - if (statusCode !== 200 && statusCode !== 307) { +function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) { + // Status code must be final and understood. + if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) { + return false + } + // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching + // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3 + if (!HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) && !resHeaders['expires'] && + !cacheControlDirectives.public && + cacheControlDirectives['max-age'] === undefined && + // RFC 9111: a private response directive, if the cache is not shared + !(cacheControlDirectives.private && cacheType === 'private') && + !(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared') + ) { return false } @@ -259,8 +373,16 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect } // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen - if (resHeaders.authorization) { - if (!cacheControlDirectives.public || typeof resHeaders.authorization !== 'string') { + if (reqHeaders?.authorization) { + if ( + !cacheControlDirectives.public && + !cacheControlDirectives['s-maxage'] && + !cacheControlDirectives['must-revalidate'] + ) { + return false + } + + if (typeof reqHeaders.authorization !== 'string') { return false } @@ -380,10 +502,18 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) { staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000) } - if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) { + if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) { immutable = now + 31536000000 } + // When no stale directives or immutable flag, add a revalidation buffer + // equal to the freshness lifetime so the entry survives past staleAt long + // enough to be revalidated instead of silently disappearing. + if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) { + const freshnessLifetime = staleAt - now + return staleAt + freshnessLifetime + } + return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable) } diff --git a/lib/handler/deduplication-handler.js b/lib/handler/deduplication-handler.js new file mode 100644 index 00000000000..537c4f70e97 --- /dev/null +++ b/lib/handler/deduplication-handler.js @@ -0,0 +1,460 @@ +'use strict' + +const { RequestAbortedError } = require('../core/errors') + +/** + * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler + */ + +const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024 + +/** + * @typedef {Object} WaitingHandler + * @property {DispatchHandler} handler + * @property {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @property {Buffer[]} bufferedChunks + * @property {number} bufferedBytes + * @property {object | null} pendingTrailers + * @property {boolean} done + */ + +/** + * Handler that forwards response events to multiple waiting handlers. + * Used for request deduplication. + * + * @implements {DispatchHandler} + */ +class DeduplicationHandler { + /** + * @type {DispatchHandler} + */ + #primaryHandler + + /** + * @type {WaitingHandler[]} + */ + #waitingHandlers = [] + + /** + * @type {number} + */ + #maxBufferSize = DEFAULT_MAX_BUFFER_SIZE + + /** + * @type {number} + */ + #statusCode = 0 + + /** + * @type {Record} + */ + #headers = {} + + /** + * @type {string} + */ + #statusMessage = '' + + /** + * @type {boolean} + */ + #aborted = false + + /** + * @type {boolean} + */ + #responseStarted = false + + /** + * @type {boolean} + */ + #responseDataStarted = false + + /** + * @type {boolean} + */ + #completed = false + + /** + * @type {import('../../types/dispatcher.d.ts').default.DispatchController | null} + */ + #controller = null + + /** + * @type {(() => void) | null} + */ + #onComplete = null + + /** + * @param {DispatchHandler} primaryHandler The primary handler + * @param {() => void} onComplete Callback when request completes + * @param {number} [maxBufferSize] Maximum paused buffer size per waiting handler + */ + constructor (primaryHandler, onComplete, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE) { + this.#primaryHandler = primaryHandler + this.#onComplete = onComplete + this.#maxBufferSize = maxBufferSize + } + + /** + * Add a waiting handler that will receive response events. + * Returns false if deduplication can no longer safely attach this handler. + * + * @param {DispatchHandler} handler + * @returns {boolean} + */ + addWaitingHandler (handler) { + if (this.#completed || this.#responseDataStarted) { + return false + } + + const waitingHandler = this.#createWaitingHandler(handler) + const waitingController = waitingHandler.controller + + try { + handler.onRequestStart?.(waitingController, null) + + if (waitingController.aborted) { + waitingHandler.done = true + return true + } + + if (this.#responseStarted) { + handler.onResponseStart?.( + waitingController, + this.#statusCode, + this.#headers, + this.#statusMessage + ) + } + } catch { + // Ignore errors from waiting handlers + waitingHandler.done = true + return true + } + + if (!waitingController.aborted) { + this.#waitingHandlers.push(waitingHandler) + } + + return true + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {any} context + */ + onRequestStart (controller, context) { + this.#controller = controller + this.#primaryHandler.onRequestStart?.(controller, context) + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {number} statusCode + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} headers + * @param {Socket} socket + */ + onRequestUpgrade (controller, statusCode, headers, socket) { + this.#primaryHandler.onRequestUpgrade?.(controller, statusCode, headers, socket) + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {number} statusCode + * @param {Record} headers + * @param {string} statusMessage + */ + onResponseStart (controller, statusCode, headers, statusMessage) { + this.#responseStarted = true + this.#statusCode = statusCode + this.#headers = headers + this.#statusMessage = statusMessage + + this.#primaryHandler.onResponseStart?.(controller, statusCode, headers, statusMessage) + + for (const waitingHandler of this.#waitingHandlers) { + const { handler, controller: waitingController } = waitingHandler + + if (waitingHandler.done || waitingController.aborted) { + waitingHandler.done = true + continue + } + + try { + handler.onResponseStart?.( + waitingController, + statusCode, + headers, + statusMessage + ) + } catch { + // Ignore errors from waiting handlers + } + + if (waitingController.aborted) { + waitingHandler.done = true + } + } + + this.#pruneDoneWaitingHandlers() + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {Buffer} chunk + */ + onResponseData (controller, chunk) { + if (this.#aborted || this.#completed) { + return + } + + this.#responseDataStarted = true + + this.#primaryHandler.onResponseData?.(controller, chunk) + + for (const waitingHandler of this.#waitingHandlers) { + const { handler, controller: waitingController } = waitingHandler + + if (waitingHandler.done || waitingController.aborted) { + waitingHandler.done = true + continue + } + + if (waitingController.paused) { + this.#bufferWaitingChunk(waitingHandler, chunk) + continue + } + + try { + handler.onResponseData?.(waitingController, chunk) + } catch { + // Ignore errors from waiting handlers + } + + if (waitingController.aborted) { + waitingHandler.done = true + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + } + } + + this.#pruneDoneWaitingHandlers() + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {object} trailers + */ + onResponseEnd (controller, trailers) { + if (this.#aborted || this.#completed) { + return + } + + this.#completed = true + this.#primaryHandler.onResponseEnd?.(controller, trailers) + + for (const waitingHandler of this.#waitingHandlers) { + if (waitingHandler.done || waitingHandler.controller.aborted) { + waitingHandler.done = true + continue + } + + this.#flushWaitingHandler(waitingHandler) + + if (waitingHandler.done || waitingHandler.controller.aborted) { + waitingHandler.done = true + continue + } + + if (waitingHandler.controller.paused && waitingHandler.bufferedChunks.length > 0) { + waitingHandler.pendingTrailers = trailers + continue + } + + try { + waitingHandler.handler.onResponseEnd?.(waitingHandler.controller, trailers) + } catch { + // Ignore errors from waiting handlers + } + + waitingHandler.done = true + } + + this.#pruneDoneWaitingHandlers() + this.#onComplete?.() + } + + /** + * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller + * @param {Error} err + */ + onResponseError (controller, err) { + if (this.#completed) { + return + } + + this.#aborted = true + this.#completed = true + + this.#primaryHandler.onResponseError?.(controller, err) + + for (const waitingHandler of this.#waitingHandlers) { + this.#errorWaitingHandler(waitingHandler, err) + } + + this.#waitingHandlers = [] + this.#onComplete?.() + } + + /** + * @param {DispatchHandler} handler + * @returns {WaitingHandler} + */ + #createWaitingHandler (handler) { + /** @type {WaitingHandler} */ + const waitingHandler = { + handler, + controller: null, + bufferedChunks: [], + bufferedBytes: 0, + pendingTrailers: null, + done: false + } + + const state = { + aborted: false, + paused: false, + reason: null + } + + waitingHandler.controller = { + resume: () => { + if (state.aborted) { + return + } + + state.paused = false + this.#flushWaitingHandler(waitingHandler) + + if ( + this.#completed && + waitingHandler.pendingTrailers && + waitingHandler.bufferedChunks.length === 0 && + !state.paused && + !state.aborted + ) { + try { + waitingHandler.handler.onResponseEnd?.(waitingHandler.controller, waitingHandler.pendingTrailers) + } catch { + // Ignore errors from waiting handlers + } + + waitingHandler.pendingTrailers = null + waitingHandler.done = true + } + + this.#pruneDoneWaitingHandlers() + }, + pause: () => { + if (!state.aborted) { + state.paused = true + } + }, + get paused () { return state.paused }, + get aborted () { return state.aborted }, + get reason () { return state.reason }, + abort: (reason) => { + state.aborted = true + state.reason = reason ?? null + waitingHandler.done = true + waitingHandler.pendingTrailers = null + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + } + } + + return waitingHandler + } + + /** + * @param {WaitingHandler} waitingHandler + * @param {Buffer} chunk + */ + #bufferWaitingChunk (waitingHandler, chunk) { + if (waitingHandler.done || waitingHandler.controller.aborted) { + waitingHandler.done = true + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + return + } + + const bufferedChunk = Buffer.from(chunk) + waitingHandler.bufferedChunks.push(bufferedChunk) + waitingHandler.bufferedBytes += bufferedChunk.length + + if (waitingHandler.bufferedBytes > this.#maxBufferSize) { + const err = new RequestAbortedError(`Deduplicated waiting handler exceeded maxBufferSize (${this.#maxBufferSize} bytes) while paused`) + this.#errorWaitingHandler(waitingHandler, err) + } + } + + /** + * @param {WaitingHandler} waitingHandler + */ + #flushWaitingHandler (waitingHandler) { + const { handler, controller } = waitingHandler + + while ( + !waitingHandler.done && + !controller.aborted && + !controller.paused && + waitingHandler.bufferedChunks.length > 0 + ) { + const bufferedChunk = waitingHandler.bufferedChunks.shift() + waitingHandler.bufferedBytes -= bufferedChunk.length + + try { + handler.onResponseData?.(controller, bufferedChunk) + } catch { + // Ignore errors from waiting handlers + } + + if (controller.aborted) { + waitingHandler.done = true + waitingHandler.pendingTrailers = null + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + break + } + } + } + + /** + * @param {WaitingHandler} waitingHandler + * @param {Error} err + */ + #errorWaitingHandler (waitingHandler, err) { + if (waitingHandler.done) { + return + } + + waitingHandler.done = true + waitingHandler.pendingTrailers = null + waitingHandler.bufferedChunks = [] + waitingHandler.bufferedBytes = 0 + + try { + waitingHandler.controller.abort(err) + waitingHandler.handler.onResponseError?.(waitingHandler.controller, err) + } catch { + // Ignore errors from waiting handlers + } + } + + #pruneDoneWaitingHandlers () { + this.#waitingHandlers = this.#waitingHandlers.filter(waitingHandler => waitingHandler.done === false) + } +} + +module.exports = DeduplicationHandler diff --git a/lib/handler/redirect-handler.js b/lib/handler/redirect-handler.js index dd28e1d7426..d088d5488af 100644 --- a/lib/handler/redirect-handler.js +++ b/lib/handler/redirect-handler.js @@ -42,7 +42,8 @@ class RedirectHandler { this.dispatch = dispatch this.location = null - this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy + const { maxRedirections: _, ...cleanOpts } = opts + this.opts = cleanOpts // opts must be a copy, exclude maxRedirections this.maxRedirections = maxRedirections this.handler = handler this.history = [] @@ -132,13 +133,22 @@ class RedirectHandler { const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) const path = search ? `${pathname}${search}` : pathname + // Check for redirect loops by seeing if we've already visited this URL in our history + // This catches the case where Client/Pool try to handle cross-origin redirects but fail + // and keep redirecting to the same URL in an infinite loop + const redirectUrlString = `${origin}${path}` + for (const historyUrl of this.history) { + if (historyUrl.toString() === redirectUrlString) { + throw new InvalidArgumentError(`Redirect loop detected. Cannot redirect to ${origin}. This typically happens when using a Client or Pool with cross-origin redirects. Use an Agent for cross-origin redirects.`) + } + } + // Remove headers referring to the original URL. // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. // https://tools.ietf.org/html/rfc7231#section-6.4 this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin) this.opts.path = path this.opts.origin = origin - this.opts.maxRedirections = 0 this.opts.query = null } @@ -212,7 +222,8 @@ function cleanRequestHeaders (headers, removeContent, unknownOrigin) { } } } else if (headers && typeof headers === 'object') { - const entries = typeof headers[Symbol.iterator] === 'function' ? headers : Object.entries(headers) + const entries = util.hasSafeIterator(headers) ? headers : Object.entries(headers) + for (const [key, value] of entries) { if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) { ret.push(key, value) diff --git a/lib/handler/retry-handler.js b/lib/handler/retry-handler.js index 0d4b2affdca..1cbc78981b1 100644 --- a/lib/handler/retry-handler.js +++ b/lib/handler/retry-handler.js @@ -92,8 +92,6 @@ class RetryHandler { function shouldRetry (passedErr) { if (passedErr) { - this.headersSent = true - this.headersSent = true this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage) controller.resume() diff --git a/lib/handler/unwrap-handler.js b/lib/handler/unwrap-handler.js index 865593a327b..2949faea0fe 100644 --- a/lib/handler/unwrap-handler.js +++ b/lib/handler/unwrap-handler.js @@ -13,6 +13,9 @@ class UnwrapController { [kResume] = null + rawHeaders = null + rawTrailers = null + constructor (abort) { this.#abort = abort } @@ -67,12 +70,18 @@ module.exports = class UnwrapHandler { this.#handler.onRequestStart?.(this.#controller, context) } + onResponseStarted () { + return this.#handler.onResponseStarted?.() + } + onUpgrade (statusCode, rawHeaders, socket) { + this.#controller.rawHeaders = rawHeaders this.#handler.onRequestUpgrade?.(this.#controller, statusCode, parseHeaders(rawHeaders), socket) } onHeaders (statusCode, rawHeaders, resume, statusMessage) { this.#controller[kResume] = resume + this.#controller.rawHeaders = rawHeaders this.#handler.onResponseStart?.(this.#controller, statusCode, parseHeaders(rawHeaders), statusMessage) return !this.#controller.paused } @@ -83,6 +92,7 @@ module.exports = class UnwrapHandler { } onComplete (rawTrailers) { + this.#controller.rawTrailers = rawTrailers this.#handler.onResponseEnd?.(this.#controller, parseHeaders(rawTrailers)) } diff --git a/lib/handler/wrap-handler.js b/lib/handler/wrap-handler.js index 47caa5fa68b..01440f621ae 100644 --- a/lib/handler/wrap-handler.js +++ b/lib/handler/wrap-handler.js @@ -20,6 +20,10 @@ module.exports = class WrapHandler { return this.#handler.onConnect?.(abort, context) } + onResponseStarted () { + return this.#handler.onResponseStarted?.() + } + onHeaders (statusCode, rawHeaders, resume, statusMessage) { return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage) } @@ -53,7 +57,7 @@ module.exports = class WrapHandler { onRequestUpgrade (controller, statusCode, headers, socket) { const rawHeaders = [] for (const [key, val] of Object.entries(headers)) { - rawHeaders.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val)) + rawHeaders.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val)) } this.#handler.onUpgrade?.(statusCode, rawHeaders, socket) @@ -62,7 +66,7 @@ module.exports = class WrapHandler { onResponseStart (controller, statusCode, headers, statusMessage) { const rawHeaders = [] for (const [key, val] of Object.entries(headers)) { - rawHeaders.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val)) + rawHeaders.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val)) } if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) { @@ -79,7 +83,7 @@ module.exports = class WrapHandler { onResponseEnd (controller, trailers) { const rawTrailers = [] for (const [key, val] of Object.entries(trailers)) { - rawTrailers.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val)) + rawTrailers.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val)) } this.#handler.onComplete?.(rawTrailers) @@ -93,3 +97,9 @@ module.exports = class WrapHandler { this.#handler.onError?.(err) } } + +function toRawHeaderValue (value) { + return Array.isArray(value) + ? value.map((item) => Buffer.from(item, 'latin1')) + : Buffer.from(value, 'latin1') +} diff --git a/lib/interceptor/cache.js b/lib/interceptor/cache.js index b981ebf9e01..81d7cb12cbb 100644 --- a/lib/interceptor/cache.js +++ b/lib/interceptor/cache.js @@ -6,9 +6,28 @@ const util = require('../core/util') const CacheHandler = require('../handler/cache-handler') const MemoryCacheStore = require('../cache/memory-cache-store') const CacheRevalidationHandler = require('../handler/cache-revalidation-handler') -const { assertCacheStore, assertCacheMethods, makeCacheKey, normaliseHeaders, parseCacheControlHeader } = require('../util/cache.js') +const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js') const { AbortError } = require('../core/errors.js') +/** + * @param {(string | RegExp)[] | undefined} origins + * @param {string} name + */ +function assertCacheOrigins (origins, name) { + if (origins === undefined) return + if (!Array.isArray(origins)) { + throw new TypeError(`expected ${name} to be an array or undefined, got ${typeof origins}`) + } + for (let i = 0; i < origins.length; i++) { + const origin = origins[i] + if (typeof origin !== 'string' && !(origin instanceof RegExp)) { + throw new TypeError(`expected ${name}[${i}] to be a string or RegExp, got ${typeof origin}`) + } + } +} + +const nop = () => {} + /** * @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn */ @@ -16,19 +35,34 @@ const { AbortError } = require('../core/errors.js') /** * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives + * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts * @returns {boolean} */ -function needsRevalidation (result, cacheControlDirectives) { +function needsRevalidation (result, cacheControlDirectives, { headers = {} }) { + // Always revalidate requests with the no-cache request directive. if (cacheControlDirectives?.['no-cache']) { - // Always revalidate requests with the no-cache request directive return true } + // Always revalidate requests with unqualified no-cache response directive. if (result.cacheControlDirectives?.['no-cache'] && !Array.isArray(result.cacheControlDirectives['no-cache'])) { - // Always revalidate requests with unqualified no-cache response directive return true } + // Always revalidate requests with conditional headers. + if (headers['if-modified-since'] || headers['if-none-match']) { + return true + } + + return false +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives + * @returns {boolean} + */ +function isStale (result, cacheControlDirectives) { const now = Date.now() if (now > result.staleAt) { // Response is stale @@ -56,6 +90,22 @@ function needsRevalidation (result, cacheControlDirectives) { return false } +/** + * Check if we're within the stale-while-revalidate window for a stale response + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @returns {boolean} + */ +function withinStaleWhileRevalidateWindow (result) { + const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate'] + if (!staleWhileRevalidate) { + return false + } + + const now = Date.now() + const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000) + return now <= staleWhileRevalidateExpiry +} + /** * @param {DispatchFn} dispatch * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts @@ -86,7 +136,7 @@ function handleUncachedResponse ( } if (typeof handler.onHeaders === 'function') { - handler.onHeaders(504, [], () => {}, 'Gateway Timeout') + handler.onHeaders(504, [], nop, 'Gateway Timeout') if (aborted) { return } @@ -223,21 +273,70 @@ function handleResult ( return dispatch(opts, handler) } + const stale = isStale(result, reqCacheControl) + const revalidate = needsRevalidation(result, reqCacheControl, opts) + // Check if the response is stale - if (needsRevalidation(result, reqCacheControl)) { + if (stale || revalidate) { if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { // If body is a stream we can't revalidate... // TODO (fix): This could be less strict... return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) } + // RFC 5861: If we're within stale-while-revalidate window, serve stale immediately + // and revalidate in background, unless immediate revalidation is necessary + if (!revalidate && withinStaleWhileRevalidateWindow(result)) { + // Serve stale response immediately + sendCachedValue(handler, opts, result, age, null, true) + + // Start background revalidation (fire-and-forget) + queueMicrotask(() => { + const headers = { + ...opts.headers, + 'if-modified-since': new Date(result.cachedAt).toUTCString() + } + + if (result.etag) { + headers['if-none-match'] = result.etag + } + + if (result.vary) { + for (const key in result.vary) { + if (result.vary[key] != null) { + headers[key] = result.vary[key] + } + } + } + + // Background revalidation - update cache if we get new data + dispatch( + { + ...opts, + headers + }, + new CacheHandler(globalOpts, cacheKey, { + // Silent handler that just updates the cache + onRequestStart () {}, + onRequestUpgrade () {}, + onResponseStart () {}, + onResponseData () {}, + onResponseEnd () {}, + onResponseError () {} + }) + ) + }) + + return true + } + let withinStaleIfErrorThreshold = false const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error'] if (staleIfErrorExpiry) { withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000)) } - let headers = { + const headers = { ...opts.headers, 'if-modified-since': new Date(result.cachedAt).toUTCString() } @@ -247,9 +346,10 @@ function handleResult ( } if (result.vary) { - headers = { - ...headers, - ...result.vary + for (const key in result.vary) { + if (result.vary[key] != null) { + headers[key] = result.vary[key] + } } } @@ -262,9 +362,10 @@ function handleResult ( new CacheRevalidationHandler( (success, context) => { if (success) { - sendCachedValue(handler, opts, result, age, context, true) + // TODO: successful revalidation should be considered fresh (not give stale warning). + sendCachedValue(handler, opts, result, age, context, stale) } else if (util.isStream(result.body)) { - result.body.on('error', () => {}).destroy() + result.body.on('error', nop).destroy() } }, new CacheHandler(globalOpts, cacheKey, handler), @@ -275,7 +376,7 @@ function handleResult ( // Dump request body. if (util.isStream(opts.body)) { - opts.body.on('error', () => {}).destroy() + opts.body.on('error', nop).destroy() } sendCachedValue(handler, opts, result, age, null, false) @@ -290,7 +391,8 @@ module.exports = (opts = {}) => { store = new MemoryCacheStore(), methods = ['GET'], cacheByDefault = undefined, - type = 'shared' + type = 'shared', + origins = undefined } = opts if (typeof opts !== 'object' || opts === null) { @@ -299,13 +401,14 @@ module.exports = (opts = {}) => { assertCacheStore(store, 'opts.store') assertCacheMethods(methods, 'opts.methods') + assertCacheOrigins(origins, 'opts.origins') if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') { - throw new TypeError(`exepcted opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`) + throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`) } if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') { - throw new TypeError(`exepcted opts.type to be shared, private, or undefined, got ${typeof type}`) + throw new TypeError(`expected opts.type to be shared, private, or undefined, got ${typeof type}`) } const globalOpts = { @@ -324,9 +427,32 @@ module.exports = (opts = {}) => { return dispatch(opts, handler) } + // Check if origin is in whitelist + if (origins !== undefined) { + const requestOrigin = opts.origin.toString().toLowerCase() + let isAllowed = false + + for (let i = 0; i < origins.length; i++) { + const allowed = origins[i] + if (typeof allowed === 'string') { + if (allowed.toLowerCase() === requestOrigin) { + isAllowed = true + break + } + } else if (allowed.test(requestOrigin)) { + isAllowed = true + break + } + } + + if (!isAllowed) { + return dispatch(opts, handler) + } + } + opts = { ...opts, - headers: normaliseHeaders(opts) + headers: normalizeHeaders(opts) } const reqCacheControl = opts.headers?.['cache-control'] @@ -344,18 +470,17 @@ module.exports = (opts = {}) => { const result = store.get(cacheKey) if (result && typeof result.then === 'function') { - result.then(result => { - handleResult(dispatch, + return result + .then(result => handleResult(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl, result - ) - }) + )) } else { - handleResult( + return handleResult( dispatch, globalOpts, cacheKey, @@ -365,8 +490,6 @@ module.exports = (opts = {}) => { result ) } - - return true } } } diff --git a/lib/interceptor/decompress.js b/lib/interceptor/decompress.js new file mode 100644 index 00000000000..ee4202a96f7 --- /dev/null +++ b/lib/interceptor/decompress.js @@ -0,0 +1,259 @@ +'use strict' + +const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib') +const { pipeline } = require('node:stream') +const DecoratorHandler = require('../handler/decorator-handler') +const { runtimeFeatures } = require('../util/runtime-features') + +/** @typedef {import('node:stream').Transform} Transform */ +/** @typedef {import('node:stream').Transform} Controller */ +/** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */ + +/** @type {Record DecompressorStream>} */ +const supportedEncodings = { + gzip: createGunzip, + 'x-gzip': createGunzip, + br: createBrotliDecompress, + deflate: createInflate, + compress: createInflate, + 'x-compress': createInflate, + ...(runtimeFeatures.has('zstd') ? { zstd: createZstdDecompress } : {}) +} + +const defaultSkipStatusCodes = /** @type {const} */ ([204, 304]) + +let warningEmitted = /** @type {boolean} */ (false) + +/** + * @typedef {Object} DecompressHandlerOptions + * @property {number[]|Readonly} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for + * @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400) + */ + +class DecompressHandler extends DecoratorHandler { + /** @type {Transform[]} */ + #decompressors = [] + /** @type {Readonly} */ + #skipStatusCodes + /** @type {boolean} */ + #skipErrorResponses + + constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true } = {}) { + super(handler) + this.#skipStatusCodes = skipStatusCodes + this.#skipErrorResponses = skipErrorResponses + } + + /** + * Determines if decompression should be skipped based on encoding and status code + * @param {string} contentEncoding - Content-Encoding header value + * @param {number} statusCode - HTTP status code of the response + * @returns {boolean} - True if decompression should be skipped + */ + #shouldSkipDecompression (contentEncoding, statusCode) { + if (!contentEncoding || statusCode < 200) return true + if (this.#skipStatusCodes.includes(statusCode)) return true + if (this.#skipErrorResponses && statusCode >= 400) return true + return false + } + + /** + * Creates a chain of decompressors for multiple content encodings + * + * @param {string} encodings - Comma-separated list of content encodings + * @returns {Array} - Array of decompressor streams + * @throws {Error} - If the number of content-encodings exceeds the maximum allowed + */ + #createDecompressionChain (encodings) { + const parts = encodings.split(',') + + // Limit the number of content-encodings to prevent resource exhaustion. + // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). + const maxContentEncodings = 5 + if (parts.length > maxContentEncodings) { + throw new Error(`too many content-encodings in response: ${parts.length}, maximum allowed is ${maxContentEncodings}`) + } + + /** @type {DecompressorStream[]} */ + const decompressors = [] + + for (let i = parts.length - 1; i >= 0; i--) { + const encoding = parts[i].trim() + if (!encoding) continue + + if (!supportedEncodings[encoding]) { + decompressors.length = 0 // Clear if unsupported encoding + return decompressors // Unsupported encoding + } + + decompressors.push(supportedEncodings[encoding]()) + } + + return decompressors + } + + /** + * Sets up event handlers for a decompressor stream using readable events + * @param {DecompressorStream} decompressor - The decompressor stream + * @param {Controller} controller - The controller to coordinate with + * @returns {void} + */ + #setupDecompressorEvents (decompressor, controller) { + decompressor.on('readable', () => { + let chunk + while ((chunk = decompressor.read()) !== null) { + const result = super.onResponseData(controller, chunk) + if (result === false) { + break + } + } + }) + + decompressor.on('error', (error) => { + super.onResponseError(controller, error) + }) + } + + /** + * Sets up event handling for a single decompressor + * @param {Controller} controller - The controller to handle events + * @returns {void} + */ + #setupSingleDecompressor (controller) { + const decompressor = this.#decompressors[0] + this.#setupDecompressorEvents(decompressor, controller) + + decompressor.on('end', () => { + super.onResponseEnd(controller, {}) + }) + } + + /** + * Sets up event handling for multiple chained decompressors using pipeline + * @param {Controller} controller - The controller to handle events + * @returns {void} + */ + #setupMultipleDecompressors (controller) { + const lastDecompressor = this.#decompressors[this.#decompressors.length - 1] + this.#setupDecompressorEvents(lastDecompressor, controller) + + pipeline(this.#decompressors, (err) => { + if (err) { + super.onResponseError(controller, err) + return + } + super.onResponseEnd(controller, {}) + }) + } + + /** + * Cleans up decompressor references to prevent memory leaks + * @returns {void} + */ + #cleanupDecompressors () { + this.#decompressors.length = 0 + } + + /** + * @param {Controller} controller + * @param {number} statusCode + * @param {Record} headers + * @param {string} statusMessage + * @returns {void} + */ + onResponseStart (controller, statusCode, headers, statusMessage) { + const contentEncoding = headers['content-encoding'] + + // If content encoding is not supported or status code is in skip list + if (this.#shouldSkipDecompression(contentEncoding, statusCode)) { + return super.onResponseStart(controller, statusCode, headers, statusMessage) + } + + const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase()) + + if (decompressors.length === 0) { + this.#cleanupDecompressors() + return super.onResponseStart(controller, statusCode, headers, statusMessage) + } + + this.#decompressors = decompressors + + // Remove compression headers since we're decompressing + const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers + + if (this.#decompressors.length === 1) { + this.#setupSingleDecompressor(controller) + } else { + this.#setupMultipleDecompressors(controller) + } + + return super.onResponseStart(controller, statusCode, newHeaders, statusMessage) + } + + /** + * @param {Controller} controller + * @param {Buffer} chunk + * @returns {void} + */ + onResponseData (controller, chunk) { + if (this.#decompressors.length > 0) { + this.#decompressors[0].write(chunk) + return + } + super.onResponseData(controller, chunk) + } + + /** + * @param {Controller} controller + * @param {Record | undefined} trailers + * @returns {void} + */ + onResponseEnd (controller, trailers) { + if (this.#decompressors.length > 0) { + this.#decompressors[0].end() + this.#cleanupDecompressors() + return + } + super.onResponseEnd(controller, trailers) + } + + /** + * @param {Controller} controller + * @param {Error} err + * @returns {void} + */ + onResponseError (controller, err) { + if (this.#decompressors.length > 0) { + for (const decompressor of this.#decompressors) { + decompressor.destroy(err) + } + this.#cleanupDecompressors() + } + super.onResponseError(controller, err) + } +} + +/** + * Creates a decompression interceptor for HTTP responses + * @param {DecompressHandlerOptions} [options] - Options for the interceptor + * @returns {Function} - Interceptor function + */ +function createDecompressInterceptor (options = {}) { + // Emit experimental warning only once + if (!warningEmitted) { + process.emitWarning( + 'DecompressInterceptor is experimental and subject to change', + 'ExperimentalWarning' + ) + warningEmitted = true + } + + return (dispatch) => { + return (opts, handler) => { + const decompressHandler = new DecompressHandler(handler, options) + return dispatch(opts, decompressHandler) + } + } +} + +module.exports = createDecompressInterceptor diff --git a/lib/interceptor/deduplicate.js b/lib/interceptor/deduplicate.js new file mode 100644 index 00000000000..e81525ac5ea --- /dev/null +++ b/lib/interceptor/deduplicate.js @@ -0,0 +1,117 @@ +'use strict' + +const diagnosticsChannel = require('node:diagnostics_channel') +const util = require('../core/util') +const DeduplicationHandler = require('../handler/deduplication-handler') +const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js') + +const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests') + +/** + * @param {import('../../types/interceptors.d.ts').default.DeduplicateInterceptorOpts} [opts] + * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor} + */ +module.exports = (opts = {}) => { + const { + methods = ['GET'], + skipHeaderNames = [], + excludeHeaderNames = [], + maxBufferSize = 5 * 1024 * 1024 + } = opts + + if (typeof opts !== 'object' || opts === null) { + throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`) + } + + if (!Array.isArray(methods)) { + throw new TypeError(`expected opts.methods to be an array, got ${typeof methods}`) + } + + for (const method of methods) { + if (!util.safeHTTPMethods.includes(method)) { + throw new TypeError(`expected opts.methods to only contain safe HTTP methods, got ${method}`) + } + } + + if (!Array.isArray(skipHeaderNames)) { + throw new TypeError(`expected opts.skipHeaderNames to be an array, got ${typeof skipHeaderNames}`) + } + + if (!Array.isArray(excludeHeaderNames)) { + throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`) + } + + if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) { + throw new TypeError(`expected opts.maxBufferSize to be a positive finite number, got ${maxBufferSize}`) + } + + // Convert to lowercase Set for case-insensitive header matching + const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase())) + + // Convert to lowercase Set for case-insensitive header exclusion from deduplication key + const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase())) + + /** + * Map of pending requests for deduplication + * @type {Map} + */ + const pendingRequests = new Map() + + return dispatch => { + return (opts, handler) => { + if (!opts.origin || methods.includes(opts.method) === false) { + return dispatch(opts, handler) + } + + opts = { + ...opts, + headers: normalizeHeaders(opts) + } + + // Skip deduplication if request contains any of the specified headers + if (skipHeaderNamesSet.size > 0) { + for (const headerName of Object.keys(opts.headers)) { + if (skipHeaderNamesSet.has(headerName.toLowerCase())) { + return dispatch(opts, handler) + } + } + } + + const cacheKey = makeCacheKey(opts) + const dedupeKey = makeDeduplicationKey(cacheKey, excludeHeaderNamesSet) + + // Check if there's already a pending request for this key + const pendingHandler = pendingRequests.get(dedupeKey) + if (pendingHandler) { + // Add this handler to the waiting list when safe. + // If body streaming has already started, this request must be sent independently. + if (pendingHandler.addWaitingHandler(handler)) { + return true + } + + return dispatch(opts, handler) + } + + // Create a new deduplication handler + const deduplicationHandler = new DeduplicationHandler( + handler, + () => { + // Clean up when request completes + pendingRequests.delete(dedupeKey) + if (pendingRequestsChannel.hasSubscribers) { + pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'removed' }) + } + }, + maxBufferSize + ) + + // Register the pending request + pendingRequests.set(dedupeKey, deduplicationHandler) + if (pendingRequestsChannel.hasSubscribers) { + pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'added' }) + } + + return dispatch(opts, deduplicationHandler) + } + } +} diff --git a/lib/interceptor/dns.js b/lib/interceptor/dns.js index 38287607143..38cb2fcbef3 100644 --- a/lib/interceptor/dns.js +++ b/lib/interceptor/dns.js @@ -5,14 +5,143 @@ const DecoratorHandler = require('../handler/decorator-handler') const { InvalidArgumentError, InformationalError } = require('../core/errors') const maxInt = Math.pow(2, 31) - 1 +function hasSafeIterator (headers) { + const prototype = Object.getPrototypeOf(headers) + const ownIterator = Object.prototype.hasOwnProperty.call(headers, Symbol.iterator) + return ownIterator || (prototype != null && prototype !== Object.prototype && typeof headers[Symbol.iterator] === 'function') +} + +function isHostHeader (key) { + return typeof key === 'string' && key.toLowerCase() === 'host' +} + +function normalizeHeaders (headers) { + if (headers == null) { + return null + } + + if (Array.isArray(headers)) { + if (headers.length === 0 || !Array.isArray(headers[0])) { + return headers + } + + const normalized = [] + for (const header of headers) { + if (Array.isArray(header) && header.length === 2) { + normalized.push(header[0], header[1]) + } else { + normalized.push(header) + } + } + + return normalized + } + + if (typeof headers === 'object' && hasSafeIterator(headers)) { + const normalized = [] + for (const header of headers) { + if (Array.isArray(header) && header.length === 2) { + normalized.push(header[0], header[1]) + } else { + normalized.push(header) + } + } + + return normalized + } + + return headers +} + +function hasHostHeader (headers) { + if (headers == null) { + return false + } + + if (Array.isArray(headers)) { + if (headers.length === 0) { + return false + } + + for (let i = 0; i < headers.length; i += 2) { + if (isHostHeader(headers[i])) { + return true + } + } + + return false + } + + if (typeof headers === 'object') { + for (const key in headers) { + if (isHostHeader(key)) { + return true + } + } + } + + return false +} + +function withHostHeader (host, headers) { + const normalizedHeaders = normalizeHeaders(headers) + + if (hasHostHeader(normalizedHeaders)) { + return normalizedHeaders + } + + if (Array.isArray(normalizedHeaders)) { + return ['host', host, ...normalizedHeaders] + } + + if (normalizedHeaders && typeof normalizedHeaders === 'object') { + return { + host, + ...normalizedHeaders + } + } + + return { host } +} + +class DNSStorage { + #maxItems = 0 + #records = new Map() + + constructor (opts) { + this.#maxItems = opts.maxItems + } + + get size () { + return this.#records.size + } + + get (hostname) { + return this.#records.get(hostname) ?? null + } + + set (hostname, records) { + this.#records.set(hostname, records) + } + + delete (hostname) { + this.#records.delete(hostname) + } + + // Delegate to storage decide can we do more lookups or not + full () { + return this.size >= this.#maxItems + } +} + class DNSInstance { #maxTTL = 0 #maxItems = 0 - #records = new Map() dualStack = true affinity = null lookup = null pick = null + storage = null constructor (opts) { this.#maxTTL = opts.maxTTL @@ -21,17 +150,14 @@ class DNSInstance { this.affinity = opts.affinity this.lookup = opts.lookup ?? this.#defaultLookup this.pick = opts.pick ?? this.#defaultPick - } - - get full () { - return this.#records.size === this.#maxItems + this.storage = opts.storage ?? new DNSStorage(opts) } runLookup (origin, opts, cb) { - const ips = this.#records.get(origin.hostname) + const ips = this.storage.get(origin.hostname) // If full, we just return the origin - if (ips == null && this.full) { + if (ips == null && this.storage.full()) { cb(null, origin) return } @@ -55,7 +181,7 @@ class DNSInstance { } this.setRecords(origin, addresses) - const records = this.#records.get(origin.hostname) + const records = this.storage.get(origin.hostname) const ip = this.pick( origin, @@ -89,7 +215,7 @@ class DNSInstance { // If no IPs we lookup - deleting old records if (ip == null) { - this.#records.delete(origin.hostname) + this.storage.delete(origin.hostname) this.runLookup(origin, opts, cb) return } @@ -193,7 +319,7 @@ class DNSInstance { } pickFamily (origin, ipFamily) { - const records = this.#records.get(origin.hostname)?.records + const records = this.storage.get(origin.hostname)?.records if (!records) { return null } @@ -227,11 +353,13 @@ class DNSInstance { setRecords (origin, addresses) { const timestamp = Date.now() const records = { records: { 4: null, 6: null } } + let minTTL = this.#maxTTL for (const record of addresses) { record.timestamp = timestamp if (typeof record.ttl === 'number') { // The record TTL is expected to be in ms record.ttl = Math.min(record.ttl, this.#maxTTL) + minTTL = Math.min(minTTL, record.ttl) } else { record.ttl = this.#maxTTL } @@ -242,11 +370,12 @@ class DNSInstance { records.records[record.family] = familyRecords } - this.#records.set(origin.hostname, records) + // We provide a default TTL if external storage will be used without TTL per record-level support + this.storage.set(origin.hostname, records, { ttl: minTTL }) } deleteRecords (origin) { - this.#records.delete(origin.hostname) + this.storage.delete(origin.hostname) } getHandler (meta, opts) { @@ -303,8 +432,9 @@ class DNSDispatchHandler extends DecoratorHandler { const dispatchOpts = { ...this.#opts, origin: `${this.#origin.protocol}//${ - ip.family === 6 ? `[${ip.address}]` : ip.address - }${port}` + ip.family === 6 ? `[${ip.address}]` : ip.address + }${port}`, + headers: withHostHeader(this.#origin.host, this.#opts.headers) } this.#dispatch(dispatchOpts, this) return @@ -372,6 +502,17 @@ module.exports = interceptorOpts => { throw new InvalidArgumentError('Invalid pick. Must be a function') } + if ( + interceptorOpts?.storage != null && + (typeof interceptorOpts?.storage?.get !== 'function' || + typeof interceptorOpts?.storage?.set !== 'function' || + typeof interceptorOpts?.storage?.full !== 'function' || + typeof interceptorOpts?.storage?.delete !== 'function' + ) + ) { + throw new InvalidArgumentError('Invalid storage. Must be a object with methods: { get, set, full, delete }') + } + const dualStack = interceptorOpts?.dualStack ?? true let affinity if (dualStack) { @@ -386,7 +527,8 @@ module.exports = interceptorOpts => { pick: interceptorOpts?.pick ?? null, dualStack, affinity, - maxItems: interceptorOpts?.maxItems ?? Infinity + maxItems: interceptorOpts?.maxItems ?? Infinity, + storage: interceptorOpts?.storage } const instance = new DNSInstance(opts) @@ -411,10 +553,7 @@ module.exports = interceptorOpts => { ...origDispatchOpts, servername: origin.hostname, // For SNI on TLS origin: newOrigin.origin, - headers: { - host: origin.host, - ...origDispatchOpts.headers - } + headers: withHostHeader(origin.host, origDispatchOpts.headers) } dispatch( diff --git a/lib/interceptor/dump.js b/lib/interceptor/dump.js index 61c09d5c9ee..4810a09f382 100644 --- a/lib/interceptor/dump.js +++ b/lib/interceptor/dump.js @@ -57,7 +57,8 @@ class DumpHandler extends DecoratorHandler { return } - err = this.#controller.reason ?? err + // On network errors before connect, controller will be null + err = this.#controller?.reason ?? err super.onResponseError(controller, err) } diff --git a/lib/interceptor/redirect.js b/lib/interceptor/redirect.js index 55bad59c6ee..b7df180433e 100644 --- a/lib/interceptor/redirect.js +++ b/lib/interceptor/redirect.js @@ -11,7 +11,7 @@ function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections } return dispatch(opts, handler) } - const dispatchOpts = { ...rest, maxRedirections: 0 } // Stop sub dispatcher from also redirecting. + const dispatchOpts = { ...rest } // Stop sub dispatcher from also redirecting. const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler) return dispatch(dispatchOpts, redirectHandler) } diff --git a/lib/llhttp/constants.d.ts b/lib/llhttp/constants.d.ts index 83fa4eb4622..def2436cc4e 100644 --- a/lib/llhttp/constants.d.ts +++ b/lib/llhttp/constants.d.ts @@ -15,7 +15,7 @@ export declare const H_METHOD_MAP: { [k: string]: number; }; export declare const STATUSES_HTTP: number[]; -export type CharList = Array; +export type CharList = (string | number)[]; export declare const ALPHA: CharList; export declare const NUM_MAP: { 0: number; @@ -95,3 +95,101 @@ export declare const SPECIAL_HEADERS: { 'transfer-encoding': number; upgrade: number; }; +declare const _default: { + ERROR: IntDict; + TYPE: IntDict; + FLAGS: IntDict; + LENIENT_FLAGS: IntDict; + METHODS: IntDict; + STATUSES: IntDict; + FINISH: IntDict; + HEADER_STATE: IntDict; + ALPHA: CharList; + NUM_MAP: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + }; + HEX_MAP: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + A: number; + B: number; + C: number; + D: number; + E: number; + F: number; + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; + }; + NUM: CharList; + ALPHANUM: CharList; + MARK: CharList; + USERINFO_CHARS: CharList; + URL_CHAR: CharList; + HEX: CharList; + TOKEN: CharList; + HEADER_CHARS: CharList; + CONNECTION_TOKEN_CHARS: CharList; + QUOTED_STRING: CharList; + HTAB_SP_VCHAR_OBS_TEXT: CharList; + MAJOR: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + }; + MINOR: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + 8: number; + 9: number; + }; + SPECIAL_HEADERS: { + connection: number; + 'content-length': number; + 'proxy-connection': number; + 'transfer-encoding': number; + upgrade: number; + }; + METHODS_HTTP: number[]; + METHODS_ICE: number[]; + METHODS_RTSP: number[]; + METHOD_MAP: IntDict; + H_METHOD_MAP: { + [k: string]: number; + }; + STATUSES_HTTP: number[]; +}; +export default _default; diff --git a/lib/llhttp/constants.js b/lib/llhttp/constants.js index 3e1dc01cdb5..8b88dfdf62c 100644 --- a/lib/llhttp/constants.js +++ b/lib/llhttp/constants.js @@ -40,6 +40,7 @@ exports.ERROR = { CB_CHUNK_EXTENSION_NAME_COMPLETE: 34, CB_CHUNK_EXTENSION_VALUE_COMPLETE: 35, CB_RESET: 31, + CB_PROTOCOL_COMPLETE: 38, }; exports.TYPE = { BOTH: 0, // default @@ -495,4 +496,36 @@ exports.SPECIAL_HEADERS = { 'transfer-encoding': exports.HEADER_STATE.TRANSFER_ENCODING, 'upgrade': exports.HEADER_STATE.UPGRADE, }; -//# sourceMappingURL=constants.js.map \ No newline at end of file +exports.default = { + ERROR: exports.ERROR, + TYPE: exports.TYPE, + FLAGS: exports.FLAGS, + LENIENT_FLAGS: exports.LENIENT_FLAGS, + METHODS: exports.METHODS, + STATUSES: exports.STATUSES, + FINISH: exports.FINISH, + HEADER_STATE: exports.HEADER_STATE, + ALPHA: exports.ALPHA, + NUM_MAP: exports.NUM_MAP, + HEX_MAP: exports.HEX_MAP, + NUM: exports.NUM, + ALPHANUM: exports.ALPHANUM, + MARK: exports.MARK, + USERINFO_CHARS: exports.USERINFO_CHARS, + URL_CHAR: exports.URL_CHAR, + HEX: exports.HEX, + TOKEN: exports.TOKEN, + HEADER_CHARS: exports.HEADER_CHARS, + CONNECTION_TOKEN_CHARS: exports.CONNECTION_TOKEN_CHARS, + QUOTED_STRING: exports.QUOTED_STRING, + HTAB_SP_VCHAR_OBS_TEXT: exports.HTAB_SP_VCHAR_OBS_TEXT, + MAJOR: exports.MAJOR, + MINOR: exports.MINOR, + SPECIAL_HEADERS: exports.SPECIAL_HEADERS, + METHODS_HTTP: exports.METHODS_HTTP, + METHODS_ICE: exports.METHODS_ICE, + METHODS_RTSP: exports.METHODS_RTSP, + METHOD_MAP: exports.METHOD_MAP, + H_METHOD_MAP: exports.H_METHOD_MAP, + STATUSES_HTTP: exports.STATUSES_HTTP, +}; diff --git a/lib/llhttp/constants.js.map b/lib/llhttp/constants.js.map deleted file mode 100644 index 8373fd699eb..00000000000 --- a/lib/llhttp/constants.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/llhttp/constants.ts"],"names":[],"mappings":";;;AAAA,mCAAoC;AAIpC,QAAQ;AAEK,QAAA,KAAK,GAAY;IAC5B,EAAE,EAAE,CAAC;IACL,QAAQ,EAAE,CAAC;IACX,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,EAAE;IACf,WAAW,EAAE,CAAC;IACd,yBAAyB,EAAE,CAAC;IAC5B,gBAAgB,EAAE,EAAE;IACpB,iBAAiB,EAAE,CAAC;IACpB,cAAc,EAAE,CAAC;IACjB,WAAW,EAAE,CAAC;IACd,gBAAgB,EAAE,CAAC;IACnB,eAAe,EAAE,CAAC;IAClB,oBAAoB,EAAE,EAAE;IACxB,sBAAsB,EAAE,EAAE;IAC1B,kBAAkB,EAAE,EAAE;IACtB,cAAc,EAAE,EAAE;IAClB,iBAAiB,EAAE,EAAE;IACrB,yBAAyB,EAAE,EAAE;IAE7B,gBAAgB,EAAE,EAAE;IACpB,mBAAmB,EAAE,EAAE;IACvB,mBAAmB,EAAE,EAAE;IACvB,eAAe,EAAE,EAAE;IACnB,iBAAiB,EAAE,EAAE;IAErB,MAAM,EAAE,EAAE;IACV,cAAc,EAAE,EAAE;IAClB,iBAAiB,EAAE,EAAE;IAErB,IAAI,EAAE,EAAE;IAER,eAAe,EAAE,EAAE;IACnB,kBAAkB,EAAE,EAAE;IACtB,kBAAkB,EAAE,EAAE;IACtB,mBAAmB,EAAE,EAAE;IACvB,wBAAwB,EAAE,EAAE;IAC5B,wBAAwB,EAAE,EAAE;IAC5B,gCAAgC,EAAE,EAAE;IACpC,iCAAiC,EAAE,EAAE;IACrC,QAAQ,EAAE,EAAE;CACb,CAAC;AAEW,QAAA,IAAI,GAAY;IAC3B,IAAI,EAAE,CAAC,EAAE,UAAU;IACnB,OAAO,EAAE,CAAC;IACV,QAAQ,EAAE,CAAC;CACZ,CAAC;AAEW,QAAA,KAAK,GAAY;IAC5B,qBAAqB,EAAE,CAAC,IAAI,CAAC;IAC7B,gBAAgB,EAAE,CAAC,IAAI,CAAC;IACxB,kBAAkB,EAAE,CAAC,IAAI,CAAC;IAC1B,OAAO,EAAE,CAAC,IAAI,CAAC;IACf,OAAO,EAAE,CAAC,IAAI,CAAC;IACf,cAAc,EAAE,CAAC,IAAI,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,CAAC;IAChB,mBAAmB;IACnB,iBAAiB,EAAE,CAAC,IAAI,CAAC;CAC1B,CAAC;AAEW,QAAA,aAAa,GAAY;IACpC,OAAO,EAAE,CAAC,IAAI,CAAC;IACf,cAAc,EAAE,CAAC,IAAI,CAAC;IACtB,UAAU,EAAE,CAAC,IAAI,CAAC;IAClB,iBAAiB,EAAE,CAAC,IAAI,CAAC;IACzB,OAAO,EAAE,CAAC,IAAI,CAAC;IACf,gBAAgB,EAAE,CAAC,IAAI,CAAC;IACxB,oBAAoB,EAAE,CAAC,IAAI,CAAC;IAC5B,yBAAyB,EAAE,CAAC,IAAI,CAAC;IACjC,qBAAqB,EAAE,CAAC,IAAI,CAAC;IAC7B,uBAAuB,EAAE,CAAC,IAAI,CAAC;CAChC,CAAC;AAEW,QAAA,OAAO,GAAY;IAC9B,QAAQ,EAAE,CAAC;IACX,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC;IACT,MAAM,EAAE,CAAC;IACT,KAAK,EAAE,CAAC;IACR,kBAAkB;IAClB,SAAS,EAAE,CAAC;IACZ,SAAS,EAAE,CAAC;IACZ,OAAO,EAAE,CAAC;IACV,YAAY;IACZ,MAAM,EAAE,CAAC;IACT,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,EAAE;IACX,MAAM,EAAE,EAAE;IACV,UAAU,EAAE,EAAE;IACd,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE;IACZ,QAAQ,EAAE,EAAE;IACZ,MAAM,EAAE,EAAE;IACV,QAAQ,EAAE,EAAE;IACZ,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE,EAAE;IACT,gBAAgB;IAChB,QAAQ,EAAE,EAAE;IACZ,YAAY,EAAE,EAAE;IAChB,UAAU,EAAE,EAAE;IACd,OAAO,EAAE,EAAE;IACX,UAAU;IACV,UAAU,EAAE,EAAE;IACd,QAAQ,EAAE,EAAE;IACZ,WAAW,EAAE,EAAE;IACf,aAAa,EAAE,EAAE;IACjB,cAAc;IACd,OAAO,EAAE,EAAE;IACX,OAAO,EAAE,EAAE;IACX,YAAY;IACZ,YAAY,EAAE,EAAE;IAChB,gCAAgC;IAChC,MAAM,EAAE,EAAE;IACV,QAAQ,EAAE,EAAE;IACZ,aAAa;IACb,QAAQ,EAAE,EAAE;IACZ,4BAA4B;IAC5B,KAAK,EAAE,EAAE;IACT,mBAAmB;IACnB,UAAU,EAAE,EAAE;IACd,UAAU,EAAE,EAAE;IACd,OAAO,EAAE,EAAE;IACX,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,UAAU,EAAE,EAAE;IACd,eAAe,EAAE,EAAE;IACnB,eAAe,EAAE,EAAE;IACnB,UAAU,EAAE,EAAE;IACd,QAAQ,EAAE,EAAE;IACZ,UAAU;IACV,OAAO,EAAE,EAAE;IACX,yFAAyF;IACzF,OAAO,EAAE,EAAE;CACZ,CAAC;AAEW,QAAA,QAAQ,GAAY;IAC/B,QAAQ,EAAE,GAAG;IACb,mBAAmB,EAAE,GAAG;IACxB,UAAU,EAAE,GAAG;IACf,WAAW,EAAE,GAAG;IAChB,iBAAiB,EAAE,GAAG,EAAE,aAAa;IACrC,mBAAmB,EAAE,GAAG,EAAE,aAAa;IACvC,sBAAsB,EAAE,GAAG,EAAE,aAAa;IAC1C,oBAAoB,EAAE,GAAG,EAAE,aAAa;IACxC,qBAAqB,EAAE,GAAG,EAAE,aAAa;IACzC,EAAE,EAAE,GAAG;IACP,OAAO,EAAE,GAAG;IACZ,QAAQ,EAAE,GAAG;IACb,6BAA6B,EAAE,GAAG;IAClC,UAAU,EAAE,GAAG;IACf,aAAa,EAAE,GAAG;IAClB,eAAe,EAAE,GAAG;IACpB,YAAY,EAAE,GAAG;IACjB,gBAAgB,EAAE,GAAG;IACrB,sBAAsB,EAAE,GAAG,EAAE,aAAa;IAC1C,OAAO,EAAE,GAAG;IACZ,gCAAgC,EAAE,GAAG,EAAE,aAAa;IACpD,gBAAgB,EAAE,GAAG;IACrB,iBAAiB,EAAE,GAAG;IACtB,KAAK,EAAE,GAAG;IACV,SAAS,EAAE,GAAG;IACd,YAAY,EAAE,GAAG;IACjB,SAAS,EAAE,GAAG;IACd,YAAY,EAAE,GAAG,EAAE,iBAAiB;IACpC,kBAAkB,EAAE,GAAG;IACvB,kBAAkB,EAAE,GAAG;IACvB,WAAW,EAAE,GAAG;IAChB,YAAY,EAAE,GAAG;IACjB,gBAAgB,EAAE,GAAG;IACrB,SAAS,EAAE,GAAG;IACd,SAAS,EAAE,GAAG;IACd,kBAAkB,EAAE,GAAG;IACvB,cAAc,EAAE,GAAG;IACnB,6BAA6B,EAAE,GAAG;IAClC,eAAe,EAAE,GAAG;IACpB,QAAQ,EAAE,GAAG;IACb,IAAI,EAAE,GAAG;IACT,eAAe,EAAE,GAAG;IACpB,mBAAmB,EAAE,GAAG;IACxB,iBAAiB,EAAE,GAAG;IACtB,YAAY,EAAE,GAAG;IACjB,sBAAsB,EAAE,GAAG;IAC3B,qBAAqB,EAAE,GAAG;IAC1B,kBAAkB,EAAE,GAAG;IACvB,WAAW,EAAE,GAAG;IAChB,YAAY,EAAE,GAAG,EAAE,aAAa;IAChC,iBAAiB,EAAE,GAAG,EAAE,aAAa;IACrC,mBAAmB,EAAE,GAAG;IACxB,oBAAoB,EAAE,GAAG;IACzB,MAAM,EAAE,GAAG;IACX,iBAAiB,EAAE,GAAG;IACtB,SAAS,EAAE,GAAG;IACd,gBAAgB,EAAE,GAAG;IACrB,qBAAqB,EAAE,GAAG;IAC1B,iBAAiB,EAAE,GAAG;IACtB,0CAA0C,EAAE,GAAG,EAAE,aAAa;IAC9D,+BAA+B,EAAE,GAAG;IACpC,aAAa,EAAE,GAAG,EAAE,aAAa;IACjC,WAAW,EAAE,GAAG,EAAE,aAAa;IAC/B,UAAU,EAAE,GAAG,EAAE,aAAa;IAC9B,2BAA2B,EAAE,GAAG,EAAE,aAAa;IAC/C,6BAA6B,EAAE,GAAG;IAClC,mCAAmC,EAAE,GAAG,EAAE,aAAa;IACvD,uBAAuB,EAAE,GAAG,EAAE,aAAa;IAC3C,wBAAwB,EAAE,GAAG,EAAE,aAAa;IAC5C,qBAAqB,EAAE,GAAG,EAAE,aAAa;IACzC,wBAAwB,EAAE,GAAG,EAAE,aAAa;IAC5C,+BAA+B,EAAE,GAAG,EAAE,aAAa;IACnD,aAAa,EAAE,GAAG,EAAE,aAAa;IACjC,qBAAqB,EAAE,GAAG,EAAE,aAAa;IACzC,qBAAqB,EAAE,GAAG;IAC1B,eAAe,EAAE,GAAG;IACpB,WAAW,EAAE,GAAG;IAChB,mBAAmB,EAAE,GAAG;IACxB,eAAe,EAAE,GAAG;IACpB,0BAA0B,EAAE,GAAG;IAC/B,uBAAuB,EAAE,GAAG;IAC5B,oBAAoB,EAAE,GAAG;IACzB,aAAa,EAAE,GAAG;IAClB,wBAAwB,EAAE,GAAG;IAC7B,YAAY,EAAE,GAAG;IACjB,+BAA+B,EAAE,GAAG;IACpC,wBAAwB,EAAE,GAAG,EAAE,aAAa;IAC5C,kBAAkB,EAAE,GAAG,EAAE,aAAa;IACtC,kBAAkB,EAAE,GAAG,EAAE,aAAa;IACtC,qBAAqB,EAAE,GAAG,EAAE,aAAa;IACzC,eAAe,EAAE,GAAG,EAAE,aAAa;IACnC,oBAAoB,EAAE,GAAG,EAAE,aAAa;IACxC,uBAAuB,EAAE,GAAG,EAAE,aAAa;IAC3C,aAAa,EAAE,GAAG,EAAE,aAAa;IACjC,kBAAkB,EAAE,GAAG,EAAE,aAAa;IACtC,cAAc,EAAE,GAAG,EAAE,aAAa;IAClC,sCAAsC,EAAE,GAAG,EAAE,aAAa;IAC1D,oBAAoB,EAAE,GAAG,EAAE,aAAa;IACxC,uBAAuB,EAAE,GAAG,EAAE,aAAa;CAC5C,CAAC;AAEW,QAAA,MAAM,GAAY;IAC7B,IAAI,EAAE,CAAC;IACP,YAAY,EAAE,CAAC;IACf,MAAM,EAAE,CAAC;CACV,CAAC;AAEW,QAAA,YAAY,GAAY;IACnC,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,CAAC;IACb,cAAc,EAAE,CAAC;IACjB,iBAAiB,EAAE,CAAC;IACpB,OAAO,EAAE,CAAC;IACV,qBAAqB,EAAE,CAAC;IACxB,gBAAgB,EAAE,CAAC;IACnB,kBAAkB,EAAE,CAAC;IACrB,yBAAyB,EAAE,CAAC;CAC7B,CAAC;AAEF,YAAY;AACC,QAAA,YAAY,GAAG;IAC1B,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,GAAG;IACX,eAAO,CAAC,IAAI;IACZ,eAAO,CAAC,IAAI;IACZ,eAAO,CAAC,GAAG;IACX,eAAO,CAAC,OAAO;IACf,eAAO,CAAC,OAAO;IACf,eAAO,CAAC,KAAK;IACb,eAAO,CAAC,IAAI;IACZ,eAAO,CAAC,IAAI;IACZ,eAAO,CAAC,KAAK;IACb,eAAO,CAAC,IAAI;IACZ,eAAO,CAAC,QAAQ;IAChB,eAAO,CAAC,SAAS;IACjB,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,IAAI;IACZ,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,GAAG;IACX,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,UAAU;IAClB,eAAO,CAAC,QAAQ;IAChB,eAAO,CAAC,KAAK;IACb,eAAO,CAAC,UAAU,CAAC;IACnB,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,SAAS;IACjB,eAAO,CAAC,WAAW;IACnB,eAAO,CAAC,KAAK;IACb,eAAO,CAAC,KAAK;IACb,eAAO,CAAC,UAAU;IAClB,eAAO,CAAC,IAAI;IACZ,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,GAAG;IAEX,+CAA+C;IAC/C,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,KAAK;CACd,CAAC;AAEW,QAAA,WAAW,GAAG;IACzB,eAAO,CAAC,MAAM;CACf,CAAC;AAEW,QAAA,YAAY,GAAG;IAC1B,eAAO,CAAC,OAAO;IACf,eAAO,CAAC,QAAQ;IAChB,eAAO,CAAC,QAAQ;IAChB,eAAO,CAAC,KAAK;IACb,eAAO,CAAC,IAAI;IACZ,eAAO,CAAC,KAAK;IACb,eAAO,CAAC,QAAQ;IAChB,eAAO,CAAC,aAAa;IACrB,eAAO,CAAC,aAAa;IACrB,eAAO,CAAC,QAAQ;IAChB,eAAO,CAAC,MAAM;IACd,eAAO,CAAC,KAAK;IAEb,cAAc;IACd,eAAO,CAAC,GAAG;IACX,eAAO,CAAC,IAAI;CACb,CAAC;AAEW,QAAA,UAAU,GAAG,IAAA,iBAAS,EAAC,eAAO,CAAC,CAAC;AAEhC,QAAA,YAAY,GAAG,MAAM,CAAC,WAAW,CAC5C,MAAM,CAAC,OAAO,CAAC,eAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAE,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAC7D,CAAC;AAEW,QAAA,aAAa,GAAG;IAC3B,gBAAQ,CAAC,QAAQ;IACjB,gBAAQ,CAAC,mBAAmB;IAC5B,gBAAQ,CAAC,UAAU;IACnB,gBAAQ,CAAC,WAAW;IACpB,gBAAQ,CAAC,iBAAiB;IAC1B,gBAAQ,CAAC,mBAAmB;IAC5B,gBAAQ,CAAC,sBAAsB;IAC/B,gBAAQ,CAAC,oBAAoB;IAC7B,gBAAQ,CAAC,qBAAqB;IAC9B,gBAAQ,CAAC,EAAE;IACX,gBAAQ,CAAC,OAAO;IAChB,gBAAQ,CAAC,QAAQ;IACjB,gBAAQ,CAAC,6BAA6B;IACtC,gBAAQ,CAAC,UAAU;IACnB,gBAAQ,CAAC,aAAa;IACtB,gBAAQ,CAAC,eAAe;IACxB,gBAAQ,CAAC,YAAY;IACrB,gBAAQ,CAAC,gBAAgB;IACzB,gBAAQ,CAAC,sBAAsB;IAC/B,gBAAQ,CAAC,OAAO;IAChB,gBAAQ,CAAC,gCAAgC;IACzC,gBAAQ,CAAC,gBAAgB;IACzB,gBAAQ,CAAC,iBAAiB;IAC1B,gBAAQ,CAAC,KAAK;IACd,gBAAQ,CAAC,SAAS;IAClB,gBAAQ,CAAC,YAAY;IACrB,gBAAQ,CAAC,SAAS;IAClB,gBAAQ,CAAC,YAAY;IACrB,gBAAQ,CAAC,kBAAkB;IAC3B,gBAAQ,CAAC,kBAAkB;IAC3B,gBAAQ,CAAC,WAAW;IACpB,gBAAQ,CAAC,YAAY;IACrB,gBAAQ,CAAC,gBAAgB;IACzB,gBAAQ,CAAC,SAAS;IAClB,gBAAQ,CAAC,SAAS;IAClB,gBAAQ,CAAC,kBAAkB;IAC3B,gBAAQ,CAAC,cAAc;IACvB,gBAAQ,CAAC,6BAA6B;IACtC,gBAAQ,CAAC,eAAe;IACxB,gBAAQ,CAAC,QAAQ;IACjB,gBAAQ,CAAC,IAAI;IACb,gBAAQ,CAAC,eAAe;IACxB,gBAAQ,CAAC,mBAAmB;IAC5B,gBAAQ,CAAC,iBAAiB;IAC1B,gBAAQ,CAAC,YAAY;IACrB,gBAAQ,CAAC,sBAAsB;IAC/B,gBAAQ,CAAC,qBAAqB;IAC9B,gBAAQ,CAAC,kBAAkB;IAC3B,gBAAQ,CAAC,WAAW;IACpB,gBAAQ,CAAC,YAAY;IACrB,gBAAQ,CAAC,iBAAiB;IAC1B,gBAAQ,CAAC,mBAAmB;IAC5B,gBAAQ,CAAC,oBAAoB;IAC7B,gBAAQ,CAAC,MAAM;IACf,gBAAQ,CAAC,iBAAiB;IAC1B,gBAAQ,CAAC,SAAS;IAClB,gBAAQ,CAAC,gBAAgB;IACzB,gBAAQ,CAAC,qBAAqB;IAC9B,gBAAQ,CAAC,iBAAiB;IAC1B,gBAAQ,CAAC,0CAA0C;IACnD,gBAAQ,CAAC,+BAA+B;IACxC,gBAAQ,CAAC,aAAa;IACtB,gBAAQ,CAAC,WAAW;IACpB,gBAAQ,CAAC,UAAU;IACnB,gBAAQ,CAAC,2BAA2B;IACpC,gBAAQ,CAAC,6BAA6B;IACtC,gBAAQ,CAAC,mCAAmC;IAC5C,gBAAQ,CAAC,uBAAuB;IAChC,gBAAQ,CAAC,wBAAwB;IACjC,gBAAQ,CAAC,qBAAqB;IAC9B,gBAAQ,CAAC,wBAAwB;IACjC,gBAAQ,CAAC,+BAA+B;IACxC,gBAAQ,CAAC,aAAa;IACtB,gBAAQ,CAAC,qBAAqB;IAC9B,gBAAQ,CAAC,qBAAqB;IAC9B,gBAAQ,CAAC,eAAe;IACxB,gBAAQ,CAAC,WAAW;IACpB,gBAAQ,CAAC,mBAAmB;IAC5B,gBAAQ,CAAC,eAAe;IACxB,gBAAQ,CAAC,0BAA0B;IACnC,gBAAQ,CAAC,uBAAuB;IAChC,gBAAQ,CAAC,oBAAoB;IAC7B,gBAAQ,CAAC,aAAa;IACtB,gBAAQ,CAAC,wBAAwB;IACjC,gBAAQ,CAAC,YAAY;IACrB,gBAAQ,CAAC,+BAA+B;IACxC,gBAAQ,CAAC,wBAAwB;IACjC,gBAAQ,CAAC,kBAAkB;IAC3B,gBAAQ,CAAC,kBAAkB;IAC3B,gBAAQ,CAAC,qBAAqB;IAC9B,gBAAQ,CAAC,eAAe;IACxB,gBAAQ,CAAC,oBAAoB;IAC7B,gBAAQ,CAAC,uBAAuB;IAChC,gBAAQ,CAAC,aAAa;IACtB,gBAAQ,CAAC,kBAAkB;IAC3B,gBAAQ,CAAC,cAAc;IACvB,gBAAQ,CAAC,sCAAsC;IAC/C,gBAAQ,CAAC,oBAAoB;IAC7B,gBAAQ,CAAC,uBAAuB;CACjC,CAAC;AAMW,QAAA,KAAK,GAAa,EAAE,CAAC;AAElC,KAAK,IAAI,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAC5D,aAAa;IACb,aAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnC,aAAa;IACb,aAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;AAC5C,CAAC;AAEY,QAAA,OAAO,GAAG;IACrB,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IAC5B,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;CAC7B,CAAC;AAEW,QAAA,OAAO,GAAG;IACrB,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IAC5B,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IAC5B,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG;IAC9C,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG;CAC/C,CAAC;AAEW,QAAA,GAAG,GAAa;IAC3B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CACjD,CAAC;AAEW,QAAA,QAAQ,GAAa,aAAK,CAAC,MAAM,CAAC,WAAG,CAAC,CAAC;AACvC,QAAA,IAAI,GAAa,CAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,CAAE,CAAC;AAClE,QAAA,cAAc,GAAa,gBAAQ;KAC7C,MAAM,CAAC,YAAI,CAAC;KACZ,MAAM,CAAC,CAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAE,CAAC,CAAC;AAEtD,yBAAyB;AACZ,QAAA,QAAQ,GAAc;IACjC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI;IAC7B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IACtC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IACvB,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IAC7B,GAAG;IACH,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CACN,CAAC,MAAM,CAAC,gBAAQ,CAAC,CAAC;AAEnB,QAAA,GAAG,GAAa,WAAG,CAAC,MAAM,CACrC,CAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAE,CAAC,CAAC;AAElE;;;;;;GAMG;AACU,QAAA,KAAK,GAAc;IAC9B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI;IAC7B,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;IAClB,GAAG,EAAE,GAAG,EAAE,GAAG;IACb,GAAG,EAAE,GAAG;CACI,CAAC,MAAM,CAAC,gBAAQ,CAAC,CAAC;AAEhC;;;GAGG;AACU,QAAA,YAAY,GAAa,CAAE,IAAI,CAAE,CAAC;AAC/C,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;IAC/B,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QACd,oBAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED,aAAa;AACA,QAAA,sBAAsB,GACjC,oBAAY,CAAC,MAAM,CAAC,CAAC,CAAkB,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;AAE3C,QAAA,aAAa,GAAa,CAAE,IAAI,EAAE,GAAG,CAAE,CAAC;AACrD,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,yCAAyC;QACvE,qBAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;AACH,CAAC;AAEY,QAAA,sBAAsB,GAAa,CAAE,IAAI,EAAE,GAAG,CAAE,CAAC;AAE9D,0DAA0D;AAC1D,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,8BAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjC,CAAC;AACD,8EAA8E;AAC9E,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,8BAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjC,CAAC;AAEY,QAAA,KAAK,GAAG,eAAO,CAAC;AAChB,QAAA,KAAK,GAAG,aAAK,CAAC;AAEd,QAAA,eAAe,GAAG;IAC7B,YAAY,EAAE,oBAAY,CAAC,UAAU;IACrC,gBAAgB,EAAE,oBAAY,CAAC,cAAc;IAC7C,kBAAkB,EAAE,oBAAY,CAAC,UAAU;IAC3C,mBAAmB,EAAE,oBAAY,CAAC,iBAAiB;IACnD,SAAS,EAAE,oBAAY,CAAC,OAAO;CAChC,CAAC"} \ No newline at end of file diff --git a/lib/llhttp/llhttp-wasm.js b/lib/llhttp/llhttp-wasm.js index 8a7fd760ca1..8e898063575 100644 --- a/lib/llhttp/llhttp-wasm.js +++ b/lib/llhttp/llhttp-wasm.js @@ -2,7 +2,7 @@ const { Buffer } = require('node:buffer') -const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzQzBQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEDAAADAAAABAUBcAESEgUDAQACBggBfwFBgNgECwfFBygGbWVtb3J5AgALX2luaXRpYWxpemUACBlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQACRhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUANgxsbGh0dHBfYWxsb2MACwZtYWxsb2MAOAtsbGh0dHBfZnJlZQAMBGZyZWUADA9sbGh0dHBfZ2V0X3R5cGUADRVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADhVsbGh0dHBfZ2V0X2h0dHBfbWlub3IADxFsbGh0dHBfZ2V0X21ldGhvZAAQFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAERJsbGh0dHBfZ2V0X3VwZ3JhZGUAEgxsbGh0dHBfcmVzZXQAEw5sbGh0dHBfZXhlY3V0ZQAUFGxsaHR0cF9zZXR0aW5nc19pbml0ABUNbGxodHRwX2ZpbmlzaAAWDGxsaHR0cF9wYXVzZQAXDWxsaHR0cF9yZXN1bWUAGBtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGRBsbGh0dHBfZ2V0X2Vycm5vABoXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AGxdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAcFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB0RbGxodHRwX2Vycm5vX25hbWUAHhJsbGh0dHBfbWV0aG9kX25hbWUAHxJsbGh0dHBfc3RhdHVzX25hbWUAIBpsbGh0dHBf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a0E+aiECCyABIAI2AhwgAUIANwIQIAJBAnRBsNYAaiEAAkBBhNQAKAIAIgNBASACdCIHcUUEQCAAIAE2AgBBhNQAIAMgB3I2AgAgASAANgIYIAEgATYCCCABIAE2AgwMAQsgBEEZIAJBAXZrQQAgAkEfRxt0IQIgACgCACEAAkADQCAAIgMoAgRBeHEgBEYNASACQR12IQAgAkEBdCECIAMgAEEEcWpBEGoiBygCACIADQALIAcgATYCACABIAM2AhggASABNgIMIAEgATYCCAwBCyADKAIIIgAgATYCDCADIAE2AgggAUEANgIYIAEgAzYCDCABIAA2AggLQaDUAEGg1AAoAgBBAWsiAEF/IAAbNgIACwsHACAALQAoCwcAIAAtACoLBwAgAC0AKwsHACAALQApCwcAIAAvATQLBwAgAC0AMAtAAQR/IAAoAhghASAALwEuIQIgAC0AKCEDIAAoAjghBCAAEDcgACAENgI4IAAgAzoAKCAAIAI7AS4gACABNgIYC8X4AQIHfwN+IAEgAmohBAJAIAAiAygCDCIADQAgAygCBARAIAMgATYCBAsjAEEQayIJJAACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJ/AkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCACDtQBAAECAwQFBgcICQoLDA0ODxARFBUWFxgZGhscHR4fICEjJCUnKCmIA4cDhQOEA/wC9QLuAusC6ALmAuMC4ALfAt0C2wLWAtUC1ALTAtICygLJAsgCxwLGAsUCxALDAr0CvAK6ArkCuAK3ArYCtQK0ArICsQKsAqoCqAKnAqYCpQKkAqMCogKhAqACnwKbApoCmQKYApcCkAKIAoQCgwKCAvkB9gH1AfQB8wHyAfEB8AHvAe0B6wHoAeMB4QHgAd8B3gHdAdwB2wHaAdkB2AHX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QEBAQEBAQEBAQEBAAEALQQAhAiADQQA2AhwgA0G3CzYCECADQQI2AgwgAyABQQFqNgIUDIEDCyADQS9qIQIDQCABIARGBEBBISECDIIDCwJAAkACQCABLQAAIgBBCWsOGAIAKioBKioqKioqKioqKioqKioqKioqAigLIAFBAWohASADQS9qLQAAQQFxRQ0LDBkLIAFBAWohAQwYCyABQQFqIQEgAi0AAEECcQ0AC0EAIQIgA0EANgIcIAMgATYCFCADQc4UNgIQIANBDDYCDAyAAwsgAUEBaiEBC0EAIQACQCADKAI4IgJFDQAgAigCVCICRQ0AIAMgAhEAACEACyAADQEM0QILIANCADcDIAw8CyAAQRVGBEAgA0EkNgIcIAMgATYCFCADQYYaNgIQIANBFTYCDEEAIQIM/QILQQAhAiADQQA2AhwgAyABNgIUIANB4g02AhAgA0EUNgIMDPwCCyADKAIEIQBBACECIANBADYCBCADIAAgASAMp2oiARAxIgBFDSsgA0EHNgIcIAMgATYCFCADIAA2AgwM+wILIAMtAC5BwABxRQ0BC0EAIQACQCADKAI4IgJFDQAgAigCUCICRQ0AIAMgAhEAACEACyAARQ0rIABBFUYEQCADQQo2AhwgAyABNgIUIANB8Rg2AhAgA0EVNgIMQQAhAgz6AgtBACECIANBADYCHCADIAE2AhQgA0GLDDYCECADQRM2AgwM+QILQQAhAiADQQA2AhwgAyABNgIUIANBsRQ2AhAgA0ECNgIMDPgCC0EAIQIgA0EANgIcIAMgATYCFCADQYwUNgIQIANBGTYCDAz3AgtBACECIANBADYCHCADIAE2AhQgA0HRHDYCECADQRk2AgwM9gILIABBFUYNPUEAIQIgA0EANgIcIAMgATYCFCADQaIPNgIQIANBIjYCDAz1AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQMiIARQ0oIANBDTYCHCADIAE2AhQgAyAANgIMDPQCCyAAQRVGDTpBACECIANBADYCHCADIAE2AhQgA0GiDzYCECADQSI2AgwM8wILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDIiAEUEQCABQQFqIQEMKAsgA0EONgIcIAMgADYCDCADIAFBAWo2AhQM8gILIABBFUYNN0EAIQIgA0EANgIcIAMgATYCFCADQaIPNgIQIANBIjYCDAzxAgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQMiIARQRAIAFBAWohAQwnCyADQQ82AhwgAyAANgIMIAMgAUEBajYCFAzwAgtBACECIANBADYCHCADIAE2AhQgA0HoFjYCECADQRk2AgwM7wILIABBFUYNM0EAIQIgA0EANgIcIAMgATYCFCADQc4MNgIQIANBIzYCDAzuAgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQMyIARQ0lIANBETYCHCADIAE2AhQgAyAANgIMDO0CCyAAQRVGDTBBACECIANBADYCHCADIAE2AhQgA0HODDYCECADQSM2AgwM7AILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDMiAEUEQCABQQFqIQEMJQsgA0ESNgIcIAMgADYCDCADIAFBAWo2AhQM6wILIANBL2otAABBAXFFDQELQRUhAgzPAgtBACECIANBADYCHCADIAE2AhQgA0HoFjYCECADQRk2AgwM6AILIABBO0cNACABQQFqIQEMDAtBACECIANBADYCHCADIAE2AhQgA0GYFzYCECADQQI2AgwM5gILIABBFUYNKEEAIQIgA0EANgIcIAMgATYCFCADQc4MNgIQIANBIzYCDAzlAgsgA0EUNgIcIAMgATYCFCADIAA2AgwM5AILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDMiAEUEQCABQQFqIQEM3AILIANBFTYCHCADIAA2AgwgAyABQQFqNgIUDOMCCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDNoCCyADQRc2AhwgAyAANgIMIAMgAUEBajYCFAziAgsgAEEVRg0jQQAhAiADQQA2AhwgAyABNgIUIANBzgw2AhAgA0EjNgIMDOECCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDB0LIANBGTYCHCADIAA2AgwgAyABQQFqNgIUDOACCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDNYCCyADQRo2AhwgAyAANgIMIAMgAUEBajYCFAzfAgsgAEEVRg0fQQAhAiADQQA2AhwgAyABNgIUIANBog82AhAgA0EiNgIMDN4CCyADKAIEIQBBACEC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DLMCCyABIARGBEBB6QEhAgyzAgsgAS0AAEHIAEYNASADQQE6ACgLQbYBIQIMlwILQcoBIQIMlgILIAEgBEcEQCADQQw2AgggAyABNgIEQckBIQIMlgILQegBIQIMrwILIAEgBEYEQEHnASECDK8CCyABLQAAQcgARw0EIAFBAWohAUHIASECDJQCCyABIARGBEBB5gEhAgyuAgsCQAJAIAEtAABBxQBrDhAABQUFBQUFBQUFBQUFBQUBBQsgAUEBaiEBQcYBIQIMlAILIAFBAWohAUHHASECDJMCC0HlASECIAEgBEYNrAIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB99MAai0AAEcNAyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMrQILIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAtIgBFBEBB1AEhAgyTAgsgA0HkATYCHCADIAE2AhQgAyAANgIMQQAhAgysAgtB4wEhAiABIARGDasCIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQfXTAGotAABHDQIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADKwCCyADQYEEOwEoIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAtIgANAwwCCyADQQA2AgALQQAhAiADQQA2AhwgAyABNgIUIANB0B42AhAgA0EINgIMDKkCC0HFASECDI4CCyADQeIBNgIcIAMgATYCFCADIAA2AgxBACECDKcCC0EAIQACQCADKAI4IgJFDQAgAigCOCICRQ0AIAMgAhEAACEACyAARQ1lIABBFUcEQCADQQA2AhwgAyABNgIUIANB1A42AhAgA0EgNgIMQQAhAgynAgsgA0GFATYCHCADIAE2AhQgA0HXGjYCECADQRU2AgxBACECDKYCC0HhASECIAQgASIARg2lAiAEIAFrIAMoAgAiAWohBSAAIAFrQQRqIQYCQANAIAAtAAAgAUHw0wBqLQAARw0BIAFBBEYNAyABQQFqIQEgBCAAQQFqIgBHDQALIAMgBTYCAAymAgsgA0EANgIcIAMgADYCFCADQYQ3NgIQIANBCDYCDCADQQA2AgBBACECDKUCCyABIARHBEAgA0ENNgIIIAMgATYCBEHCASECDIsCC0HgASECDKQCCyADQQA2AgAgBkEBaiEBC0HDASECDIgCCyABIARGBEBB3wEhAgyiAgsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFBwQEhAgyIAgsgAygCBCEAIANBADYCBCADIAAgARAuIgBFDYgCIANB3gE2AhwgAyABNgIUIAMgADYCDEEAIQIMoQILIAEgBEYEQEHdASECDKECCwJAIAEtAABBLkYEQCABQQFqIQEMAQsgAygCBCEAIANBADYCBCADIAAgARAuIgBFDYkCIANB3AE2AhwgAyABNgIUIAMgADYCDEEAIQIMoQILQcABIQIMhgILIAEgBEYEQEHbASECDKACC0EAIQBBASEFQQEhB0EAIQICQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQCABLQAAQTBrDgoKCQABAgMEBQYICwtBAgwGC0EDDAULQQQMBAtBBQwDC0EGDAILQQcMAQtBCAshAkEAIQVBACEHDAILQQkhAkEBIQBBACEFQQAhBwwBC0EAIQVBASECCyADIAI6ACsgAUEBaiEBAkACQCADLQAuQRBxDQACQAJAAkAgAy0AKg4DAQACBAsgB0UNAwwCCyAADQEMAgsgBUUNAQsgAygCBCEAIANBADYCBCADIAAgARAuIgBFDQIgA0HYATYCHCADIAE2AhQgAyAANgIMQQAhAgyiAgsgAygCBCEAIANBADYCBCADIAAgARAuIgBFDYsCIANB2QE2AhwgAyABNgIUIAMgADYCDEEAIQIMoQILIAMoAgQhACADQQA2AgQgAyAAIAEQLiIARQ2JAiADQdoBNgIcIAMgATYCFCADIAA2AgwMoAILQb8BIQIMhQILQQAhAAJAIAMoAjgiAkUNACACKAI8IgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANBnA02AhAgA0EhNgIMQQAhAgygAgtBvgEhAgyFAgsgA0HXATYCHCADIAE2AhQgA0HWGTYCECADQRU2AgxBACECDJ4CCyABIARGBEBB1wEhAgyeAgsCQCABLQAAQSBGBEAgA0EAOwE0IAFBAWohAQwBCyADQQA2AhwgAyABNgIUIANB6xA2AhAgA0EJNgIMQQAhAgyeAgtBvQEhAgyDAgsgASAERgRAQdYBIQIM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LgsgAUEBaiEBQZcBIQIM2AELIAFBAWohAUEiDCsLIAEgBEYEQEGvASECDPEBCyABLQAAQdAARw0rIAFBAWohAUGWASECDNYBCyABIARGBEBBrgEhAgzwAQsCQAJAIAEtAABBxgBrDgsALCwsLCwsLCwsASwLIAFBAWohAUGUASECDNYBCyABQQFqIQFBlQEhAgzVAQtBrQEhAiABIARGDe4BIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQazTAGotAABHDSggAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADO8BCyADQQA2AgAgBkEBaiEBQQ0MKAtBrAEhAiABIARGDe0BIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQeHTAGotAABHDScgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADO4BCyADQQA2AgAgBkEBaiEBQQwMJwtBqwEhAiABIARGDewBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQarTAGotAABHDSYgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADO0BCyADQQA2AgAgBkEBaiEBQQMMJgtBqgEhAiABIARGDesBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQajTAGotAABHDSUgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADOwBCyADQQA2AgAgBkEBaiEBQSYMJQsgASAERgRAQakBIQIM6wELAkACQCABLQAAQdQAaw4CAAEnCyABQQFqIQFBjwEhAgzRAQsgAUEBaiEBQZABIQIM0AELQagBIQIgASAERg3pASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGm0wBqLQAARw0jIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzqAQsgA0EANgIAIAZBAWohAUEnDCMLQacBIQIgASAERg3oASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGk0wBqLQAARw0iIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzpAQsgA0EANgIAIAZBAWohAUEcDCILQaYBIQIgASAERg3nASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGe0wBqLQAARw0hIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzoAQsgA0EANgIAIAZBAWohAUEGDCELQaUBIQIgASAERg3mASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEGZ0wBqLQAARw0gIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAznAQsgA0EANgIAIAZBAWohAUEZDCALIAEgBEYEQEGkASECDOYBCwJAAkACQAJAIAEtAABBLWsOIwAkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJAEkJCQkJAIkJCQDJAsgAUEBaiEBQYQBIQIMzgELIAFBAWohAUGFASECDM0BCyABQQFqIQFBigEhAgzMAQsgAUEBaiEBQYsBIQIMywELQaMBIQIgASAERg3kASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGX0wBqLQAARw0eIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzlAQsgA0EANgIAIAZBAWohAUELDB4LIAEgBEYEQEGiASECDOQBCwJAAkAgAS0AAEHBAGsOAwAgASALIAFBAWohAUGGASECDMoBCyABQQFqIQFBiQEhAgzJAQsgASAERgRAQaEBIQIM4wELAkACQCABLQAAQcEAaw4PAB8fHx8fHx8fHx8fHx8BHwsgAUEBaiEBQYcBIQIMyQELIAFBAWohAUGIASECDMgBCyABIARGBEBBoAEhAgziAQsgAS0AAEHMAEcNHCABQQFqIQFBCgwbC0GfASECIAEgBEYN4AEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBkdMAai0AAEcNGiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4QELIANBADYCACAGQQFqIQFBHgwaC0GeASECIAEgBEYN3wEgAygCACIAIAQgAWtqIQUgASAAa0EGaiEGAkADQCABLQAAIABBitMAai0AAEcNGSAAQQZGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4AELIANBADYCACAGQQFqIQFBFQwZC0GdASECIAEgBEYN3gEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBh9MAai0AAEcNGCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM3wELIANBADYCACAGQQFqIQFBFwwYC0GcASECIAEgBEYN3QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBgdMAai0AAEcNFyAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM3gELIANBADYCACAGQQFqIQFBGAwXCyABIARGBEBBmwEhAgzdAQsCQAJAIAEtAABByQBrDgcAGRkZGRkBGQsgAUEBaiEBQYEBIQIMwwELIAFBAWohAUGCASECDMIBC0GaASECIAEgBEYN2wEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB5tMAai0AAEcNFSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM3AELIANBADYCACAGQQFqIQFBCQwVC0GZASECIAEgBEYN2gEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB5NMAai0AAEcNFCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM2wELIANBADYCACAGQQFqIQFBHwwUC0GYASECIAEgBEYN2QEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB/tIAai0AAEcNEyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM2gELIANBADYCACAGQQFqIQFBAgwTC0GXASECIAEgBEYN2AEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGA0AgAS0AACAAQfzSAGotAABHDREgAEEBRg0CIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNgBCyABIARGBEBBlgEhAgzYAQtBASABLQAAQd8ARw0RGiABQQFqIQFB/QAhAgy9AQsgA0EANgIAIAZBAWohAUH+ACECDLwBC0GVASECIAEgBEYN1QEgAygCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABBxNMAai0AAEcNDyAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM1gELIANBADYCACAGQQFqIQFBKQwPC0GUASECIAEgBEYN1AEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABB+NIAai0AAEcNDiAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM1QELIANBADYCACAGQQFqIQFBLQwOCyABIARGBEBBkwEhAgzUAQsgAS0AAEHFAEcNDiABQQFqIQFB+gAhAgy5AQsgASAERgRAQZIBIQIM0wELAkACQCABLQAAQcwAaw4IAA8PDw8PDwEPCyABQQFqIQFB+AAhAgy5AQsgAUEBaiEBQfkAIQIMuAELQZEBIQIgASAERg3RASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEHz0gBqLQAARw0LIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzSAQsgA0EANgIAIAZBAWohAUEjDAsLQZABIQIgASAERg3QASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHw0gBqLQAARw0KIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzRAQsgA0EANgIAIAZBAWohAUEADAoLIAEgBEYEQEGPASECDNABCwJAAkAgAS0AAEHIAGsOCAAMDAwMDAwBDAsgAUEBaiEBQfMAIQIMtgELIAFBAWohAUH2ACECDLUBCyABIARGBEBBjgEhAgzPAQsCQAJAIAEtAABBzgBrDgMACwELCyABQQFqIQFB9AAhAgy1AQsgAUEBaiEBQfUAIQIMtAELIAEgBEYEQEGNASECDM4BCyABLQAAQdkARw0IIAFBAWohAUEIDAcLQYwBIQIgASAERg3MASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEHs0gBqLQAARw0GIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzNAQsgA0EANgIAIAZBAWohAUEFDAYLQYsBIQIgASAERg3LASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHm0gBqLQAARw0FIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzMAQsgA0EANgIAIAZBAWohAUEWDAULQYoBIQIgASAERg3KASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHh0wBqLQAARw0EIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzLAQsgA0EANgIAIAZBAWohAUEQDAQLIAEgBEYEQEGJASECDMoBCwJAAkAgAS0AAEHDAGsODAAGBgYGBgYGBgYGAQYLIAFBAWohAUHvACECDLABCyABQQFqIQFB8AAhAgyvAQtBiAEhAiABIARGDcgBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQeDSAGotAABHDQIgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMkBCyADQQA2AgAgBkEBaiEBQSQMAgsgA0EANgIADAILIAEgBEYEQEGHASECDMcBCyABLQAAQcwARw0BIAFBAWohAUETCzoAKSADKAIEIQAgA0EANgIEIAMgACABEC0iAA0CDAELQQAhAiADQQA2AhwgAyABNgIUIANB6R42AhAgA0EGNgIMDMQBC0HuACECDKkBCyADQYYBNgIcIAMgATYCFCADIAA2AgxBACECDMIBC0EAIQACQCADKAI4IgJFDQAgAigCOCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUYNASADQQA2AhwgAyABNgIUIANB1A42AhAgA0EgNgIMQQAhAgzBAQtB7QAhAgymAQsgA0GFATYCHCADIAE2AhQgA0HXGjYCECADQRU2AgxBACECDL8BCyABIARGBEBBhQEhAgy/AQsCQCABLQAAQSBGBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GGHjYCECADQQY2AgxBACECDL8BC0ECIQIMpAELA0AgAS0AAEEgRw0CIAQgAUEBaiIBRw0AC0GEASECDL0BCyABIARGBEBBgwEhAgy9AQsCQCABLQAAQQlrDgRAAABAAAtB6wAhAgyiAQsgAy0AKUEFRgRAQewAIQIMogELQeoAIQIMoQELIAEgBEYEQEGCASECDLsBCyADQQ82AgggAyABNgIEDAoLIAEgBEYEQEGBASECDLoBCwJAIAEtAABBCWsOBD0AAD0AC0HpACECDJ8BCyABIARHBEAgA0EPNgIIIAMgATYCBEHnACECDJ8BC0GAASECDLgBCwJAIAEgBEcEQANAIAEtAABB4M4Aai0AACIAQQNHBEACQCAAQQFrDgI/AAQLQeYAIQIMoQELIAQgAUEBaiIBRw0AC0H+ACECDLkBC0H+ACECDLgBCyADQQA2AhwgAyABNgIUIANBxh82AhAgA0EHNgIMQQAhAgy3AQsgASAERgRAQf8AIQIMtwELAkACQAJAIAEtAABB4NAAai0AAEEBaw4DPAIAAQtB6AAhAgyeAQsgA0EANgIcIAMgATYCFCADQYYSNgIQIANBBzYCDEEAIQIMtwELQeAAIQIMnAELIAEgBEcEQCABQQFqIQFB5QAhAgycAQtB/QAhAgy1AQsgBCABIgBGBEBB/AAhAgy1AQsgAC0AACIBQS9GBEAgAEEBaiEBQeQAIQIMmwELIAFBCWsiAkEXSw0BIAAhAUEBIAJ0QZuAgARxDTcMAQsgBCABIgBGBEBB+wAhAgy0AQsgAC0AAEEvRw0AIABBAWohAQwDC0EAIQIgA0EANgIcIAMgADYCFCADQcYfNgIQIANBBzYCDAyyAQsCQAJAAkACQAJAA0AgAS0AAEHgzABqLQAAIgBBBUcEQAJAAkAgAEEBaw4IPQUGBwgABAEIC0HhACECDJ8BCyABQQFqIQFB4wAhAgyeAQsgBCABQQFqIgFHDQALQfoAIQIMtgELIAFBAWoMFAsgAygCBCEAIANBADYCBCADIAAgARArIgBFDR4gA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgy0AQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDR4gA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyzAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDR4gA0HwADYCHCADIAE2AhQgAyAANgIMQQAhAgyyAQsgA0EANgIcIAMgATYCFCADQcsPNgIQIANBBzYCDEEAIQIMsQELIAEgBEYEQEH5ACECDLEBCwJAIAEtAABB4MwAai0AAEEBaw4INAQFBgAIAgMHCyABQQFqIQELQQMhAgyVAQsgAUEBagwNC0EAIQIgA0EANgIcIANBoxI2AhAgA0EHNgIMIAMgAUEBajYCFAytAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDRYgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgysAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDRYgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyrAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDRYgA0HwADYCHCADIAE2AhQgAyAANgIMQQAhAgyqAQsgA0EANgIcIAMgATYCFCADQcsPNgIQIANBBzYCDEEAIQIMqQELQeIAIQIMjgELIAEgBEYEQEH4ACECDKgBCyABQQFqDAILIAEgBEYEQEH3ACECDKcBCyABQQFqDAELIAEgBEYNASABQQFqCyEBQQQhAgyKAQtB9gAhAgyjAQsDQCABLQAAQeDKAGotAAAiAEECRwRAIABBAUcEQEHfACECDIsBCwwnCyAEIAFBAWoiAUcNAAtB9QAhAgyiAQsgASAERgRAQfQAIQIMogELAkAgAS0AAEEJaw43JQMGJQQGBgYGBgYGBgYGBgYGBgYGBgYFBgYCBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAAYLIAFBAWoLIQFBBSECDIYBCyABQQFqDAYLIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ0IIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMngELIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ0IIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMnQELIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ0IIANB8AA2AhwgAyABNgIUIAMgADYCDEEAIQIMnAELIANBADYCHCADIAE2AhQgA0G8EzYCECADQQc2AgxBACECDJsBCwJAAkACQAJAA0AgAS0AAEHgyABqLQAAIgBBBUcEQAJAIABBAWsOBiQDBAUGAAYLQd4AIQIMhgELIAQgAUEBaiIBRw0AC0HzACECDJ4BCyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNByADQdsANgIcIAMgATYCFCADIAA2AgxBACECDJ0BCyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNByADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDJwBCyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNByADQfAANgIcIAMgATYCFCADIAA2AgxBACECDJsBCyADQQA2AhwgAyABNgIUIANB3Ag2AhAgA0EHNgIMQQAhAgyaAQsgASAERg0BIAFBAWoLIQFBBiECDH4LQfIAIQIMlwELAkACQAJAAkADQCABLQAAQeDGAGotAAAiAEEFRwRAIABBAWsOBB8CAwQFCyAEIAFBAWoiAUcNAAtB8QAhAgyaAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDQMgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgyZAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDQMgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyYAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDQMgA0HwADYCHCADIAE2AhQgAyAANgIMQQAhAgyXAQsgA0EANgIcIAMgATYCFCADQbQKNgIQIANBBzYCDEEAIQIMlgELQc4AIQIMewtB0AAhAgx6C0HdACECDHkLIAEgBEYEQEHwACECDJMBCwJAIAEtAABBCWsOBBYAABYACyABQQFqIQFB3AAhAgx4CyABIARGBEBB7wAhAgySAQsCQCABLQAAQQlrDgQVAAAVAAtBACEAAkAgAygCOCICRQ0AIAIoAjAiAkUNACADIAIRAAAhAAsgAEUEQEHTASECDHgLIABBFUcEQCADQQA2AhwgAyABNgIUIANBwQ02AhAgA0EaNgIMQQAhAgySAQsgA0HuADYCHCADIAE2AhQgA0HwGTYCECADQRU2AgxBACECDJEBC0HtACECIAEgBEYNkAEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABB18YAai0AAEcNBCAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMkQELIANBADYCACAGQQFqIQEgAy0AKSIAQSNrQQtJDQQCQCAAQQZLDQBBASAAdEHKAHFFDQAMBQtBACECIANBADYCHCADIAE2AhQgA0HlCTYCECADQQg2AgwMkAELQewAIQIgASAERg2PASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHUxgBqLQAARw0DIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyQAQsgA0EANgIAIAZBAWohASADLQApQSFGDQMgA0EANgIcIAMgATYCFCADQYkKNgIQIANBCDYCDEEAIQIMjwELQesAIQIgASAERg2OASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEHQxgBqLQAARw0CIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyPAQsgA0EANgIAIAZBAWohASADLQApIgBBI0kNAiAAQS5GDQIgA0EANgIcIAMgATYCFCADQcEJNgIQIANBCDYCDEEAIQIMjgELIANBADYCAAtBACECIANBADYCHCADIAE2AhQgA0GENzYCECADQQg2AgwMjAELQdgAIQIMcQsgASAERwRAIANBDTYCCCADIAE2AgRB1wAhAgxxC0HqACECDIoBCyABIARGBEBB6QAhAgyKAQsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFB1gAhAgxwCyADKAIEIQAgA0EANgIEIAMgACABEC4iAEUNdCADQegANgIcIAMgATYCFCADIAA2AgxBACECDIkBCyABIARGBEBB5wAhAgyJAQsCQCABLQAAQS5GBEAgAUEBaiEBDAELIAMoAgQhACADQQA2AgQgAyAAIAEQLiIARQ11IANB5gA2AhwgAyABNgIUIAMgADYCDEEAIQIMiQELQdUAIQIMbgsgASAERgRAQeUAIQIMiAELQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC4iAEUNAiADQeIANgIcIAMgATYCFCADIAA2AgxBACECDIoBCyADKAIEIQAgA0EANgIEIAMgACABEC4iAEUNdyADQeMANgIcIAMgATYCFCADIAA2AgxBACECDIkBCyADKAIEIQAgA0EANgIEIAMgACABEC4iAEUNdSADQeQANgIcIAMgATYCFCADIAA2AgwMiAELQdMAIQIMbQsgAy0AKUEiRg2AAUHSACECDGwLQQAhAAJAIAMoAjgiAkUNACACKAI8IgJFDQAgAyACEQAAIQALIABFBEBB1AAhAgxsCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQZwNNgIQIANBITYCDEEAIQIMhgELIANB4QA2AhwgAyABNgIUIANB1hk2AhAgA0EVNgIMQQAhAgyFAQsgASAERgRAQeAAIQIMhQELAkACQAJAAkACQCABLQAAQQprDgQBBAQABAsgAUEBaiEBDAELIAFBAWohASADQS9qLQAAQQFxRQ0BC0HRACECDGwLIANBADYCHCADIAE2AhQgA0GIETYCECADQQk2AgxBACECDIUBCyADQQA2AhwgAyABNgIUIANBiBE2AhAgA0EJNgIMQQAhAgyEAQsgASAERgRAQd8AIQIMhAELIAEtAABBCkYEQCABQQFqIQEMCQsgAy0ALkHAAHENCCADQQA2AhwgAyABNgIUIANBiBE2AhAgA0ECNgIMQQAhAgyDAQsgASAERgRAQd0AIQIMgwELIAEtAAAiAkENRgRAIAFBAWohAUHPACECDGkLIAEhACACQQlrDgQFAQEFAQsgBCABIgBGBEBB3AAhAgyCAQsgAC0AAEEKRw0AIABBAWoMAgtBACECIANBADYCHCADIAA2AhQgA0G1LDYCECADQQc2AgwMgAELIAEgBEYEQEHbACECDIABCwJAIAEtAABBCWsOBAMAAAMACyABQQFqCyEBQc0AIQIMZAsgASAERgRAQdoAIQIMfgsgAS0AAEEJaw4EAAEBAAELQQAhAiADQQA2AhwgA0HsETYCECADQQc2AgwgAyABQQFqNgIUDHwLIANBgBI7ASpBACEAAkAgAygCOCICRQ0AIAIoAjAiAkUNACADIAIRAAAhAAsgAEUNACAAQRVHDQEgA0HZADYCHCADIAE2AhQgA0HwGTYCECADQRU2AgxBACECDHsLQcwAIQIMYAsgA0EANgIcIAMgATYCFCADQcENNgIQIANBGjYCDEEAIQIMeQsgASAERgRAQdkAIQIMeQsgAS0AAEEgRw06IAFBAWohASADLQAuQQFxDTogA0EANgIcIAMgATYCFCADQa0bNgIQIANBHjYCDEEAIQIMeAsgASAERgRAQdgAIQIMeAsCQAJAAkACQAJAIAEtAAAiAEEKaw4EAgMDAAELIAFBAWohAUErIQIMYQsgAEE6Rw0BIANBADYCHCADIAE2AhQgA0G5ETYCECADQQo2AgxBACECDHoLIAFBAWohASADQS9qLQAAQQFxRQ1tIAMtADJBgAFxRQRAIANBMmohAiADEDRBACEAAkAgAygCOCIGRQ0AIAYoAiQiBkUNACADIAYRAAAhAAsCQAJAIAAOFkpJSAEBAQEBAQEBAQEBAQEBAQEBAQABCyADQSk2AhwgAyABNgIUIANBshg2AhAgA0EVNgIMQQAhAgx7CyADQQA2AhwgAyABNgIUIANB3Qs2AhAgA0ERNgIMQQAhAgx6C0EAIQACQCADKAI4IgJFDQAgAigCVCICRQ0AIAMgAhEAACEACyAARQ1VIABBFUcNASADQQU2AhwgAyABNgIUIANBhho2AhAgA0EVNgIMQQAhAgx5C0HKACECDF4LQQAhAiADQQA2AhwgAyABNgIUIANB4g02AhAgA0EUNgIMDHcLIAMgAy8BMkGAAXI7ATIMOAsgASAERwRAIANBEDYCCCADIAE2AgRByQAhAgxcC0HXACECDHULIAEgBEYEQEHWACECDHULAkACQAJAAkAgAS0AACIAQSByIAAgAEHBAGtB/wFxQRpJG0H/AXFB4wBrDhMAPT09PT09PT09PT09AT09PQIDPQsgAUEBaiEBQcUAIQIMXQsgAUEBaiEBQcYAIQIMXAsgAUEBaiEBQccAIQIMWwsgAUEBaiEBQcgAIQIMWgtB1QAhAiAEIAEiAEYNcyAEIAFrIAMoAgAiAWohBiAAIAFrQQVqIQcDQCABQcDGAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQhBBCABQQVGDQoaIAFBAWohASAEIABBAWoiAEcNAAsgAyAGNgIADHMLQdQAIQIgBCABIgBGDXIgBCABayADKAIAIgFqIQYgACABa0EPaiEHA0AgAUGwxgBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0HQQMgAUEPRg0JGiABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAxyC0HTACECIAQgASIARg1xIAQgAWsgAygCACIBaiEGIAAgAWtBDmohBwNAIAFBksYAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNBiABQQ5GDQcgAUEBaiEBIAQgAEEBaiIARw0ACyADIAY2AgAMcQtB0gAhAiAEIAEiAEYNcCAEIAFrIAMoAgAiAWohBSAAIAFrQQFqIQYDQCABQZDGAGotAAAgAC0AACIHQSByIAcgB0HBAGtB/wFxQRpJG0H/AXFHDQUgAUEBRg0CIAFBAWohASAEIABBAWoiAEcNAAsgAyAFNgIADHALIAEgBEYEQEHRACECDHALAkACQCABLQAAIgBBIHIgACAAQcEAa0H/AXFBGkkbQf8BcUHuAGsOBwA2NjY2NgE2CyABQQFqIQFBwgAhAgxWCyABQQFqIQFBwwAhAgxVCyADQQA2AgAgBkEBaiEBQcQAIQIMVAtB0AAhAiAEIAEiAEYNbSAEIAFrIAMoAgAiAWohBiAAIAFrQQlqIQcDQCABQYbGAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQJBAiABQQlGDQQaIAFBAWohASAEIABBAWoiAEcNAAsgAyAGNgIADG0LQc8AIQIgBCABIgBGDWwgBCABayADKAIAIgFqIQYgACABa0EFaiEHA0AgAUGAxgBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBBUYNAiABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAxsCyAAIQEgA0EANgIADDALQQELOgAsIANBADYCACAHQQFqIQELQSwhAgxOCwJAA0AgAS0AAEGAxABqLQAAQQFHDQEgBCABQQFqIgFHDQALQc0AIQIMaAtBwQAhAgxNCyABIARGBEBBzAAhAgxnCyABLQAAQTpGBEAgAygCBCEAIANBADYCBCADIAAgARAvIgBFDTAgA0HLADYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgxnCyADQQA2AhwgAyABNgIUIANBuRE2AhAgA0EKNgIMQQAhAgxmCwJAAkAgAy0ALEECaw4CAAEkCyADQTNqLQAAQQJxRQ0jIAMtAC5BAnENIyADQQA2AhwgAyABNgIUIANB1RM2AhAgA0ELNgIMQQAhAgxmCyADLQAyQSBxRQ0iIAMtAC5BAnENIiADQQA2AhwgAyABNgIUIANB7BI2AhAgA0EPNgIMQQAhAgxlC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQRAQcAAIQIMSwsgAEEVRwRAIANBADYCHCADIAE2AhQgA0H4DjYCECADQRw2AgxBACECDGULIANBygA2AhwgAyABNgIUIANB8Bo2AhAgA0EVNgIMQQAhAgxkCyABIARHBEADQCABLQAAQfA/ai0AAEEBRw0XIAQgAUEBaiIBRw0AC0HEACECDGQLQcQAIQIMYwsgASAERwRAA0ACQCABLQAAIgBBIHIgACAAQcEAa0H/AXFBGkkbQf8BcSIAQQlGDQAgAEEgRg0AAkACQAJAAkAgAEHjAGsOEwADAwMDAwMDAQMDAwMDAwMDAwIDCyABQQFqIQFBNSECDE4LIAFBAWohAUE2IQIMTQsgAUEBaiEBQTchAgxMCwwVCyAEIAFBAWoiAUcNAAtBPCECDGMLQTwhAgxiCyABIARGBEBByAAhAgxiCyADQRE2AgggAyABNgIEAkACQAJAAkACQCADLQAsQQFrDgQUAAECCQsgAy0AMkEgcQ0DQdEBIQIMSwsCQCADLwEyIgBBCHFFDQAgAy0AKEEBRw0AIAMtAC5BCHFFDQILIAMgAEH3+wNxQYAEcjsBMgwLCyADIAMvATJBEHI7ATIMBAsgA0EANgIEIAMgASABEDAiAARAIANBwQA2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMYwsgAUEBaiEBDFILIANBADYCHCADIAE2AhQgA0GjEzYCECADQQQ2AgxBACECDGELQccAIQIgASAERg1gIAMoAgAiACAEIAFraiEFIAEgAGtBBmohBgJAA0AgAEHwwwBqLQAAIAEtAABBIHJHDQEgAEEGRg1GIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADGELIANBADYCAAwFCwJAIAEgBEcEQANAIAEtAABB8MEAai0AACIAQQFHBEAgAEECRw0DIAFBAWohAQwFCyAEIAFBAWoiAUcNAAtBxQAhAgxhC0HFACECDGALCyADQQA6ACwMAQtBCyECDEMLQT4hAgxCCwJAAkADQCABLQAAIgBBIEcEQAJAIABBCmsOBAMFBQMACyAAQSxGDQMMBAsgBCABQQFqIgFHDQALQcYAIQIMXQsgA0EIOgAsDA4LIAMtAChBAUcNAiADLQAuQQhxDQIgAygCBCEAIANBADYCBCADIAAgARAwIgAEQCADQcIANgIcIAMgADYCDCADIAFBAWo2AhRBACECDFwLIAFBAWohAQxKC0E6IQIMQAsCQANAIAEtAAAiAEEgRyAAQQlHcQ0BIAQgAUEBaiIBRw0AC0HDACECDFoLC0E7IQIMPgsCQAJAIAEgBEcEQANAIAEtAAAiAEEgRwRAIABBCmsOBAMEBAMECyAEIAFBAWoiAUcNAAtBPyECDFoLQT8hAgxZCyADIAMvATJBIHI7ATIMCgsgAygCBCEAIANBADYCBCADIAAgARAwIgBFDUggA0E+NgIcIAMgATYCFCADIAA2AgxBACECDFcLAkAgASAERwRAA0AgAS0AAEHwwQBqLQAAIgBBAUcEQCAAQQJGDQMMDAsgBCABQQFqIgFHDQALQTchAgxYC0E3IQIMVwsgAUEBaiEBDAQLQTshAiAEIAEiAEYNVSAEIAFrIAMoAgAiAWohBiAAIAFrQQVqIQcCQANAIAFBwMYAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNASABQQVGBEBBByEBDDsLIAFBAWohASAEIABBAWoiAEcNAAsgAyAGNgIADFYLIANBADYCACAAIQEMBQtBOiECIAQgASIARg1UIAQgAWsgAygCACIBaiEGIAAgAWtBCGohBwJAA0AgAUHkP2otAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQEgAUEIRgRAQQUhAQw6CyABQQFqIQEgBCAAQQFqIgBHDQALIAMgBjYCAAxVCyADQQA2AgAgACEBDAQLQTkhAiAEIAEiAEYNUyAEIAFrIAMoAgAiAWohBiAAIAFrQQNqIQcCQANAIAFB4D9qLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBA0YEQEEGIQEMOQsgAUEBaiEBIAQgAEEBaiIARw0ACyADIAY2AgAMVAsgA0EANgIAIAAhAQwDCwJAA0AgAS0AACIAQSBHBEAgAEEKaw4EBwQEBwILIAQgAUEBaiIBRw0AC0E4IQIMUwsgAEEsRw0BIAFBAWohAEEBIQECQAJAAkACQAJAIAMtACxBBWsOBAMBAgQACyAAIQEMBAtBAiEBDAELQQQhAQsgA0EBOgAsIAMgAy8BMiABcjsBMiAAIQEMAQsgAyADLwEyQQhyOwEyIAAhAQtBPSECDDcLIANBADoALAtBOCECDDULIAEgBEYEQEE2IQIMTwsCQAJAAkACQAJAIAEtAABBCmsOBAACAgECCyADKAIEIQAgA0EANgIEIAMgACABEDAiAEUNAiADQTM2AhwgAyABNgIUIAMgADYCDEEAIQIMUgsgAygCBCEAIANBADYCBCADIAAgARAwIgBFBEAgAUEBaiEBDAYLIANBMjYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgxRCyADLQAuQQFxBEBB0AEhAgw3CyADKAIEIQAgA0EANgIEIAMgACABEDAiAA0BDEMLQTMhAgw1CyADQTU2AhwgAyABNgIUIAMgADYCDEEAIQIMTgtBNCECDDMLIANBL2otAABBAXENACADQQA2AhwgAyABNgIUIANB8RU2AhAgA0EZNgIMQQAhAgxMC0EyIQIMMQsgASAERgRAQTIhAgxLCwJAIAEtAABBCkYEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQZgWNgIQIANBAzYCDEEAIQIMSwtBMSECDDALIAEgBEYEQEExIQIMSgsgAS0AACIAQQlHIABBIEdxDQEgAy0ALEEIRw0AIANBADoALAtBPCECDC4LQQEhAgJAAkACQAJAIAMtACxBBWsOBAMBAgAKCyADIAMvATJBCHI7ATIMCQtBAiECDAELQQQhAgsgA0EBOgAsIAMgAy8BMiACcjsBMgwGCyABIARGBEBBMCECDEcLIAEtAABBCkYEQCABQQFqIQEMAQsgAy0ALkEBcQ0AIANBADYCHCADIAE2AhQgA0HHJzYCECADQQI2AgxBACECDEYLQS8hAgwrCyABQQFqIQFBMCECDCoLIAEgBEYEQEEvIQIMRAsgAS0AACIAQQlHIABBIEdxRQRAIAFBAWohASADLQAuQQFxDQEgA0EANgIcIAMgATYCFCADQekPNgIQIANBCjYCDEEAIQIMRAtBASECAkACQAJAAkACQAJAIAMtACxBAmsOBwUEBAMBAgAECyADIAMvATJBCHI7ATIMAwtBAiECDAELQQQhAgsgA0EBOgAsIAMgAy8BMiACcjsBMgtBLiECDCoLIANBADYCHCADIAE2AhQgA0GzEjYCECADQQs2AgxBACECDEMLQdIBIQIMKAsgASAERgRAQS4hAgxCCyADQQA2AgQgA0ERNgIIIAMgASABEDAiAA0BC0EtIQIMJgsgA0EtNgIcIAMgATYCFCADIAA2AgxBACECDD8LQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB2AA2AhwgAyABNgIUIANBnho2AhAgA0EVNgIMQQAhAgw+C0HLACECDCMLIANBADYCHCADIAE2AhQgA0GFDjYCECADQR02AgxBACECDDwLIAEgBEYEQEHOACECDDwLIAEtAAAiAEEgRg0CIABBOkYNAQsgA0EAOgAsQQkhAgwgCyADKAIEIQAgA0EANgIEIAMgACABEC8iAA0BDAILIAMtAC5BAXEEQEHPASECDB8LIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ0CIANBKjYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgw4CyADQcsANgIcIAMgADYCDCADIAFBAWo2AhRBACECDDcLIAFBAWohAUE/IQIMHAsgAUEBaiEBDCkLIAEgBEYEQEErIQIMNQsCQCABLQAAQQpGBEAgAUEBaiEBDAELIAMtAC5BwABxRQ0GCyADLQAyQYABcQRAQQAhAAJAIAMoAjgiAkUNACACKAJUIgJFDQAgAyACEQAAIQALIABFDREgAEEVRgRAIANBBTYCHCADIAE2AhQgA0GGGjYCECADQRU2AgxBACECDDYLIANBADYCHCADIAE2AhQgA0HiDTYCECADQRQ2AgxBACECDDULIANBMmohAiADEDRBACEAAkAgAygCOCIGRQ0AIAYoAiQiBkUNACADIAYRAAAhAAsgAA4WAgEABAQEBAQEBAQEBAQEBAQEBAQEAwQLIANBAToAMAsgAiACLwEAQcAAcjsBAAtBKiECDBcLIANBKTYCHCADIAE2AhQgA0GyGDYCECADQRU2AgxBACECDDALIANBADYCHCADIAE2AhQgA0HdCzYCECADQRE2AgxBACECDC8LIANBADYCHCADIAE2AhQgA0GdCzYCECADQQI2AgxBACECDC4LQQEhByADLwEyIgVBCHFFBEAgAykDIEIAUiEHCwJAIAMtADAEQEEBIQAgAy0AKUEFRg0BIAVBwABxRSAHcUUNAQsCQCADLQAoIgJBAkYEQEEBIQAgAy8BNCIGQeUARg0CQQAhACAFQcAAcQ0CIAZB5ABGDQIgBkHmAGtBAkkNAiAGQcwBRg0CIAZBsAJGDQIMAQtBACEAIAVBwABxDQELQQIhACAFQQhxDQAgBUGABHEEQAJAIAJBAUcNACADLQAuQQpxDQBBBSEADAILQQQhAAwBCyAFQSBxRQRAIAMQNUEAR0ECdCEADAELQQBBAyADKQMgUBshAAsCQCAAQQFrDgUAAQYHAgMLQQAhAgJAIAMoAjgiAEUNACAAKAIsIgBFDQAgAyAAEQAAIQILIAJFDSYgAkEVRgRAIANBAzYCHCADIAE2AhQgA0G9GjYCECADQRU2AgxBACECDC4LQQAhAiADQQA2AhwgAyABNgIUIANBrw42AhAgA0ESNgIMDC0LQc4BIQIMEgtBACECIANBADYCHCADIAE2AhQgA0HkHzYCECADQQ82AgwMKwtBACEAAkAgAygCOCICRQ0AIAIoAiwiAkUNACADIAIRAAAhAAsgAA0BC0EOIQIMDwsgAEEVRgRAIANBAjYCHCADIAE2AhQgA0G9GjYCECADQRU2AgxBACECDCkLQQAhAiADQQA2AhwgAyABNgIUIANBrw42AhAgA0ESNgIMDCgLQSkhAgwNCyADQQE6ADEMJAsgASAERwRAIANBCTYCCCADIAE2AgRBKCECDAwLQSYhAgwlCyADIAMpAyAiDCAEIAFrrSIKfSILQgAgCyAMWBs3AyAgCiAMVARAQSUhAgwlCyADKAIEIQBBACECIANBADYCBCADIAAgASAMp2oiARAxIgBFDQAgA0EFNgIcIAMgATYCFCADIAA2AgwMJAtBDyECDAkLIAEgBEYEQEEjIQIMIwtCACEKAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAEtAABBMGsONxcWAAECAwQFBgcUFBQUFBQUCAkKCwwNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQODxAREhMUC0ICIQoMFgtCAyEKDBULQgQhCgwUC0IFIQoMEwtCBiEKDBILQgchCgwRC0IIIQoMEAtCCSEKDA8LQgohCgwOC0ILIQoMDQtCDCEKDAwLQg0hCgwLC0IOIQoMCgtCDyEKDAkLQgohCgwIC0ILIQoMBwtCDCEKDAYLQg0hCgwFC0IOIQoMBAtCDyEKDAMLQQAhAiADQQA2AhwgAyABNgIUIANBzhQ2AhAgA0EMNgIMDCILIAEgBEYEQEEiIQIMIgtCACEKAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCABLQAAQTBrDjcVFAABAgMEBQYHFhYWFhYWFggJCgsMDRYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWDg8QERITFgtCAiEKDBQLQgMhCgwTC0IEIQoMEgtCBSEKDBELQgYhCgwQC0IHIQoMDwtCCCEKDA4LQgkhCgwNC0IKIQoMDAtCCyEKDAsLQgwhCgwKC0INIQoMCQtCDiEKDAgLQg8hCgwHC0IKIQoMBgtCCyEKDAULQgwhCgwEC0INIQoMAwtCDiEKDAILQg8hCgwBC0IBIQoLIAFBAWohASADKQMgIgtC//////////8PWARAIAMgC0IEhiAKhDcDIAwCC0EAIQIgA0EANgIcIAMgATYCFCADQa0JNgIQIANBDDYCDAwfC0ElIQIMBAtBJiECDAMLIAMgAToALCADQQA2AgAgB0EBaiEBQQwhAgwCCyADQQA2AgAgBkEBaiEBQQohAgwBCyABQQFqIQFBCCECDAALAAtBACECIANBADYCHCADIAE2AhQgA0HVEDYCECADQQk2AgwMGAtBACECIANBADYCHCADIAE2AhQgA0HXCjYCECADQQk2AgwMFwtBACECIANBADYCHCADIAE2AhQgA0G/EDYCECADQQk2AgwMFgtBACECIANBADYCHCADIAE2AhQgA0GkETYCECADQQk2AgwMFQtBACECIANBADYCHCADIAE2AhQgA0HVEDYCECADQQk2AgwMFAtBACECIANBADYCHCADIAE2AhQgA0HXCjYCECADQQk2AgwMEwtBACECIANBADYCHCADIAE2AhQgA0G/EDYCECADQQk2AgwMEgtBACECIANBADYCHCADIAE2AhQgA0GkETYCECADQQk2AgwMEQtBACECIANBADYCHCADIAE2AhQgA0G/FjYCECADQQ82AgwMEAtBACECIANBADYCHCADIAE2AhQgA0G/FjYCECADQQ82AgwMDwtBACECIANBADYCHCADIAE2AhQgA0HIEjYCECADQQs2AgwMDgtBACECIANBADYCHCADIAE2AhQgA0GVCTYCECADQQs2AgwMDQtBACECIANBADYCHCADIAE2AhQgA0HpDzYCECADQQo2AgwMDAtBACECIANBADYCHCADIAE2AhQgA0GDEDYCECADQQo2AgwMCwtBACECIANBADYCHCADIAE2AhQgA0GmHDYCECADQQI2AgwMCgtBACECIANBADYCHCADIAE2AhQgA0HFFTYCECADQQI2AgwMCQtBACECIANBADYCHCADIAE2AhQgA0H/FzYCECADQQI2AgwMCAtBACECIANBADYCHCADIAE2AhQgA0HKFzYCECADQQI2AgwMBwsgA0ECNgIcIAMgATYCFCADQZQdNgIQIANBFjYCDEEAIQIMBgtB3gAhAiABIARGDQUgCUEIaiEHIAMoAgAhBQJAAkAgASAERwRAIAVBxsYAaiEIIAQgBWogAWshBiAFQX9zQQpqIgUgAWohAANAIAEtAAAgCC0AAEcEQEECIQgMAwsgBUUEQEEAIQggACEBDAMLIAVBAWshBSAIQQFqIQggBCABQQFqIgFHDQALIAYhBSAEIQELIAdBATYCACADIAU2AgAMAQsgA0EANgIAIAcgCDYCAAsgByABNgIEIAkoAgwhACAJKAIIDgMBBQIACwALIANBADYCHCADQa0dNgIQIANBFzYCDCADIABBAWo2AhRBACECDAMLIANBADYCHCADIAA2AhQgA0HCHTYCECADQQk2AgxBACECDAILIAEgBEYEQEEoIQIMAgsgA0EJNgIIIAMgATYCBEEnIQIMAQsgASAERgRAQQEhAgwBCwNAAkACQAJAIAEtAABBCmsOBAABAQABCyABQQFqIQEMAQsgAUEBaiEBIAMtAC5BIHENAEEAIQIgA0EANgIcIAMgATYCFCADQYwgNgIQIANBBTYCDAwCC0EBIQIgASAERw0ACwsgCUEQaiQAIAJFBEAgAygCDCEADAELIAMgAjYCHEEAIQAgAygCBCIBRQ0AIAMgASAEIAMoAggRAQAiAUUNACADIAQ2AhQgAyABNgIMIAEhAAsgAAu+AgECfyAAQQA6AAAgAEHcAGoiAUEBa0EAOgAAIABBADoAAiAAQQA6AAEgAUEDa0EAOgAAIAFBAmtBADoAACAAQQA6AAMgAUEEa0EAOgAAQQAgAGtBA3EiASAAaiIAQQA2AgBB3AAgAWtBfHEiAiAAaiIBQQRrQQA2AgACQCACQQlJDQAgAEEANgIIIABBADYCBCABQQhrQQA2AgAgAUEMa0EANgIAIAJBGUkNACAAQQA2AhggAEEANgIUIABBADYC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YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABUcmFuc2Zlci1FbmNvZGluZyBjYW4ndCBiZSBwcmVzZW50IHdpdGggQ29udGVudC1MZW5ndGgARHVwbGljYXRlIENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhciBpbiB1cmwgcGF0aABDb250ZW50LUxlbmd0aCBjYW4ndCBiZSBwcmVzZW50IHdpdGggVHJhbnNmZXItRW5jb2RpbmcATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBzaXplAEV4cGVjdGVkIExGIGFmdGVyIGNodW5rIHNpemUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgc2l6ZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2hlYWRlcl92YWx1ZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAUhUAABoVAAAPEgAA5BkAAJEVAAAJFAAALRkAAOQUAADpEQAAaRQAAKEUAAB2FQAAQxYAAF4SAACUFwAAFxYAAH0UAAB/FgAAQRcAALMTAADDFgAABBoAAL0YAADQGAAAoBMAANQZAACvFgAAaBYAAHAXAADZFgAA/BgAAP4RAABZFwAAlxYAABwXAAD2FgAAjRcAAAsSAAB/GwAALhEAALMQAABJEgAArRIAAPYYAABoEAAAYhUAABAVAABaFgAAShkAALUVAADBFQAAYBUAAFwZAABaGQAAUxkAABYVAACtEQAAQhAAALcQAABXGAAAvxUAAIkQAAAcGQAAGhkAALkVAABRGAAA3BMAAFsVAABZFQAA5hgAAGcVAAARGQAA7RgAAOcTAACuEAAAwhcAAAAUAACSEwAAhBMAAEASAAAmGQAArxUAAGIQAEHpOQsBAQBBgDoL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBB6jsLBAEAAAIAQYE8C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEHqPQsEAQAAAgBBgT4LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQeA/Cw1sb3NlZWVwLWFsaXZlAEH5PwsBAQBBkMAAC+ABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAQfnBAAsBAQBBkMIAC+cBAQEBAQEBAQEBAQEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQFjaHVua2VkAEGhxAALXgEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAQYDGAAshZWN0aW9uZW50LWxlbmd0aG9ucm94eS1jb25uZWN0aW9uAEGwxgALK3JhbnNmZXItZW5jb2RpbmdwZ3JhZGUNCg0KU00NCg0KVFRQL0NFL1RTUC8AQenGAAsFAQIAAQMAQYDHAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQenIAAsFAQIAAQMAQYDJAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQenKAAsEAQAAAQBBgcsAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEHpzAALBQECAAEDAEGAzQALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEHpzgALBQEBAAEBAEGAzwALAQEAQZrPAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQenQAAsFAQEAAQEAQYDRAAsBAQBBitEACwYCAAAAAAIAQaHRAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBB4NIAC5oBTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFSFRUUC9BRFRQLw==' +const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzU0BQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEBAwAAAwAAAAQFAXABExMFAwEAAgYIAX8BQcDZBAsHxQcoBm1lbW9yeQIAC19pbml0aWFsaXplAAgZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAC2xsaHR0cF9pbml0AAkYbGxodHRwX3Nob3VsZF9rZWVwX2FsaXZlADcMbGxodHRwX2FsbG9jAAsGbWFsbG9jADkLbGxodHRwX2ZyZWUADARmcmVlAAwPbGxodHRwX2dldF90eXBlAA0VbGxodHRwX2dldF9odHRwX21ham9yAA4VbGxodHRwX2dldF9odHRwX21pbm9yAA8RbGxodHRwX2dldF9tZXRob2QAEBZsbGh0dHBfZ2V0X3N0YXR1c19jb2RlABESbGxodHRwX2dldF91cGdyYWRlABIMbGxodHRwX3Jlc2V0ABMObGxodHRwX2V4ZWN1dGUAFBRsbGh0dHBfc2V0dGluZ3NfaW5pdAAVDWxsaHR0cF9maW5pc2gAFgxsbGh0dHBfcGF1c2UAFw1sbGh0dHBfcmVzdW1lABgbbGxodHRwX3Jlc3VtZV9hZnRlcl91cGdyYWRlABkQbGxodHRwX2dldF9lcnJubwAaF2xsaHR0cF9nZXRfZXJyb3JfcmVhc29uABsXbGxodHRwX3NldF9lcnJvcl9yZWFzb24AHBRsbGh0dHBfZ2V0X2Vycm9yX3BvcwAdEWxsaHR0cF9lcnJub19uYW1lAB4SbGxodHRwX21ldGhvZF9uYW1lAB8SbGxodHRwX3N0YXR1c19uYW1lACAabGxodHRw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AXRrQT5qIQILIAEgAjYCHCABQgA3AhAgAkECdEH01wBqIQACQEHI1QAoAgAiA0EBIAJ0IgdxRQRAIAAgATYCAEHI1QAgAyAHcjYCACABIAA2AhggASABNgIIIAEgATYCDAwBCyAEQRkgAkEBdmtBACACQR9HG3QhAiAAKAIAIQACQANAIAAiAygCBEF4cSAERg0BIAJBHXYhACACQQF0IQIgAyAAQQRxakEQaiIHKAIAIgANAAsgByABNgIAIAEgAzYCGCABIAE2AgwgASABNgIIDAELIAMoAggiACABNgIMIAMgATYCCCABQQA2AhggASADNgIMIAEgADYCCAtB5NUAQeTVACgCAEEBayIAQX8gABs2AgALCwcAIAAtACgLBwAgAC0AKgsHACAALQArCwcAIAAtACkLBwAgAC8BNAsHACAALQAwC0ABBH8gACgCGCEBIAAvAS4hAiAALQAoIQMgACgCOCEEIAAQOCAAIAQ2AjggACADOgAoIAAgAjsBLiAAIAE2AhgL5YUCAgd/A34gASACaiEEAkAgACIDKAIMIgANACADKAIEBEAgAyABNgIECyMAQRBrIgkkAAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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C0HjAQwHC0HXAQwGC0HYAQwFC0HZAQwEC0HaAQwDC0HbAQwCC0HdAQwBC0HcAQshAgNAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJ/AkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAg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ogKiAqICogKiAqICogKiAqICogKiAqICogIAogILIAQgAUEBaiIBRw0AC0HJACECDOwCCyABQQFqIQEgA0Evai0AAEEBcQ2lAiADQQA2AhwgAyABNgIUIANBlxA2AhAgA0EKNgIMQQAhAgzrAgsgASAERwRAA0AgAS0AAEEgRw0VIAQgAUEBaiIBRw0AC0H4ACECDOsCC0H4ACECDOoCCyADQQI6ACgMOAtBACECIANBADYCHCADQb8LNgIQIANBAjYCDCADIAFBAWo2AhQM6AILQQAhAgzOAgtBDSECDM0CC0ETIQIMzAILQRUhAgzLAgtBFiECDMoCC0EYIQIMyQILQRkhAgzIAgtBGiECDMcCC0EbIQIMxgILQRwhAgzFAgtBHSECDMQCC0EeIQIMwwILQR8hAgzCAgtBICECDMECC0EiIQIMwAILQSMhAgy/AgtBJSECDL4CC0HlACECDL0CCyADQT02AhwgAyABNgIUIAMgADYCDEEAIQIM1QILIANBGzYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDNQCCyADQSA2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzTAgsgA0ETNgIcIAMgATYCFCADQZgaNgIQIANBFTYCDEEAIQIM0gILIANBCzYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNECCyADQRA2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzQAgsgA0EgNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIMzwILIANBCzYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDM4CCyADQQw2AhwgAyABNgIUIANBpBw2AhAgA0EVNgIMQQAhAgzNAgtBACECIANBADYCHCADIAE2AhQgA0HdDjYCECADQRI2AgwMzAILAkADQAJAIAEtAABBCmsOBAACAgACCyAEIAFBAWoiAUcNAAtB/QEhAgzMAgsCQAJAIAMtADZBAUcNAEEAIQACQCADKAI4IgJFDQAgAigCYCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUcNASADQfwBNgIcIAMgATYCFCADQdwZNgIQIANBFTYCDEEAIQIMzQILQdwBIQIMswILIANBADYCHCADIAE2AhQgA0H5CzYCECADQR82AgxBACECDMsCCwJAAkAgAy0AKEEBaw4CBAEAC0HbASECDLICC0HUASECDLECCyADQQI6ADFBACEAAkAgAygCOCICRQ0AIAIoAgAiAkUNACADIAIRAAAhAAsgAEUEQEHdASECDLECCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQbQMNgIQIANBEDYCDEEAIQIMygILIANB+wE2AhwgAyABNgIUIANBgRo2AhAgA0EVNgIMQQAhAgzJAgsgASAERgRAQfoBIQIMyQILIAEtAABByABGDQEgA0EBOgAoC0HAASECDK4CC0HaASECDK0CCyABIARHBEAgA0EMNgIIIAMgATYCBEHZASECDK0CC0H5ASECDMUCCyABIARGBEBB+AEhAgzFAgsgAS0AAEHIAEcNBCABQQFqIQFB2AEhAgyrAgsgASAERgRAQfcBIQIMxAILAkACQCABLQAAQcUAaw4QAAUFBQUFBQUFBQUFBQUFAQULIAFBAWohAUHWASECDKsCCyABQQFqIQFB1wEhAgyqAgtB9gEhAiABIARGDcICIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbrVAGotAABHDQMgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMMCCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQLiIARQRAQeMBIQIMqgILIANB9QE2AhwgAyABNgIUIAMgADYCDEEAIQIMwgILQfQBIQIgASAERg3BAiADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEG41QBqLQAARw0CIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzCAgsgA0GBBDsBKCADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQLiIADQMMAgsgA0EANgIAC0EAIQIgA0EANgIcIAMgATYCFCADQeUfNgIQIANBCDYCDAy/AgtB1QEhAgylAgsgA0HzATYCHCADIAE2AhQgAyAANgIMQQAhAgy9AgtBACEAAkAgAygCOCICRQ0AIAIoAkAiAkUNACADIAIRAAAhAAsgAEUNbiAAQRVHBEAgA0EANgIcIAMgATYCFCADQYIPNgIQIANBIDYCDEEAIQIMvQILIANBjwE2AhwgAyABNgIUIANB7Bs2AhAgA0EVNgIMQQAhAgy8AgsgASAERwRAIANBDTYCCCADIAE2AgRB0wEhAgyjAgtB8gEhAgy7AgsgASAERgRAQfEBIQIMuwILAkACQAJAIAEtAABByABrDgsAAQgICAgICAgIAggLIAFBAWohAUHQASECDKMCCyABQQFqIQFB0QEhAgyiAgsgAUEBaiEBQdIBIQIMoQILQfABIQIgASAERg25AiADKAIAIgAgBCABa2ohBiABIABrQQJqIQUDQCABLQAAIABBtdUAai0AAEcNBCAAQQJGDQMgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMuQILQe8BIQIgASAERg24AiADKAIAIgAgBCABa2ohBiABIABrQQFqIQUDQCABLQAAIABBs9UAai0AAEcNAyAAQQFGDQIgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMuAILQe4BIQIgASAERg23AiADKAIAIgAgBCABa2ohBiABIABrQQJqIQUDQCABLQAAIABBsNUAai0AAEcNAiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMtwILIAMoAgQhACADQgA3AwAgAyAAIAVBAWoiARArIgBFDQIgA0HsATYCHCADIAE2AhQgAyAANgIMQQAhAgy2AgsgA0EANgIACyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNnAIgA0HtATYCHCADIAE2AhQgAyAANgIMQQAhAgy0AgtBzwEhAgyaAgtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDLQCC0HOASECDJoCCyADQesBNgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMsgILIAEgBEYEQEHrASECDLICCyABLQAAQS9GBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GyODYCECADQQg2AgxBACECDLECC0HNASECDJcCCyABIARHBEAgA0EONgIIIAMgATYCBEHMASECDJcCC0HqASECDK8CCyABIARGBEBB6QEhAgyvAgsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFBywEhAgyWAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZcCIANB6AE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILIAEgBEYEQEHnASECDK4CCwJAIAEtAABBLkYEQCABQQFqIQEMAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZgCIANB5gE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILQcoBIQIMlAILIAEgBEYEQEHlASECDK0CC0EAIQBBASEFQQEhB0EAIQICQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQCABLQAAQTBrDgoKCQABAgMEBQYICwtBAgwGC0EDDAULQQQMBAtBBQwDC0EGDAILQQcMAQtBCAshAkEAIQVBACEHDAILQQkhAkEBIQBBACEFQQAhBwwBC0EAIQVBASECCyADIAI6ACsgAUEBaiEBAkACQCADLQAuQRBxDQACQAJAAkAgAy0AKg4DAQACBAsgB0UNAwwCCyAADQEMAgsgBUUNAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDQIgA0HiATYCHCADIAE2AhQgAyAANgIMQQAhAgyvAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZoCIANB4wE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ2YAiADQeQBNgIcIAMgATYCFCADIAA2AgwMrQILQckBIQIMkwILQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANBpA02AhAgA0EhNgIMQQAhAgytAgtByAEhAgyTAgsgA0HhATYCHCADIAE2AhQgA0HQGjYCECADQRU2AgxBACECDKsCCyABIARGBEBB4QEhAgyrAgsCQCABLQAAQSBGBEAgA0EAOwE0IAFBAWohAQwBCyADQQA2AhwgAyABNgIUIANBmRE2AhAgA0EJNgIMQQAhAgyrAgtBxwEhAgyRAgsgASAERgRAQeABIQIMqgILAkAgAS0AAEEwa0H/AXEiAkEKSQRAIAFBAWohAQJAIAMvATQiAEGZM0sNACADIABBCmwiADsBNCAAQf7/A3EgAkH//wNzSw0AIAMgACACajsBNAwCC0EAIQIgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDAyrAgsgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDEEAIQIMqgILQcYBIQIMkAILIAEgBEYEQEHfASECDKkCCwJAIAEtAABBMGtB/wFxIgJBCkkEQCABQQFqIQECQCADLwE0IgBBmTNLDQAgAyAAQQpsIgA7ATQgAEH+/wNxIAJB//8Dc0sNACADIAAgAmo7ATQMAgtBACECIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgwMqgILIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgxBACECDKkCC0HFASECDI8CCyABIARGBEBB3gEhAgyoAgsCQCABLQAAQTBrQf8BcSICQQpJBEAgAUEBaiEBAkAgAy8BNCIAQZkzSw0AIAMgAEEKbCIAOwE0IABB/v8DcSACQf//A3NLDQAgAyAAIAJqOwE0DAILQQAhAiADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMDKkCCyADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMQQAhAgyoAgtBxAEhAgyOAgsgASAERgRAQd0BIQIMpwILAkACQAJAAkAgAS0AAEEKaw4XAgMDAAMDAwMDAwMDAwMDAwMDAwMDAwEDCyABQQFqDAULIAFBAWohAUHDASECDI8CCyABQQFqIQEgA0Evai0AAEEBcQ0IIANBADYCHCADIAE2AhQgA0GNCzYCECADQQ02AgxBACECDKcCCyADQQA2AhwgAyABNgIUIANBjQs2AhAgA0ENNgIMQQAhAgymAgsgASAERwRAIANBDzYCCCADIAE2AgRBASECDI0CC0HcASECDKUCCwJAAkADQAJAIAEtAABBCmsOBAIAAAMACyAEIAFBAWoiAUcNAAtB2wEhAgymAgsgAygCBCEAIANBADYCBCADIAAgARAtIgBFBEAgAUEBaiEBDAQLIANB2gE2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMpQILIAMoAgQhACADQQA2AgQgAyAAIAEQLSIADQEgAUEBagshAUHBASECDIoCCyADQdkBNgIcIAMgADYCDCADIAFBAWo2AhRBACECDKICC0HCASECDIgCCyADQS9qLQAAQQFxDQEgA0EANgIcIAMgATYCFCADQeQcNgIQIANBGTYCDEEAIQIMoAILIAEgBEYEQEHZASEC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IAFBAWohAUGeASECDOQBCyABQQFqIQFBnwEhAgzjAQtBtwEhAiABIARGDfsBIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQezUAGotAABHDSggAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPwBCyADQQA2AgAgBkEBaiEBQQ0MKAtBtgEhAiABIARGDfoBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDScgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPsBCyADQQA2AgAgBkEBaiEBQQwMJwtBtQEhAiABIARGDfkBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQerUAGotAABHDSYgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPoBCyADQQA2AgAgBkEBaiEBQQMMJgtBtAEhAiABIARGDfgBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQejUAGotAABHDSUgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPkBCyADQQA2AgAgBkEBaiEBQSYMJQsgASAERgRAQbMBIQIM+AELAkACQCABLQAAQdQAaw4CAAEnCyABQQFqIQFBmQEhAgzfAQsgAUEBaiEBQZoBIQIM3gELQbIBIQIgASAERg32ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHm1ABqLQAARw0jIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz3AQsgA0EANgIAIAZBAWohAUEnDCMLQbEBIQIgASAERg31ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHk1ABqLQAARw0iIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz2AQsgA0EANgIAIAZBAWohAUEcDCILQbABIQIgASAERg30ASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHe1ABqLQAARw0hIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz1AQsgA0EANgIAIAZBAWohAUEGDCELQa8BIQIgASAERg3zASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEHZ1ABqLQAARw0gIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz0AQsgA0EANgIAIAZBAWohAUEZDCALIAEgBEYEQEGuASECDPMBCwJAAkACQAJAIAEtAABBLWsOIwAkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJAEkJCQkJAIkJCQDJAsgAUEBaiEBQY4BIQIM3AELIAFBAWohAUGPASECDNsBCyABQQFqIQFBlAEhAgzaAQsgAUEBaiEBQZUBIQIM2QELQa0BIQIgASAERg3xASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHX1ABqLQAARw0eIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzyAQsgA0EANgIAIAZBAWohAUELDB4LIAEgBEYEQEGsASECDPEBCwJAAkAgAS0AAEHBAGsOAwAgASALIAFBAWohAUGQASECDNgBCyABQQFqIQFBkwEhAgzXAQsgASAERgRAQasBIQIM8AELAkACQCABLQAAQcEAaw4PAB8fHx8fHx8fHx8fHx8BHwsgAUEBaiEBQZEBIQIM1wELIAFBAWohAUGSASECDNYBCyABIARGBEBBqgEhAgzvAQsgAS0AAEHMAEcNHCABQQFqIQFBCgwbC0GpASECIAEgBEYN7QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABB0dQAai0AAEcNGiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7gELIANBADYCACAGQQFqIQFBHgwaC0GoASECIAEgBEYN7AEgAygCACIAIAQgAWtqIQUgASAAa0EGaiEGAkADQCABLQAAIABBytQAai0AAEcNGSAAQQZGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7QELIANBADYCACAGQQFqIQFBFQwZC0GnASECIAEgBEYN6wEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBx9QAai0AAEcNGCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7AELIANBADYCACAGQQFqIQFBFwwYC0GmASECIAEgBEYN6gEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBwdQAai0AAEcNFyAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6wELIANBADYCACAGQQFqIQFBGAwXCyABIARGBEBBpQEhAgzqAQsCQAJAIAEtAABByQBrDgcAGRkZGRkBGQsgAUEBaiEBQYsBIQIM0QELIAFBAWohAUGMASECDNABC0GkASECIAEgBEYN6AEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBptUAai0AAEcNFSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6QELIANBADYCACAGQQFqIQFBCQwVC0GjASECIAEgBEYN5wEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBpNUAai0AAEcNFCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6AELIANBADYCACAGQQFqIQFBHwwUC0GiASECIAEgBEYN5gEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBvtQAai0AAEcNEyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM5wELIANBADYCACAGQQFqIQFBAgwTC0GhASECIAEgBEYN5QEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGA0AgAS0AACAAQbzUAGotAABHDREgAEEBRg0CIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADOUBCyABIARGBEBBoAEhAgzlAQtBASABLQAAQd8ARw0RGiABQQFqIQFBhwEhAgzLAQsgA0EANgIAIAZBAWohAUGIASECDMoBC0GfASECIAEgBEYN4gEgAygCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABBhNUAai0AAEcNDyAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4wELIANBADYCACAGQQFqIQFBKQwPC0GeASECIAEgBEYN4QEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBuNQAai0AAEcNDiAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4gELIANBADYCACAGQQFqIQFBLQwOCyABIARGBEBBnQEhAgzhAQsgAS0AAEHFAEcNDiABQQFqIQFBhAEhAgzHAQsgASAERgRAQZwBIQIM4AELAkACQCABLQAAQcwAaw4IAA8PDw8PDwEPCyABQQFqIQFBggEhAgzHAQsgAUEBaiEBQYMBIQIMxgELQZsBIQIgASAERg3eASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEGz1ABqLQAARw0LIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzfAQsgA0EANgIAIAZBAWohAUEjDAsLQZoBIQIgASAERg3dASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGw1ABqLQAARw0KIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzeAQsgA0EANgIAIAZBAWohAUEADAoLIAEgBEYEQEGZASECDN0BCwJAAkAgAS0AAEHIAGsOCAAMDAwMDAwBDAsgAUEBaiEBQf0AIQIMxAELIAFBAWohAUGAASECDMMBCyABIARGBEBBmAEhAgzcAQsCQAJAIAEtAABBzgBrDgMACwELCyABQQFqIQFB/gAhAgzDAQsgAUEBaiEBQf8AIQIMwgELIAEgBEYEQEGXASECDNsBCyABLQAAQdkARw0IIAFBAWohAUEIDAcLQZYBIQIgASAERg3ZASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEGs1ABqLQAARw0GIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzaAQsgA0EANgIAIAZBAWohAUEFDAYLQZUBIQIgASAERg3YASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGm1ABqLQAARw0FIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzZAQsgA0EANgIAIAZBAWohAUEWDAULQZQBIQIgASAERg3XASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGh1QBqLQAARw0EIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzYAQsgA0EANgIAIAZBAWohAUEQDAQLIAEgBEYEQEGTASECDNcBCwJAAkAgAS0AAEHDAGsODAAGBgYGBgYGBgYGAQYLIAFBAWohAUH5ACECDL4BCyABQQFqIQFB+gAhAgy9AQtBkgEhAiABIARGDdUBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQaDUAGotAABHDQIgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNYBCyADQQA2AgAgBkEBaiEBQSQMAgsgA0EANgIADAILIAEgBEYEQEGRASECDNQBCyABLQAAQcwARw0BIAFBAWohAUETCzoAKSADKAIEIQAgA0EANgIEIAMgACABEC4iAA0CDAELQQAhAiADQQA2AhwgAyABNgIUIANB/h82AhAgA0EGNgIMDNEBC0H4ACECDLcBCyADQZABNgIcIAMgATYCFCADIAA2AgxBACECDM8BC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUYNASADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgzOAQtB9wAhAgy0AQsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDMwBCyABIARGBEBBjwEhAgzMAQsCQCABLQAAQSBGBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GbHzYCECADQQY2AgxBACECDMwBC0ECIQIMsgELA0AgAS0AAEEgRw0CIAQgAUEBaiIBRw0AC0GOASECDMoBCyABIARGBEBBjQEhAgzKAQsCQCABLQAAQQlrDgRKAABKAAtB9QAhAgywAQsgAy0AKUEFRgRAQfYAIQIMsAELQfQAIQIMrwELIAEgBEYEQEGMASECDMgBCyADQRA2AgggAyABNgIEDAoLIAEgBEYEQEGLASECDMcBCwJAIAEtAABBCWsOBEcAAEcAC0HzACECDK0BCyABIARHBEAgA0EQNgIIIAMgATYCBEHxACECDK0BC0GKASECDMUBCwJAIAEgBEcEQANAIAEtAABBoNAAai0AACIAQQNHBEACQCAAQQFrDgJJAAQLQfAAIQIMrwELIAQgAUEBaiIBRw0AC0GIASEC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BCABQQFqIgFHDQALQfsAIQIMpwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMpgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMpQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMpAELIANBADYCHCADIAE2AhQgA0G8CjYCECADQQc2AgxBACECDKMBC0HPACECDIkBC0HRACECDIgBC0HnACECDIcBCyABIARGBEBB+gAhAgygAQsCQCABLQAAQQlrDgQgAAAgAAsgAUEBaiEBQeYAIQIMhgELIAEgBEYEQEH5ACECDJ8BCwJAIAEtAABBCWsOBB8AAB8AC0EAIQACQCADKAI4IgJFDQAgAigCOCICRQ0AIAMgAhEAACEACyAARQRAQeIBIQIMhgELIABBFUcEQCADQQA2AhwgAyABNgIUIANByQ02AhAgA0EaNgIMQQAhAgyfAQsgA0H4ADYCHCADIAE2AhQgA0HqGjYCECADQRU2AgxBACECDJ4BCyABIARHBEAgA0ENNgIIIAMgATYCBEHkACECDIUBC0H3ACECDJ0BCyABIARGBEBB9gAhAgydAQsCQAJAAkAgAS0AAEHIAGsOCwABCwsLCwsLCwsCCwsgAUEBaiEBQd0AIQIMhQELIAFBAWohAUHgACECDIQBCyABQQFqIQFB4wAhAgyDAQtB9QAhAiABIARGDZsBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbXVAGotAABHDQggAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJwBCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQKyIABEAgA0H0ADYCHCADIAE2AhQgAyAANgIMQQAhAgycAQtB4gAhAgyCAQtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJwBC0HhACECDIIBCyADQfMANgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMmgELIAMtACkiAEEja0ELSQ0JAkAgAEEGSw0AQQEgAHRBygBxRQ0ADAoLQQAhAiADQQA2AhwgAyABNgIUIANB7Qk2AhAgA0EINgIMDJkBC0HyACECIAEgBEYNmAEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBs9UAai0AAEcNBSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMmQELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgAEQCADQfEANgIcIAMgATYCFCADIAA2AgxBACECDJkBC0HfACECDH8LQQAhAAJAIAMoAjgiAkUNACACKAI0IgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANB6g02AhAgA0EmNgIMQQAhAgyZAQtB3gAhAgx/CyADQfAANgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMlwELIAMtAClBIUYNBiADQQA2AhwgAyABNgIUIANBkQo2AhAgA0EINgIMQQAhAgyWAQtB7wAhAiABIARGDZUBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbDVAGotAABHDQIgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJYBCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQKyIARQ0CIANB7QA2AhwgAyABNgIUIAMgADYCDEEAIQIMlQELIANBADYCAAsgAygCBCEAIANBADYCBCADIAAgARArIgBFDYABIANB7gA2AhwgAyABNgIUIAMgADYCDEEAIQIMkwELQdwAIQIMeQtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJMBC0HbACECDHkLIANB7AA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyRAQsgAy0AKSIAQSNJDQAgAEEuRg0AIANBADYCHCADIAE2AhQgA0HJCTYCECADQQg2AgxBACECDJABC0HaACECDHYLIAEgBEYEQEHrACECDI8BCwJAIAEtAABBL0YEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDEEAIQIMjwELQdkAIQIMdQsgASAERwRAIANBDjYCCCADIAE2AgRB2AAhAgx1C0HqACECDI0BCyABIARGBEBB6QAhAgyNAQsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFB1wAhAgx0CyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNeiADQegANgIcIAMgATYCFCADIAA2AgxBACECDIwBCyABIARGBEBB5wAhAgyMAQsCQCABLQAAQS5GBEAgAUEBaiEBDAELIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ17IANB5gA2AhwgAyABNgIUIAMgADYCDEEAIQIMjAELQdYAIQIMcgsgASAERgRAQeUAIQIMiwELQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNAiADQeIANgIcIAMgATYCFCADIAA2AgxBACECDI0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNfSADQeMANgIcIAMgATYCFCADIAA2AgxBACECDIwBCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNeyADQeQANgIcIAMgATYCFCADIAA2AgwMiwELQdQAIQIMcQsgAy0AKUEiRg2GAUHTACECDHALQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALIABFBEBB1QAhAgxwCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQaQNNgIQIANBITYCDEEAIQIMiQELIANB4QA2AhwgAyABNgIUIANB0Bo2AhAgA0EVNgIMQQAhAgyIAQsgASAERgRAQeAAIQIMiAELAkACQAJAAkACQCABLQAAQQprDgQBBAQABAsgAUEBaiEBDAELIAFBAWohASADQS9qLQAAQQFxRQ0BC0HSACECDHALIANBADYCHCADIAE2AhQgA0G2ETYCECADQQk2AgxBACECDIgBCyADQQA2AhwgAyABNgIUIANBthE2AhAgA0EJNgIMQQAhAgyHAQsgASAERgRAQd8AIQIMhwELIAEtAABBCkYEQCABQQFqIQEMCQsgAy0ALkHAAHENCCADQQA2AhwgAyABNgIUIANBthE2AhAgA0ECNgIMQQAhAgyGAQsgASAERgRAQd0AIQIMhgELIAEtAAAiAkENRgRAIAFBAWohAUHQACECDG0LIAEhACACQQlrDgQFAQEFAQsgBCABIgBGBEBB3AAhAgyFAQsgAC0AAEEKRw0AIABBAWoMAgtBACECIANBADYCHCADIAA2AhQgA0HKLTYCECADQQc2AgwMgwELIAEgBEYEQEHbACECDIMBCwJAIAEtAABBCWsOBAMAAAMACyABQQFqCyEBQc4AIQIMaAsgASAERgRAQdoAIQIMgQELIAEtAABBCWsOBAABAQABC0EAIQIgA0EANgIcIANBmhI2AhAgA0EHNgIMIAMgAUEBajYCFAx/CyADQYASOwEqQQAhAAJAIAMoAjgiAkUNACACKAI4IgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB2QA2AhwgAyABNgIUIANB6ho2AhAgA0EVNgIMQQAhAgx+C0HNACECDGQLIANBADYCHCADIAE2AhQgA0HJDTYCECADQRo2AgxBACECDHwLIAEgBEYEQEHZACECDHwLIAEtAABBIEcNPSABQQFqIQEgAy0ALkEBcQ09IANBADYCHCADIAE2AhQgA0HCHDYCECADQR42AgxBACECDHsLIAEgBEYEQEHYACECDHsLAkACQAJAAkACQCABLQAAIgBBCmsOBAIDAwABCyABQQFqIQFBLCECDGULIABBOkcNASADQQA2AhwgAyABNgIUIANB5xE2AhAgA0EKNgIMQQAhAgx9CyABQQFqIQEgA0Evai0AAEEBcUUNcyADLQAyQYABcUUEQCADQTJqIQIgAxA1QQAhAAJAIAMoAjgiBkUNACAGKAIoIgZFDQAgAyAGEQAAIQALAkACQCAADhZNTEsBAQEBAQEBAQEBAQEBAQEBAQEAAQsgA0EpNgIcIAMgATYCFCADQawZNgIQIANBFTYCDEEAIQIMfgsg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c3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3Byb3RvY29sX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fcHJvdG9jb2wARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBVbmV4cGVjdGVkIHdoaXRlc3BhY2UgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fcHJvdG9jb2xfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX1BST1RPQ09MX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8sIFJUU1AvIG9yIElDRS8A5xUAAK8VAACkEgAAkhoAACYWAACeFAAA2xkAAHkVAAB+EgAA/hQAADYVAAALFgAA2BYAAPMSAABCGAAArBYAABIVAAAUFwAA7xcAAEgUAABxFwAAshoAAGsZAAB+GQAANRQAAIIaAABEFwAA/RYAAB4YAACHFwAAqhkAAJMSAAAHGAAALBcAAMoXAACkFwAA5xUAAOcVAABYFwAAOxgAAKASAAAtHAAAwxEAAEgRAADeEgAAQhMAAKQZAAD9EAAA9xUAAKUVAADvFgAA+BkAAEoWAABWFgAA9RUAAAoaAAAIGgAAARoAAKsVAABCEgAA1xAAAEwRAAAFGQAAVBYAAB4RAADKGQAAyBkAAE4WAAD/GAAAcRQAAPAVAADuFQAAlBkAAPwVAAC/GQAAmxkAAHwUAABDEQAAcBgAAJUUAAAnFAAAGRQAANUSAADUGQAARBYAAPcQAEG5OwsBAQBB0DsL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBuj0LBAEAAAIAQdE9C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEG6PwsEAQAAAgBB0T8LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQbDBAAsNbG9zZWVlcC1hbGl2ZQBBycEACwEBAEHgwQAL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBycMACwEBAEHgwwAL5wEBAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAQfHFAAteAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBB0McACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQYDIAAsgcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQpTTQ0KDQoAQanIAAsFAQIAAQMAQcDIAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanKAAsFAQIAAQMAQcDKAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanMAAsEAQAAAQBBwcwAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEGpzgALBQECAAEDAEHAzgALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEGp0AALBQEBAAEBAEHA0AALAQEAQdrQAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQanSAAsFAQEAAQEAQcDSAAsBAQBBytIACwYCAAAAAAIAQeHSAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBoNQAC50BTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFVFRQQ0VUU1BBRFRQLw==' let wasmBuffer diff --git a/lib/llhttp/llhttp.wasm b/lib/llhttp/llhttp.wasm index 3d888a595ba..76ae48373e1 100755 Binary files a/lib/llhttp/llhttp.wasm and b/lib/llhttp/llhttp.wasm differ diff --git a/lib/llhttp/llhttp_simd-wasm.js b/lib/llhttp/llhttp_simd-wasm.js index 1875d85352b..4508cb14d0a 100644 --- a/lib/llhttp/llhttp_simd-wasm.js +++ b/lib/llhttp/llhttp_simd-wasm.js @@ -2,7 +2,7 @@ const { Buffer } = require('node:buffer') -const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzQzBQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEDAAADAAAABAUBcAESEgUDAQACBggBfwFBgNgECwfFBygGbWVtb3J5AgALX2luaXRpYWxpemUACBlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQACRhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUANgxsbGh0dHBfYWxsb2MACwZtYWxsb2MAOAtsbGh0dHBfZnJlZQAMBGZyZWUADA9sbGh0dHBfZ2V0X3R5cGUADRVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADhVsbGh0dHBfZ2V0X2h0dHBfbWlub3IADxFsbGh0dHBfZ2V0X21ldGhvZAAQFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAERJsbGh0dHBfZ2V0X3VwZ3JhZGUAEgxsbGh0dHBfcmVzZXQAEw5sbGh0dHBfZXhlY3V0ZQAUFGxsaHR0cF9zZXR0aW5nc19pbml0ABUNbGxodHRwX2ZpbmlzaAAWDGxsaHR0cF9wYXVzZQAXDWxsaHR0cF9yZXN1bWUAGBtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGRBsbGh0dHBfZ2V0X2Vycm5vABoXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AGxdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAcFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB0RbGxodHRwX2Vycm5vX25hbWUAHhJsbGh0dHBfbWV0aG9kX25hbWUAHxJsbGh0dHBfc3RhdHVzX25hbWUAIBpsbGh0dHBfc2V0X2xlbmllbnRfaGVhZGVycwAhIWxsaHR0cF9zZXRfbGVuaWVudF9jaHVua2VkX2xlbmd0aAAiHWxsaHR0cF9zZXRfbGVuaWVudF9rZWVwX2FsaXZlACMkbGxodHRwX3NldF9sZW5pZW50X3RyYW5zZmVyX2VuY29kaW5nACQabGxodHRwX3NldF9sZW5pZW50X3ZlcnNpb24AJSNsbGh0dHBfc2V0X2xlbmllbnRfZGF0YV9hZnRlcl9jbG9zZQAmJ2xsaHR0cF9zZXRfbGVuaWVudF9vcHRpb25hbF9sZl9hZnRlcl9jcgAnLGxsaHR0cF9zZXRfbGVuaWVudF9vcHRpb25hbF9jcmxmX2FmdGVyX2NodW5rACgobGxodHRwX3NldF9sZW5pZW50X29wdGlvbmFsX2NyX2JlZm9yZV9sZgApKmxsaHR0cF9zZXRfbGVuaWVudF9zcGFjZXNfYWZ0ZXJfY2h1bmtfc2l6ZQAqGGxsaHR0cF9tZXNzYWdlX25lZWRzX2VvZgA1CRcBAEEBCxEBAgMEBQoGBzEzMi0uLCsvMArYywIzFgBB/NMAKAIABEAAC0H80wBBATYCAAsUACAAEDcgACACNgI4IAAgAToAKAsUACAAIAAvATQgAC0AMCAAEDYQAAseAQF/QcAAEDkiARA3IAFBgAg2AjggASAAOgAoIAELjwwBB38CQCAARQ0AIABBCGsiASAAQQRrKAIAIgBBeHEiBGohBQJAIABBAXENACAAQQNxRQ0BIAEgASgCACIAayIBQZDUACgCAEkNASAAIARqIQQCQAJAQZTUACgCACABRwRAIABB/wFNBEAgAEEDdiEDIAEoAggiACABKAIMIgJGBEBBgNQAQYDUACgCAEF+IAN3cTYCAAwFCyACIAA2AgggACACNgIMDAQLIAEoAhghBiABIAEoAgwiAEcEQCAAIAEoAggiAjYCCCACIAA2AgwMAwsgAUEUaiIDKAIAIgJFBEAgASgCECICRQ0CIAFBEGohAwsDQCADIQcgAiIAQRRqIgMoAgAiAg0AIABBEGohAyAAKAIQIgINAAsgB0EANgIADAILIAUoAgQiAEEDcUEDRw0CIAUgAEF+cTYCBEGI1AAgBDYCACAFIAQ2AgAgASAEQQFyNgIEDAMLQQAhAAsgBkUNAAJAIAEoAhwiAkECdEGw1gBqIgMoAgAgAUYEQCADIAA2AgAgAA0BQYTUAEGE1AAoAgBBfiACd3E2AgAMAgsgBkEQQRQgBigCECABRhtqIAA2AgAgAEUNAQsgACAGNgIYIAEoAhAiAgRAIAAgAjYCECACIAA2AhgLIAFBFGooAgAiAkUNACAAQRRqIAI2AgAgAiAANgIYCyABIAVPDQAgBSgCBCIAQQFxRQ0AAkACQAJAAkAgAEECcUUEQEGY1AAoAgAgBUYEQEGY1AAgATYCAEGM1ABBjNQAKAIAIARqIgA2AgAgASAAQQFyNgIEIAFBlNQAKAIARw0GQYjUAEEANgIAQZTUAEEANgIADAYLQZTUACgCACAFRgRAQZTUACABNgIAQYjUAEGI1AAoAgAgBGoiADYCACABIABBAXI2AgQgACABaiAANgIADAYLIABBeHEgBGohBCAAQf8BTQRAIABBA3YhAyAFKAIIIgAgBSgCDCICRgRAQYDUAEGA1AAoAgBBfiADd3E2AgAMBQsgAiAANgIIIAAgAjYCDAwECyAFKAIYIQYgBSAFKAIMIgBHBEBBkNQAKAIAGiAAIAUoAggiAjYCCCACIAA2AgwMAwsgBUEUaiIDKAIAIgJFBEAgBSgCECICRQ0CIAVBEGohAwsDQCADIQcgAiIAQRRqIgMoAgAiAg0AIABBEGohAyAAKAIQIgINAAsgB0EANgIADAILIAUgAEF+cTYCBCABIARqIAQ2AgAgASAEQQFyNgIEDAMLQQAhAAsgBkUNAAJAIAUoAhwiAkECdEGw1gBqIgMoAgAgBUYEQCADIAA2AgAgAA0BQYTUAEGE1AAoAgBBfiACd3E2AgAMAgsgBkEQQRQgBigCECAFRhtqIAA2AgAgAEUNAQsgACAGNgIYIAUoAhAiAgRAIAAgAjYCECACIAA2AhgLIAVBFGooAgAiAkUNACAAQRRqIAI2AgAgAiAANgIYCyABIARqIAQ2AgAgASAEQQFyNgIEIAFBlNQAKAIARw0AQYjUACAENgIADAELIARB/wFNBEAgBEF4cUGo1ABqIQACf0GA1AAoAgAiAkEBIARBA3Z0IgNxRQRAQYDUACACIANyNgIAIAAMAQsgACgCCAsiAiABNgIMIAAgATYCCCABIAA2AgwgASACNgIIDAELQR8hAiAEQf///wdNBEAgBEEmIARBCHZnIgBrdkEBcSAAQQF0a0E+aiECCyABIAI2AhwgAUIANwIQIAJBAnRBsNYAaiEAAkBBhNQAKAIAIgNBASACdCIHcUUEQCAAIAE2AgBBhNQAIAMgB3I2AgAgASAANgIYIAEgATYCCCABIAE2AgwMAQsgBEEZIAJBAXZrQQAgAkEfRxt0IQIgACgCACEAAkADQCAAIgMoAgRBeHEgBEYNASACQR12IQAgAkEBdCECIAMgAEEEcWpBEGoiBygCACIADQALIAcgATYCACABIAM2AhggASABNgIMIAEgATYCCAwBCyADKAIIIgAgATYCDCADIAE2AgggAUEANgIYIAEgAzYCDCABIAA2AggLQaDUAEGg1AAoAgBBAWsiAEF/IAAbNgIACwsHACAALQAoCwcAIAAtACoLBwAgAC0AKwsHACAALQApCwcAIAAvATQLBwAgAC0AMAtAAQR/IAAoAhghASAALwEuIQIgAC0AKCEDIAAoAjghBCAAEDcgACAENgI4IAAgAzoAKCAAIAI7AS4gACABNgIYC8X4AQIHfwN+IAEgAmohBAJAIAAiAygCDCIADQAgAygCBARAIAMgATYCBAsjAEEQayIJJAACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJ/AkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCACDtQBAAECAwQFBgcICQoLDA0ODxARFBUWFxgZGhscHR4fICEjJCUnKCmIA4cDhQOEA/wC9QLuAusC6ALmAuMC4ALfAt0C2wLWAtUC1ALTAtICygLJAsgCxwLGAsUCxALDAr0CvAK6ArkCuAK3ArYCtQK0ArICsQKsAqoCqAKnAqYCpQKkAqMCogKhAqACnwKbApoCmQKYApcCkAKIAoQCgwKCAvkB9gH1AfQB8wHyAfEB8AHvAe0B6wHoAeMB4QHgAd8B3gHdAdwB2wHaAdkB2AHX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QEBAQEBAQEBAQEBAAEALQQAhAiADQQA2AhwgA0G3CzYCECADQQI2AgwgAyABQQFqNgIUDIEDCyADQS9qIQIDQCABIARGBEBBISECDIIDCwJAAkACQCABLQAAIgBBCWsOGAIAKioBKioqKioqKioqKioqKioqKioqAigLIAFBAWohASADQS9qLQAAQQFxRQ0LDBkLIAFBAWohAQwYCyABQQFqIQEgAi0AAEECcQ0AC0EAIQIgA0EANgIcIAMgATYCFCADQc4UNgIQIANBDDYCDAyAAwsgAUEBaiEBC0EAIQACQCADKAI4IgJFDQAgAigCVCICRQ0AIAMgAhEAACEACyAADQEM0QILIANCADcDIAw8CyAAQRVGBEAgA0EkNgIcIAMgATYCFCADQYYaNgIQIANBFTYCDEEAIQIM/QILQQAhAiADQQA2AhwgAyABNgIUIANB4g02AhAgA0EUNgIMDPwCCyADKAIEIQBBACECIANBADYCBCADIAAgASAMp2oiARAxIgBFDSsgA0EHNgIcIAMgATYCFCADIAA2AgwM+wILIAMtAC5BwABxRQ0BC0EAIQACQCADKAI4IgJFDQAgAigCUCICRQ0AIAMgAhEAACEACyAARQ0rIABBFUYEQCADQQo2AhwgAyABNgIUIANB8Rg2AhAgA0EVNgIMQQAhAgz6AgtBACECIANBADYCHCADIAE2AhQgA0GLDDYCECADQRM2AgwM+QILQQAhAiADQQA2AhwgAyABNgIUIANBsRQ2AhAgA0ECNgIMDPgCC0EAIQIgA0EANgIcIAMgATYCFCADQYwUNgIQIANBGTYCDAz3AgtBACECIANBADYCHCADIAE2AhQgA0HRHDYCECADQRk2AgwM9gILIABBFUYNPUEAIQIgA0EANgIcIAMgATYCFCADQaIPNgIQIANBIjYCDAz1AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQMiIARQ0oIANBDTYCHCADIAE2AhQgAyAANgIMDPQCCyAAQRVGDTpBACECIANBADYCHCADIAE2AhQgA0GiDzYCECADQSI2AgwM8wILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDIiAEUEQCABQQFqIQEMKAsgA0EONgIcIAMgADYCDCADIAFBAWo2AhQM8gILIABBFUYNN0EAIQIgA0EANgIcIAMgATYCFCADQaIPNgIQIANBIjYCDAzxAgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQMiIARQRAIAFBAWohAQwnCyADQQ82AhwgAyAANgIMIAMgAUEBajYCFAzwAgtBACECIANBADYCHCADIAE2AhQgA0HoFjYCECADQRk2AgwM7wILIABBFUYNM0EAIQIgA0EANgIcIAMgATYCFCADQc4MNgIQIANBIzYCDAzuAgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQMyIARQ0lIANBETYCHCADIAE2AhQgAyAANgIMDO0CCyAAQRVGDTBBACECIANBADYCHCADIAE2AhQgA0HODDYCECADQSM2AgwM7AILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDMiAEUEQCABQQFqIQEMJQsgA0ESNgIcIAMgADYCDCADIAFBAWo2AhQM6wILIANBL2otAABBAXFFDQELQRUhAgzPAgtBACECIANBADYCHCADIAE2AhQgA0HoFjYCECADQRk2AgwM6AILIABBO0cNACABQQFqIQEMDAtBACECIANBADYCHCADIAE2AhQgA0GYFzYCECADQQI2AgwM5gILIABBFUYNKEEAIQIgA0EANgIcIAMgATYCFCADQc4MNgIQIANBIzYCDAzlAgsgA0EUNgIcIAMgATYCFCADIAA2AgwM5AILIAMoAgQhAEEAIQIgA0EANgIEIAMgACABEDMiAEUEQCABQQFqIQEM3AILIANBFTYCHCADIAA2AgwgAyABQQFqNgIUDOMCCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDNoCCyADQRc2AhwgAyAANgIMIAMgAUEBajYCFAziAgsgAEEVRg0jQQAhAiADQQA2AhwgAyABNgIUIANBzgw2AhAgA0EjNgIMDOECCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDB0LIANBGTYCHCADIAA2AgwgAyABQQFqNgIUDOACCyADKAIEIQBBACECIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDNYCCyADQRo2AhwgAyAANgIMIAMgAUEBajYCFAzfAgsgAEEVRg0fQQAhAiADQQA2AhwgAyABNgIUIANBog82AhAgA0EiNgIMDN4CCyADKAIEIQBBACEC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DLMCCyABIARGBEBB6QEhAgyzAgsgAS0AAEHIAEYNASADQQE6ACgLQbYBIQIMlwILQcoBIQIMlgILIAEgBEcEQCADQQw2AgggAyABNgIEQckBIQIMlgILQegBIQIMrwILIAEgBEYEQEHnASECDK8CCyABLQAAQcgARw0EIAFBAWohAUHIASECDJQCCyABIARGBEBB5gEhAgyuAgsCQAJAIAEtAABBxQBrDhAABQUFBQUFBQUFBQUFBQUBBQsgAUEBaiEBQcYBIQIMlAILIAFBAWohAUHHASECDJMCC0HlASECIAEgBEYNrAIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB99MAai0AAEcNAyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMrQILIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAtIgBFBEBB1AEhAgyTAgsgA0HkATYCHCADIAE2AhQgAyAANgIMQQAhAgysAgtB4wEhAiABIARGDasCIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQfXTAGotAABHDQIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADKwCCyADQYEEOwEoIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAtIgANAwwCCyADQQA2AgALQQAhAiADQQA2AhwgAyABNgIUIANB0B42AhAgA0EINgIMDKkCC0HFASECDI4CCyADQeIBNgIcIAMgATYCFCADIAA2AgxBACECDKcCC0EAIQACQCADKAI4IgJFDQAgAigCOCICRQ0AIAMgAhEAACEACyAARQ1lIABBFUcEQCADQQA2AhwgAyABNgIUIANB1A42AhAgA0EgNgIMQQAhAgynAgsgA0GFATYCHCADIAE2AhQgA0HXGjYCECADQRU2AgxBACECDKYCC0HhASECIAQgASIARg2lAiAEIAFrIAMoAgAiAWohBSAAIAFrQQRqIQYCQANAIAAtAAAgAUHw0wBqLQAARw0BIAFBBEYNAyABQQFqIQEgBCAAQQFqIgBHDQALIAMgBTYCAAymAgsgA0EANgIcIAMgADYCFCADQYQ3NgIQIANBCDYCDCADQQA2AgBBACECDKUCCyABIARHBEAgA0ENNgIIIAMgATYCBEHCASECDIsCC0HgASECDKQCCyADQQA2AgAgBkEBaiEBC0HDASECDIgCCyABIARGBEBB3wEhAgyiAgsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFBwQEhAgyIAgsgAygCBCEAIANBADYCBCADIAAgARAuIgBFDYgCIANB3gE2AhwgAyABNgIUIAMgADYCDEEAIQIMoQILIAEgBEYEQEHdASECDKECCwJAIAEtAABBLkYEQCABQQFqIQEMAQsgAygCBCEAIANBADYCBCADIAAgARAuIgBFDYkCIANB3AE2AhwgAyABNgIUIAMgADYCDEEAIQIMoQILQcABIQIMhgILIAEgBEYEQEHbASECDKACC0EAIQBBASEFQQEhB0EAIQICQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQCABLQAAQTBrDgoKCQABAgMEBQYICwtBAgwGC0EDDAULQQQMBAtBBQwDC0EGDAILQQcMAQtBCAshAkEAIQVBACEHDAILQQkhAkEBIQBBACEFQQAhBwwBC0EAIQVBASECCyADIAI6ACsgAUEBaiEBAkACQCADLQAuQRBxDQACQAJAAkAgAy0AKg4DAQACBAsgB0UNAwwCCyAADQEMAgsgBUUNAQsgAygCBCEAIANBADYCBCADIAAgARAuIgBFDQIgA0HYATYCHCADIAE2AhQgAyAANgIMQQAhAgyiAgsgAygCBCEAIANBADYCBCADIAAgARAuIgBFDYsCIANB2QE2AhwgAyABNgIUIAMgADYCDEEAIQIMoQILIAMoAgQhACADQQA2AgQgAyAAIAEQLiIARQ2JAiADQdoBNgIcIAMgATYCFCADIAA2AgwMoAILQb8BIQIMhQILQQAhAAJAIAMoAjgiAkUNACACKAI8IgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANBnA02AhAgA0EhNgIMQQAhAgygAgtBvgEhAgyFAgsgA0HXATYCHCADIAE2AhQgA0HWGTYCECADQRU2AgxBACECDJ4CCyABIARGBEBB1wEhAgyeAgsCQCABLQAAQSBGBEAgA0EAOwE0IAFBAWohAQwBCyADQQA2AhwgAyABNgIUIANB6xA2AhAgA0EJNgIMQQAhAgyeAgtBvQEhAgyDAgsgASAERgRAQdYBIQIM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LgsgAUEBaiEBQZcBIQIM2AELIAFBAWohAUEiDCsLIAEgBEYEQEGvASECDPEBCyABLQAAQdAARw0rIAFBAWohAUGWASECDNYBCyABIARGBEBBrgEhAgzwAQsCQAJAIAEtAABBxgBrDgsALCwsLCwsLCwsASwLIAFBAWohAUGUASECDNYBCyABQQFqIQFBlQEhAgzVAQtBrQEhAiABIARGDe4BIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQazTAGotAABHDSggAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADO8BCyADQQA2AgAgBkEBaiEBQQ0MKAtBrAEhAiABIARGDe0BIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQeHTAGotAABHDScgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADO4BCyADQQA2AgAgBkEBaiEBQQwMJwtBqwEhAiABIARGDewBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQarTAGotAABHDSYgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADO0BCyADQQA2AgAgBkEBaiEBQQMMJgtBqgEhAiABIARGDesBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQajTAGotAABHDSUgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADOwBCyADQQA2AgAgBkEBaiEBQSYMJQsgASAERgRAQakBIQIM6wELAkACQCABLQAAQdQAaw4CAAEnCyABQQFqIQFBjwEhAgzRAQsgAUEBaiEBQZABIQIM0AELQagBIQIgASAERg3pASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGm0wBqLQAARw0jIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzqAQsgA0EANgIAIAZBAWohAUEnDCMLQacBIQIgASAERg3oASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGk0wBqLQAARw0iIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzpAQsgA0EANgIAIAZBAWohAUEcDCILQaYBIQIgASAERg3nASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGe0wBqLQAARw0hIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzoAQsgA0EANgIAIAZBAWohAUEGDCELQaUBIQIgASAERg3mASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEGZ0wBqLQAARw0gIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAznAQsgA0EANgIAIAZBAWohAUEZDCALIAEgBEYEQEGkASECDOYBCwJAAkACQAJAIAEtAABBLWsOIwAkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJAEkJCQkJAIkJCQDJAsgAUEBaiEBQYQBIQIMzgELIAFBAWohAUGFASECDM0BCyABQQFqIQFBigEhAgzMAQsgAUEBaiEBQYsBIQIMywELQaMBIQIgASAERg3kASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGX0wBqLQAARw0eIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzlAQsgA0EANgIAIAZBAWohAUELDB4LIAEgBEYEQEGiASECDOQBCwJAAkAgAS0AAEHBAGsOAwAgASALIAFBAWohAUGGASECDMoBCyABQQFqIQFBiQEhAgzJAQsgASAERgRAQaEBIQIM4wELAkACQCABLQAAQcEAaw4PAB8fHx8fHx8fHx8fHx8BHwsgAUEBaiEBQYcBIQIMyQELIAFBAWohAUGIASECDMgBCyABIARGBEBBoAEhAgziAQsgAS0AAEHMAEcNHCABQQFqIQFBCgwbC0GfASECIAEgBEYN4AEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBkdMAai0AAEcNGiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4QELIANBADYCACAGQQFqIQFBHgwaC0GeASECIAEgBEYN3wEgAygCACIAIAQgAWtqIQUgASAAa0EGaiEGAkADQCABLQAAIABBitMAai0AAEcNGSAAQQZGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4AELIANBADYCACAGQQFqIQFBFQwZC0GdASECIAEgBEYN3gEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBh9MAai0AAEcNGCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM3wELIANBADYCACAGQQFqIQFBFwwYC0GcASECIAEgBEYN3QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBgdMAai0AAEcNFyAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM3gELIANBADYCACAGQQFqIQFBGAwXCyABIARGBEBBmwEhAgzdAQsCQAJAIAEtAABByQBrDgcAGRkZGRkBGQsgAUEBaiEBQYEBIQIMwwELIAFBAWohAUGCASECDMIBC0GaASECIAEgBEYN2wEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB5tMAai0AAEcNFSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM3AELIANBADYCACAGQQFqIQFBCQwVC0GZASECIAEgBEYN2gEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB5NMAai0AAEcNFCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM2wELIANBADYCACAGQQFqIQFBHwwUC0GYASECIAEgBEYN2QEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB/tIAai0AAEcNEyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM2gELIANBADYCACAGQQFqIQFBAgwTC0GXASECIAEgBEYN2AEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGA0AgAS0AACAAQfzSAGotAABHDREgAEEBRg0CIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNgBCyABIARGBEBBlgEhAgzYAQtBASABLQAAQd8ARw0RGiABQQFqIQFB/QAhAgy9AQsgA0EANgIAIAZBAWohAUH+ACECDLwBC0GVASECIAEgBEYN1QEgAygCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABBxNMAai0AAEcNDyAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM1gELIANBADYCACAGQQFqIQFBKQwPC0GUASECIAEgBEYN1AEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABB+NIAai0AAEcNDiAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM1QELIANBADYCACAGQQFqIQFBLQwOCyABIARGBEBBkwEhAgzUAQsgAS0AAEHFAEcNDiABQQFqIQFB+gAhAgy5AQsgASAERgRAQZIBIQIM0wELAkACQCABLQAAQcwAaw4IAA8PDw8PDwEPCyABQQFqIQFB+AAhAgy5AQsgAUEBaiEBQfkAIQIMuAELQZEBIQIgASAERg3RASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEHz0gBqLQAARw0LIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzSAQsgA0EANgIAIAZBAWohAUEjDAsLQZABIQIgASAERg3QASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHw0gBqLQAARw0KIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzRAQsgA0EANgIAIAZBAWohAUEADAoLIAEgBEYEQEGPASECDNABCwJAAkAgAS0AAEHIAGsOCAAMDAwMDAwBDAsgAUEBaiEBQfMAIQIMtgELIAFBAWohAUH2ACECDLUBCyABIARGBEBBjgEhAgzPAQsCQAJAIAEtAABBzgBrDgMACwELCyABQQFqIQFB9AAhAgy1AQsgAUEBaiEBQfUAIQIMtAELIAEgBEYEQEGNASECDM4BCyABLQAAQdkARw0IIAFBAWohAUEIDAcLQYwBIQIgASAERg3MASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEHs0gBqLQAARw0GIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzNAQsgA0EANgIAIAZBAWohAUEFDAYLQYsBIQIgASAERg3LASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHm0gBqLQAARw0FIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzMAQsgA0EANgIAIAZBAWohAUEWDAULQYoBIQIgASAERg3KASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHh0wBqLQAARw0EIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzLAQsgA0EANgIAIAZBAWohAUEQDAQLIAEgBEYEQEGJASECDMoBCwJAAkAgAS0AAEHDAGsODAAGBgYGBgYGBgYGAQYLIAFBAWohAUHvACECDLABCyABQQFqIQFB8AAhAgyvAQtBiAEhAiABIARGDcgBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQeDSAGotAABHDQIgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMkBCyADQQA2AgAgBkEBaiEBQSQMAgsgA0EANgIADAILIAEgBEYEQEGHASECDMcBCyABLQAAQcwARw0BIAFBAWohAUETCzoAKSADKAIEIQAgA0EANgIEIAMgACABEC0iAA0CDAELQQAhAiADQQA2AhwgAyABNgIUIANB6R42AhAgA0EGNgIMDMQBC0HuACECDKkBCyADQYYBNgIcIAMgATYCFCADIAA2AgxBACECDMIBC0EAIQACQCADKAI4IgJFDQAgAigCOCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUYNASADQQA2AhwgAyABNgIUIANB1A42AhAgA0EgNgIMQQAhAgzBAQtB7QAhAgymAQsgA0GFATYCHCADIAE2AhQgA0HXGjYCECADQRU2AgxBACECDL8BCyABIARGBEBBhQEhAgy/AQsCQCABLQAAQSBGBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GGHjYCECADQQY2AgxBACECDL8BC0ECIQIMpAELA0AgAS0AAEEgRw0CIAQgAUEBaiIBRw0AC0GEASECDL0BCyABIARGBEBBgwEhAgy9AQsCQCABLQAAQQlrDgRAAABAAAtB6wAhAgyiAQsgAy0AKUEFRgRAQewAIQIMogELQeoAIQIMoQELIAEgBEYEQEGCASECDLsBCyADQQ82AgggAyABNgIEDAoLIAEgBEYEQEGBASECDLoBCwJAIAEtAABBCWsOBD0AAD0AC0HpACECDJ8BCyABIARHBEAgA0EPNgIIIAMgATYCBEHnACECDJ8BC0GAASECDLgBCwJAIAEgBEcEQANAIAEtAABB4M4Aai0AACIAQQNHBEACQCAAQQFrDgI/AAQLQeYAIQIMoQELIAQgAUEBaiIBRw0AC0H+ACECDLkBC0H+ACECDLgBCyADQQA2AhwgAyABNgIUIANBxh82AhAgA0EHNgIMQQAhAgy3AQsgASAERgRAQf8AIQIMtwELAkACQAJAIAEtAABB4NAAai0AAEEBaw4DPAIAAQtB6AAhAgyeAQsgA0EANgIcIAMgATYCFCADQYYSNgIQIANBBzYCDEEAIQIMtwELQeAAIQIMnAELIAEgBEcEQCABQQFqIQFB5QAhAgycAQtB/QAhAgy1AQsgBCABIgBGBEBB/AAhAgy1AQsgAC0AACIBQS9GBEAgAEEBaiEBQeQAIQIMmwELIAFBCWsiAkEXSw0BIAAhAUEBIAJ0QZuAgARxDTcMAQsgBCABIgBGBEBB+wAhAgy0AQsgAC0AAEEvRw0AIABBAWohAQwDC0EAIQIgA0EANgIcIAMgADYCFCADQcYfNgIQIANBBzYCDAyyAQsCQAJAAkACQAJAA0AgAS0AAEHgzABqLQAAIgBBBUcEQAJAAkAgAEEBaw4IPQUGBwgABAEIC0HhACECDJ8BCyABQQFqIQFB4wAhAgyeAQsgBCABQQFqIgFHDQALQfoAIQIMtgELIAFBAWoMFAsgAygCBCEAIANBADYCBCADIAAgARArIgBFDR4gA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgy0AQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDR4gA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyzAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDR4gA0HwADYCHCADIAE2AhQgAyAANgIMQQAhAgyyAQsgA0EANgIcIAMgATYCFCADQcsPNgIQIANBBzYCDEEAIQIMsQELIAEgBEYEQEH5ACECDLEBCwJAIAEtAABB4MwAai0AAEEBaw4INAQFBgAIAgMHCyABQQFqIQELQQMhAgyVAQsgAUEBagwNC0EAIQIgA0EANgIcIANBoxI2AhAgA0EHNgIMIAMgAUEBajYCFAytAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDRYgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgysAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDRYgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyrAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDRYgA0HwADYCHCADIAE2AhQgAyAANgIMQQAhAgyqAQsgA0EANgIcIAMgATYCFCADQcsPNgIQIANBBzYCDEEAIQIMqQELQeIAIQIMjgELIAEgBEYEQEH4ACECDKgBCyABQQFqDAILIAEgBEYEQEH3ACECDKcBCyABQQFqDAELIAEgBEYNASABQQFqCyEBQQQhAgyKAQtB9gAhAgyjAQsDQCABLQAAQeDKAGotAAAiAEECRwRAIABBAUcEQEHfACECDIsBCwwnCyAEIAFBAWoiAUcNAAtB9QAhAgyiAQsgASAERgRAQfQAIQIMogELAkAgAS0AAEEJaw43JQMGJQQGBgYGBgYGBgYGBgYGBgYGBgYFBgYCBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAAYLIAFBAWoLIQFBBSECDIYBCyABQQFqDAYLIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ0IIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMngELIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ0IIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMnQELIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ0IIANB8AA2AhwgAyABNgIUIAMgADYCDEEAIQIMnAELIANBADYCHCADIAE2AhQgA0G8EzYCECADQQc2AgxBACECDJsBCwJAAkACQAJAA0AgAS0AAEHgyABqLQAAIgBBBUcEQAJAIABBAWsOBiQDBAUGAAYLQd4AIQIMhgELIAQgAUEBaiIBRw0AC0HzACECDJ4BCyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNByADQdsANgIcIAMgATYCFCADIAA2AgxBACECDJ0BCyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNByADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDJwBCyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNByADQfAANgIcIAMgATYCFCADIAA2AgxBACECDJsBCyADQQA2AhwgAyABNgIUIANB3Ag2AhAgA0EHNgIMQQAhAgyaAQsgASAERg0BIAFBAWoLIQFBBiECDH4LQfIAIQIMlwELAkACQAJAAkADQCABLQAAQeDGAGotAAAiAEEFRwRAIABBAWsOBB8CAwQFCyAEIAFBAWoiAUcNAAtB8QAhAgyaAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDQMgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgyZAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDQMgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyYAQsgAygCBCEAIANBADYCBCADIAAgARArIgBFDQMgA0HwADYCHCADIAE2AhQgAyAANgIMQQAhAgyXAQsgA0EANgIcIAMgATYCFCADQbQKNgIQIANBBzYCDEEAIQIMlgELQc4AIQIMewtB0AAhAgx6C0HdACECDHkLIAEgBEYEQEHwACECDJMBCwJAIAEtAABBCWsOBBYAABYACyABQQFqIQFB3AAhAgx4CyABIARGBEBB7wAhAgySAQsCQCABLQAAQQlrDgQVAAAVAAtBACEAAkAgAygCOCICRQ0AIAIoAjAiAkUNACADIAIRAAAhAAsgAEUEQEHTASECDHgLIABBFUcEQCADQQA2AhwgAyABNgIUIANBwQ02AhAgA0EaNgIMQQAhAgySAQsgA0HuADYCHCADIAE2AhQgA0HwGTYCECADQRU2AgxBACECDJEBC0HtACECIAEgBEYNkAEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABB18YAai0AAEcNBCAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMkQELIANBADYCACAGQQFqIQEgAy0AKSIAQSNrQQtJDQQCQCAAQQZLDQBBASAAdEHKAHFFDQAMBQtBACECIANBADYCHCADIAE2AhQgA0HlCTYCECADQQg2AgwMkAELQewAIQIgASAERg2PASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHUxgBqLQAARw0DIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyQAQsgA0EANgIAIAZBAWohASADLQApQSFGDQMgA0EANgIcIAMgATYCFCADQYkKNgIQIANBCDYCDEEAIQIMjwELQesAIQIgASAERg2OASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEHQxgBqLQAARw0CIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyPAQsgA0EANgIAIAZBAWohASADLQApIgBBI0kNAiAAQS5GDQIgA0EANgIcIAMgATYCFCADQcEJNgIQIANBCDYCDEEAIQIMjgELIANBADYCAAtBACECIANBADYCHCADIAE2AhQgA0GENzYCECADQQg2AgwMjAELQdgAIQIMcQsgASAERwRAIANBDTYCCCADIAE2AgRB1wAhAgxxC0HqACECDIoBCyABIARGBEBB6QAhAgyKAQsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFB1gAhAgxwCyADKAIEIQAgA0EANgIEIAMgACABEC4iAEUNdCADQegANgIcIAMgATYCFCADIAA2AgxBACECDIkBCyABIARGBEBB5wAhAgyJAQsCQCABLQAAQS5GBEAgAUEBaiEBDAELIAMoAgQhACADQQA2AgQgAyAAIAEQLiIARQ11IANB5gA2AhwgAyABNgIUIAMgADYCDEEAIQIMiQELQdUAIQIMbgsgASAERgRAQeUAIQIMiAELQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC4iAEUNAiADQeIANgIcIAMgATYCFCADIAA2AgxBACECDIoBCyADKAIEIQAgA0EANgIEIAMgACABEC4iAEUNdyADQeMANgIcIAMgATYCFCADIAA2AgxBACECDIkBCyADKAIEIQAgA0EANgIEIAMgACABEC4iAEUNdSADQeQANgIcIAMgATYCFCADIAA2AgwMiAELQdMAIQIMbQsgAy0AKUEiRg2AAUHSACECDGwLQQAhAAJAIAMoAjgiAkUNACACKAI8IgJFDQAgAyACEQAAIQALIABFBEBB1AAhAgxsCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQZwNNgIQIANBITYCDEEAIQIMhgELIANB4QA2AhwgAyABNgIUIANB1hk2AhAgA0EVNgIMQQAhAgyFAQsgASAERgRAQeAAIQIMhQELAkACQAJAAkACQCABLQAAQQprDgQBBAQABAsgAUEBaiEBDAELIAFBAWohASADQS9qLQAAQQFxRQ0BC0HRACECDGwLIANBADYCHCADIAE2AhQgA0GIETYCECADQQk2AgxBACECDIUBCyADQQA2AhwgAyABNgIUIANBiBE2AhAgA0EJNgIMQQAhAgyEAQsgASAERgRAQd8AIQIMhAELIAEtAABBCkYEQCABQQFqIQEMCQsgAy0ALkHAAHENCCADQQA2AhwgAyABNgIUIANBiBE2AhAgA0ECNgIMQQAhAgyDAQsgASAERgRAQd0AIQIMgwELIAEtAAAiAkENRgRAIAFBAWohAUHPACECDGkLIAEhACACQQlrDgQFAQEFAQsgBCABIgBGBEBB3AAhAgyCAQsgAC0AAEEKRw0AIABBAWoMAgtBACECIANBADYCHCADIAA2AhQgA0G1LDYCECADQQc2AgwMgAELIAEgBEYEQEHbACECDIABCwJAIAEtAABBCWsOBAMAAAMACyABQQFqCyEBQc0AIQIMZAsgASAERgRAQdoAIQIMfgsgAS0AAEEJaw4EAAEBAAELQQAhAiADQQA2AhwgA0HsETYCECADQQc2AgwgAyABQQFqNgIUDHwLIANBgBI7ASpBACEAAkAgAygCOCICRQ0AIAIoAjAiAkUNACADIAIRAAAhAAsgAEUNACAAQRVHDQEgA0HZADYCHCADIAE2AhQgA0HwGTYCECADQRU2AgxBACECDHsLQcwAIQIMYAsgA0EANgIcIAMgATYCFCADQcENNgIQIANBGjYCDEEAIQIMeQsgASAERgRAQdkAIQIMeQsgAS0AAEEgRw06IAFBAWohASADLQAuQQFxDTogA0EANgIcIAMgATYCFCADQa0bNgIQIANBHjYCDEEAIQIMeAsgASAERgRAQdgAIQIMeAsCQAJAAkACQAJAIAEtAAAiAEEKaw4EAgMD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AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCABLQAAQTBrDjcVFAABAgMEBQYHFhYWFhYWFggJCgsMDRYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWDg8QERITFgtCAiEKDBQLQgMhCgwTC0IEIQoMEgtCBSEKDBELQgYhCgwQC0IHIQoMDwtCCCEKDA4LQgkhCgwNC0IKIQoMDAtCCyEKDAsLQgwhCgwKC0INIQoMCQtCDiEKDAgLQg8hCgwHC0IKIQoMBgtCCyEKDAULQgwhCgwEC0INIQoMAwtCDiEKDAILQg8hCgwBC0IBIQoLIAFBAWohASADKQMgIgtC//////////8PWARAIAMgC0IEhiAKhDcDIAwCC0EAIQIgA0EANgIcIAMgATYCFCADQa0JNgIQIANBDDYCDAwfC0ElIQIMBAtBJiECDAMLIAMgAToALCADQQA2AgAgB0EBaiEBQQwhAgwCCyADQQA2AgAgBkEBaiEBQQohAgwBCyABQQFqIQFBCCECDAALAAtBACECIANBADYCHCADIAE2AhQgA0HVEDYCECADQQk2AgwMGAtBACECIANBADYCHCADIAE2AhQgA0HXCjYCECADQQk2AgwMFwtBACECIANBADYCHCADIAE2AhQgA0G/EDYCECADQQk2AgwMFgtBACECIANBADYCHCADIAE2AhQgA0GkETYCECADQQk2AgwMFQtBACECIANBADYCHCADIAE2AhQgA0HVEDYCECADQQk2AgwMFAtBACECIANBADYCHCADIAE2AhQgA0HXCjYCECADQQk2AgwMEwtBACECIANBADYCHCADIAE2AhQgA0G/EDYCECADQQk2AgwMEgtBACECIANBADYCHCADIAE2AhQgA0GkETYCECADQQk2AgwMEQtBACECIANBADYCHCADIAE2AhQgA0G/FjYCECADQQ82AgwMEAtBACECIANBADYCHCADIAE2AhQgA0G/FjYCECADQQ82AgwMDwtBACECIANBADYCHCADIAE2AhQgA0HIEjYCECADQQs2AgwMDgtBACECIANBADYCHCADIAE2AhQgA0GVCTYCECADQQs2AgwMDQtBACECIANBADYCHCADIAE2AhQgA0HpDzYCECADQQo2AgwMDAtBACECIANBADYCHCADIAE2AhQgA0GDEDYCECADQQo2AgwMCwtBACECIANBADYCHCADIAE2AhQgA0GmHDYCECADQQI2AgwMCgtBACECIANBADYCHCADIAE2AhQgA0HFFTYCECADQQI2AgwMCQtBACECIANBADYCHCADIAE2AhQgA0H/FzYCECADQQI2AgwMCAtBACECIANBADYCHCADIAE2AhQgA0HKFzYCECADQQI2AgwMBwsgA0ECNgIcIAMgATYCFCADQZQdNgIQIANBFjYCDEEAIQIMBgtB3gAhAiABIARGDQUgCUEIaiEHIAMoAgAhBQJAAkAgASAERwRAIAVBxsYAaiEIIAQgBWogAWshBiAFQX9zQQpqIgUgAWohAANAIAEtAAAgCC0AAEcEQEECIQgMAwsgBUUEQEEAIQggACEBDAMLIAVBAWshBSAIQQFqIQggBCABQQFqIgFHDQALIAYhBSAEIQELIAdBATYCACADIAU2AgAMAQsgA0EANgIAIAcgCDYCAAsgByABNgIEIAkoAgwhACAJKAIIDgMBBQIACwALIANBADYCHCADQa0dNgIQIANBFzYCDCADIABBAWo2AhRBACECDAMLIANBADYCHCADIAA2AhQgA0HCHTYCECADQQk2AgxBACECDAILIAEgBEYEQEEoIQIMAgsgA0EJNgIIIAMgATYCBEEnIQIMAQsgASAERgRAQQEhAgwBCwNAAkACQAJAIAEtAABBCmsOBAABAQABCyABQQFqIQEMAQsgAUEBaiEBIAMtAC5BIHENAEEAIQIgA0EANgIcIAMgATYCFCADQYwgNgIQIANBBTYCDAwCC0EBIQIgASAERw0ACwsgCUEQaiQAIAJFBEAgAygCDCEADAELIAMgAjYCHEEAIQAgAygCBCIBRQ0AIAMgASAEIAMoAggRAQAiAUUNACADIAQ2AhQgAyABNgIMIAEhAAsgAAu+AgECfyAAQQA6AAAgAEHcAGoiAUEBa0EAOgAAIABBADoAAiAAQQA6AAEgAUEDa0EAOgAAIAFBAmtBADoAACAAQQA6AAMgAUEEa0EAOgAAQQAgAGtBA3EiASAAaiIAQQA2AgBB3AAgAWtBfHEiAiAAaiIBQQRrQQA2AgACQCACQQlJDQAgAEEANgIIIABBADYCBCABQQhrQQA2AgAgAUEMa0EANgIAIAJBGUkNACAAQQA2AhggAEEANgIUIABBADYC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U3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9zdGF0dXMASW52YWxpZCByZXNwb25zZSBzdGF0dXMARXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVycwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zAFVzZXIgY2FsbGJhY2sgZXJyb3IAYG9uX3Jlc2V0YCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfaGVhZGVyYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9iZWdpbmAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3N0YXR1c19jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3ZlcnNpb25fY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl91cmxfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl92YWx1ZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21lc3NhZ2VfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXRob2RfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfZmllbGRfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fbmFtZWAgY2FsbGJhY2sgZXJyb3IAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzZXJ2ZXIASW52YWxpZCBoZWFkZXIgdmFsdWUgY2hhcgBJbnZhbGlkIGhlYWRlciBmaWVsZCBjaGFyAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fdmVyc2lvbgBJbnZhbGlkIG1pbm9yIHZlcnNpb24ASW52YWxpZCBtYWpvciB2ZXJzaW9uAEV4cGVjdGVkIHNwYWNlIGFmdGVyIHZlcnNpb24ARXhwZWN0ZWQgQ1JMRiBhZnRlciB2ZXJzaW9uAEludmFsaWQgSFRUUCB2ZXJzaW9uAEludmFsaWQgaGVhZGVyIHRva2VuAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fdXJsAEludmFsaWQgY2hhcmFjdGVycyBpbiB1cmwAVW5leHBlY3RlZCBzdGFydCBjaGFyIGluIHVybABEb3VibGUgQCBpbiB1cmwARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGhlYWRlciB2YWx1ZQBNaXNzaW5nIGV4cGVjdGVkIExGIGFmdGVyIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGUgdmFsdWUASW52YWxpZCBxdW90ZWQtcGFpciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlZCB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlZCB2YWx1ZQBQYXVzZWQgYnkgb25faGVhZGVyc19jb21wbGV0ZQBJbnZhbGlkIEVPRiBzdGF0ZQBvbl9yZXNldCBwYXVzZQBvbl9jaHVua19oZWFkZXIgcGF1c2UAb25fbWVzc2FnZV9iZWdpbiBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fdmFsdWUgcGF1c2UAb25fc3RhdHVzX2NvbXBsZXRlIHBhdXNlAG9uX3ZlcnNpb25fY29tcGxldGUgcGF1c2UAb25fdXJsX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl92YWx1ZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXNzYWdlX2NvbXBsZXRlIHBhdXNlAG9uX21ldGhvZF9jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfZmllbGRfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUgcGF1c2UAVW5leHBlY3RlZCBzcGFjZSBhZnRlciBzdGFydCBsaW5lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgcmVzcG9uc2UgbGluZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgbmFtZQBNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGV4dGVuc2lvbiBuYW1lAEludmFsaWQgc3RhdHVzIGNvZGUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBkYXRhAEV4cGVjdGVkIExGIGFmdGVyIGNodW5rIGRhdGEAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAERhdGEgYWZ0ZXIgYENvbm5lY3Rpb246IGNsb3NlYABTV0lUQ0hfUFJPWFkAVVNFX1BST1hZAE1LQUNUSVZJVFkAVU5QUk9DRVNTQUJMRV9FTlRJVFkAUVVFUlkAQ09QWQBNT1ZFRF9QRVJNQU5FTlRMWQBUT09fRUFSTFkATk9USUZZAEZBSUxFRF9ERVBFTkRFTkNZAEJBRF9HQVRFV0FZAFBMQVkAUFVUAENIRUNLT1VUAEdBVEVXQVlfVElNRU9VVABSRVFVRVNUX1RJTUVPVVQATkVUV09SS19DT05ORUNUX1RJTUVPVVQAQ09OTkVDVElPTl9USU1FT1VUAExPR0lOX1RJTUVPVVQATkVUV09SS19SRUFEX1RJTUVPVVQAUE9TVABNSVNESVJFQ1RFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX0xPQURfQkFMQU5DRURfUkVRVUVTVABCQURfUkVRVUVTVABIVFRQX1JFUVVFU1RfU0VOVF9UT19IVFRQU19QT1JUAFJFUE9SVABJTV9BX1RFQVBPVABSRVNFVF9DT05URU5UAE5PX0NPTlRFTlQAUEFSVElBTF9DT05URU5UAEhQRV9JTlZBTElEX0NPTlNUQU5UAEhQRV9DQl9SRVNFVABHRVQASFBFX1NUUklDVABDT05GTElDVABURU1QT1JBUllfUkVESVJFQ1QAUEVSTUFORU5UX1JFRElSRUNUAENPTk5FQ1QATVVMVElfU1RBVFVTAEhQRV9JTlZBTElEX1NUQVRVUwBUT09fTUFOWV9SRVFVRVNUUwBFQVJMWV9ISU5UUwBVTkFWQUlMQUJMRV9GT1JfTEVHQUxfUkVBU09OUwBPUFRJT05TAFNXSVRDSElOR19QUk9UT0NPTFMAVkFSSUFOVF9BTFNPX05FR09USUFURVMATVVMVElQTEVfQ0hPSUNFUwBJTlRFUk5BTF9TRVJWRVJfRVJST1IAV0VCX1NFUlZFUl9VTktOT1dOX0VSUk9SAFJBSUxHVU5fRVJST1IASURFTlRJVFlfUFJPVklERVJfQVVUSEVOVElDQVRJT05fRVJST1IAU1NMX0NFUlRJRklDQVRFX0VSUk9SAElOVkFMSURfWF9GT1JXQVJERURfRk9SAFNFVF9QQVJBTUVURVIAR0VUX1BBUkFNRVRFUgBIUEVfVVNFUgBTRUVfT1RIRVIASFBFX0NCX0NIVU5LX0hFQURFUgBFeHBlY3RlZCBMRiBhZnRlciBDUgBNS0NBTEVOREFSAFNFVFVQAFdFQl9TRVJWRVJfSVNfRE9XTgBURUFSRE9XTgBIUEVfQ0xPU0VEX0NPTk5FQ1RJT04ASEVVUklTVElDX0VYUElSQVRJT04ARElTQ09OTkVDVEVEX09QRVJBVElPTgBOT05fQVVUSE9SSVRBVElWRV9JTkZPUk1BVElPTgBIUEVfSU5WQUxJRF9WRVJTSU9OAEhQRV9DQl9NRVNTQUdFX0JFR0lOAFNJVEVfSVNfRlJPWkVOAEhQRV9JTlZBTElEX0hFQURFUl9UT0tFTgBJTlZBTElEX1RPS0VOAEZPUkJJRERFTgBFTkhBTkNFX1lPVVJfQ0FMTQBIUEVfSU5WQUxJRF9VUkwAQkxPQ0tFRF9CWV9QQVJFTlRBTF9DT05UUk9MAE1LQ09MAEFDTABIUEVfSU5URVJOQUwAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRV9VTk9GRklDSUFMAEhQRV9PSwBVTkxJTksAVU5MT0NLAFBSSQBSRVRSWV9XSVRIAEhQRV9JTlZBTElEX0NPTlRFTlRfTEVOR1RIAEhQRV9VTkVYUEVDVEVEX0NPTlRFTlRfTEVOR1RIAEZMVVNIAFBST1BQQVRDSABNLVNFQVJDSABVUklfVE9PX0xPTkcAUFJPQ0VTU0lORwBNSVNDRUxMQU5FT1VTX1BFUlNJU1RFTlRfV0FSTklORwBNSVNDRUxMQU5FT1VTX1dBUk5JTkcASFBFX0lOVkFMSURfVFJBTlNGRVJfRU5DT0RJTkcARXhwZWN0ZWQgQ1JMRgBIUEVfSU5WQUxJRF9DSFVOS19TSVpFAE1PVkUAQ09OVElOVUUASFBFX0NCX1NUQVRVU19DT01QTEVURQBIUEVfQ0JfSEVBREVSU19DT01QTEVURQBIUEVfQ0JfVkVSU0lPTl9DT01QTEVURQBIUEVfQ0JfVVJMX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8AAFIVAAAaFQAADxIAAOQZAACRFQAACRQAAC0ZAADkFAAA6REAAGkUAAChFAAAdhUAAEMWAABeEgAAlBcAABcWAAB9FAAAfxYAAEEXAACzEwAAwxYAAAQaAAC9GAAA0BgAAKATAADUGQAArxYAAGgWAABwFwAA2RYAAPwYAAD+EQAAWRcAAJcWAAAcFwAA9hYAAI0XAAALEgAAfxsAAC4RAACzEAAASRIAAK0SAAD2GAAAaBAAAGIVAAAQFQAAWhYAAEoZAAC1FQAAwRUAAGAVAABcGQAAWhkAAFMZAAAWFQAArREAAEIQAAC3EAAAVxgAAL8VAACJEAAAHBkAABoZAAC5FQAAURgAANwTAABbFQAAWRUAAOYYAABnFQAAERkAAO0YAADnEwAArhAAAMIXAAAAFAAAkhMAAIQTAABAEgAAJhkAAK8VAABiEABB6TkLAQEAQYA6C+ABAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAwEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAQeo7CwQBAAACAEGBPAteAwQDAwMDAwAAAwMAAwMAAwMDAwMDAwMDAwAFAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAMAAwBB6j0LBAEAAAIAQYE+C14DAAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAQABQAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEHgPwsNbG9zZWVlcC1hbGl2ZQBB+T8LAQEAQZDAAAvgAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAEH5wQALAQEAQZDCAAvnAQEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZABBocQAC14BAAEBAQEBAAABAQABAQABAQEBAQEBAQEBAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAEGAxgALIWVjdGlvbmVudC1sZW5ndGhvbnJveHktY29ubmVjdGlvbgBBsMYACytyYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNClNNDQoNClRUUC9DRS9UU1AvAEHpxgALBQECAAEDAEGAxwALXwQFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFAEHpyAALBQECAAEDAEGAyQALXwQFBQYFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFAEHpygALBAEAAAEAQYHLAAteAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgBB6cwACwUBAgABAwBBgM0AC18EBQAABQUFBQUFBQUFBQUGBQUFBQUFBQUFBQUFAAUABwgFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUABQAFAAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFAAAABQBB6c4ACwUBAQABAQBBgM8ACwEBAEGazwALQQIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAEHp0AALBQEBAAEBAEGA0QALAQEAQYrRAAsGAgAAAAACAEGh0QALOgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQeDSAAuaAU5PVU5DRUVDS09VVE5FQ1RFVEVDUklCRUxVU0hFVEVBRFNFQVJDSFJHRUNUSVZJVFlMRU5EQVJWRU9USUZZUFRJT05TQ0hTRUFZU1RBVENIR0VVRVJZT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=' +const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzU0BQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEBAwAAAwAAAAQFAXABExMFAwEAAgYIAX8BQcDZBAsHxQcoBm1lbW9yeQIAC19pbml0aWFsaXplAAgZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAC2xsaHR0cF9pbml0AAkYbGxodHRwX3Nob3VsZF9rZWVwX2FsaXZlADcMbGxodHRwX2FsbG9jAAsGbWFsbG9jADkLbGxodHRwX2ZyZWUADARmcmVlAAwPbGxodHRwX2dldF90eXBlAA0VbGxodHRwX2dldF9odHRwX21ham9yAA4VbGxodHRwX2dldF9odHRwX21pbm9yAA8RbGxodHRwX2dldF9tZXRob2QAEBZsbGh0dHBfZ2V0X3N0YXR1c19jb2RlABESbGxodHRwX2dldF91cGdyYWRlABIMbGxodHRwX3Jlc2V0ABMObGxodHRwX2V4ZWN1dGUAFBRsbGh0dHBfc2V0dGluZ3NfaW5pdAAVDWxsaHR0cF9maW5pc2gAFgxsbGh0dHBfcGF1c2UAFw1sbGh0dHBfcmVzdW1lABgbbGxodHRwX3Jlc3VtZV9hZnRlcl91cGdyYWRlABkQbGxodHRwX2dldF9lcnJubwAaF2xsaHR0cF9nZXRfZXJyb3JfcmVhc29uABsXbGxodHRwX3NldF9lcnJvcl9yZWFzb24AHBRsbGh0dHBfZ2V0X2Vycm9yX3BvcwAdEWxsaHR0cF9lcnJub19uYW1lAB4SbGxodHRwX21ldGhvZF9uYW1lAB8SbGxodHRwX3N0YXR1c19uYW1lACAabGxodHRw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AXRrQT5qIQILIAEgAjYCHCABQgA3AhAgAkECdEH01wBqIQACQEHI1QAoAgAiA0EBIAJ0IgdxRQRAIAAgATYCAEHI1QAgAyAHcjYCACABIAA2AhggASABNgIIIAEgATYCDAwBCyAEQRkgAkEBdmtBACACQR9HG3QhAiAAKAIAIQACQANAIAAiAygCBEF4cSAERg0BIAJBHXYhACACQQF0IQIgAyAAQQRxakEQaiIHKAIAIgANAAsgByABNgIAIAEgAzYCGCABIAE2AgwgASABNgIIDAELIAMoAggiACABNgIMIAMgATYCCCABQQA2AhggASADNgIMIAEgADYCCAtB5NUAQeTVACgCAEEBayIAQX8gABs2AgALCwcAIAAtACgLBwAgAC0AKgsHACAALQArCwcAIAAtACkLBwAgAC8BNAsHACAALQAwC0ABBH8gACgCGCEBIAAvAS4hAiAALQAoIQMgACgCOCEEIAAQOCAAIAQ2AjggACADOgAoIAAgAjsBLiAAIAE2AhgLhocCAwd/A34BeyABIAJqIQQCQCAAIgMoAgwiAA0AIAMoAgQEQCADIAE2AgQLIwBBEGsiCSQAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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DAgLQeMBDAcLQdcBDAYLQdgBDAULQdkBDAQLQdoBDAMLQdsBDAILQd0BDAELQdwBCyECA0ACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAn8CQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAwJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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A0EXNgIcIAMgADYCDCADIAFBAWo2AhQM+AILIABBFUYNI0EAIQIgA0EANgIcIAMgATYCFCADQdYMNgIQIANBIzYCDAz3AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQwdCyADQRk2AhwgAyAANgIMIAMgAUEBajYCFAz2AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQzvAgsgA0EaNgIcIAMgADYCDCADIAFBAWo2AhQM9QILIABBFUYNH0EAIQIgA0EANgIcIAMgATYCFCADQdAPNgIQIANBIjYCDAz0AgsgAygCBCEAIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDBsLIANBHDYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgzzAgsgAygCBCEAIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDOsCCyADQR02AhwgAyAANgIMIAMgAUEBajYCFEEAIQIM8gILIABBO0cNASABQQFqIQELQSYhAgzXAgtBACECIANBADYCHCADIAE2AhQgA0GfFTYCECADQQw2AgwM7wILIAEgBEcEQANAIAEtAABBIEcNhAIgBCABQQFqIgFHDQALQSwhAgzvAgtBLCECDO4CCyABIARGBEBBNCECDO4CCwJAAkADQAJAIAEtAABBCmsOBAIAAAMACyAEIAFBAWoiAUcNAAtBNCECDO8CCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUNnwIgA0EyNgIcIAMgATYCFCADIAA2AgxBACECDO4CCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUEQCABQQFqIQEMnwILIANBMjYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgztAgsgASAERwRAAkADQCABLQAAQTBrIgBB/wFxQQpPBEBBOiECDNcCCyADKQMgIgtCmbPmzJmz5swZVg0BIAMgC0IKfiIKNwMgIAogAK1C/wGDIgtCf4VWDQEgAyAKIAt8NwMgIAQgAUEBaiIBRw0AC0HAACECDO4CCyADKAIEIQAgA0EANgIEIAMgACABQQFqIgEQMSIADRcM4gILQcAAIQIM7AILIAEgBEYEQEHJACECDOwCCwJAA0ACQCABLQAAQQlrDhgAAqICogKpAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAgCiAgsgBCABQQFqIgFHDQALQckAIQIM7AILIAFBAWohASADQS9qLQAAQQFxDaUCIANBADYCHCADIAE2AhQgA0GXEDYCECADQQo2AgxBACECDOsCCyABIARHBEADQCABLQAAQSBHDRUgBCABQQFqIgFHDQALQfgAIQIM6wILQfgAIQIM6gILIANBAjoAKAw4C0EAIQIgA0EANgIcIANBvws2AhAgA0ECNgIMIAMgAUEBajYCFAzoAgtBACECDM4CC0ENIQIMzQILQRMhAgzMAgtBFSECDMsCC0EWIQIMygILQRghAgzJAgtBGSECDMgCC0EaIQIMxwILQRshAgzGAgtBHCECDMUCC0EdIQIMxAILQR4hAgzDAgtBHyECDMICC0EgIQIMwQILQSIhAgzAAgtBIyECDL8CC0ElIQIMvgILQeUAIQIMvQILIANBPTYCHCADIAE2AhQgAyAANgIMQQAhAgzVAgsgA0EbNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIM1AILIANBIDYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNMCCyADQRM2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzSAgsgA0ELNgIcIAMgATYCFCADQZgaNgIQIANBFTYCDEEAIQIM0QILIANBEDYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNACCyADQSA2AhwgAyABNgIUIANBpBw2AhAgA0EVNgIMQQAhAgzPAgsgA0ELNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIMzgILIANBDDYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDM0CC0EAIQIgA0EANgIcIAMgATYCFCADQd0ONgIQIANBEjYCDAzMAgsCQANAAkAgAS0AAEEKaw4EAAICAAILIAQgAUEBaiIBRw0AC0H9ASECDMwCCwJAAkAgAy0ANkEBRw0AQQAhAAJAIAMoAjgiAkUNACACKAJgIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB/AE2AhwgAyABNgIUIANB3Bk2AhAgA0EVNgIMQQAhAgzNAgtB3AEhAgyzAgsgA0EANgIcIAMgATYCFCADQfkLNgIQIANBHzYCDEEAIQIMywILAkACQCADLQAoQQFrDgIEAQALQdsBIQIMsgILQdQBIQIMsQILIANBAjoAMUEAIQACQCADKAI4IgJFDQAgAigCACICRQ0AIAMgAhEAACEACyAARQRAQd0BIQIMsQILIABBFUcEQCADQQA2AhwgAyABNgIUIANBtAw2AhAgA0EQNgIMQQAhAgzKAgsgA0H7ATYCHCADIAE2AhQgA0GBGjYCECADQRU2AgxBACECDMkCCyABIARGBEBB+gEhAgzJAgsgAS0AAEHIAEYNASADQQE6ACgLQcABIQIMrgILQdoBIQIMrQILIAEgBEcEQCADQQw2AgggAyABNgIEQdkBIQIMrQILQfkBIQIMxQILIAEgBEYEQEH4ASECDMUCCyABLQAAQcgARw0EIAFBAWohAUHYASECDKsCCyABIARGBEBB9wEhAgzEAgsCQAJAIAEtAABBxQBrDhAABQUFBQUFBQUFBQUFBQUBBQsgAUEBaiEBQdYBIQIMqwILIAFBAWohAUHXASECDKoCC0H2ASECIAEgBEYNwgIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABButUAai0AAEcNAyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMwwILIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAuIgBFBEBB4wEhAgyqAgsgA0H1ATYCHCADIAE2AhQgAyAANgIMQQAhAgzCAgtB9AEhAiABIARGDcECIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQbjVAGotAABHDQIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMICCyADQYEEOwEoIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAuIgANAwwCCyADQQA2AgALQQAhAiADQQA2AhwgAyABNgIUIANB5R82AhAgA0EINgIMDL8CC0HVASECDKUCCyADQfMBNgIcIAMgATYCFCADIAA2AgxBACECDL0CC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ1uIABBFUcEQCADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgy9AgsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDLwCCyABIARHBEAgA0ENNgIIIAMgATYCBEHTASECDKMCC0HyASECDLsCCyABIARGBEBB8QEhAgy7AgsCQAJAAkAgAS0AAEHIAGsOCwABCAgICAgICAgCCAsgAUEBaiEBQdABIQIMowILIAFBAWohAUHRASECDKICCyABQQFqIQFB0gEhAgyhAgtB8AEhAiABIARGDbkCIAMoAgAiACAEIAFraiEGIAEgAGtBAmohBQNAIAEtAAAgAEG11QBqLQAARw0EIABBAkYNAyAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy5AgtB7wEhAiABIARGDbgCIAMoAgAiACAEIAFraiEGIAEgAGtBAWohBQNAIAEtAAAgAEGz1QBqLQAARw0DIABBAUYNAiAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy4AgtB7gEhAiABIARGDbcCIAMoAgAiACAEIAFraiEGIAEgAGtBAmohBQNAIAEtAAAgAEGw1QBqLQAARw0CIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy3AgsgAygCBCEAIANCADcDACADIAAgBUEBaiIBECsiAEUNAiADQewBNgIcIAMgATYCFCADIAA2AgxBACECDLYCCyADQQA2AgALIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ2cAiADQe0BNgIcIAMgATYCFCADIAA2AgxBACECDLQCC0HPASECDJoCC0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMtAILQc4BIQIMmgILIANB6wE2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyyAgsgASAERgRAQesBIQIMsgILIAEtAABBL0YEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDEEAIQIMsQILQc0BIQIMlwILIAEgBEcEQCADQQ42AgggAyABNgIEQcwBIQIMlwILQeoBIQIMrwILIAEgBEYEQEHpASECDK8CCyABLQAAQTBrIgBB/wFxQQpJBEAgAyAAOgAqIAFBAWohAUHLASECDJYCCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNlwIgA0HoATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgsgASAERgRAQecBIQIMrgILAkAgAS0AAEEuRgRAIAFBAWohAQwBCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNmAIgA0HmATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgtBygEhAgyUAgsgASAERgRAQeUBIQIMrQILQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNAiADQeIBNgIcIAMgATYCFCADIAA2AgxBACECDK8CCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNmgIgA0HjATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZgCIANB5AE2AhwgAyABNgIUIAMgADYCDAytAgtByQEhAgyTAgtBACEAAkAgAygCOCICRQ0AIAIoAkQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0GkDTYCECADQSE2AgxBACECDK0CC0HIASECDJMCCyADQeEBNgIcIAMgATYCFCADQdAaNgIQIANBFTYCDEEAIQIMqwILIAEgBEYEQEHhASECDKsCCwJAIAEtAABBIEYEQCADQQA7ATQgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GZETYCECADQQk2AgxBACECDKsCC0HHASECDJECCyABIARGBEBB4AEhAgyqAgsCQCABLQAAQTBrQf8BcSICQQpJBEAgAUEBaiEBAkAgAy8BNCIAQZkzSw0AIAMgAEEKbCIAOwE0IABB/v8DcSACQf//A3NLDQAgAyAAIAJqOwE0DAILQQAhAiADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMDKsCCyADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMQQAhAgyqAgtBxgEhAgyQAgsgASAERgRAQd8BIQIMqQILAkAgAS0AAEEwa0H/AXEiAkEKSQRAIAFBAWohAQJAIAMvATQiAEGZM0sNACADIABBCmwiADsBNCAAQf7/A3EgAkH//wNzSw0AIAMgACACajsBNAwCC0EAIQIgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDAyqAgsgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDEEAIQIMqQILQcUBIQIMjwILIAEgBEYEQEHeASECDKgCCwJAIAEtAABBMGtB/wFxIgJBCkkEQCABQQFqIQECQCADLwE0IgBBmTNLDQAgAyAAQQpsIgA7ATQgAEH+/wNxIAJB//8Dc0sNACADIAAgAmo7ATQMAgtBACECIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgwMqQILIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgxBACECDKgCC0HEASECDI4CCyABIARGBEBB3QEhAgynAgsCQAJAAkACQCABLQAAQQprDhcCAwMAAwMDAwMDAwMDAwMDAwMDAwMDAQMLIAFBAWoMBQsgAUEBaiEBQcMBIQIMjwILIAFBAWohASADQS9qLQAAQQFxDQggA0EANgIcIAMgATYCFCADQY0LNgIQIANBDTYCDEEAIQIMpwILIANBADYCHCADIAE2AhQgA0GNCzYCECADQQ02AgxBACECDKYCCyABIARHBEAgA0EPNgIIIAMgATYCBEEBIQIMjQILQdwBIQIMpQILAkACQANAAkAgAS0AAEEKaw4EAgAAAwALIAQgAUEBaiIBRw0AC0HbASECDKYCCyADKAIEIQAgA0EANgIEIAMgACABEC0iAEUEQCABQQFqIQEMBAsgA0HaATYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgylAgsgAygCBCEAIANBADYCBCADIAAgARAtIgANASABQQFqCyEBQcEBIQIMigILIANB2QE2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMogILQcIBIQIMiAILIANBL2otAABBAXENASADQQA2AhwgAyABNgIUIANB5Bw2AhAgA0EZNgIMQQAhAgygAgsgASAERgRAQdkB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CyADIAU2AgAMkAILIANBADYCACAGQQFqIQFBGgw8C0HKASECIAEgBEYNjgIgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBjdUAai0AAEcNOyAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMjwILIANBADYCACAGQQFqIQFBIQw7CyABIARGBEBByQEhAgyOAgsCQAJAIAEtAABBwQBrDhQAPT09PT09PT09PT09PT09PT09AT0LIAFBAWohAUGtASECDPUBCyABQQFqIQFBsAEhAgz0AQsgASAERgRAQcgBIQIMjQILAkACQCABLQAAQdUAaw4LADw8PDw8PDw8PAE8CyABQQFqIQFBrgEhAgz0AQsgAUEBaiEBQa8BIQIM8wELQccBIQIgASAERg2LAiADKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEGE1QBqLQAARw04IABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyMAgsgA0EANgIAIAZBAWohAUEqDDgLIAEgBEYEQEHGASECDIsCCyABLQAAQdAARw04IAFBAWohAUElDDcLQcUBIQIgASAERg2JAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGB1QBqLQAARw02IABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyKAgsgA0EANgIAIAZBAWohAUEODDYLIAEgBEYEQEHEASECDIkCCyABLQAAQcUARw02IAFBAWohAUGrASECDO8BCyABIARGBEBBwwEhAgyIAgsCQAJAAkACQCABLQAAQcIAaw4PAAECOTk5OTk5OTk5OTkDOQsgAUEBaiEBQacBIQIM8QELIAFBAWohAUGoASECDPABCyABQQFqIQFBqQEhAgzvAQsgAUEBaiEBQaoBIQIM7gELQcIBIQIgASAERg2GAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEH+1ABqLQAARw0zIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyHAgsgA0EANgIAIAZBAWohAUEUDDMLQcEBIQIgASAERg2FAiADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANA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BTYCAAzsAQsgA0EANgIAIAZBAWohAUEXDBgLQaYBIQIgASAERg3qASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHB1ABqLQAARw0XIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzrAQsgA0EANgIAIAZBAWohAUEYDBcLIAEgBEYEQEGlASECDOoBCwJAAkAgAS0AAEHJAGsOBwAZGRkZGQEZCyABQQFqIQFBiwEhAgzRAQsgAUEBaiEBQYwBIQIM0AELQaQBIQIgASAERg3oASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGm1QBqLQAARw0VIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzpAQsgA0EANgIAIAZBAWohAUEJDBULQaMBIQIgASAERg3nASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGk1QBqLQAARw0UIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzoAQsgA0EANgIAIAZBAWohAUEfDBQLQaIBIQIgASAERg3mASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEG+1ABqLQAARw0TIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAznAQsgA0EANgIAIAZBAWohAUECDBMLQaEBIQIgASAERg3lASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYDQCABLQAAIABBvNQAai0AAEcNESAAQQFGDQIgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM5QELIAEgBEYEQEGgASECDOUBC0EBIAEtAABB3wBHDREaIAFBAWohAUGHASECDMsBCyADQQA2AgAgBkEBaiEBQYgBIQIMygELQZ8BIQIgASAERg3iASADKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEGE1QBqLQAARw0PIABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzjAQsgA0EANgIAIAZBAWohAUEpDA8LQZ4BIQIgASAERg3hASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEG41ABqLQAARw0OIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAziAQsgA0EANgIAIAZBAWohAUEtDA4LIAEgBEYEQEGdASECDOEBCyABLQAAQcUARw0OIAFBAWohAUGEASECDMcBCyABIARGBEBBnAEhAgzgAQsCQAJAIAEtAABBzABrDggADw8PDw8PAQ8LIAFBAWohAUGCASECDMcBCyABQQFqIQFBgwEhAgzGAQtBmwEhAiABIARGDd4BIAMoAgAiACAEIAFraiEFIAEgAGtBBGohBgJAA0AgAS0AACAAQbPUAGotAABHDQsgAEEERg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADN8BCyADQQA2AgAgBkEBaiEBQSMMCwtBmgEhAiABIARGDd0BIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbDUAGotAABHDQogAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADN4BCyADQQA2AgAgBkEBaiEBQQAMCgsgASAERgRAQZkBIQIM3QELAkACQCABLQAAQcgAaw4IAAwMDAwMDAEMCyABQQFqIQFB/QAhAgzEAQsgAUEBaiEBQYABIQIMwwELIAEgBEYEQEGYASECDNwBCwJAAkAgAS0AAEHOAGsOAwALAQsLIAFBAWohAUH+ACECDMMBCyABQQFqIQFB/wAhAgzCAQsgASAERgRAQZcBIQIM2wELIAEtAABB2QBHDQggAUEBaiEBQQgMBwtBlgEhAiABIARGDdkBIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQazUAGotAABHDQYgAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNoBCyADQQA2AgAgBkEBaiEBQQUMBgtBlQEhAiABIARGDdgBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQabUAGotAABHDQUgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNkBCyADQQA2AgAgBkEBaiEBQRYMBQtBlAEhAiABIARGDdcBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDQQgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNgBCyADQQA2AgAgBkEBaiEBQRAMBAsgASAERgRAQZMBIQIM1wELAkACQCABLQAAQcMAaw4MAAYGBgYGBgYGBgYBBgsgAUEBaiEBQfkAIQIMvgELIAFBAWohAUH6ACECDL0BC0GSASECIAEgBEYN1QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBoNQAai0AAEcNAiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM1gELIANBADYCACAGQQFqIQFBJAwCCyADQQA2AgAMAgsgASAERgRAQZEBIQIM1AELIAEtAABBzABHDQEgAUEBaiEBQRMLOgApIAMoAgQhACADQQA2AgQgAyAAIAEQLiIADQIMAQtBACECIANBADYCHCADIAE2AhQgA0H+HzYCECADQQY2AgwM0QELQfgAIQIMtwELIANBkAE2AhwgAyABNgIUIAMgADYCDEEAIQIMzwELQQAhAAJAIAMoAjgiAkUNACACKAJAIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRg0BIANBADYCHCADIAE2AhQgA0GCDzYCECADQSA2AgxBACECDM4BC0H3ACECDLQBCyADQY8BNgIcIAMgATYCFCADQewbNgIQIANBFTYCDEEAIQIMzAELIAEgBEYEQEGPASECDMwBCwJAIAEtAABBIEYEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQZsfNgIQIANBBjYCDEEAIQIMzAELQQIhAgyyAQsDQCABLQAAQSBHDQIgBCABQQFqIgFHDQALQY4BIQIMygELIAEgBEYEQEGNASECDMoBCwJAIAEtAABBCWsOBEoAAEoAC0H1ACECDLABCyADLQApQQVGBEBB9gAhAgywAQtB9AAhAgyvAQsgASAERgRAQYwBIQIMyAELIANBEDYCCCADIAE2AgQMCgsgASAERgRAQYsBIQIMxwELAkAgAS0AAEEJaw4ERwAARwALQfMAIQIMrQELIAEgBEcEQCADQRA2AgggAyABNgIEQfEAIQIMrQELQYoBIQIMxQELAkAgASAERwRAA0AgAS0AAEGg0ABqLQAAIgBBA0cEQAJAIABBAWsOAkkABAtB8AAhAgyvAQsgBCABQQFqIgFHDQALQYgBIQIMxgELQYgBIQIMxQELIANBADYCHCADIAE2AhQgA0HbIDYCECADQQc2AgxBACECDMQBCyABIARGBEBBiQEhAgzEAQsCQAJAAkAgAS0AAEGg0gBqLQAAQQFrDgNGAgABC0HyACECDKwBCyADQQA2AhwgAyABNgIUIANBtBI2AhAgA0EHNgIMQQAhAgzEAQtB6gAhAgyqAQsgASAERwRAIAFBAWohAUHvACECDKoBC0GHASECDMIBCyAEIAEiAEYEQEGGASECDMIBCyAALQAAIgFBL0YEQCAAQQFqIQFB7gAhAgypAQsgAUEJayICQRdLDQEgACEBQQEgAnRBm4CABHENQQwBCyAEIAEiAEYEQEGFASECDMEBCyAALQAAQS9HDQAgAEEBaiEBDAMLQQAhAiADQQA2AhwgAyAANgIUIANB2yA2AhAgA0EHNgIMDL8BCwJAAkACQAJAAkADQCABLQAAQaDOAGotAAAiAEEFRwRAAkACQCAAQQFrDghHBQYHCAAEAQgLQesAIQIMrQELIAFBAWohAUHtACECDKwBCyAEIAFBAWoiAUcNAAtBhAEhAgzDAQsgAUEBagwUCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQdsANgIcIAMgATYCFCADIAA2AgxBACECDMEBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDMABCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQfoANgIcIAMgATYCFCADIAA2AgxBACECDL8BCyADQQA2AhwgAyABNgIUIANB+Q82AhAgA0EHNgIMQQAhAgy+AQsgASAERgRAQYMBIQIMvgELAkAgAS0AAEGgzgBqLQAAQQFrDgg+BAUGAAgCAwcLIAFBAWohAQtBAyECDKMBCyABQQFqDA0LQQAhAiADQQA2AhwgA0HREjYCECADQQc2AgwgAyABQQFqNgIUDLoBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQdsANgIcIAMgATYCFCADIAA2AgxBACECDLkBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDLgBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQfoANgIcIAMgATYCFCADIAA2AgxBACECDLcBCyADQQA2AhwgAyABNgIUIANB+Q82AhAgA0EHNgIMQQAhAgy2AQtB7AAhAgycAQsgASAERgRAQYIBIQIMtQELIAFBAWoMAgsgASAERgRAQYEBIQIMtAELIAFBAWoMAQsgASAERg0BIAFBAWoLIQFBBCECDJgBC0GAASECDLABCwNAIAEtAABBoMwAai0AACIAQQJHBEAgAEEBRwRAQekAIQIMmQELDDELIAQgAUEBaiIBRw0AC0H/ACECDK8BCyABIARGBEBB/gAhAgyvAQsCQCABLQAAQQlrDjcvAwYvBAYGBgYGBgYGBgYGBgYGBgYGBgUGBgIGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYABgsgAUEBagshAUEFIQIMlAELIAFBAWoMBgsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgyrAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyqAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgypAQsgA0EANgIcIAMgATYCFCADQY0UNgIQIANBBzYCDEEAIQIMqAELAkACQAJAAkADQCABLQAAQaDKAGotAAAiAEEFRwRAAkAgAEEBaw4GLgMEBQYABgtB6AAhAgyUAQsgBCABQQFqIgFHDQALQf0AIQIMqwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMqgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMqQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMqAELIANBADYCHCADIAE2AhQgA0HkCDYCECADQQc2AgxBACECDKcBCyABIARGDQEgAUEBagshAUEGIQIMjAELQfwAIQIMpAELAkACQAJAAkADQCABLQAAQaDIAGotAAAiAEEFRwRAIABBAWsOBCkCAwQFCyAEIAFBAWoiAUcNAAtB+wAhAgynAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgymAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgylAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgykAQsgA0EANgIcIAMgATYCFCADQbwKNgIQIANBBzYCDEEAIQIMowELQc8AIQIMiQELQdEAIQIMiAELQecAIQIMhwELIAEgBEYEQEH6ACECDKABCwJAIAEtAABBCWsOBCAAACAACyABQQFqIQFB5gAhAgyGAQsgASAERgRAQfkAIQIMnwELAkAgAS0AAEEJaw4EHwAAHwALQQAhAAJAIAMoAjgiAkUNACACKAI4IgJFDQAgAyACEQAAIQALIABFBEBB4gEhAgyGAQsgAEEVRwRAIANBADYCHCADIAE2AhQgA0HJDTYCECADQRo2AgxBACECDJ8BCyADQfgANgIcIAMgATYCFCADQeoaNgIQIANBFTYCDEEAIQIMngELIAEgBEcEQCADQQ02AgggAyABNgIEQeQAIQIMhQELQfcAIQIMnQELIAEgBEYEQEH2ACECDJ0BCwJAAkACQCABLQAAQcgAaw4LAAELCwsLCwsLCwILCyABQQFqIQFB3QAhAgyFAQsgAUEBaiEBQeAAIQIMhAELIAFBAWohAUHjACECDIMBC0H1ACECIAEgBEYNmwEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBtdUAai0AAEcNCCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMnAELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgAEQCADQfQANgIcIAMgATYCFCADIAA2AgxBACECDJwBC0HiACECDIIBC0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMnAELQeEAIQIMggELIANB8wA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyaAQsgAy0AKSIAQSNrQQtJDQkCQCAAQQZLDQBBASAAdEHKAHFFDQAMCgtBACECIANBADYCHCADIAE2AhQgA0HtCTYCECADQQg2AgwMmQELQfIAIQIgASAERg2YASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGz1QBqLQAARw0FIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyZAQsgAygCBCEAIANCADcDACADIAAgBkEBaiIBECsiAARAIANB8QA2AhwgAyABNgIUIAMgADYCDEEAIQIMmQELQd8AIQIMfwtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJkBC0HeACECDH8LIANB8AA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyXAQsgAy0AKUEhRg0GIANBADYCHCADIAE2AhQgA0GRCjYCECADQQg2AgxBACECDJYBC0HvACECIAEgBEYNlQEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBsNUAai0AAEcNAiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMlgELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgBFDQIgA0HtADYCHCADIAE2AhQgAyAANgIMQQAhAgyVAQsgA0EANgIACyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNgAEgA0HuADYCHCADIAE2AhQgAyAANgIMQQAhAgyTAQtB3AAhAgx5C0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMkwELQdsAIQIMeQsgA0HsADYCHCADIAE2AhQgA0GAGzYCECADQRU2AgxBACECDJEBCyADLQApIgBBI0kNACAAQS5GDQAgA0EANgIcIAMgATYCFCADQckJNgIQIANBCDYCDEEAIQIMkAELQdoAIQIMdgsgASAERgRAQesAIQIMjwELAkAgAS0AAEEvRgRAIAFBAWohAQwBCyAD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ciBIVFRQL3gueCByZXF1ZXN0AEludmFsaWQgbWV0aG9kIGZvciBSVFNQL3gueCByZXF1ZXN0AEV4cGVjdGVkIFNPVVJDRSBtZXRob2QgZm9yIElDRS94LnggcmVxdWVzdABJbnZhbGlkIGNoYXIgaW4gdXJsIGZyYWdtZW50IHN0YXJ0AEV4cGVjdGVkIGRvdABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3N0YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3Byb3RvY29sX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fcHJvdG9jb2wARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBVbmV4cGVjdGVkIHdoaXRlc3BhY2UgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fcHJvdG9jb2xfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX1BST1RPQ09MX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8sIFJUU1AvIG9yIElDRS8A5xUAAK8VAACkEgAAkhoAACYWAACeFAAA2xkAAHkVAAB+EgAA/hQAADYVAAALFgAA2BYAAPMSAABCGAAArBYAABIVAAAUFwAA7xcAAEgUAABxFwAAshoAAGsZAAB+GQAANRQAAIIaAABEFwAA/RYAAB4YAACHFwAAqhkAAJMSAAAHGAAALBcAAMoXAACkFwAA5xUAAOcVAABYFwAAOxgAAKASAAAtHAAAwxEAAEgRAADeEgAAQhMAAKQZAAD9EAAA9xUAAKUVAADvFgAA+BkAAEoWAABWFgAA9RUAAAoaAAAIGgAAARoAAKsVAABCEgAA1xAAAEwRAAAFGQAAVBYAAB4RAADKGQAAyBkAAE4WAAD/GAAAcRQAAPAVAADuFQAAlBkAAPwVAAC/GQAAmxkAAHwUAABDEQAAcBgAAJUUAAAnFAAAGRQAANUSAADUGQAARBYAAPcQAEG5OwsBAQBB0DsL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBuj0LBAEAAAIAQdE9C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEG6PwsEAQAAAgBB0T8LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQbDBAAsNbG9zZWVlcC1hbGl2ZQBBycEACwEBAEHgwQAL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBycMACwEBAEHgwwAL5wEBAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAQfHFAAteAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBB0McACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQYDIAAsgcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQpTTQ0KDQoAQanIAAsFAQIAAQMAQcDIAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanKAAsFAQIAAQMAQcDKAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanMAAsEAQAAAQBBwcwAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEGpzgALBQECAAEDAEHAzgALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEGp0AALBQEBAAEBAEHA0AALAQEAQdrQAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQanSAAsFAQEAAQEAQcDSAAsBAQBBytIACwYCAAAAAAIAQeHSAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBoNQAC50BTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFVFRQQ0VUU1BBRFRQLw==' let wasmBuffer diff --git a/lib/llhttp/llhttp_simd.wasm b/lib/llhttp/llhttp_simd.wasm index da92146a39c..9741da19c67 100755 Binary files a/lib/llhttp/llhttp_simd.wasm and b/lib/llhttp/llhttp_simd.wasm differ diff --git a/lib/llhttp/utils.d.ts b/lib/llhttp/utils.d.ts index b1a9a3a4379..681b22b9429 100644 --- a/lib/llhttp/utils.d.ts +++ b/lib/llhttp/utils.d.ts @@ -1,2 +1,2 @@ -import { IntDict } from './constants'; -export declare function enumToMap(obj: IntDict, filter?: ReadonlyArray, exceptions?: ReadonlyArray): IntDict; +import type { IntDict } from './constants'; +export declare function enumToMap(obj: IntDict, filter?: readonly number[], exceptions?: readonly number[]): IntDict; diff --git a/lib/llhttp/utils.js b/lib/llhttp/utils.js index aaaa5d8cf52..95081ead593 100644 --- a/lib/llhttp/utils.js +++ b/lib/llhttp/utils.js @@ -1,15 +1,12 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.enumToMap = void 0; +exports.enumToMap = enumToMap; function enumToMap(obj, filter = [], exceptions = []) { - var _a, _b; - const emptyFilter = ((_a = filter === null || filter === void 0 ? void 0 : filter.length) !== null && _a !== void 0 ? _a : 0) === 0; - const emptyExceptions = ((_b = exceptions === null || exceptions === void 0 ? void 0 : exceptions.length) !== null && _b !== void 0 ? _b : 0) === 0; + const emptyFilter = (filter?.length ?? 0) === 0; + const emptyExceptions = (exceptions?.length ?? 0) === 0; return Object.fromEntries(Object.entries(obj).filter(([, value]) => { return (typeof value === 'number' && (emptyFilter || filter.includes(value)) && (emptyExceptions || !exceptions.includes(value))); })); } -exports.enumToMap = enumToMap; -//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/lib/llhttp/utils.js.map b/lib/llhttp/utils.js.map deleted file mode 100644 index 5d578522eea..00000000000 --- a/lib/llhttp/utils.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/llhttp/utils.ts"],"names":[],"mappings":";;;AAEA,SAAgB,SAAS,CACvB,GAAY,EACZ,SAAgC,EAAE,EAClC,aAAoC,EAAE;;IAEtC,MAAM,WAAW,GAAG,CAAC,MAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,MAAM,mCAAI,CAAC,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,eAAe,GAAG,CAAC,MAAA,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAE,MAAM,mCAAI,CAAC,CAAC,KAAK,CAAC,CAAC;IAExD,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,AAAD,EAAG,KAAK,CAAE,EAAE,EAAE;QACnE,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,WAAW,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC,eAAe,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CACjD,CAAC;IACJ,CAAC,CAAC,CAAC,CAAC;AACN,CAAC;AAfD,8BAeC"} \ No newline at end of file diff --git a/lib/mock/mock-agent.js b/lib/mock/mock-agent.js index 19bb01c2661..61449e077ea 100644 --- a/lib/mock/mock-agent.js +++ b/lib/mock/mock-agent.js @@ -17,26 +17,28 @@ const { kMockAgentAddCallHistoryLog, kMockAgentMockCallHistoryInstance, kMockAgentAcceptsNonStandardSearchParameters, - kMockCallHistoryAddLog + kMockCallHistoryAddLog, + kIgnoreTrailingSlash } = require('./mock-symbols') const MockClient = require('./mock-client') const MockPool = require('./mock-pool') -const { matchValue, normalizeSearchParams, buildAndValidateMockOptions } = require('./mock-utils') +const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = require('./mock-utils') const { InvalidArgumentError, UndiciError } = require('../core/errors') const Dispatcher = require('../dispatcher/dispatcher') const PendingInterceptorsFormatter = require('./pending-interceptors-formatter') const { MockCallHistory } = require('./mock-call-history') class MockAgent extends Dispatcher { - constructor (opts) { + constructor (opts = {}) { super(opts) const mockOptions = buildAndValidateMockOptions(opts) this[kNetConnect] = true this[kIsMockActive] = true - this[kMockAgentIsCallHistoryEnabled] = mockOptions?.enableCallHistory ?? false - this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions?.acceptNonStandardSearchParameters ?? false + this[kMockAgentIsCallHistoryEnabled] = mockOptions.enableCallHistory ?? false + this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions.acceptNonStandardSearchParameters ?? false + this[kIgnoreTrailingSlash] = mockOptions.ignoreTrailingSlash ?? false // Instantiate Agent and encapsulate if (opts?.agent && typeof opts.agent.dispatch !== 'function') { @@ -54,16 +56,22 @@ class MockAgent extends Dispatcher { } get (origin) { - let dispatcher = this[kMockAgentGet](origin) + // Normalize origin to handle URL objects and case-insensitive hostnames + const normalizedOrigin = normalizeOrigin(origin) + const originKey = this[kIgnoreTrailingSlash] ? normalizedOrigin.replace(/\/$/, '') : normalizedOrigin + + let dispatcher = this[kMockAgentGet](originKey) if (!dispatcher) { - dispatcher = this[kFactory](origin) - this[kMockAgentSet](origin, dispatcher) + dispatcher = this[kFactory](originKey) + this[kMockAgentSet](originKey, dispatcher) } return dispatcher } dispatch (opts, handler) { + opts.origin = normalizeOrigin(opts.origin) + // Call MockAgent.get to perform additional setup before dispatching as normal this.get(opts.origin) diff --git a/lib/mock/mock-call-history.js b/lib/mock/mock-call-history.js index d4a92b2b24b..74de68247c7 100644 --- a/lib/mock/mock-call-history.js +++ b/lib/mock/mock-call-history.js @@ -3,14 +3,14 @@ const { kMockCallHistoryAddLog } = require('./mock-symbols') const { InvalidArgumentError } = require('../core/errors') -function handleFilterCallsWithOptions (criteria, options, handler, store) { +function handleFilterCallsWithOptions (criteria, options, handler, store, allLogs) { switch (options.operator) { case 'OR': - store.push(...handler(criteria)) + store.push(...handler(criteria, allLogs)) return store case 'AND': - return handler.call({ logs: store }, criteria) + return handler(criteria, store) default: // guard -- should never happens because buildAndValidateFilterCallsOptions is called before throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'') @@ -35,14 +35,14 @@ function buildAndValidateFilterCallsOptions (options = {}) { } function makeFilterCalls (parameterName) { - return (parameterValue) => { + return (parameterValue, logs) => { if (typeof parameterValue === 'string' || parameterValue == null) { - return this.logs.filter((log) => { + return logs.filter((log) => { return log[parameterName] === parameterValue }) } if (parameterValue instanceof RegExp) { - return this.logs.filter((log) => { + return logs.filter((log) => { return parameterValue.test(log[parameterName]) }) } @@ -175,30 +175,30 @@ class MockCallHistory { const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) } - let maybeDuplicatedLogsFiltered = [] + let maybeDuplicatedLogsFiltered = finalOptions.operator === 'AND' ? this.logs : [] if ('protocol' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered) + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered, this.logs) } if ('host' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered) + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered, this.logs) } if ('port' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered) + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered, this.logs) } if ('origin' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered) + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered, this.logs) } if ('path' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered) + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered, this.logs) } if ('hash' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered) + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered, this.logs) } if ('fullUrl' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered) + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered, this.logs) } if ('method' in criteria) { - maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered) + maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered, this.logs) } const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)] diff --git a/lib/mock/mock-errors.js b/lib/mock/mock-errors.js index ebdc786c56e..69e4f9cc371 100644 --- a/lib/mock/mock-errors.js +++ b/lib/mock/mock-errors.js @@ -2,6 +2,8 @@ const { UndiciError } = require('../core/errors') +const kMockNotMatchedError = Symbol.for('undici.error.UND_MOCK_ERR_MOCK_NOT_MATCHED') + /** * The request does not match any registered mock dispatches. */ @@ -12,6 +14,14 @@ class MockNotMatchedError extends UndiciError { this.message = message || 'The request does not match any registered mock dispatches' this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED' } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kMockNotMatchedError] === true + } + + get [kMockNotMatchedError] () { + return true + } } module.exports = { diff --git a/lib/mock/mock-symbols.js b/lib/mock/mock-symbols.js index 940dbe6e3f8..9b23e8e3cf0 100644 --- a/lib/mock/mock-symbols.js +++ b/lib/mock/mock-symbols.js @@ -27,5 +27,6 @@ module.exports = { kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'), kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'), kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'), - kMockCallHistoryAddLog: Symbol('mock call history add log') + kMockCallHistoryAddLog: Symbol('mock call history add log'), + kTotalDispatchCount: Symbol('total dispatch count') } diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 822d45d153f..a51816f4082 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -6,7 +6,8 @@ const { kMockAgent, kOriginalDispatch, kOrigin, - kGetNetConnect + kGetNetConnect, + kTotalDispatchCount } = require('./mock-symbols') const { serializePathWithQuery } = require('../core/util') const { STATUS_CODES } = require('node:http') @@ -206,6 +207,8 @@ function addMockDispatch (mockDispatches, key, data, opts) { const replyData = typeof data === 'function' ? { callback: data } : { ...data } const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } mockDispatches.push(newMockDispatch) + // Track total number of intercepts ever registered for better error messages + mockDispatches[kTotalDispatchCount] = (mockDispatches[kTotalDispatchCount] || 0) + 1 return newMockDispatch } @@ -312,9 +315,33 @@ function mockDispatch (opts, handler) { return true } + // Track whether the request has been aborted + let aborted = false + let timer = null + + function abort (err) { + if (aborted) { + return + } + aborted = true + + // Clear the pending delayed response if any + if (timer !== null) { + clearTimeout(timer) + timer = null + } + + // Notify the handler of the abort + handler.onError(err) + } + + // Call onConnect to allow the handler to register the abort callback + handler.onConnect?.(abort, null) + // Handle the request with a delay if necessary if (typeof delay === 'number' && delay > 0) { - setTimeout(() => { + timer = setTimeout(() => { + timer = null handleReply(this[kDispatches]) }, delay) } else { @@ -322,6 +349,11 @@ function mockDispatch (opts, handler) { } function handleReply (mockDispatches, _data = data) { + // Don't send response if the request was aborted + if (aborted) { + return + } + // fetch's HeadersList is a 1D string array const optsHeaders = Array.isArray(opts.headers) ? buildHeadersFromArray(opts.headers) @@ -337,7 +369,11 @@ function mockDispatch (opts, handler) { // synchronously throw the error, which breaks some tests. // Rather, we wait for the callback to resolve if it is a // promise, and then re-run handleReply with the new body. - body.then((newData) => handleReply(mockDispatches, newData)) + return body.then((newData) => handleReply(mockDispatches, newData)) + } + + // Check again if aborted after async body resolution + if (aborted) { return } @@ -345,7 +381,6 @@ function mockDispatch (opts, handler) { const responseHeaders = generateKeyValues(headers) const responseTrailers = generateKeyValues(trailers) - handler.onConnect?.(err => handler.onError(err), null) handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode)) handler.onData?.(Buffer.from(responseData)) handler.onComplete?.(responseTrailers) @@ -367,15 +402,18 @@ function buildMockDispatch () { try { mockDispatch.call(this, opts, handler) } catch (error) { - if (error instanceof MockNotMatchedError) { + if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') { const netConnect = agent[kGetNetConnect]() + const totalInterceptsCount = this[kDispatches][kTotalDispatchCount] || this[kDispatches].length + const pendingInterceptsCount = this[kDispatches].filter(({ consumed }) => !consumed).length + const interceptsMessage = `, ${pendingInterceptsCount} interceptor(s) remaining out of ${totalInterceptsCount} defined` if (netConnect === false) { - throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)${interceptsMessage}`) } if (checkNetConnect(netConnect, origin)) { originalDispatch.call(this, opts, handler) } else { - throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)${interceptsMessage}`) } } else { throw error @@ -397,20 +435,34 @@ function checkNetConnect (netConnect, origin) { return false } +function normalizeOrigin (origin) { + if (typeof origin !== 'string' && !(origin instanceof URL)) { + return origin + } + + if (origin instanceof URL) { + return origin.origin + } + + return origin.toLowerCase() +} + function buildAndValidateMockOptions (opts) { - if (opts) { - const { agent, ...mockOptions } = opts + const { agent, ...mockOptions } = opts - if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') { - throw new InvalidArgumentError('options.enableCallHistory must to be a boolean') - } + if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') { + throw new InvalidArgumentError('options.enableCallHistory must to be a boolean') + } - if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') { - throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean') - } + if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') { + throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean') + } - return mockOptions + if ('ignoreTrailingSlash' in mockOptions && typeof mockOptions.ignoreTrailingSlash !== 'boolean') { + throw new InvalidArgumentError('options.ignoreTrailingSlash must to be a boolean') } + + return mockOptions } module.exports = { @@ -429,5 +481,6 @@ module.exports = { buildAndValidateMockOptions, getHeaderByName, buildHeadersFromArray, - normalizeSearchParams + normalizeSearchParams, + normalizeOrigin } diff --git a/lib/mock/snapshot-agent.js b/lib/mock/snapshot-agent.js new file mode 100644 index 00000000000..80280111921 --- /dev/null +++ b/lib/mock/snapshot-agent.js @@ -0,0 +1,353 @@ +'use strict' + +const Agent = require('../dispatcher/agent') +const MockAgent = require('./mock-agent') +const { SnapshotRecorder } = require('./snapshot-recorder') +const WrapHandler = require('../handler/wrap-handler') +const { InvalidArgumentError, UndiciError } = require('../core/errors') +const { validateSnapshotMode } = require('./snapshot-utils') + +const kSnapshotRecorder = Symbol('kSnapshotRecorder') +const kSnapshotMode = Symbol('kSnapshotMode') +const kSnapshotPath = Symbol('kSnapshotPath') +const kSnapshotLoaded = Symbol('kSnapshotLoaded') +const kRealAgent = Symbol('kRealAgent') + +// Static flag to ensure warning is only emitted once per process +let warningEmitted = false + +class SnapshotAgent extends MockAgent { + constructor (opts = {}) { + // Emit experimental warning only once + if (!warningEmitted) { + process.emitWarning( + 'SnapshotAgent is experimental and subject to change', + 'ExperimentalWarning' + ) + warningEmitted = true + } + + const { + mode = 'record', + snapshotPath = null, + ...mockAgentOpts + } = opts + + super(mockAgentOpts) + + validateSnapshotMode(mode) + + // Validate snapshotPath is provided when required + if ((mode === 'playback' || mode === 'update') && !snapshotPath) { + throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`) + } + + this[kSnapshotMode] = mode + this[kSnapshotPath] = snapshotPath + + this[kSnapshotRecorder] = new SnapshotRecorder({ + snapshotPath: this[kSnapshotPath], + mode: this[kSnapshotMode], + maxSnapshots: opts.maxSnapshots, + autoFlush: opts.autoFlush, + flushInterval: opts.flushInterval, + matchHeaders: opts.matchHeaders, + ignoreHeaders: opts.ignoreHeaders, + excludeHeaders: opts.excludeHeaders, + matchBody: opts.matchBody, + matchQuery: opts.matchQuery, + caseSensitive: opts.caseSensitive, + shouldRecord: opts.shouldRecord, + shouldPlayback: opts.shouldPlayback, + excludeUrls: opts.excludeUrls + }) + this[kSnapshotLoaded] = false + + // For recording/update mode, we need a real agent to make actual requests + // For playback mode, we need a real agent if there are excluded URLs + if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update' || + (this[kSnapshotMode] === 'playback' && opts.excludeUrls && opts.excludeUrls.length > 0)) { + this[kRealAgent] = new Agent(opts) + } + + // Auto-load snapshots in playback/update mode + if ((this[kSnapshotMode] === 'playback' || this[kSnapshotMode] === 'update') && this[kSnapshotPath]) { + this.loadSnapshots().catch(() => { + // Ignore load errors - file might not exist yet + }) + } + } + + dispatch (opts, handler) { + handler = WrapHandler.wrap(handler) + const mode = this[kSnapshotMode] + + // Check if URL should be excluded (pass through without mocking/recording) + if (this[kSnapshotRecorder].isUrlExcluded(opts)) { + // Real agent is guaranteed by constructor when excludeUrls is configured + return this[kRealAgent].dispatch(opts, handler) + } + + if (mode === 'playback' || mode === 'update') { + // Ensure snapshots are loaded + if (!this[kSnapshotLoaded]) { + // Need to load asynchronously, delegate to async version + return this.#asyncDispatch(opts, handler) + } + + // Try to find existing snapshot (synchronous) + const snapshot = this[kSnapshotRecorder].findSnapshot(opts) + + if (snapshot) { + // Use recorded response (synchronous) + return this.#replaySnapshot(snapshot, handler) + } else if (mode === 'update') { + // Make real request and record it (async required) + return this.#recordAndReplay(opts, handler) + } else { + // Playback mode but no snapshot found + const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`) + if (handler.onError) { + handler.onError(error) + return + } + throw error + } + } else if (mode === 'record') { + // Record mode - make real request and save response (async required) + return this.#recordAndReplay(opts, handler) + } + } + + /** + * Async version of dispatch for when we need to load snapshots first + */ + async #asyncDispatch (opts, handler) { + await this.loadSnapshots() + return this.dispatch(opts, handler) + } + + /** + * Records a real request and replays the response + */ + #recordAndReplay (opts, handler) { + const responseData = { + statusCode: null, + headers: {}, + trailers: {}, + body: [] + } + + const self = this // Capture 'this' context for use within nested handler callbacks + + const recordingHandler = { + onRequestStart (controller, context) { + return handler.onRequestStart(controller, { ...context, history: this.history }) + }, + + onRequestUpgrade (controller, statusCode, headers, socket) { + return handler.onRequestUpgrade(controller, statusCode, headers, socket) + }, + + onResponseStart (controller, statusCode, headers, statusMessage) { + responseData.statusCode = statusCode + responseData.headers = headers + return handler.onResponseStart(controller, statusCode, headers, statusMessage) + }, + + onResponseData (controller, chunk) { + responseData.body.push(chunk) + return handler.onResponseData(controller, chunk) + }, + + onResponseEnd (controller, trailers) { + responseData.trailers = trailers + + // Record the interaction using captured 'self' context (fire and forget) + const responseBody = Buffer.concat(responseData.body) + self[kSnapshotRecorder].record(opts, { + statusCode: responseData.statusCode, + headers: responseData.headers, + body: responseBody, + trailers: responseData.trailers + }) + .then(() => handler.onResponseEnd(controller, trailers)) + .catch((error) => handler.onResponseError(controller, error)) + } + } + + // Use composed agent if available (includes interceptors), otherwise use real agent + const agent = this[kRealAgent] + return agent.dispatch(opts, recordingHandler) + } + + /** + * Replays a recorded response + * + * @param {Object} snapshot - The recorded snapshot to replay. + * @param {Object} handler - The handler to call with the response data. + * @returns {void} + */ + #replaySnapshot (snapshot, handler) { + try { + const { response } = snapshot + + const controller = { + pause () { }, + resume () { }, + abort (reason) { + this.aborted = true + this.reason = reason + }, + + aborted: false, + paused: false + } + + handler.onRequestStart(controller) + + handler.onResponseStart(controller, response.statusCode, response.headers) + + // Body is always stored as base64 string + const body = Buffer.from(response.body, 'base64') + handler.onResponseData(controller, body) + + handler.onResponseEnd(controller, response.trailers) + } catch (error) { + handler.onError?.(error) + } + } + + /** + * Loads snapshots from file + * + * @param {string} [filePath] - Optional file path to load snapshots from. + * @returns {Promise} - Resolves when snapshots are loaded. + */ + async loadSnapshots (filePath) { + await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath]) + this[kSnapshotLoaded] = true + + // In playback mode, set up MockAgent interceptors for all snapshots + if (this[kSnapshotMode] === 'playback') { + this.#setupMockInterceptors() + } + } + + /** + * Saves snapshots to file + * + * @param {string} [filePath] - Optional file path to save snapshots to. + * @returns {Promise} - Resolves when snapshots are saved. + */ + async saveSnapshots (filePath) { + return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath]) + } + + /** + * Sets up MockAgent interceptors based on recorded snapshots. + * + * This method creates MockAgent interceptors for each recorded snapshot, + * allowing the SnapshotAgent to fall back to MockAgent's standard intercept + * mechanism in playback mode. Each interceptor is configured to persist + * (remain active for multiple requests) and responds with the recorded + * response data. + * + * Called automatically when loading snapshots in playback mode. + * + * @returns {void} + */ + #setupMockInterceptors () { + for (const snapshot of this[kSnapshotRecorder].getSnapshots()) { + const { request, responses, response } = snapshot + const url = new URL(request.url) + + const mockPool = this.get(url.origin) + + // Handle both new format (responses array) and legacy format (response object) + const responseData = responses ? responses[0] : response + if (!responseData) continue + + mockPool.intercept({ + path: url.pathname + url.search, + method: request.method, + headers: request.headers, + body: request.body + }).reply(responseData.statusCode, responseData.body, { + headers: responseData.headers, + trailers: responseData.trailers + }).persist() + } + } + + /** + * Gets the snapshot recorder + * @return {SnapshotRecorder} - The snapshot recorder instance + */ + getRecorder () { + return this[kSnapshotRecorder] + } + + /** + * Gets the current mode + * @return {import('./snapshot-utils').SnapshotMode} - The current snapshot mode + */ + getMode () { + return this[kSnapshotMode] + } + + /** + * Clears all snapshots + * @returns {void} + */ + clearSnapshots () { + this[kSnapshotRecorder].clear() + } + + /** + * Resets call counts for all snapshots (useful for test cleanup) + * @returns {void} + */ + resetCallCounts () { + this[kSnapshotRecorder].resetCallCounts() + } + + /** + * Deletes a specific snapshot by request options + * @param {import('./snapshot-recorder').SnapshotRequestOptions} requestOpts - Request options to identify the snapshot + * @return {Promise} - Returns true if the snapshot was deleted, false if not found + */ + deleteSnapshot (requestOpts) { + return this[kSnapshotRecorder].deleteSnapshot(requestOpts) + } + + /** + * Gets information about a specific snapshot + * @returns {import('./snapshot-recorder').SnapshotInfo|null} - Snapshot information or null if not found + */ + getSnapshotInfo (requestOpts) { + return this[kSnapshotRecorder].getSnapshotInfo(requestOpts) + } + + /** + * Replaces all snapshots with new data (full replacement) + * @param {Array<{hash: string; snapshot: import('./snapshot-recorder').SnapshotEntryshotEntry}>|Record} snapshotData - New snapshot data to replace existing snapshots + * @returns {void} + */ + replaceSnapshots (snapshotData) { + this[kSnapshotRecorder].replaceSnapshots(snapshotData) + } + + /** + * Closes the agent, saving snapshots and cleaning up resources. + * + * @returns {Promise} + */ + async close () { + await this[kSnapshotRecorder].close() + await this[kRealAgent]?.close() + await super.close() + } +} + +module.exports = SnapshotAgent diff --git a/lib/mock/snapshot-recorder.js b/lib/mock/snapshot-recorder.js new file mode 100644 index 00000000000..b5d07fae381 --- /dev/null +++ b/lib/mock/snapshot-recorder.js @@ -0,0 +1,588 @@ +'use strict' + +const { writeFile, readFile, mkdir } = require('node:fs/promises') +const { dirname, resolve } = require('node:path') +const { setTimeout, clearTimeout } = require('node:timers') +const { InvalidArgumentError, UndiciError } = require('../core/errors') +const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require('./snapshot-utils') + +/** + * @typedef {Object} SnapshotRequestOptions + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} path - Request path + * @property {string} origin - Request origin (base URL) + * @property {import('./snapshot-utils').Headers|import('./snapshot-utils').UndiciHeaders} headers - Request headers + * @property {import('./snapshot-utils').NormalizedHeaders} _normalizedHeaders - Request headers as a lowercase object + * @property {string|Buffer} [body] - Request body (optional) + */ + +/** + * @typedef {Object} SnapshotEntryRequest + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} url - Full URL of the request + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object + * @property {string|Buffer} [body] - Request body (optional) + */ + +/** + * @typedef {Object} SnapshotEntryResponse + * @property {number} statusCode - HTTP status code of the response + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized response headers as a lowercase object + * @property {string} body - Response body as a base64url encoded string + * @property {Object} [trailers] - Optional response trailers + */ + +/** + * @typedef {Object} SnapshotEntry + * @property {SnapshotEntryRequest} request - The request object + * @property {Array} responses - Array of response objects + * @property {number} callCount - Number of times this snapshot has been called + * @property {string} timestamp - ISO timestamp of when the snapshot was created + */ + +/** + * @typedef {Object} SnapshotRecorderMatchOptions + * @property {Array} [matchHeaders=[]] - Headers to match (empty array means match all headers) + * @property {Array} [ignoreHeaders=[]] - Headers to ignore for matching + * @property {Array} [excludeHeaders=[]] - Headers to exclude from matching + * @property {boolean} [matchBody=true] - Whether to match request body + * @property {boolean} [matchQuery=true] - Whether to match query properties + * @property {boolean} [caseSensitive=false] - Whether header matching is case-sensitive + */ + +/** + * @typedef {Object} SnapshotRecorderOptions + * @property {string} [snapshotPath] - Path to save/load snapshots + * @property {import('./snapshot-utils').SnapshotMode} [mode='record'] - Mode: 'record' or 'playback' + * @property {number} [maxSnapshots=Infinity] - Maximum number of snapshots to keep + * @property {boolean} [autoFlush=false] - Whether to automatically flush snapshots to disk + * @property {number} [flushInterval=30000] - Auto-flush interval in milliseconds (default: 30 seconds) + * @property {Array} [excludeUrls=[]] - URLs to exclude from recording + * @property {function} [shouldRecord=null] - Function to filter requests for recording + * @property {function} [shouldPlayback=null] - Function to filter requests + */ + +/** + * @typedef {Object} SnapshotFormattedRequest + * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.) + * @property {string} url - Full URL of the request (with query parameters if matchQuery is true) + * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object + * @property {string} body - Request body (optional, only if matchBody is true) + */ + +/** + * @typedef {Object} SnapshotInfo + * @property {string} hash - Hash key for the snapshot + * @property {SnapshotEntryRequest} request - The request object + * @property {number} responseCount - Number of responses recorded for this request + * @property {number} callCount - Number of times this snapshot has been called + * @property {string} timestamp - ISO timestamp of when the snapshot was created + */ + +/** + * Formats a request for consistent snapshot storage + * Caches normalized headers to avoid repeated processing + * + * @param {SnapshotRequestOptions} opts - Request options + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached header sets for performance + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers and body + * @returns {SnapshotFormattedRequest} - Formatted request object + */ +function formatRequestKey (opts, headerFilters, matchOptions = {}) { + const url = new URL(opts.path, opts.origin) + + // Cache normalized headers if not already done + const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers) + if (!opts._normalizedHeaders) { + opts._normalizedHeaders = normalized + } + + return { + method: opts.method || 'GET', + url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`, + headers: filterHeadersForMatching(normalized, headerFilters, matchOptions), + body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : '' + } +} + +/** + * Filters headers based on matching configuration + * + * @param {import('./snapshot-utils').Headers} headers - Headers to filter + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers + */ +function filterHeadersForMatching (headers, headerFilters, matchOptions = {}) { + if (!headers || typeof headers !== 'object') return {} + + const { + caseSensitive = false + } = matchOptions + + const filtered = {} + const { ignore, exclude, match } = headerFilters + + for (const [key, value] of Object.entries(headers)) { + const headerKey = caseSensitive ? key : key.toLowerCase() + + // Skip if in exclude list (for security) + if (exclude.has(headerKey)) continue + + // Skip if in ignore list (for matching) + if (ignore.has(headerKey)) continue + + // If matchHeaders is specified, only include those headers + if (match.size !== 0) { + if (!match.has(headerKey)) continue + } + + filtered[headerKey] = value + } + + return filtered +} + +/** + * Filters headers for storage (only excludes sensitive headers) + * + * @param {import('./snapshot-utils').Headers} headers - Headers to filter + * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers + * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers + */ +function filterHeadersForStorage (headers, headerFilters, matchOptions = {}) { + if (!headers || typeof headers !== 'object') return {} + + const { + caseSensitive = false + } = matchOptions + + const filtered = {} + const { exclude: excludeSet } = headerFilters + + for (const [key, value] of Object.entries(headers)) { + const headerKey = caseSensitive ? key : key.toLowerCase() + + // Skip if in exclude list (for security) + if (excludeSet.has(headerKey)) continue + + filtered[headerKey] = value + } + + return filtered +} + +/** + * Creates a hash key for request matching + * Properly orders headers to avoid conflicts and uses crypto hashing when available + * + * @param {SnapshotFormattedRequest} formattedRequest - Request object + * @returns {string} - Base64url encoded hash of the request + */ +function createRequestHash (formattedRequest) { + const parts = [ + formattedRequest.method, + formattedRequest.url + ] + + // Process headers in a deterministic way to avoid conflicts + if (formattedRequest.headers && typeof formattedRequest.headers === 'object') { + const headerKeys = Object.keys(formattedRequest.headers).sort() + for (const key of headerKeys) { + const values = Array.isArray(formattedRequest.headers[key]) + ? formattedRequest.headers[key] + : [formattedRequest.headers[key]] + + // Add header name + parts.push(key) + + // Add all values for this header, sorted for consistency + for (const value of values.sort()) { + parts.push(String(value)) + } + } + } + + // Add body + parts.push(formattedRequest.body) + + const content = parts.join('|') + + return hashId(content) +} + +class SnapshotRecorder { + /** @type {NodeJS.Timeout | null} */ + #flushTimeout + + /** @type {import('./snapshot-utils').IsUrlExcluded} */ + #isUrlExcluded + + /** @type {Map} */ + #snapshots = new Map() + + /** @type {string|undefined} */ + #snapshotPath + + /** @type {number} */ + #maxSnapshots = Infinity + + /** @type {boolean} */ + #autoFlush = false + + /** @type {import('./snapshot-utils').HeaderFilters} */ + #headerFilters + + /** + * Creates a new SnapshotRecorder instance + * @param {SnapshotRecorderOptions&SnapshotRecorderMatchOptions} [options={}] - Configuration options for the recorder + */ + constructor (options = {}) { + this.#snapshotPath = options.snapshotPath + this.#maxSnapshots = options.maxSnapshots || Infinity + this.#autoFlush = options.autoFlush || false + this.flushInterval = options.flushInterval || 30000 // 30 seconds default + this._flushTimer = null + + // Matching configuration + /** @type {Required} */ + this.matchOptions = { + matchHeaders: options.matchHeaders || [], // empty means match all headers + ignoreHeaders: options.ignoreHeaders || [], + excludeHeaders: options.excludeHeaders || [], + matchBody: options.matchBody !== false, // default: true + matchQuery: options.matchQuery !== false, // default: true + caseSensitive: options.caseSensitive || false + } + + // Cache processed header sets to avoid recreating them on every request + this.#headerFilters = createHeaderFilters(this.matchOptions) + + // Request filtering callbacks + this.shouldRecord = options.shouldRecord || (() => true) // function(requestOpts) -> boolean + this.shouldPlayback = options.shouldPlayback || (() => true) // function(requestOpts) -> boolean + + // URL pattern filtering + this.#isUrlExcluded = isUrlExcludedFactory(options.excludeUrls) // Array of regex patterns or strings + + // Start auto-flush timer if enabled + if (this.#autoFlush && this.#snapshotPath) { + this.#startAutoFlush() + } + } + + /** + * Records a request-response interaction + * @param {SnapshotRequestOptions} requestOpts - Request options + * @param {SnapshotEntryResponse} response - Response data to record + * @return {Promise} - Resolves when the recording is complete + */ + async record (requestOpts, response) { + // Check if recording should be filtered out + if (!this.shouldRecord(requestOpts)) { + return // Skip recording + } + + // Check URL exclusion patterns + if (this.isUrlExcluded(requestOpts)) { + return // Skip recording + } + + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) + const hash = createRequestHash(request) + + // Extract response data - always store body as base64 + const normalizedHeaders = normalizeHeaders(response.headers) + + /** @type {SnapshotEntryResponse} */ + const responseData = { + statusCode: response.statusCode, + headers: filterHeadersForStorage(normalizedHeaders, this.#headerFilters, this.matchOptions), + body: Buffer.isBuffer(response.body) + ? response.body.toString('base64') + : Buffer.from(String(response.body || '')).toString('base64'), + trailers: response.trailers + } + + // Remove oldest snapshot if we exceed maxSnapshots limit + if (this.#snapshots.size >= this.#maxSnapshots && !this.#snapshots.has(hash)) { + const oldestKey = this.#snapshots.keys().next().value + this.#snapshots.delete(oldestKey) + } + + // Support sequential responses - if snapshot exists, add to responses array + const existingSnapshot = this.#snapshots.get(hash) + if (existingSnapshot && existingSnapshot.responses) { + existingSnapshot.responses.push(responseData) + existingSnapshot.timestamp = new Date().toISOString() + } else { + this.#snapshots.set(hash, { + request, + responses: [responseData], // Always store as array for consistency + callCount: 0, + timestamp: new Date().toISOString() + }) + } + + // Auto-flush if enabled + if (this.#autoFlush && this.#snapshotPath) { + this.#scheduleFlush() + } + } + + /** + * Checks if a URL should be excluded from recording/playback + * @param {SnapshotRequestOptions} requestOpts - Request options to check + * @returns {boolean} - True if URL is excluded + */ + isUrlExcluded (requestOpts) { + const url = new URL(requestOpts.path, requestOpts.origin).toString() + return this.#isUrlExcluded(url) + } + + /** + * Finds a matching snapshot for the given request + * Returns the appropriate response based on call count for sequential responses + * + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {SnapshotEntry&Record<'response', SnapshotEntryResponse>|undefined} - Matching snapshot response or undefined if not found + */ + findSnapshot (requestOpts) { + // Check if playback should be filtered out + if (!this.shouldPlayback(requestOpts)) { + return undefined // Skip playback + } + + // Check URL exclusion patterns + if (this.isUrlExcluded(requestOpts)) { + return undefined // Skip playback + } + + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) + const hash = createRequestHash(request) + const snapshot = this.#snapshots.get(hash) + + if (!snapshot) return undefined + + // Handle sequential responses + const currentCallCount = snapshot.callCount || 0 + const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1) + snapshot.callCount = currentCallCount + 1 + + return { + ...snapshot, + response: snapshot.responses[responseIndex] + } + } + + /** + * Loads snapshots from file + * @param {string} [filePath] - Optional file path to load snapshots from + * @return {Promise} - Resolves when snapshots are loaded + */ + async loadSnapshots (filePath) { + const path = filePath || this.#snapshotPath + if (!path) { + throw new InvalidArgumentError('Snapshot path is required') + } + + try { + const data = await readFile(resolve(path), 'utf8') + const parsed = JSON.parse(data) + + // Convert array format back to Map + if (Array.isArray(parsed)) { + this.#snapshots.clear() + for (const { hash, snapshot } of parsed) { + this.#snapshots.set(hash, snapshot) + } + } else { + // Legacy object format + this.#snapshots = new Map(Object.entries(parsed)) + } + } catch (error) { + if (error.code === 'ENOENT') { + // File doesn't exist yet - that's ok for recording mode + this.#snapshots.clear() + } else { + throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error }) + } + } + } + + /** + * Saves snapshots to file + * + * @param {string} [filePath] - Optional file path to save snapshots + * @returns {Promise} - Resolves when snapshots are saved + */ + async saveSnapshots (filePath) { + const path = filePath || this.#snapshotPath + if (!path) { + throw new InvalidArgumentError('Snapshot path is required') + } + + const resolvedPath = resolve(path) + + // Ensure directory exists + await mkdir(dirname(resolvedPath), { recursive: true }) + + // Convert Map to serializable format + const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({ + hash, + snapshot + })) + + await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true }) + } + + /** + * Clears all recorded snapshots + * @returns {void} + */ + clear () { + this.#snapshots.clear() + } + + /** + * Gets all recorded snapshots + * @return {Array} - Array of all recorded snapshots + */ + getSnapshots () { + return Array.from(this.#snapshots.values()) + } + + /** + * Gets snapshot count + * @return {number} - Number of recorded snapshots + */ + size () { + return this.#snapshots.size + } + + /** + * Resets call counts for all snapshots (useful for test cleanup) + * @returns {void} + */ + resetCallCounts () { + for (const snapshot of this.#snapshots.values()) { + snapshot.callCount = 0 + } + } + + /** + * Deletes a specific snapshot by request options + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {boolean} - True if snapshot was deleted, false if not found + */ + deleteSnapshot (requestOpts) { + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) + const hash = createRequestHash(request) + return this.#snapshots.delete(hash) + } + + /** + * Gets information about a specific snapshot + * @param {SnapshotRequestOptions} requestOpts - Request options to match + * @returns {SnapshotInfo|null} - Snapshot information or null if not found + */ + getSnapshotInfo (requestOpts) { + const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions) + const hash = createRequestHash(request) + const snapshot = this.#snapshots.get(hash) + + if (!snapshot) return null + + return { + hash, + request: snapshot.request, + responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0), // .response for legacy snapshots + callCount: snapshot.callCount || 0, + timestamp: snapshot.timestamp + } + } + + /** + * Replaces all snapshots with new data (full replacement) + * @param {Array<{hash: string; snapshot: SnapshotEntry}>|Record} snapshotData - New snapshot data to replace existing ones + * @returns {void} + */ + replaceSnapshots (snapshotData) { + this.#snapshots.clear() + + if (Array.isArray(snapshotData)) { + for (const { hash, snapshot } of snapshotData) { + this.#snapshots.set(hash, snapshot) + } + } else if (snapshotData && typeof snapshotData === 'object') { + // Legacy object format + this.#snapshots = new Map(Object.entries(snapshotData)) + } + } + + /** + * Starts the auto-flush timer + * @returns {void} + */ + #startAutoFlush () { + return this.#scheduleFlush() + } + + /** + * Stops the auto-flush timer + * @returns {void} + */ + #stopAutoFlush () { + if (this.#flushTimeout) { + clearTimeout(this.#flushTimeout) + // Ensure any pending flush is completed + this.saveSnapshots().catch(() => { + // Ignore flush errors + }) + this.#flushTimeout = null + } + } + + /** + * Schedules a flush (debounced to avoid excessive writes) + */ + #scheduleFlush () { + this.#flushTimeout = setTimeout(() => { + this.saveSnapshots().catch(() => { + // Ignore flush errors + }) + if (this.#autoFlush) { + this.#flushTimeout?.refresh() + } else { + this.#flushTimeout = null + } + }, 1000) // 1 second debounce + } + + /** + * Cleanup method to stop timers + * @returns {void} + */ + destroy () { + this.#stopAutoFlush() + if (this.#flushTimeout) { + clearTimeout(this.#flushTimeout) + this.#flushTimeout = null + } + } + + /** + * Async close method that saves all recordings and performs cleanup + * @returns {Promise} + */ + async close () { + // Save any pending recordings if we have a snapshot path + if (this.#snapshotPath && this.#snapshots.size !== 0) { + await this.saveSnapshots() + } + + // Perform cleanup + this.destroy() + } +} + +module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, createHeaderFilters } diff --git a/lib/mock/snapshot-utils.js b/lib/mock/snapshot-utils.js new file mode 100644 index 00000000000..a14b69c15d4 --- /dev/null +++ b/lib/mock/snapshot-utils.js @@ -0,0 +1,158 @@ +'use strict' + +const { InvalidArgumentError } = require('../core/errors') +const { runtimeFeatures } = require('../util/runtime-features.js') + +/** + * @typedef {Object} HeaderFilters + * @property {Set} ignore - Set of headers to ignore for matching + * @property {Set} exclude - Set of headers to exclude from matching + * @property {Set} match - Set of headers to match (empty means match + */ + +/** + * Creates cached header sets for performance + * + * @param {import('./snapshot-recorder').SnapshotRecorderMatchOptions} matchOptions - Matching options for headers + * @returns {HeaderFilters} - Cached sets for ignore, exclude, and match headers + */ +function createHeaderFilters (matchOptions = {}) { + const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = [], caseSensitive = false } = matchOptions + + return { + ignore: new Set(ignoreHeaders.map(header => caseSensitive ? header : header.toLowerCase())), + exclude: new Set(excludeHeaders.map(header => caseSensitive ? header : header.toLowerCase())), + match: new Set(matchHeaders.map(header => caseSensitive ? header : header.toLowerCase())) + } +} + +const crypto = runtimeFeatures.has('crypto') + ? require('node:crypto') + : null + +/** + * @callback HashIdFunction + * @param {string} value - The value to hash + * @returns {string} - The base64url encoded hash of the value + */ + +/** + * Generates a hash for a given value + * @type {HashIdFunction} + */ +const hashId = crypto?.hash + ? (value) => crypto.hash('sha256', value, 'base64url') + : (value) => Buffer.from(value).toString('base64url') + +/** + * @typedef {(url: string) => boolean} IsUrlExcluded Checks if a URL matches any of the exclude patterns + */ + +/** @typedef {{[key: Lowercase]: string}} NormalizedHeaders */ +/** @typedef {Array} UndiciHeaders */ +/** @typedef {Record} Headers */ + +/** + * @param {*} headers + * @returns {headers is UndiciHeaders} + */ +function isUndiciHeaders (headers) { + return Array.isArray(headers) && (headers.length & 1) === 0 +} + +/** + * Factory function to create a URL exclusion checker + * @param {Array} [excludePatterns=[]] - Array of patterns to exclude + * @returns {IsUrlExcluded} - A function that checks if a URL matches any of the exclude patterns + */ +function isUrlExcludedFactory (excludePatterns = []) { + if (excludePatterns.length === 0) { + return () => false + } + + return function isUrlExcluded (url) { + let urlLowerCased + + for (const pattern of excludePatterns) { + if (typeof pattern === 'string') { + if (!urlLowerCased) { + // Convert URL to lowercase only once + urlLowerCased = url.toLowerCase() + } + // Simple string match (case-insensitive) + if (urlLowerCased.includes(pattern.toLowerCase())) { + return true + } + } else if (pattern instanceof RegExp) { + // Regex pattern match + if (pattern.test(url)) { + return true + } + } + } + + return false + } +} + +/** + * Normalizes headers for consistent comparison + * + * @param {Object|UndiciHeaders} headers - Headers to normalize + * @returns {NormalizedHeaders} - Normalized headers as a lowercase object + */ +function normalizeHeaders (headers) { + /** @type {NormalizedHeaders} */ + const normalizedHeaders = {} + + if (!headers) return normalizedHeaders + + // Handle array format (undici internal format: [name, value, name, value, ...]) + if (isUndiciHeaders(headers)) { + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i] + const value = headers[i + 1] + if (key && value !== undefined) { + // Convert Buffers to strings if needed + const keyStr = Buffer.isBuffer(key) ? key.toString() : key + const valueStr = Buffer.isBuffer(value) ? value.toString() : value + normalizedHeaders[keyStr.toLowerCase()] = valueStr + } + } + return normalizedHeaders + } + + // Handle object format + if (headers && typeof headers === 'object') { + for (const [key, value] of Object.entries(headers)) { + if (key && typeof key === 'string') { + normalizedHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value) + } + } + } + + return normalizedHeaders +} + +const validSnapshotModes = /** @type {const} */ (['record', 'playback', 'update']) + +/** @typedef {typeof validSnapshotModes[number]} SnapshotMode */ + +/** + * @param {*} mode - The snapshot mode to validate + * @returns {asserts mode is SnapshotMode} + */ +function validateSnapshotMode (mode) { + if (!validSnapshotModes.includes(mode)) { + throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validSnapshotModes.join(', ')}`) + } +} + +module.exports = { + createHeaderFilters, + hashId, + isUndiciHeaders, + normalizeHeaders, + isUrlExcludedFactory, + validateSnapshotMode +} diff --git a/lib/util/cache.js b/lib/util/cache.js index 10b0d8afcbc..a8f112168e6 100644 --- a/lib/util/cache.js +++ b/lib/util/cache.js @@ -1,7 +1,9 @@ 'use strict' const { - safeHTTPMethods + safeHTTPMethods, + pathHasQueryOrFragment, + hasSafeIterator } = require('../core/util') const { serializePathWithQuery } = require('../core/util') @@ -14,12 +16,10 @@ function makeCacheKey (opts) { throw new Error('opts.origin is undefined') } - let fullPath - try { - fullPath = serializePathWithQuery(opts.path || '/', opts.query) - } catch (error) { - // If fails (path already has query params), use as-is - fullPath = opts.path || '/' + let fullPath = opts.path || '/' + + if (opts.query && !pathHasQueryOrFragment(fullPath)) { + fullPath = serializePathWithQuery(fullPath, opts.query) } return { @@ -32,29 +32,30 @@ function makeCacheKey (opts) { /** * @param {Record} - * @return {Record} + * @returns {Record} */ -function normaliseHeaders (opts) { +function normalizeHeaders (opts) { let headers if (opts.headers == null) { headers = {} - } else if (typeof opts.headers[Symbol.iterator] === 'function') { - headers = {} - for (const x of opts.headers) { - if (!Array.isArray(x)) { - throw new Error('opts.headers is not a valid header map') - } - const [key, val] = x - if (typeof key !== 'string' || typeof val !== 'string') { - throw new Error('opts.headers is not a valid header map') - } - headers[key.toLowerCase()] = val - } } else if (typeof opts.headers === 'object') { headers = {} - for (const key of Object.keys(opts.headers)) { - headers[key.toLowerCase()] = opts.headers[key] + if (hasSafeIterator(opts.headers)) { + for (const x of opts.headers) { + if (!Array.isArray(x)) { + throw new Error('opts.headers is not a valid header map') + } + const [key, val] = x + if (typeof key !== 'string' || typeof val !== 'string') { + throw new Error('opts.headers is not a valid header map') + } + headers[key.toLowerCase()] = val + } + } else { + for (const key of Object.keys(opts.headers)) { + headers[key.toLowerCase()] = opts.headers[key] + } } } else { throw new Error('opts.headers is not an object') @@ -227,6 +228,10 @@ function parseCacheControlHeader (header) { headers[headers.length - 1] = lastHeader } + for (let j = 0; j < headers.length; j++) { + headers[j] = headers[j].trim() + } + if (key in output) { output[key] = output[key].concat(headers) } else { @@ -234,11 +239,13 @@ function parseCacheControlHeader (header) { } } } else { - // Something like `no-cache=some-header` + // Something like `no-cache="some-header"` + const fieldName = value.trim() + if (key in output) { - output[key] = output[key].concat(value) + output[key] = output[key].concat(fieldName) } else { - output[key] = [value] + output[key] = [fieldName] } } @@ -365,14 +372,43 @@ function assertCacheMethods (methods, name = 'CacheMethods') { } } +/** + * Creates a string key for request deduplication purposes. + * This key is used to identify in-flight requests that can be shared. + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey + * @param {Set} [excludeHeaders] Set of lowercase header names to exclude from the key + * @returns {string} + */ +function makeDeduplicationKey (cacheKey, excludeHeaders) { + // Use JSON.stringify to produce a collision-resistant key. + // Previous format used `:` and `=` delimiters without escaping, which + // allowed different header sets to produce identical keys (e.g. + // {a:"x:b=y"} vs {a:"x", b:"y"}). See: https://github.com/nodejs/undici/issues/5012 + const headers = {} + + if (cacheKey.headers) { + const sortedHeaders = Object.keys(cacheKey.headers).sort() + for (const header of sortedHeaders) { + // Skip excluded headers + if (excludeHeaders?.has(header.toLowerCase())) { + continue + } + headers[header] = cacheKey.headers[header] + } + } + + return JSON.stringify([cacheKey.origin, cacheKey.method, cacheKey.path, headers]) +} + module.exports = { makeCacheKey, - normaliseHeaders, + normalizeHeaders, assertCacheKey, assertCacheValue, parseCacheControlHeader, parseVaryHeader, isEtagUsable, assertCacheMethods, - assertCacheStore + assertCacheStore, + makeDeduplicationKey } diff --git a/lib/util/date.js b/lib/util/date.js index b871c4497bf..99dd150ab38 100644 --- a/lib/util/date.js +++ b/lib/util/date.js @@ -1,32 +1,20 @@ 'use strict' -const IMF_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] -const IMF_SPACES = [4, 7, 11, 16, 25] -const IMF_MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] -const IMF_COLONS = [19, 22] - -const ASCTIME_SPACES = [3, 7, 10, 19] - -const RFC850_DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] - /** * @see https://www.rfc-editor.org/rfc/rfc9110.html#name-date-time-formats * * @param {string} date - * @param {Date} [now] * @returns {Date | undefined} */ -function parseHttpDate (date, now) { +function parseHttpDate (date) { // Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate // Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format // Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format - date = date.toLowerCase() - switch (date[3]) { case ',': return parseImfDate(date) case ' ': return parseAscTimeDate(date) - default: return parseRfc850Date(date, now) + default: return parseRfc850Date(date) } } @@ -37,69 +25,207 @@ function parseHttpDate (date, now) { * @returns {Date | undefined} */ function parseImfDate (date) { - if (date.length !== 29) { + if ( + date.length !== 29 || + date[4] !== ' ' || + date[7] !== ' ' || + date[11] !== ' ' || + date[16] !== ' ' || + date[19] !== ':' || + date[22] !== ':' || + date[25] !== ' ' || + date[26] !== 'G' || + date[27] !== 'M' || + date[28] !== 'T' + ) { return undefined } - if (!date.endsWith('gmt')) { - // Unsupported timezone - return undefined + let weekday = -1 + if (date[0] === 'S' && date[1] === 'u' && date[2] === 'n') { // Sunday + weekday = 0 + } else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n') { // Monday + weekday = 1 + } else if (date[0] === 'T' && date[1] === 'u' && date[2] === 'e') { // Tuesday + weekday = 2 + } else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd') { // Wednesday + weekday = 3 + } else if (date[0] === 'T' && date[1] === 'h' && date[2] === 'u') { // Thursday + weekday = 4 + } else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i') { // Friday + weekday = 5 + } else if (date[0] === 'S' && date[1] === 'a' && date[2] === 't') { // Saturday + weekday = 6 + } else { + return undefined // Not a valid day of the week } - for (const spaceInx of IMF_SPACES) { - if (date[spaceInx] !== ' ') { - return undefined + let day = 0 + if (date[5] === '0') { + // Single digit day, e.g. "Sun Nov 6 08:49:37 1994" + const code = date.charCodeAt(6) + if (code < 49 || code > 57) { + return undefined // Not a digit } - } - - for (const colonIdx of IMF_COLONS) { - if (date[colonIdx] !== ':') { - return undefined + day = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(5) + if (code1 < 49 || code1 > 51) { + return undefined // Not a digit between 1 and 3 + } + const code2 = date.charCodeAt(6) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit } + day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const dayName = date.substring(0, 3) - if (!IMF_DAYS.includes(dayName)) { + let monthIdx = -1 + if ( + (date[8] === 'J' && date[9] === 'a' && date[10] === 'n') + ) { + monthIdx = 0 // Jan + } else if ( + (date[8] === 'F' && date[9] === 'e' && date[10] === 'b') + ) { + monthIdx = 1 // Feb + } else if ( + (date[8] === 'M' && date[9] === 'a') + ) { + if (date[10] === 'r') { + monthIdx = 2 // Mar + } else if (date[10] === 'y') { + monthIdx = 4 // May + } else { + return undefined // Invalid month + } + } else if ( + (date[8] === 'J') + ) { + if (date[9] === 'a' && date[10] === 'n') { + monthIdx = 0 // Jan + } else if (date[9] === 'u') { + if (date[10] === 'n') { + monthIdx = 5 // Jun + } else if (date[10] === 'l') { + monthIdx = 6 // Jul + } else { + return undefined // Invalid month + } + } else { + return undefined // Invalid month + } + } else if ( + (date[8] === 'A') + ) { + if (date[9] === 'p' && date[10] === 'r') { + monthIdx = 3 // Apr + } else if (date[9] === 'u' && date[10] === 'g') { + monthIdx = 7 // Aug + } else { + return undefined // Invalid month + } + } else if ( + (date[8] === 'S' && date[9] === 'e' && date[10] === 'p') + ) { + monthIdx = 8 // Sep + } else if ( + (date[8] === 'O' && date[9] === 'c' && date[10] === 't') + ) { + monthIdx = 9 // Oct + } else if ( + (date[8] === 'N' && date[9] === 'o' && date[10] === 'v') + ) { + monthIdx = 10 // Nov + } else if ( + (date[8] === 'D' && date[9] === 'e' && date[10] === 'c') + ) { + monthIdx = 11 // Dec + } else { + // Not a valid month return undefined } - const dayString = date.substring(5, 7) - const day = Number.parseInt(dayString) - if (isNaN(day) || (day < 10 && dayString[0] !== '0')) { - // Not a number, 0, or it's less than 10 and didn't start with a 0 - return undefined + const yearDigit1 = date.charCodeAt(12) + if (yearDigit1 < 48 || yearDigit1 > 57) { + return undefined // Not a digit } - - const month = date.substring(8, 11) - const monthIdx = IMF_MONTHS.indexOf(month) - if (monthIdx === -1) { - return undefined + const yearDigit2 = date.charCodeAt(13) + if (yearDigit2 < 48 || yearDigit2 > 57) { + return undefined // Not a digit } - - const year = Number.parseInt(date.substring(12, 16)) - if (isNaN(year)) { - return undefined + const yearDigit3 = date.charCodeAt(14) + if (yearDigit3 < 48 || yearDigit3 > 57) { + return undefined // Not a digit + } + const yearDigit4 = date.charCodeAt(15) + if (yearDigit4 < 48 || yearDigit4 > 57) { + return undefined // Not a digit } + const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48) - const hourString = date.substring(17, 19) - const hour = Number.parseInt(hourString) - if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) { - return undefined + let hour = 0 + if (date[17] === '0') { + const code = date.charCodeAt(18) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + hour = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(17) + if (code1 < 48 || code1 > 50) { + return undefined // Not a digit between 0 and 2 + } + const code2 = date.charCodeAt(18) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + if (code1 === 50 && code2 > 51) { + return undefined // Hour cannot be greater than 23 + } + hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const minuteString = date.substring(20, 22) - const minute = Number.parseInt(minuteString) - if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) { - return undefined + let minute = 0 + if (date[20] === '0') { + const code = date.charCodeAt(21) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + minute = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(20) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(21) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const secondString = date.substring(23, 25) - const second = Number.parseInt(secondString) - if (isNaN(second) || (second < 10 && secondString[0] !== '0')) { - return undefined + let second = 0 + if (date[23] === '0') { + const code = date.charCodeAt(24) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + second = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(23) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(24) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - return new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) + const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) + return result.getUTCDay() === weekday ? result : undefined } /** @@ -111,147 +237,415 @@ function parseImfDate (date) { function parseAscTimeDate (date) { // This is assumed to be in UTC - if (date.length !== 24) { + if ( + date.length !== 24 || + date[7] !== ' ' || + date[10] !== ' ' || + date[19] !== ' ' + ) { return undefined } - for (const spaceIdx of ASCTIME_SPACES) { - if (date[spaceIdx] !== ' ') { - return undefined - } + let weekday = -1 + if (date[0] === 'S' && date[1] === 'u' && date[2] === 'n') { // Sunday + weekday = 0 + } else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n') { // Monday + weekday = 1 + } else if (date[0] === 'T' && date[1] === 'u' && date[2] === 'e') { // Tuesday + weekday = 2 + } else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd') { // Wednesday + weekday = 3 + } else if (date[0] === 'T' && date[1] === 'h' && date[2] === 'u') { // Thursday + weekday = 4 + } else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i') { // Friday + weekday = 5 + } else if (date[0] === 'S' && date[1] === 'a' && date[2] === 't') { // Saturday + weekday = 6 + } else { + return undefined // Not a valid day of the week } - const dayName = date.substring(0, 3) - if (!IMF_DAYS.includes(dayName)) { + let monthIdx = -1 + if ( + (date[4] === 'J' && date[5] === 'a' && date[6] === 'n') + ) { + monthIdx = 0 // Jan + } else if ( + (date[4] === 'F' && date[5] === 'e' && date[6] === 'b') + ) { + monthIdx = 1 // Feb + } else if ( + (date[4] === 'M' && date[5] === 'a') + ) { + if (date[6] === 'r') { + monthIdx = 2 // Mar + } else if (date[6] === 'y') { + monthIdx = 4 // May + } else { + return undefined // Invalid month + } + } else if ( + (date[4] === 'J') + ) { + if (date[5] === 'a' && date[6] === 'n') { + monthIdx = 0 // Jan + } else if (date[5] === 'u') { + if (date[6] === 'n') { + monthIdx = 5 // Jun + } else if (date[6] === 'l') { + monthIdx = 6 // Jul + } else { + return undefined // Invalid month + } + } else { + return undefined // Invalid month + } + } else if ( + (date[4] === 'A') + ) { + if (date[5] === 'p' && date[6] === 'r') { + monthIdx = 3 // Apr + } else if (date[5] === 'u' && date[6] === 'g') { + monthIdx = 7 // Aug + } else { + return undefined // Invalid month + } + } else if ( + (date[4] === 'S' && date[5] === 'e' && date[6] === 'p') + ) { + monthIdx = 8 // Sep + } else if ( + (date[4] === 'O' && date[5] === 'c' && date[6] === 't') + ) { + monthIdx = 9 // Oct + } else if ( + (date[4] === 'N' && date[5] === 'o' && date[6] === 'v') + ) { + monthIdx = 10 // Nov + } else if ( + (date[4] === 'D' && date[5] === 'e' && date[6] === 'c') + ) { + monthIdx = 11 // Dec + } else { + // Not a valid month return undefined } - const month = date.substring(4, 7) - const monthIdx = IMF_MONTHS.indexOf(month) - if (monthIdx === -1) { - return undefined + let day = 0 + if (date[8] === ' ') { + // Single digit day, e.g. "Sun Nov 6 08:49:37 1994" + const code = date.charCodeAt(9) + if (code < 49 || code > 57) { + return undefined // Not a digit + } + day = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(8) + if (code1 < 49 || code1 > 51) { + return undefined // Not a digit between 1 and 3 + } + const code2 = date.charCodeAt(9) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const dayString = date.substring(8, 10) - const day = Number.parseInt(dayString) - if (isNaN(day) || (day < 10 && dayString[0] !== ' ')) { - return undefined + let hour = 0 + if (date[11] === '0') { + const code = date.charCodeAt(12) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + hour = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(11) + if (code1 < 48 || code1 > 50) { + return undefined // Not a digit between 0 and 2 + } + const code2 = date.charCodeAt(12) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + if (code1 === 50 && code2 > 51) { + return undefined // Hour cannot be greater than 23 + } + hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const hourString = date.substring(11, 13) - const hour = Number.parseInt(hourString) - if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) { - return undefined + let minute = 0 + if (date[14] === '0') { + const code = date.charCodeAt(15) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + minute = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(14) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(15) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const minuteString = date.substring(14, 16) - const minute = Number.parseInt(minuteString) - if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) { - return undefined + let second = 0 + if (date[17] === '0') { + const code = date.charCodeAt(18) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + second = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(17) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(18) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const secondString = date.substring(17, 19) - const second = Number.parseInt(secondString) - if (isNaN(second) || (second < 10 && secondString[0] !== '0')) { - return undefined + const yearDigit1 = date.charCodeAt(20) + if (yearDigit1 < 48 || yearDigit1 > 57) { + return undefined // Not a digit } - - const year = Number.parseInt(date.substring(20, 24)) - if (isNaN(year)) { - return undefined + const yearDigit2 = date.charCodeAt(21) + if (yearDigit2 < 48 || yearDigit2 > 57) { + return undefined // Not a digit } + const yearDigit3 = date.charCodeAt(22) + if (yearDigit3 < 48 || yearDigit3 > 57) { + return undefined // Not a digit + } + const yearDigit4 = date.charCodeAt(23) + if (yearDigit4 < 48 || yearDigit4 > 57) { + return undefined // Not a digit + } + const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48) - return new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) + const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) + return result.getUTCDay() === weekday ? result : undefined } /** * @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats * * @param {string} date - * @param {Date} [now] * @returns {Date | undefined} */ -function parseRfc850Date (date, now = new Date()) { - if (!date.endsWith('gmt')) { - // Unsupported timezone - return undefined - } - - const commaIndex = date.indexOf(',') - if (commaIndex === -1) { - return undefined - } - - if ((date.length - commaIndex - 1) !== 23) { - return undefined - } - - const dayName = date.substring(0, commaIndex) - if (!RFC850_DAYS.includes(dayName)) { +function parseRfc850Date (date) { + let commaIndex = -1 + + let weekday = -1 + if (date[0] === 'S') { + if (date[1] === 'u' && date[2] === 'n' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') { + weekday = 0 // Sunday + commaIndex = 6 + } else if (date[1] === 'a' && date[2] === 't' && date[3] === 'u' && date[4] === 'r' && date[5] === 'd' && date[6] === 'a' && date[7] === 'y') { + weekday = 6 // Saturday + commaIndex = 8 + } + } else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') { + weekday = 1 // Monday + commaIndex = 6 + } else if (date[0] === 'T') { + if (date[1] === 'u' && date[2] === 'e' && date[3] === 's' && date[4] === 'd' && date[5] === 'a' && date[6] === 'y') { + weekday = 2 // Tuesday + commaIndex = 7 + } else if (date[1] === 'h' && date[2] === 'u' && date[3] === 'r' && date[4] === 's' && date[5] === 'd' && date[6] === 'a' && date[7] === 'y') { + weekday = 4 // Thursday + commaIndex = 8 + } + } else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd' && date[3] === 'n' && date[4] === 'e' && date[5] === 's' && date[6] === 'd' && date[7] === 'a' && date[8] === 'y') { + weekday = 3 // Wednesday + commaIndex = 9 + } else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') { + weekday = 5 // Friday + commaIndex = 6 + } else { + // Not a valid day name return undefined } if ( + date[commaIndex] !== ',' || + (date.length - commaIndex - 1) !== 23 || date[commaIndex + 1] !== ' ' || date[commaIndex + 4] !== '-' || date[commaIndex + 8] !== '-' || date[commaIndex + 11] !== ' ' || date[commaIndex + 14] !== ':' || date[commaIndex + 17] !== ':' || - date[commaIndex + 20] !== ' ' + date[commaIndex + 20] !== ' ' || + date[commaIndex + 21] !== 'G' || + date[commaIndex + 22] !== 'M' || + date[commaIndex + 23] !== 'T' ) { return undefined } - const dayString = date.substring(commaIndex + 2, commaIndex + 4) - const day = Number.parseInt(dayString) - if (isNaN(day) || (day < 10 && dayString[0] !== '0')) { - // Not a number, or it's less than 10 and didn't start with a 0 - return undefined + let day = 0 + if (date[commaIndex + 2] === '0') { + // Single digit day, e.g. "Sun Nov 6 08:49:37 1994" + const code = date.charCodeAt(commaIndex + 3) + if (code < 49 || code > 57) { + return undefined // Not a digit + } + day = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(commaIndex + 2) + if (code1 < 49 || code1 > 51) { + return undefined // Not a digit between 1 and 3 + } + const code2 = date.charCodeAt(commaIndex + 3) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const month = date.substring(commaIndex + 5, commaIndex + 8) - const monthIdx = IMF_MONTHS.indexOf(month) - if (monthIdx === -1) { + let monthIdx = -1 + if ( + (date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'n') + ) { + monthIdx = 0 // Jan + } else if ( + (date[commaIndex + 5] === 'F' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'b') + ) { + monthIdx = 1 // Feb + } else if ( + (date[commaIndex + 5] === 'M' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'r') + ) { + monthIdx = 2 // Mar + } else if ( + (date[commaIndex + 5] === 'A' && date[commaIndex + 6] === 'p' && date[commaIndex + 7] === 'r') + ) { + monthIdx = 3 // Apr + } else if ( + (date[commaIndex + 5] === 'M' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'y') + ) { + monthIdx = 4 // May + } else if ( + (date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'n') + ) { + monthIdx = 5 // Jun + } else if ( + (date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'l') + ) { + monthIdx = 6 // Jul + } else if ( + (date[commaIndex + 5] === 'A' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'g') + ) { + monthIdx = 7 // Aug + } else if ( + (date[commaIndex + 5] === 'S' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'p') + ) { + monthIdx = 8 // Sep + } else if ( + (date[commaIndex + 5] === 'O' && date[commaIndex + 6] === 'c' && date[commaIndex + 7] === 't') + ) { + monthIdx = 9 // Oct + } else if ( + (date[commaIndex + 5] === 'N' && date[commaIndex + 6] === 'o' && date[commaIndex + 7] === 'v') + ) { + monthIdx = 10 // Nov + } else if ( + (date[commaIndex + 5] === 'D' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'c') + ) { + monthIdx = 11 // Dec + } else { + // Not a valid month return undefined } - // As of this point year is just the decade (i.e. 94) - let year = Number.parseInt(date.substring(commaIndex + 9, commaIndex + 11)) - if (isNaN(year)) { - return undefined + const yearDigit1 = date.charCodeAt(commaIndex + 9) + if (yearDigit1 < 48 || yearDigit1 > 57) { + return undefined // Not a digit + } + const yearDigit2 = date.charCodeAt(commaIndex + 10) + if (yearDigit2 < 48 || yearDigit2 > 57) { + return undefined // Not a digit } - const currentYear = now.getUTCFullYear() - const currentDecade = currentYear % 100 - const currentCentury = Math.floor(currentYear / 100) + let year = (yearDigit1 - 48) * 10 + (yearDigit2 - 48) // Convert ASCII codes to number - if (year > currentDecade && year - currentDecade >= 50) { - // Over 50 years in future, go to previous century - year += (currentCentury - 1) * 100 - } else { - year += currentCentury * 100 - } + // RFC 6265 states that the year is in the range 1970-2069. + // @see https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.1 + // + // 3. If the year-value is greater than or equal to 70 and less than or + // equal to 99, increment the year-value by 1900. + // 4. If the year-value is greater than or equal to 0 and less than or + // equal to 69, increment the year-value by 2000. + year += year < 70 ? 2000 : 1900 - const hourString = date.substring(commaIndex + 12, commaIndex + 14) - const hour = Number.parseInt(hourString) - if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) { - return undefined + let hour = 0 + if (date[commaIndex + 12] === '0') { + const code = date.charCodeAt(commaIndex + 13) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + hour = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(commaIndex + 12) + if (code1 < 48 || code1 > 50) { + return undefined // Not a digit between 0 and 2 + } + const code2 = date.charCodeAt(commaIndex + 13) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + if (code1 === 50 && code2 > 51) { + return undefined // Hour cannot be greater than 23 + } + hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const minuteString = date.substring(commaIndex + 15, commaIndex + 17) - const minute = Number.parseInt(minuteString) - if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) { - return undefined + let minute = 0 + if (date[commaIndex + 15] === '0') { + const code = date.charCodeAt(commaIndex + 16) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + minute = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(commaIndex + 15) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(commaIndex + 16) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const secondString = date.substring(commaIndex + 18, commaIndex + 20) - const second = Number.parseInt(secondString) - if (isNaN(second) || (second < 10 && secondString[0] !== '0')) { - return undefined + let second = 0 + if (date[commaIndex + 18] === '0') { + const code = date.charCodeAt(commaIndex + 19) + if (code < 48 || code > 57) { + return undefined // Not a digit + } + second = code - 48 // Convert ASCII code to number + } else { + const code1 = date.charCodeAt(commaIndex + 18) + if (code1 < 48 || code1 > 53) { + return undefined // Not a digit between 0 and 5 + } + const code2 = date.charCodeAt(commaIndex + 19) + if (code2 < 48 || code2 > 57) { + return undefined // Not a digit + } + second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - return new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) + const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) + return result.getUTCDay() === weekday ? result : undefined } module.exports = { diff --git a/lib/util/promise.js b/lib/util/promise.js new file mode 100644 index 00000000000..048f86e34ef --- /dev/null +++ b/lib/util/promise.js @@ -0,0 +1,28 @@ +'use strict' + +/** + * @template {*} T + * @typedef {Object} DeferredPromise + * @property {Promise} promise + * @property {(value?: T) => void} resolve + * @property {(reason?: any) => void} reject + */ + +/** + * @template {*} T + * @returns {DeferredPromise} An object containing a promise and its resolve/reject methods. + */ +function createDeferredPromise () { + let res + let rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { promise, resolve: res, reject: rej } +} + +module.exports = { + createDeferredPromise +} diff --git a/lib/util/runtime-features.js b/lib/util/runtime-features.js new file mode 100644 index 00000000000..3e62dc004d3 --- /dev/null +++ b/lib/util/runtime-features.js @@ -0,0 +1,124 @@ +'use strict' + +/** @typedef {`node:${string}`} NodeModuleName */ + +/** @type {Record any>} */ +const lazyLoaders = { + __proto__: null, + 'node:crypto': () => require('node:crypto'), + 'node:sqlite': () => require('node:sqlite'), + 'node:worker_threads': () => require('node:worker_threads'), + 'node:zlib': () => require('node:zlib') +} + +/** + * @param {NodeModuleName} moduleName + * @returns {boolean} + */ +function detectRuntimeFeatureByNodeModule (moduleName) { + try { + lazyLoaders[moduleName]() + return true + } catch (err) { + if (err.code !== 'ERR_UNKNOWN_BUILTIN_MODULE' && err.code !== 'ERR_NO_CRYPTO') { + throw err + } + return false + } +} + +/** + * @param {NodeModuleName} moduleName + * @param {string} property + * @returns {boolean} + */ +function detectRuntimeFeatureByExportedProperty (moduleName, property) { + const module = lazyLoaders[moduleName]() + return typeof module[property] !== 'undefined' +} + +const runtimeFeaturesByExportedProperty = /** @type {const} */ (['markAsUncloneable', 'zstd']) + +/** @type {Record} */ +const exportedPropertyLookup = { + markAsUncloneable: ['node:worker_threads', 'markAsUncloneable'], + zstd: ['node:zlib', 'createZstdDecompress'] +} + +/** @typedef {typeof runtimeFeaturesByExportedProperty[number]} RuntimeFeatureByExportedProperty */ + +const runtimeFeaturesAsNodeModule = /** @type {const} */ (['crypto', 'sqlite']) +/** @typedef {typeof runtimeFeaturesAsNodeModule[number]} RuntimeFeatureByNodeModule */ + +const features = /** @type {const} */ ([ + ...runtimeFeaturesAsNodeModule, + ...runtimeFeaturesByExportedProperty +]) + +/** @typedef {typeof features[number]} Feature */ + +/** + * @param {Feature} feature + * @returns {boolean} + */ +function detectRuntimeFeature (feature) { + if (runtimeFeaturesAsNodeModule.includes(/** @type {RuntimeFeatureByNodeModule} */ (feature))) { + return detectRuntimeFeatureByNodeModule(`node:${feature}`) + } else if (runtimeFeaturesByExportedProperty.includes(/** @type {RuntimeFeatureByExportedProperty} */ (feature))) { + const [moduleName, property] = exportedPropertyLookup[feature] + return detectRuntimeFeatureByExportedProperty(moduleName, property) + } + throw new TypeError(`unknown feature: ${feature}`) +} + +/** + * @class + * @name RuntimeFeatures + */ +class RuntimeFeatures { + /** @type {Map} */ + #map = new Map() + + /** + * Clears all cached feature detections. + */ + clear () { + this.#map.clear() + } + + /** + * @param {Feature} feature + * @returns {boolean} + */ + has (feature) { + return ( + this.#map.get(feature) ?? this.#detectRuntimeFeature(feature) + ) + } + + /** + * @param {Feature} feature + * @param {boolean} value + */ + set (feature, value) { + if (features.includes(feature) === false) { + throw new TypeError(`unknown feature: ${feature}`) + } + this.#map.set(feature, value) + } + + /** + * @param {Feature} feature + * @returns {boolean} + */ + #detectRuntimeFeature (feature) { + const result = detectRuntimeFeature(feature) + this.#map.set(feature, result) + return result + } +} + +const instance = new RuntimeFeatures() + +module.exports.runtimeFeatures = instance +module.exports.default = instance diff --git a/lib/web/cache/cache.js b/lib/web/cache/cache.js index afc82f639e4..10decbede6a 100644 --- a/lib/web/cache/cache.js +++ b/lib/web/cache/cache.js @@ -1,5 +1,7 @@ 'use strict' +const assert = require('node:assert') + const { kConstruct } = require('../../core/symbols') const { urlEquals, getFieldValues } = require('./util') const { kEnumerableProperty, isDisturbed } = require('../../core/util') @@ -7,8 +9,8 @@ const { webidl } = require('../webidl') const { cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response') const { Request, fromInnerRequest, getRequestState } = require('../fetch/request') const { fetching } = require('../fetch/index') -const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util') -const assert = require('node:assert') +const { urlIsHttpHttpsScheme, readAllBytes } = require('../fetch/util') +const { createDeferredPromise } = require('../../util/promise') /** * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation @@ -16,7 +18,7 @@ const assert = require('node:assert') * @property {'delete' | 'put'} type * @property {any} request * @property {any} response - * @property {import('../../types/cache').CacheQueryOptions} options + * @property {import('../../../types/cache').CacheQueryOptions} options */ /** @@ -46,7 +48,7 @@ class Cache { const prefix = 'Cache.match' webidl.argumentLengthCheck(arguments, 1, prefix) - request = webidl.converters.RequestInfo(request, prefix, 'request') + request = webidl.converters.RequestInfo(request) options = webidl.converters.CacheQueryOptions(options, prefix, 'options') const p = this.#internalMatchAll(request, options, 1) @@ -62,7 +64,7 @@ class Cache { webidl.brandCheck(this, Cache) const prefix = 'Cache.matchAll' - if (request !== undefined) request = webidl.converters.RequestInfo(request, prefix, 'request') + if (request !== undefined) request = webidl.converters.RequestInfo(request) options = webidl.converters.CacheQueryOptions(options, prefix, 'options') return this.#internalMatchAll(request, options) @@ -74,7 +76,7 @@ class Cache { const prefix = 'Cache.add' webidl.argumentLengthCheck(arguments, 1, prefix) - request = webidl.converters.RequestInfo(request, prefix, 'request') + request = webidl.converters.RequestInfo(request) // 1. const requests = [request] @@ -262,7 +264,7 @@ class Cache { const prefix = 'Cache.put' webidl.argumentLengthCheck(arguments, 2, prefix) - request = webidl.converters.RequestInfo(request, prefix, 'request') + request = webidl.converters.RequestInfo(request) response = webidl.converters.Response(response, prefix, 'response') // 1. @@ -393,7 +395,7 @@ class Cache { const prefix = 'Cache.delete' webidl.argumentLengthCheck(arguments, 1, prefix) - request = webidl.converters.RequestInfo(request, prefix, 'request') + request = webidl.converters.RequestInfo(request) options = webidl.converters.CacheQueryOptions(options, prefix, 'options') /** @@ -450,7 +452,7 @@ class Cache { /** * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys * @param {any} request - * @param {import('../../types/cache').CacheQueryOptions} options + * @param {import('../../../types/cache').CacheQueryOptions} options * @returns {Promise} */ async keys (request = undefined, options = {}) { @@ -458,7 +460,7 @@ class Cache { const prefix = 'Cache.keys' - if (request !== undefined) request = webidl.converters.RequestInfo(request, prefix, 'request') + if (request !== undefined) request = webidl.converters.RequestInfo(request) options = webidl.converters.CacheQueryOptions(options, prefix, 'options') // 1. @@ -668,7 +670,7 @@ class Cache { /** * @see https://w3c.github.io/ServiceWorker/#query-cache * @param {any} requestQuery - * @param {import('../../types/cache').CacheQueryOptions} options + * @param {import('../../../types/cache').CacheQueryOptions} options * @param {requestResponseList} targetStorage * @returns {requestResponseList} */ @@ -693,7 +695,7 @@ class Cache { * @param {any} requestQuery * @param {any} request * @param {any | null} response - * @param {import('../../types/cache').CacheQueryOptions | undefined} options + * @param {import('../../../types/cache').CacheQueryOptions | undefined} options * @returns {boolean} */ #requestMatchesCachedItem (requestQuery, request, response = null, options) { @@ -792,9 +794,9 @@ class Cache { // 5.5.2 for (const response of responses) { // 5.5.2.1 - const responseObject = fromInnerResponse(response, 'immutable') + const responseObject = fromInnerResponse(cloneResponse(response), 'immutable') - responseList.push(responseObject.clone()) + responseList.push(responseObject) if (responseList.length >= maxResponses) { break diff --git a/lib/web/cookies/index.js b/lib/web/cookies/index.js index 1d891f1d692..be99f6f3dd2 100644 --- a/lib/web/cookies/index.js +++ b/lib/web/cookies/index.js @@ -186,7 +186,7 @@ webidl.converters.Cookie = webidl.dictionaryConverter([ { converter: webidl.sequenceConverter(webidl.converters.DOMString), key: 'unparsed', - defaultValue: () => new Array(0) + defaultValue: () => [] } ]) diff --git a/lib/web/cookies/parse.js b/lib/web/cookies/parse.js index 4ac66dc0974..0e524908077 100644 --- a/lib/web/cookies/parse.js +++ b/lib/web/cookies/parse.js @@ -1,10 +1,9 @@ 'use strict' +const { collectASequenceOfCodePointsFast } = require('../infra') const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants') const { isCTLExcludingHtab } = require('./util') -const { collectASequenceOfCodePointsFast } = require('../fetch/data-url') const assert = require('node:assert') -const { unescape } = require('node:querystring') /** * @description Parses the field-value attributes of a set-cookie header string. @@ -82,7 +81,7 @@ function parseSetCookie (header) { // store arbitrary data in a cookie-value SHOULD encode that data, for // example, using Base64 [RFC4648]. return { - name, value: unescape(value), ...parseUnparsedAttributes(unparsedAttributes) + name, value, ...parseUnparsedAttributes(unparsedAttributes) } } @@ -280,32 +279,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) // 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' + // 1. If cookie-av's attribute-value is a case-insensitive match for + // "None", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "None". + if (attributeValueLowercase === 'none') { + cookieAttributeList.sameSite = 'None' + } else if (attributeValueLowercase === 'strict') { + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", append an attribute to the cookie-attribute-list with + // an attribute-name of "SameSite" and an attribute-value of + // "Strict". + cookieAttributeList.sameSite = 'Strict' + } else if (attributeValueLowercase === 'lax') { + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "Lax". + cookieAttributeList.sameSite = 'Lax' } - - // 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 ??= [] diff --git a/lib/web/eventsource/eventsource-stream.js b/lib/web/eventsource/eventsource-stream.js index 59cf7468800..d24e8f6a1b1 100644 --- a/lib/web/eventsource/eventsource-stream.js +++ b/lib/web/eventsource/eventsource-stream.js @@ -236,7 +236,7 @@ class EventSourceStream extends Transform { this.buffer = this.buffer.subarray(this.pos + 1) this.pos = 0 if ( - this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) { + this.event.data !== undefined || this.event.event || this.event.id !== undefined || this.event.retry) { this.processEvent(this.event) } this.clearEvent() @@ -367,7 +367,7 @@ class EventSourceStream extends Transform { this.state.reconnectionTime = parseInt(event.retry, 10) } - if (event.id && isValidLastEventId(event.id)) { + if (event.id !== undefined && isValidLastEventId(event.id)) { this.state.lastEventId = event.id } diff --git a/lib/web/eventsource/eventsource.js b/lib/web/eventsource/eventsource.js index 746400b8ea9..32dcf0e423e 100644 --- a/lib/web/eventsource/eventsource.js +++ b/lib/web/eventsource/eventsource.js @@ -8,7 +8,6 @@ const { EventSourceStream } = require('./eventsource-stream') const { parseMIMEType } = require('../fetch/data-url') const { createFastMessageEvent } = require('../websocket/events') const { isNetworkError } = require('../fetch/response') -const { delay } = require('./util') const { kEnumerableProperty } = require('../../core/util') const { environmentSettingsObject } = require('../fetch/util') @@ -124,10 +123,10 @@ class EventSource extends EventTarget { url = webidl.converters.USVString(url) eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, 'eventSourceInitDict') - this.#dispatcher = eventSourceInitDict.dispatcher + this.#dispatcher = eventSourceInitDict.node.dispatcher || eventSourceInitDict.dispatcher this.#state = { lastEventId: '', - reconnectionTime: defaultReconnectionTime + reconnectionTime: eventSourceInitDict.node.reconnectionTime } // 2. Let settings be ev's relevant settings object. @@ -318,9 +317,9 @@ class EventSource extends EventTarget { /** * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model - * @returns {Promise} + * @returns {void} */ - async #reconnect () { + #reconnect () { // When a user agent is to reestablish the connection, the user agent must // run the following steps. These steps are run in parallel, not as part of // a task. (The tasks that it queues, of course, are run like normal tasks @@ -338,27 +337,27 @@ class EventSource extends EventTarget { this.dispatchEvent(new Event('error')) // 2. Wait a delay equal to the reconnection time of the event source. - await delay(this.#state.reconnectionTime) - - // 5. Queue a task to run the following steps: - - // 1. If the EventSource object's readyState attribute is not set to - // CONNECTING, then return. - if (this.#readyState !== CONNECTING) return - - // 2. Let request be the EventSource object's request. - // 3. If the EventSource object's last event ID string is not the empty - // string, then: - // 1. Let lastEventIDValue be the EventSource object's last event ID - // string, encoded as UTF-8. - // 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header - // list. - if (this.#state.lastEventId.length) { - this.#request.headersList.set('last-event-id', this.#state.lastEventId, true) - } + setTimeout(() => { + // 5. Queue a task to run the following steps: + + // 1. If the EventSource object's readyState attribute is not set to + // CONNECTING, then return. + if (this.#readyState !== CONNECTING) return + + // 2. Let request be the EventSource object's request. + // 3. If the EventSource object's last event ID string is not the empty + // string, then: + // 1. Let lastEventIDValue be the EventSource object's last event ID + // string, encoded as UTF-8. + // 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header + // list. + if (this.#state.lastEventId.length) { + this.#request.headersList.set('last-event-id', this.#state.lastEventId, true) + } - // 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section. - this.#connect() + // 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section. + this.#connect() + }, this.#state.reconnectionTime)?.unref() } /** @@ -383,9 +382,11 @@ class EventSource extends EventTarget { this.removeEventListener('open', this.#events.open) } - if (typeof fn === 'function') { + const listener = webidl.converters.EventHandlerNonNull(fn) + + if (listener !== null) { + this.addEventListener('open', listener) this.#events.open = fn - this.addEventListener('open', fn) } else { this.#events.open = null } @@ -400,9 +401,11 @@ class EventSource extends EventTarget { this.removeEventListener('message', this.#events.message) } - if (typeof fn === 'function') { + const listener = webidl.converters.EventHandlerNonNull(fn) + + if (listener !== null) { + this.addEventListener('message', listener) this.#events.message = fn - this.addEventListener('message', fn) } else { this.#events.message = null } @@ -417,9 +420,11 @@ class EventSource extends EventTarget { this.removeEventListener('error', this.#events.error) } - if (typeof fn === 'function') { + const listener = webidl.converters.EventHandlerNonNull(fn) + + if (listener !== null) { + this.addEventListener('error', listener) this.#events.error = fn - this.addEventListener('error', fn) } else { this.#events.error = null } @@ -472,6 +477,21 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([ { key: 'dispatcher', // undici only converter: webidl.converters.any + }, + { + key: 'node', // undici only + converter: webidl.dictionaryConverter([ + { + key: 'reconnectionTime', + converter: webidl.converters['unsigned long'], + defaultValue: () => defaultReconnectionTime + }, + { + key: 'dispatcher', + converter: webidl.converters.any + } + ]), + defaultValue: () => ({}) } ]) diff --git a/lib/web/eventsource/util.js b/lib/web/eventsource/util.js index ee0b4d36df0..a87cc834eca 100644 --- a/lib/web/eventsource/util.js +++ b/lib/web/eventsource/util.js @@ -23,15 +23,7 @@ function isASCIINumber (value) { return true } -// https://github.com/nodejs/undici/issues/2664 -function delay (ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }) -} - module.exports = { isValidLastEventId, - isASCIINumber, - delay + isASCIINumber } diff --git a/lib/web/fetch/body.js b/lib/web/fetch/body.js index 65b07b92452..5d172496527 100644 --- a/lib/web/fetch/body.js +++ b/lib/web/fetch/body.js @@ -4,47 +4,48 @@ const util = require('../../core/util') const { ReadableStreamFrom, readableStreamClose, - createDeferredPromise, fullyReadBody, - extractMimeType, - utf8DecodeBytes + extractMimeType } = require('./util') const { FormData, setFormDataState } = require('./formdata') const { webidl } = require('../webidl') -const { Blob } = require('node:buffer') const assert = require('node:assert') const { isErrored, isDisturbed } = require('node:stream') -const { isArrayBuffer } = require('node:util/types') +const { isUint8Array } = require('node:util/types') const { serializeAMimeType } = require('./data-url') const { multipartFormDataParser } = require('./formdata-parser') -let random +const { createDeferredPromise } = require('../../util/promise') +const { parseJSONFromBytes } = require('../infra') +const { utf8DecodeBytes } = require('../../encoding') +const { runtimeFeatures } = require('../../util/runtime-features.js') -try { - const crypto = require('node:crypto') - random = (max) => crypto.randomInt(0, max) -} catch { - random = (max) => Math.floor(Math.random() * max) -} +const random = runtimeFeatures.has('crypto') + ? require('node:crypto').randomInt + : (max) => Math.floor(Math.random() * max) const textEncoder = new TextEncoder() function noop () {} -const hasFinalizationRegistry = globalThis.FinalizationRegistry -let streamRegistry - -if (hasFinalizationRegistry) { - streamRegistry = new FinalizationRegistry((weakRef) => { - const stream = weakRef.deref() - if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) { - stream.cancel('Response object has been garbage collected').catch(noop) - } - }) -} +const streamRegistry = new FinalizationRegistry((weakRef) => { + const stream = weakRef.deref() + if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) { + stream.cancel('Response object has been garbage collected').catch(noop) + } +}) -// https://fetch.spec.whatwg.org/#concept-bodyinit-extract +/** + * Extract a body with type from a byte sequence or BodyInit object + * + * @param {import('../../../types').BodyInit} object - The BodyInit object to extract from + * @param {boolean} [keepalive=false] - If true, indicates that the body + * @returns {[{stream: ReadableStream, source: any, length: number | null}, string | null]} - Returns a tuple containing the body and its type + * + * @see https://fetch.spec.whatwg.org/#concept-bodyinit-extract + */ function extractBody (object, keepalive = false) { // 1. Let stream be null. let stream = null + let controller = null // 2. If object is a ReadableStream object, then set stream to object. if (webidl.is.ReadableStream(object)) { @@ -57,16 +58,11 @@ function extractBody (object, keepalive = false) { // 4. Otherwise, set stream to a new ReadableStream object, and set // up stream with byte reading support. stream = new ReadableStream({ - async pull (controller) { - const buffer = typeof source === 'string' ? textEncoder.encode(source) : source - - if (buffer.byteLength) { - controller.enqueue(buffer) - } - - queueMicrotask(() => readableStreamClose(controller)) + pull () {}, + start (c) { + controller = c }, - start () {}, + cancel () {}, type: 'bytes' }) } @@ -107,22 +103,15 @@ function extractBody (object, keepalive = false) { // Set type to `application/x-www-form-urlencoded;charset=UTF-8`. type = 'application/x-www-form-urlencoded;charset=UTF-8' - } else if (isArrayBuffer(object)) { - // BufferSource/ArrayBuffer - - // Set source to a copy of the bytes held by object. - source = new Uint8Array(object.slice()) - } else if (ArrayBuffer.isView(object)) { - // BufferSource/ArrayBufferView - + } else if (webidl.is.BufferSource(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 = webidl.util.getCopyOfBytesHeldByBufferSource(object) } else if (webidl.is.FormData(object)) { const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}` const prefix = `--${boundary}\r\nContent-Disposition: form-data` /*! formdata-polyfill. MIT License. Jimmy Wärting */ - const escape = (str) => + const formdataEscape = (str) => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22') const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n') @@ -140,13 +129,13 @@ function extractBody (object, keepalive = false) { for (const [name, value] of object) { if (typeof value === 'string') { const chunk = textEncoder.encode(prefix + - `; name="${escape(normalizeLinefeeds(name))}"` + + `; name="${formdataEscape(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' + + const chunk = textEncoder.encode(`${prefix}; name="${formdataEscape(normalizeLinefeeds(name))}"` + + (value.name ? `; filename="${formdataEscape(value.name)}"` : '') + '\r\n' + `Content-Type: ${ value.type || 'application/octet-stream' }\r\n\r\n`) @@ -219,44 +208,36 @@ function extractBody (object, keepalive = false) { // 11. If source is a byte sequence, then set action to a // step that returns source and length to source’s length. - if (typeof source === 'string' || util.isBuffer(source)) { - length = Buffer.byteLength(source) + if (typeof source === 'string' || isUint8Array(source)) { + action = () => { + length = typeof source === 'string' ? Buffer.byteLength(source) : source.length + return source + } } - // 12. If action is non-null, then run these steps in in parallel: + // 12. If action is non-null, then run these steps in parallel: if (action != null) { - // Run action. - let iterator - stream = new ReadableStream({ - async start () { - iterator = action(object)[Symbol.asyncIterator]() - }, - async pull (controller) { - const { value, done } = await iterator.next() - if (done) { - // When running action is done, close stream. - queueMicrotask(() => { - controller.close() - controller.byobRequest?.respond(0) - }) - } else { - // Whenever one or more bytes are available and stream is not errored, - // enqueue a Uint8Array wrapping an ArrayBuffer containing the available - // bytes into stream. - if (!isErrored(stream)) { - const buffer = new Uint8Array(value) - if (buffer.byteLength) { - controller.enqueue(buffer) - } + ;(async () => { + // 1. Run action. + const result = action() + + // 2. Whenever one or more bytes are available and stream is not errored, + // enqueue the result of creating a Uint8Array from the available bytes into stream. + const iterator = result?.[Symbol.asyncIterator]?.() + if (iterator) { + for await (const bytes of iterator) { + if (isErrored(stream)) break + if (bytes.length) { + controller.enqueue(new Uint8Array(bytes)) } } - return controller.desiredSize > 0 - }, - async cancel (reason) { - await iterator.return() - }, - type: 'bytes' - }) + } else if (result?.length && !isErrored(stream)) { + controller.enqueue(typeof result === 'string' ? textEncoder.encode(result) : new Uint8Array(result)) + } + + // 3. When running action is done, close stream. + queueMicrotask(() => readableStreamClose(controller)) + })() } // 13. Let body be a body whose stream is stream, source is source, @@ -267,7 +248,22 @@ function extractBody (object, keepalive = false) { return [body, type] } -// https://fetch.spec.whatwg.org/#bodyinit-safely-extract +/** + * @typedef {object} ExtractBodyResult + * @property {ReadableStream>} stream - The ReadableStream containing the body data + * @property {any} source - The original source of the body data + * @property {number | null} length - The length of the body data, or null + */ + +/** + * Safely extract a body with type from a byte sequence or BodyInit object. + * + * @param {import('../../../types').BodyInit} object - The BodyInit object to extract from + * @param {boolean} [keepalive=false] - If true, indicates that the body + * @returns {[ExtractBodyResult, string | null]} - Returns a tuple containing the body and its type + * + * @see https://fetch.spec.whatwg.org/#bodyinit-safely-extract + */ function safelyExtractBody (object, keepalive = false) { // To safely extract a body and a `Content-Type` value from // a byte sequence or BodyInit object object, run these steps: @@ -275,9 +271,7 @@ function safelyExtractBody (object, keepalive = false) { // 1. If object is a ReadableStream object, then: if (webidl.is.ReadableStream(object)) { // Assert: object is neither disturbed nor locked. - // istanbul ignore next assert(!util.isDisturbed(object), 'The body has already been consumed.') - // istanbul ignore next assert(!object.locked, 'The stream is locked.') } @@ -285,17 +279,13 @@ function safelyExtractBody (object, keepalive = false) { return extractBody(object, keepalive) } -function cloneBody (instance, body) { +function cloneBody (body) { // To clone a body body, run these steps: // https://fetch.spec.whatwg.org/#concept-body-clone // 1. Let « out1, out2 » be the result of teeing body’s stream. - const [out1, out2] = body.stream.tee() - - if (hasFinalizationRegistry) { - streamRegistry.register(instance, new WeakRef(out1)) - } + const { 0: out1, 1: out2 } = body.stream.tee() // 2. Set body’s stream to out1. body.stream = out1 @@ -308,12 +298,6 @@ function cloneBody (instance, body) { } } -function throwIfAborted (state) { - if (state.aborted) { - throw new DOMException('The operation was aborted.', 'AbortError') - } -} - function bodyMixinMethods (instance, getInternalState) { const methods = { blob () { @@ -431,24 +415,26 @@ function mixinBody (prototype, getInternalState) { * @param {any} instance * @param {(target: any) => any} getInternalState */ -async function consumeBody (object, convertBytesToJSValue, instance, getInternalState) { - webidl.brandCheck(object, instance) +function consumeBody (object, convertBytesToJSValue, instance, getInternalState) { + try { + webidl.brandCheck(object, instance) + } catch (e) { + return Promise.reject(e) + } - const state = getInternalState(object) + object = getInternalState(object) // 1. If object is unusable, then return a promise rejected // with a TypeError. - if (bodyUnusable(state)) { - throw new TypeError('Body is unusable: Body has already been read') + if (bodyUnusable(object)) { + return Promise.reject(new TypeError('Body is unusable: Body has already been read')) } - throwIfAborted(state) - // 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) + const errorSteps = promise.reject // 4. Let successSteps given a byte sequence data be to resolve // promise with the result of running convertBytesToJSValue @@ -464,14 +450,14 @@ async function consumeBody (object, convertBytesToJSValue, instance, getInternal // 5. If object’s body is null, then run successSteps with an // empty byte sequence. - if (state.body == null) { + if (object.body == null) { successSteps(Buffer.allocUnsafe(0)) return promise.promise } // 6. Otherwise, fully read object’s body given successSteps, // errorSteps, and object’s relevant global object. - fullyReadBody(state.body, successSteps, errorSteps) + fullyReadBody(object.body, successSteps, errorSteps) // 7. Return promise. return promise.promise @@ -490,14 +476,6 @@ function bodyUnusable (object) { return body != null && (body.stream.locked || util.isDisturbed(body.stream)) } -/** - * @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 {any} requestOrResponse internal state @@ -527,6 +505,5 @@ module.exports = { cloneBody, mixinBody, streamRegistry, - hasFinalizationRegistry, bodyUnusable } diff --git a/lib/web/fetch/data-url.js b/lib/web/fetch/data-url.js index bc7a692a05a..27ad7ae5924 100644 --- a/lib/web/fetch/data-url.js +++ b/lib/web/fetch/data-url.js @@ -1,19 +1,20 @@ 'use strict' const assert = require('node:assert') +const { forgivingBase64, collectASequenceOfCodePoints, collectASequenceOfCodePointsFast, isomorphicDecode, removeASCIIWhitespace, removeChars } = require('../infra') 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 -const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line +const HTTP_TOKEN_CODEPOINTS = /^[-!#$%&'*+.^_|~A-Za-z0-9]+$/u +const HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/u // 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 +const HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/u // eslint-disable-line // https://fetch.spec.whatwg.org/#data-url-processor /** @param {URL} dataURL */ @@ -68,7 +69,7 @@ function dataURLProcessor (dataURL) { // 11. If mimeType ends with U+003B (;), followed by // zero or more U+0020 SPACE, followed by an ASCII // case-insensitive match for "base64", then: - if (/;(\u0020){0,}base64$/i.test(mimeType)) { + if (/;(?:\u0020*)base64$/ui.test(mimeType)) { // 1. Let stringBody be the isomorphic decode of body. const stringBody = isomorphicDecode(body) @@ -86,7 +87,7 @@ function dataURLProcessor (dataURL) { // 5. Remove trailing U+0020 SPACE code points from mimeType, // if any. - mimeType = mimeType.replace(/(\u0020)+$/, '') + mimeType = mimeType.replace(/(\u0020+)$/u, '') // 6. Remove the last U+003B (;) code point from mimeType. mimeType = mimeType.slice(0, -1) @@ -136,49 +137,6 @@ function URLSerializer (url, excludeFragment = false) { return serialized } -// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points -/** - * @param {(char: string) => boolean} condition - * @param {string} input - * @param {{ position: number }} position - */ -function collectASequenceOfCodePoints (condition, input, position) { - // 1. Let result be the empty string. - let result = '' - - // 2. While position doesn’t point past the end of input and the - // code point at position within input meets the condition condition: - while (position.position < input.length && condition(input[position.position])) { - // 1. Append that code point to the end of result. - result += input[position.position] - - // 2. Advance position by 1. - position.position++ - } - - // 3. 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 /** @param {string} input */ function stringPercentDecode (input) { @@ -219,8 +177,9 @@ function percentDecode (input) { /** @type {Uint8Array} */ const output = new Uint8Array(length) let j = 0 + let i = 0 // 2. For each byte byte in input: - for (let i = 0; i < length; ++i) { + while (i < length) { const byte = input[i] // 1. If byte is not 0x25 (%), then append byte to output. @@ -248,6 +207,7 @@ function percentDecode (input) { // 3. Skip the next two bytes in input. i += 2 } + ++i } // 3. Return output. @@ -427,45 +387,6 @@ function parseMIMEType (input) { return mimeType } -// https://infra.spec.whatwg.org/#forgiving-base64-decode -/** @param {string} data */ -function forgivingBase64 (data) { - // 1. Remove all ASCII whitespace from data. - data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, '') - - let dataLength = data.length - // 2. If data’s code point length divides by 4 leaving - // no remainder, then: - if (dataLength % 4 === 0) { - // 1. If data ends with one or two U+003D (=) code points, - // then remove them from data. - if (data.charCodeAt(dataLength - 1) === 0x003D) { - --dataLength - if (data.charCodeAt(dataLength - 1) === 0x003D) { - --dataLength - } - } - } - - // 3. If data’s code point length divides by 4 leaving - // a remainder of 1, then return failure. - if (dataLength % 4 === 1) { - return 'failure' - } - - // 4. If data contains a code point that is not one of - // U+002B (+) - // U+002F (/) - // ASCII alphanumeric - // then return failure. - if (/[^+/0-9A-Za-z]/.test(data.length === dataLength ? data : data.substring(0, dataLength))) { - return 'failure' - } - - const buffer = Buffer.from(data, 'base64') - return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) -} - // https://fetch.spec.whatwg.org/#collect-an-http-quoted-string // tests: https://fetch.spec.whatwg.org/#example-http-quoted-string /** @@ -572,7 +493,7 @@ function serializeAMimeType (mimeType) { if (!HTTP_TOKEN_CODEPOINTS.test(value)) { // 1. Precede each occurrence of U+0022 (") or // U+005C (\) in value with U+005C (\). - value = value.replace(/(\\|")/g, '\\$1') + value = value.replace(/[\\"]/ug, '\\$&') // 2. Prepend U+0022 (") to value. value = '"' + value @@ -608,71 +529,6 @@ function removeHTTPWhitespace (str, leading = true, trailing = true) { return removeChars(str, leading, trailing, isHTTPWhiteSpace) } -/** - * @see https://infra.spec.whatwg.org/#ascii-whitespace - * @param {number} char - */ -function isASCIIWhitespace (char) { - // "\r\n\t\f " - return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x00c || char === 0x020 -} - -/** - * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace - * @param {string} str - * @param {boolean} [leading=true] - * @param {boolean} [trailing=true] - */ -function removeASCIIWhitespace (str, leading = true, trailing = true) { - return removeChars(str, leading, trailing, isASCIIWhitespace) -} - -/** - * @param {string} str - * @param {boolean} leading - * @param {boolean} trailing - * @param {(charCode: number) => boolean} predicate - * @returns - */ -function removeChars (str, leading, trailing, predicate) { - let lead = 0 - let trail = str.length - 1 - - if (leading) { - while (lead < str.length && predicate(str.charCodeAt(lead))) lead++ - } - - if (trailing) { - while (trail > 0 && predicate(str.charCodeAt(trail))) trail-- - } - - return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1) -} - -/** - * @see https://infra.spec.whatwg.org/#isomorphic-decode - * @param {Uint8Array} input - * @returns {string} - */ -function isomorphicDecode (input) { - // 1. To isomorphic decode a byte sequence input, return a string whose code point - // length is equal to input’s length and whose code points have the same values - // as the values of input’s bytes, in the same order. - const length = input.length - if ((2 << 15) - 1 > length) { - return String.fromCharCode.apply(null, input) - } - let result = ''; let i = 0 - let addition = (2 << 15) - 1 - while (i < length) { - if (i + addition > length) { - addition = length - i - } - result += String.fromCharCode.apply(null, input.subarray(i, i += addition)) - } - return result -} - /** * @see https://mimesniff.spec.whatwg.org/#minimize-a-supported-mime-type * @param {Exclude, 'failure'>} mimeType @@ -730,15 +586,11 @@ function minimizeSupportedMimeType (mimeType) { module.exports = { dataURLProcessor, URLSerializer, - collectASequenceOfCodePoints, - collectASequenceOfCodePointsFast, stringPercentDecode, parseMIMEType, collectAnHTTPQuotedString, serializeAMimeType, - removeChars, removeHTTPWhitespace, minimizeSupportedMimeType, - HTTP_TOKEN_CODEPOINTS, - isomorphicDecode + HTTP_TOKEN_CODEPOINTS } diff --git a/lib/web/fetch/dispatcher-weakref.js b/lib/web/fetch/dispatcher-weakref.js deleted file mode 100644 index 59c2c31e005..00000000000 --- a/lib/web/fetch/dispatcher-weakref.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -module.exports = function () { - return { WeakRef, FinalizationRegistry } -} diff --git a/lib/web/fetch/formdata-parser.js b/lib/web/fetch/formdata-parser.js index 0691622e605..a4622ae10a3 100644 --- a/lib/web/fetch/formdata-parser.js +++ b/lib/web/fetch/formdata-parser.js @@ -1,19 +1,15 @@ 'use strict' const { bufferToLowerCasedHeaderName } = require('../../core/util') -const { utf8DecodeBytes } = require('./util') -const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url') +const { HTTP_TOKEN_CODEPOINTS } = require('./data-url') const { makeEntry } = require('./formdata') const { webidl } = require('../webidl') const assert = require('node:assert') -const { File: NodeFile } = require('node:buffer') +const { isomorphicDecode } = require('../infra') -const File = globalThis.File ?? NodeFile - -const formDataNameBuffer = Buffer.from('form-data; name="') -const filenameBuffer = Buffer.from('filename') const dd = Buffer.from('--') -const ddcrlf = Buffer.from('--\r\n') +const decoder = new TextDecoder() +const decoderIgnoreBOM = new TextDecoder('utf-8', { ignoreBOM: true }) /** * @param {string} chars @@ -87,20 +83,16 @@ function multipartFormDataParser (input, mimeType) { // the first byte. const position = { position: 0 } - // Note: undici addition, allows leading and trailing CRLFs. - while (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) { - position.position += 2 - } - - let trailing = input.length + // Note: Per RFC 2046 Section 5.1.1, we must ignore anything before the + // first boundary delimiter line (preamble). Search for the first boundary. + const firstBoundaryIndex = input.indexOf(boundary) - while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) { - trailing -= 2 + if (firstBoundaryIndex === -1) { + throw parsingError('no boundary found in multipart body') } - if (trailing !== input.length) { - input = input.subarray(0, trailing) - } + // Start parsing from the first boundary, ignoring any preamble + position.position = firstBoundaryIndex // 5. While true: while (true) { @@ -116,11 +108,11 @@ function multipartFormDataParser (input, mimeType) { // 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A // (`--` followed by CR LF) followed by the end of input, return entry list. - // Note: a body does NOT need to end with CRLF. It can end with --. - if ( - (position.position === input.length - 2 && bufferStartsWith(input, dd, position)) || - (position.position === input.length - 4 && bufferStartsWith(input, ddcrlf, position)) - ) { + // Note: Per RFC 2046 Section 5.1.1, we must ignore anything after the + // final boundary delimiter (epilogue). Check for -- or --CRLF and return + // regardless of what follows. + if (bufferStartsWith(input, dd, position)) { + // Found closing boundary delimiter (--), ignore any epilogue return entryList } @@ -196,7 +188,7 @@ function multipartFormDataParser (input, mimeType) { // 5.11. Otherwise: // 5.11.1. Let value be the UTF-8 decoding without BOM of body. - value = utf8DecodeBytes(Buffer.from(body)) + value = decoderIgnoreBOM.decode(Buffer.from(body)) } // 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object. @@ -208,6 +200,113 @@ function multipartFormDataParser (input, mimeType) { } } +/** + * Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded) + * @param {Buffer} input + * @param {{ position: number }} position + * @returns {{ name: string, value: string, extended: boolean } | null} + */ +function parseContentDispositionAttribute (input, position) { + // Skip leading semicolon and whitespace + if (input[position.position] === 0x3b /* ; */) { + position.position++ + } + + // Skip whitespace + collectASequenceOfBytes( + (char) => char === 0x20 || char === 0x09, + input, + position + ) + + // Collect attribute name (token characters) + const attributeName = collectASequenceOfBytes( + (char) => isToken(char) && char !== 0x3d && char !== 0x2a, // not = or * + input, + position + ) + + if (attributeName.length === 0) { + return null + } + + const attrNameStr = attributeName.toString('ascii').toLowerCase() + + // Check for extended notation (attribute*) + const isExtended = input[position.position] === 0x2a /* * */ + if (isExtended) { + position.position++ // skip * + } + + // Expect = sign + if (input[position.position] !== 0x3d /* = */) { + return null + } + position.position++ // skip = + + // Skip whitespace + collectASequenceOfBytes( + (char) => char === 0x20 || char === 0x09, + input, + position + ) + + let value + + if (isExtended) { + // Extended attribute format: charset'language'encoded-value + const headerValue = collectASequenceOfBytes( + (char) => char !== 0x20 && char !== 0x0d && char !== 0x0a && char !== 0x3b, // not space, CRLF, or ; + input, + position + ) + + // Check for utf-8'' prefix (case insensitive) + if ( + (headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U + (headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T + (headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F + headerValue[3] !== 0x2d || // - + headerValue[4] !== 0x38 // 8 + ) { + throw parsingError('unknown encoding, expected utf-8\'\'') + } + + // Skip utf-8'' and decode the rest + value = decodeURIComponent(decoder.decode(headerValue.subarray(7))) + } else if (input[position.position] === 0x22 /* " */) { + // Quoted string + position.position++ // skip opening quote + + const quotedValue = collectASequenceOfBytes( + (char) => char !== 0x0a && char !== 0x0d && char !== 0x22, // not LF, CR, or " + input, + position + ) + + if (input[position.position] !== 0x22) { + throw parsingError('Closing quote not found') + } + position.position++ // skip closing quote + + value = decoder.decode(quotedValue) + .replace(/%0A/ig, '\n') + .replace(/%0D/ig, '\r') + .replace(/%22/g, '"') + } else { + // Token value (no quotes) + const tokenValue = collectASequenceOfBytes( + (char) => isToken(char) && char !== 0x3b, // not ; + input, + position + ) + + value = decoder.decode(tokenValue) + } + + return { name: attrNameStr, value, extended: isExtended } +} + /** * @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers * @param {Buffer} input @@ -268,82 +367,53 @@ function parseMultipartFormDataHeaders (input, position) { // 2.8. Byte-lowercase header name and switch on the result: switch (bufferToLowerCasedHeaderName(headerName)) { case 'content-disposition': { - // 1. Set name and filename to null. name = filename = null + // Track whether filename was set from the extended (RFC 5987) form so + // a subsequent legacy `filename` attribute does not override it. + let filenameIsExtended = false + + // Collect the disposition type (should be "form-data") + const dispositionType = collectASequenceOfBytes( + (char) => isToken(char), + input, + position + ) - // 2. If position does not point to a sequence of bytes starting with - // `form-data; name="`, return failure. - if (!bufferStartsWith(input, formDataNameBuffer, position)) { - throw parsingError('expected form-data; name=" for content-disposition header') + if (dispositionType.toString('ascii').toLowerCase() !== 'form-data') { + throw parsingError('expected form-data for content-disposition header') } - // 3. Advance position so it points at the byte after the next 0x22 (") - // byte (the one in the sequence of bytes matched above). - position.position += 17 - - // 4. Set name to the result of parsing a multipart/form-data name given - // input and position, if the result is not failure. Otherwise, return - // failure. - name = parseMultipartFormDataName(input, position) - - // 5. If position points to a sequence of bytes starting with `; filename="`: - if (input[position.position] === 0x3b /* ; */ && input[position.position + 1] === 0x20 /* ' ' */) { - const at = { position: position.position + 2 } - - if (bufferStartsWith(input, filenameBuffer, at)) { - if (input[at.position + 8] === 0x2a /* '*' */) { - at.position += 10 // skip past filename*= - - // Remove leading http tab and spaces. See RFC for examples. - // https://datatracker.ietf.org/doc/html/rfc6266#section-5 - collectASequenceOfBytes( - (char) => char === 0x20 || char === 0x09, - input, - at - ) - - const headerValue = collectASequenceOfBytes( - (char) => char !== 0x20 && char !== 0x0d && char !== 0x0a, // ' ' or CRLF - input, - at - ) - - if ( - (headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U - (headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T - (headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F - headerValue[3] !== 0x2d || // - - headerValue[4] !== 0x38 // 8 - ) { - throw parsingError('unknown encoding, expected utf-8\'\'') - } - - // skip utf-8'' - filename = decodeURIComponent(new TextDecoder().decode(headerValue.subarray(7))) - - position.position = at.position - } else { - // 1. Advance position so it points at the byte after the next 0x22 (") byte - // (the one in the sequence of bytes matched above). - position.position += 11 - - // Remove leading http tab and spaces. See RFC for examples. - // https://datatracker.ietf.org/doc/html/rfc6266#section-5 - collectASequenceOfBytes( - (char) => char === 0x20 || char === 0x09, - input, - position - ) - - position.position++ // skip past " after removing whitespace - - // 2. Set filename to the result of parsing a multipart/form-data name given - // input and position, if the result is not failure. Otherwise, return failure. - filename = parseMultipartFormDataName(input, position) + // Parse attributes recursively until CRLF + while ( + position.position < input.length && + (input[position.position] !== 0x0d || + input[position.position + 1] !== 0x0a) + ) { + const attribute = parseContentDispositionAttribute(input, position) + + if (!attribute) { + break + } + + if (attribute.name === 'name') { + name = attribute.value + } else if (attribute.name === 'filename') { + // Per RFC 5987 §4.1, when both legacy and extended forms of the + // same parameter are present, the extended (filename*) form takes + // precedence regardless of the order they appear in. + if (attribute.extended) { + filename = attribute.value + filenameIsExtended = true + } else if (!filenameIsExtended) { + filename = attribute.value } } } + if (name === null) { + throw parsingError('name attribute is required in content-disposition header') + } + break } case 'content-type': { @@ -389,7 +459,7 @@ function parseMultipartFormDataHeaders (input, position) { // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A // (CR LF), return failure. Otherwise, advance position by 2 (past the newline). - if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) { + if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) { throw parsingError('expected CRLF') } else { position.position += 2 @@ -397,43 +467,6 @@ function parseMultipartFormDataHeaders (input, position) { } } -/** - * @see https://andreubotella.github.io/multipart-form-data/#parse-a-multipart-form-data-name - * @param {Buffer} input - * @param {{ position: number }} position - */ -function parseMultipartFormDataName (input, position) { - // 1. Assert: The byte at (position - 1) is 0x22 ("). - assert(input[position.position - 1] === 0x22) - - // 2. Let name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x22 ("), given position. - /** @type {string | Buffer} */ - let name = collectASequenceOfBytes( - (char) => char !== 0x0a && char !== 0x0d && char !== 0x22, - input, - position - ) - - // 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1. - if (input[position.position] !== 0x22) { - throw parsingError('expected "') - } else { - position.position++ - } - - // 4. Replace any occurrence of the following subsequences in name with the given byte: - // - `%0A`: 0x0A (LF) - // - `%0D`: 0x0D (CR) - // - `%22`: 0x22 (") - name = new TextDecoder().decode(name) - .replace(/%0A/ig, '\n') - .replace(/%0D/ig, '\r') - .replace(/%22/g, '"') - - // 5. Return the UTF-8 decoding without BOM of name. - return name -} - /** * @param {(char: number) => boolean} condition * @param {Buffer} input @@ -495,6 +528,58 @@ function parsingError (cause) { return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) }) } +/** + * CTL = + * @param {number} char + */ +function isCTL (char) { + return char <= 0x1f || char === 0x7f +} + +/** + * tspecials := "(" / ")" / "<" / ">" / "@" / + * "," / ";" / ":" / "\" / <"> + * "/" / "[" / "]" / "?" / "=" + * ; Must be in quoted-string, + * ; to use within parameter values + * @param {number} char + */ +function isTSpecial (char) { + return ( + char === 0x28 || // ( + char === 0x29 || // ) + char === 0x3c || // < + char === 0x3e || // > + char === 0x40 || // @ + char === 0x2c || // , + char === 0x3b || // ; + char === 0x3a || // : + char === 0x5c || // \ + char === 0x22 || // " + char === 0x2f || // / + char === 0x5b || // [ + char === 0x5d || // ] + char === 0x3f || // ? + char === 0x3d // + + ) +} + +/** + * token := 1* + * @param {number} char + */ +function isToken (char) { + return ( + char <= 0x7f && // ascii + char !== 0x20 && // space + char !== 0x09 && + !isCTL(char) && + !isTSpecial(char) + ) +} + module.exports = { multipartFormDataParser, validateBoundary diff --git a/lib/web/fetch/formdata.js b/lib/web/fetch/formdata.js index c34b7772997..c21fb06a3ee 100644 --- a/lib/web/fetch/formdata.js +++ b/lib/web/fetch/formdata.js @@ -3,17 +3,13 @@ const { iteratorMixin } = require('./util') const { kEnumerableProperty } = require('../../core/util') const { webidl } = require('../webidl') -const { File: NativeFile } = require('node:buffer') const nodeUtil = require('node:util') -/** @type {globalThis['File']} */ -const File = globalThis.File ?? NativeFile - // https://xhr.spec.whatwg.org/#formdata class FormData { #state = [] - constructor (form) { + constructor (form = undefined) { webidl.util.markAsUncloneable(this) if (form !== undefined) { diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index 96e9afeb2ab..8aa86ff60ee 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -14,7 +14,6 @@ const { HeadersList } = require('./headers') const { Request, cloneRequest, getRequestDispatcher, getRequestState } = require('./request') const zlib = require('node:zlib') const { - bytesMatch, makePolicyContainer, clonePolicyContainer, requestBadPort, @@ -30,14 +29,12 @@ const { crossOriginResourcePolicyCheck, determineRequestsReferrer, coarsenedSharedCurrentTime, - createDeferredPromise, sameOrigin, isCancelled, isAborted, isErrorLike, fullyReadBody, readableStreamClose, - isomorphicEncode, urlIsLocal, urlIsHttpHttpsScheme, urlHasHttpsScheme, @@ -45,7 +42,10 @@ const { simpleRangeHeaderValue, buildContentRange, createInflate, - extractMimeType + extractMimeType, + hasAuthenticationEntry, + includesCredentials, + isTraversableNavigable } = require('./util') const assert = require('node:assert') const { safelyExtractBody, extractBody } = require('./body') @@ -63,6 +63,14 @@ const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = requ const { getGlobalDispatcher } = require('../../global') const { webidl } = require('../webidl') const { STATUS_CODES } = require('node:http') +const { bytesMatch } = require('../subresource-integrity/subresource-integrity') +const { createDeferredPromise } = require('../../util/promise') +const { isomorphicEncode } = require('../infra') +const { runtimeFeatures } = require('../../util/runtime-features') + +// Node.js v23.8.0+ and v22.15.0+ supports Zstandard +const hasZstd = runtimeFeatures.has('zstd') + const GET_OR_HEAD = ['GET', 'HEAD'] const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined' @@ -149,7 +157,7 @@ function fetch (input, init = undefined) { if (requestObject.signal.aborted) { // 1. Abort the fetch() call with p, request, null, and // requestObject’s signal’s abort reason. - abortFetch(p, request, null, requestObject.signal.reason) + abortFetch(p, request, null, requestObject.signal.reason, null) // 2. Return p. return p.promise @@ -192,7 +200,7 @@ function fetch (input, init = undefined) { // 4. Abort the fetch() call with p, request, responseObject, // and requestObject’s signal’s abort reason. - abortFetch(p, request, realResponse, requestObject.signal.reason) + abortFetch(p, request, realResponse, requestObject.signal.reason, controller.controller) } ) @@ -219,7 +227,7 @@ function fetch (input, init = undefined) { // 2. Abort the fetch() call with p, request, responseObject, and // deserializedError. - abortFetch(p, request, responseObject, controller.serializedAbortReason) + abortFetch(p, request, responseObject, controller.serializedAbortReason, controller.controller) return } @@ -243,7 +251,10 @@ function fetch (input, init = undefined) { request, processResponseEndOfBody: handleFetchDone, processResponse, - dispatcher: getRequestDispatcher(requestObject) // undici + dispatcher: getRequestDispatcher(requestObject), // undici + // Keep requestObject alive to prevent its AbortController from being GC'd + // See https://github.com/nodejs/undici/issues/4627 + requestObject }) // 14. Return p. @@ -319,7 +330,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') { const markResourceTiming = performance.markResourceTiming // https://fetch.spec.whatwg.org/#abort-fetch -function abortFetch (p, request, responseObject, error) { +function abortFetch (p, request, responseObject, error, controller /* undici-specific */) { // 1. Reject promise with error. if (p) { // We might have already resolved the promise at this stage @@ -349,13 +360,7 @@ function abortFetch (p, request, responseObject, error) { // 5. If response’s body is not null and is readable, then error response’s // body with error. if (response.body?.stream != null && isReadable(response.body.stream)) { - response.body.stream.cancel(error).catch((err) => { - if (err.code === 'ERR_INVALID_STATE') { - // Node bug? - return - } - throw err - }) + controller.error(error) } } @@ -368,7 +373,8 @@ function fetching ({ processResponseEndOfBody, processResponseConsumeBody, useParallelQueue = false, - dispatcher = getGlobalDispatcher() // undici + dispatcher = getGlobalDispatcher(), // undici + requestObject = null // Keep alive to prevent AbortController GC, see #4627 }) { // Ensure that the dispatcher is set accordingly assert(dispatcher) @@ -422,7 +428,9 @@ function fetching ({ processResponseConsumeBody, processResponseEndOfBody, taskDestination, - crossOriginIsolatedCapability + crossOriginIsolatedCapability, + // Keep requestObject alive to prevent its AbortController from being GC'd + requestObject } // 7. If request’s body is a byte sequence, then set request’s body to @@ -507,257 +515,258 @@ function fetching ({ } // 16. Run main fetch given fetchParams. - mainFetch(fetchParams) - .catch(err => { - fetchParams.controller.terminate(err) - }) + mainFetch(fetchParams, false) // 17. Return fetchParam's controller return fetchParams.controller } // https://fetch.spec.whatwg.org/#concept-main-fetch -async function mainFetch (fetchParams, recursive = false) { - // 1. Let request be fetchParams’s request. - const request = fetchParams.request +async function mainFetch (fetchParams, recursive) { + try { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request - // 2. Let response be null. - let response = null + // 2. Let response be null. + let response = null - // 3. If request’s local-URLs-only flag is set and request’s current URL is - // not local, then set response to a network error. - if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) { - response = makeNetworkError('local URLs only') - } - - // 4. Run report Content Security Policy violations for request. - // TODO + // 3. If request’s local-URLs-only flag is set and request’s current URL is + // not local, then set response to a network error. + if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) { + response = makeNetworkError('local URLs only') + } - // 5. Upgrade request to a potentially trustworthy URL, if appropriate. - tryUpgradeRequestToAPotentiallyTrustworthyURL(request) + // 4. Run report Content Security Policy violations for request. + // TODO - // 6. If should request be blocked due to a bad port, should fetching request - // be blocked as mixed content, or should request be blocked by Content - // Security Policy returns blocked, then set response to a network error. - if (requestBadPort(request) === 'blocked') { - response = makeNetworkError('bad port') - } - // TODO: should fetching request be blocked as mixed content? - // TODO: should request be blocked by Content Security Policy? + // 5. Upgrade request to a potentially trustworthy URL, if appropriate. + tryUpgradeRequestToAPotentiallyTrustworthyURL(request) - // 7. If request’s referrer policy is the empty string, then set request’s - // referrer policy to request’s policy container’s referrer policy. - if (request.referrerPolicy === '') { - request.referrerPolicy = request.policyContainer.referrerPolicy - } + // 6. If should request be blocked due to a bad port, should fetching request + // be blocked as mixed content, or should request be blocked by Content + // Security Policy returns blocked, then set response to a network error. + if (requestBadPort(request) === 'blocked') { + response = makeNetworkError('bad port') + } + // TODO: should fetching request be blocked as mixed content? + // TODO: should request be blocked by Content Security Policy? - // 8. If request’s referrer is not "no-referrer", then set request’s - // referrer to the result of invoking determine request’s referrer. - if (request.referrer !== 'no-referrer') { - request.referrer = determineRequestsReferrer(request) - } + // 7. If request’s referrer policy is the empty string, then set request’s + // referrer policy to request’s policy container’s referrer policy. + if (request.referrerPolicy === '') { + request.referrerPolicy = request.policyContainer.referrerPolicy + } - // 9. Set request’s current URL’s scheme to "https" if all of the following - // conditions are true: - // - request’s current URL’s scheme is "http" - // - request’s current URL’s host is a domain - // - Matching request’s current URL’s host per Known HSTS Host Domain Name - // Matching results in either a superdomain match with an asserted - // includeSubDomains directive or a congruent match (with or without an - // asserted includeSubDomains directive). [HSTS] - // TODO + // 8. If request’s referrer is not "no-referrer", then set request’s + // referrer to the result of invoking determine request’s referrer. + if (request.referrer !== 'no-referrer') { + request.referrer = determineRequestsReferrer(request) + } - // 10. If recursive is false, then run the remaining steps in parallel. - // TODO + // 9. Set request’s current URL’s scheme to "https" if all of the following + // conditions are true: + // - request’s current URL’s scheme is "http" + // - request’s current URL’s host is a domain + // - Matching request’s current URL’s host per Known HSTS Host Domain Name + // Matching results in either a superdomain match with an asserted + // includeSubDomains directive or a congruent match (with or without an + // asserted includeSubDomains directive). [HSTS] + // TODO - // 11. If response is null, then set response to the result of running - // the steps corresponding to the first matching statement: - if (response === null) { - const currentURL = requestCurrentURL(request) - if ( - // - request’s current URL’s origin is same origin with request’s origin, - // and request’s response tainting is "basic" - (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') || - // request’s current URL’s scheme is "data" - (currentURL.protocol === 'data:') || - // - request’s mode is "navigate" or "websocket" - (request.mode === 'navigate' || request.mode === 'websocket') - ) { - // 1. Set request’s response tainting to "basic". - request.responseTainting = 'basic' - - // 2. Return the result of running scheme fetch given fetchParams. - response = await schemeFetch(fetchParams) - - // request’s mode is "same-origin" - } else if (request.mode === 'same-origin') { - // 1. Return a network error. - response = makeNetworkError('request mode cannot be "same-origin"') - - // request’s mode is "no-cors" - } else if (request.mode === 'no-cors') { - // 1. If request’s redirect mode is not "follow", then return a network - // error. - if (request.redirect !== 'follow') { - response = makeNetworkError( - 'redirect mode cannot be "follow" for "no-cors" request' - ) - } else { - // 2. Set request’s response tainting to "opaque". - request.responseTainting = 'opaque' + // 10. If recursive is false, then run the remaining steps in parallel. + // TODO - // 3. Return the result of running scheme fetch given fetchParams. + // 11. If response is null, then set response to the result of running + // the steps corresponding to the first matching statement: + if (response === null) { + const currentURL = requestCurrentURL(request) + if ( + // - request’s current URL’s origin is same origin with request’s origin, + // and request’s response tainting is "basic" + (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') || + // request’s current URL’s scheme is "data" + (currentURL.protocol === 'data:') || + // - request’s mode is "navigate" or "websocket" + (request.mode === 'navigate' || request.mode === 'websocket') + ) { + // 1. Set request’s response tainting to "basic". + request.responseTainting = 'basic' + + // 2. Return the result of running scheme fetch given fetchParams. response = await schemeFetch(fetchParams) - } - // request’s current URL’s scheme is not an HTTP(S) scheme - } else if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) { - // Return a network error. - response = makeNetworkError('URL scheme must be a HTTP(S) scheme') - - // - request’s use-CORS-preflight flag is set - // - request’s unsafe-request flag is set and either request’s method is - // not a CORS-safelisted method or CORS-unsafe request-header names with - // request’s header list is not empty - // 1. Set request’s response tainting to "cors". - // 2. Let corsWithPreflightResponse be the result of running HTTP fetch - // given fetchParams and true. - // 3. If corsWithPreflightResponse is a network error, then clear cache - // entries using request. - // 4. Return corsWithPreflightResponse. - // TODO - - // Otherwise - } else { - // 1. Set request’s response tainting to "cors". - request.responseTainting = 'cors' - // 2. Return the result of running HTTP fetch given fetchParams. - response = await httpFetch(fetchParams) - } - } + // request’s mode is "same-origin" + } else if (request.mode === 'same-origin') { + // 1. Return a network error. + response = makeNetworkError('request mode cannot be "same-origin"') + + // request’s mode is "no-cors" + } else if (request.mode === 'no-cors') { + // 1. If request’s redirect mode is not "follow", then return a network + // error. + if (request.redirect !== 'follow') { + response = makeNetworkError( + 'redirect mode cannot be "follow" for "no-cors" request' + ) + } else { + // 2. Set request’s response tainting to "opaque". + request.responseTainting = 'opaque' - // 12. If recursive is true, then return response. - if (recursive) { - return response - } + // 3. Return the result of running scheme fetch given fetchParams. + response = await schemeFetch(fetchParams) + } + // request’s current URL’s scheme is not an HTTP(S) scheme + } else if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) { + // Return a network error. + response = makeNetworkError('URL scheme must be a HTTP(S) scheme') + + // - request’s use-CORS-preflight flag is set + // - request’s unsafe-request flag is set and either request’s method is + // not a CORS-safelisted method or CORS-unsafe request-header names with + // request’s header list is not empty + // 1. Set request’s response tainting to "cors". + // 2. Let corsWithPreflightResponse be the result of running HTTP fetch + // given fetchParams and true. + // 3. If corsWithPreflightResponse is a network error, then clear cache + // entries using request. + // 4. Return corsWithPreflightResponse. + // TODO + + // Otherwise + } else { + // 1. Set request’s response tainting to "cors". + request.responseTainting = 'cors' - // 13. If response is not a network error and response is not a filtered - // response, then: - if (response.status !== 0 && !response.internalResponse) { - // If request’s response tainting is "cors", then: - if (request.responseTainting === 'cors') { - // 1. Let headerNames be the result of extracting header list values - // given `Access-Control-Expose-Headers` and response’s header list. - // TODO - // 2. If request’s credentials mode is not "include" and headerNames - // contains `*`, then set response’s CORS-exposed header-name list to - // all unique header names in response’s header list. - // TODO - // 3. Otherwise, if headerNames is not null or failure, then set - // response’s CORS-exposed header-name list to headerNames. - // TODO + // 2. Return the result of running HTTP fetch given fetchParams. + response = await httpFetch(fetchParams) + } } - // Set response to the following filtered response with response as its - // internal response, depending on request’s response tainting: - if (request.responseTainting === 'basic') { - response = filterResponse(response, 'basic') - } else if (request.responseTainting === 'cors') { - response = filterResponse(response, 'cors') - } else if (request.responseTainting === 'opaque') { - response = filterResponse(response, 'opaque') - } else { - assert(false) + // 12. If recursive is true, then return response. + if (recursive) { + return response } - } - // 14. Let internalResponse be response, if response is a network error, - // and response’s internal response otherwise. - let internalResponse = - response.status === 0 ? response : response.internalResponse + // 13. If response is not a network error and response is not a filtered + // response, then: + if (response.status !== 0 && !response.internalResponse) { + // If request’s response tainting is "cors", then: + if (request.responseTainting === 'cors') { + // 1. Let headerNames be the result of extracting header list values + // given `Access-Control-Expose-Headers` and response’s header list. + // TODO + // 2. If request’s credentials mode is not "include" and headerNames + // contains `*`, then set response’s CORS-exposed header-name list to + // all unique header names in response’s header list. + // TODO + // 3. Otherwise, if headerNames is not null or failure, then set + // response’s CORS-exposed header-name list to headerNames. + // TODO + } - // 15. If internalResponse’s URL list is empty, then set it to a clone of - // request’s URL list. - if (internalResponse.urlList.length === 0) { - internalResponse.urlList.push(...request.urlList) - } + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (request.responseTainting === 'basic') { + response = filterResponse(response, 'basic') + } else if (request.responseTainting === 'cors') { + response = filterResponse(response, 'cors') + } else if (request.responseTainting === 'opaque') { + response = filterResponse(response, 'opaque') + } else { + assert(false) + } + } - // 16. If request’s timing allow failed flag is unset, then set - // internalResponse’s timing allow passed flag. - if (!request.timingAllowFailed) { - response.timingAllowPassed = true - } + // 14. Let internalResponse be response, if response is a network error, + // and response’s internal response otherwise. + let internalResponse = + response.status === 0 ? response : response.internalResponse - // 17. If response is not a network error and any of the following returns - // blocked - // - should internalResponse to request be blocked as mixed content - // - should internalResponse to request be blocked by Content Security Policy - // - should internalResponse to request be blocked due to its MIME type - // - should internalResponse to request be blocked due to nosniff - // TODO + // 15. If internalResponse’s URL list is empty, then set it to a clone of + // request’s URL list. + if (internalResponse.urlList.length === 0) { + internalResponse.urlList.push(...request.urlList) + } - // 18. If response’s type is "opaque", internalResponse’s status is 206, - // internalResponse’s range-requested flag is set, and request’s header - // list does not contain `Range`, then set response and internalResponse - // to a network error. - if ( - response.type === 'opaque' && - internalResponse.status === 206 && - internalResponse.rangeRequested && - !request.headers.contains('range', true) - ) { - response = internalResponse = makeNetworkError() - } + // 16. If request’s timing allow failed flag is unset, then set + // internalResponse’s timing allow passed flag. + if (!request.timingAllowFailed) { + response.timingAllowPassed = true + } - // 19. If response is not a network error and either request’s method is - // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status, - // set internalResponse’s body to null and disregard any enqueuing toward - // it (if any). - if ( - response.status !== 0 && - (request.method === 'HEAD' || - request.method === 'CONNECT' || - nullBodyStatus.includes(internalResponse.status)) - ) { - internalResponse.body = null - fetchParams.controller.dump = true - } + // 17. If response is not a network error and any of the following returns + // blocked + // - should internalResponse to request be blocked as mixed content + // - should internalResponse to request be blocked by Content Security Policy + // - should internalResponse to request be blocked due to its MIME type + // - should internalResponse to request be blocked due to nosniff + // TODO - // 20. If request’s integrity metadata is not the empty string, then: - if (request.integrity) { - // 1. Let processBodyError be this step: run fetch finale given fetchParams - // and a network error. - const processBodyError = (reason) => - fetchFinale(fetchParams, makeNetworkError(reason)) + // 18. If response’s type is "opaque", internalResponse’s status is 206, + // internalResponse’s range-requested flag is set, and request’s header + // list does not contain `Range`, then set response and internalResponse + // to a network error. + if ( + response.type === 'opaque' && + internalResponse.status === 206 && + internalResponse.rangeRequested && + !request.headers.contains('range', true) + ) { + response = internalResponse = makeNetworkError() + } - // 2. If request’s response tainting is "opaque", or response’s body is null, - // then run processBodyError and abort these steps. - if (request.responseTainting === 'opaque' || response.body == null) { - processBodyError(response.error) - return + // 19. If response is not a network error and either request’s method is + // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status, + // set internalResponse’s body to null and disregard any enqueuing toward + // it (if any). + if ( + response.status !== 0 && + (request.method === 'HEAD' || + request.method === 'CONNECT' || + nullBodyStatus.includes(internalResponse.status)) + ) { + internalResponse.body = null + fetchParams.controller.dump = true } - // 3. Let processBody given bytes be these steps: - const processBody = (bytes) => { - // 1. If bytes do not match request’s integrity metadata, - // then run processBodyError and abort these steps. [SRI] - if (!bytesMatch(bytes, request.integrity)) { - processBodyError('integrity mismatch') + // 20. If request’s integrity metadata is not the empty string, then: + if (request.integrity) { + // 1. Let processBodyError be this step: run fetch finale given fetchParams + // and a network error. + const processBodyError = (reason) => + fetchFinale(fetchParams, makeNetworkError(reason)) + + // 2. If request’s response tainting is "opaque", or response’s body is null, + // then run processBodyError and abort these steps. + if (request.responseTainting === 'opaque' || response.body == null) { + processBodyError(response.error) return } - // 2. Set response’s body to bytes as a body. - response.body = safelyExtractBody(bytes)[0] + // 3. Let processBody given bytes be these steps: + const processBody = (bytes) => { + // 1. If bytes do not match request’s integrity metadata, + // then run processBodyError and abort these steps. [SRI] + if (!bytesMatch(bytes, request.integrity)) { + processBodyError('integrity mismatch') + return + } + + // 2. Set response’s body to bytes as a body. + response.body = safelyExtractBody(bytes)[0] - // 3. Run fetch finale given fetchParams and response. + // 3. Run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } + + // 4. Fully read response’s body given processBody and processBodyError. + fullyReadBody(response.body, processBody, processBodyError) + } else { + // 21. Otherwise, run fetch finale given fetchParams and response. fetchFinale(fetchParams, response) } - - // 4. Fully read response’s body given processBody and processBodyError. - await fullyReadBody(response.body, processBody, processBodyError) - } else { - // 21. Otherwise, run fetch finale given fetchParams and response. - fetchFinale(fetchParams, response) + } catch (err) { + fetchParams.controller.terminate(err) } } @@ -883,7 +892,7 @@ function schemeFetch (fetchParams) { // 8. Let slicedBlob be the result of invoking slice blob given blob, rangeStart, // rangeEnd + 1, and type. - const slicedBlob = blob.slice(rangeStart, rangeEnd, type) + const slicedBlob = blob.slice(rangeStart, rangeEnd + 1, type) // 9. Let slicedBodyWithType be the result of safely extracting slicedBlob. // Note: same reason as mentioned above as to why we use extractBody @@ -1021,7 +1030,7 @@ function fetchFinale (fetchParams, response) { let responseStatus = 0 // 7. If fetchParams’s request’s mode is not "navigate" or response’s has-cross-origin-redirects is false: - if (fetchParams.request.mode !== 'navigator' || !response.hasCrossOriginRedirects) { + if (fetchParams.request.mode !== 'navigate' || !response.hasCrossOriginRedirects) { // 1. Set responseStatus to response’s status. responseStatus = response.status @@ -1312,8 +1321,8 @@ function httpRedirectFetch (fetchParams, response) { request.headersList.delete('host', true) } - // 14. If request’s body is non-null, then set request’s body to the first return - // value of safely extracting request’s body’s source. + // 14. If request's body is non-null, then set request's body to the first return + // value of safely extracting request's body's source. if (request.body != null) { assert(request.body.source != null) request.body = safelyExtractBody(request.body.source)[0] @@ -1424,7 +1433,10 @@ async function httpNetworkOrCacheFetch ( // 8. If contentLengthHeaderValue is non-null, then append // `Content-Length`/contentLengthHeaderValue to httpRequest’s header // list. - if (contentLengthHeaderValue != null) { + if ( + contentLengthHeaderValue != null && + !httpRequest.headersList.contains('content-length', true) + ) { httpRequest.headersList.append('content-length', contentLengthHeaderValue, true) } @@ -1518,13 +1530,39 @@ async function httpNetworkOrCacheFetch ( httpRequest.headersList.delete('host', true) - // 20. If includeCredentials is true, then: + // 21. If includeCredentials is true, then: if (includeCredentials) { // 1. If the user agent is not configured to block cookies for httpRequest // (see section 7 of [COOKIES]), then: // TODO: credentials + // 2. If httpRequest’s header list does not contain `Authorization`, then: - // TODO: credentials + if (!httpRequest.headersList.contains('authorization', true)) { + // 1. Let authorizationValue be null. + let authorizationValue = null + + // 2. If there’s an authentication entry for httpRequest and either + // httpRequest’s use-URL-credentials flag is unset or httpRequest’s + // current URL does not include credentials, then set + // authorizationValue to authentication entry. + if (hasAuthenticationEntry(httpRequest) && ( + httpRequest.useURLCredentials === undefined || !includesCredentials(requestCurrentURL(httpRequest)) + )) { + // TODO + } else if (includesCredentials(requestCurrentURL(httpRequest)) && isAuthenticationFetch) { + // 3. Otherwise, if httpRequest’s current URL does include credentials + // and isAuthenticationFetch is true, set authorizationValue to + // httpRequest’s current URL, converted to an `Authorization` value + const { username, password } = requestCurrentURL(httpRequest) + authorizationValue = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` + } + + // 4. If authorizationValue is non-null, then append (`Authorization`, + // authorizationValue) to httpRequest’s header list. + if (authorizationValue !== null) { + httpRequest.headersList.append('Authorization', authorizationValue, false) + } + } } // 21. If there’s a proxy-authentication entry, use it as appropriate. @@ -1606,10 +1644,66 @@ async function httpNetworkOrCacheFetch ( // 13. Set response’s request-includes-credentials to includeCredentials. response.requestIncludesCredentials = includeCredentials - // 14. If response’s status is 401, httpRequest’s response tainting is not - // "cors", includeCredentials is true, and request’s window is an environment - // settings object, then: - // TODO + // 14. If response’s status is 401, httpRequest’s response tainting is not "cors", + // includeCredentials is true, and request’s traversable for user prompts is + // a traversable navigable: + // + // In Node.js there is no traversable navigable to prompt the user, but we + // still need to handle URL-embedded credentials so authentication retries + // for WebSocket handshakes continue to work. + if (response.status === 401 && httpRequest.responseTainting !== 'cors' && includeCredentials && ( + request.useURLCredentials !== undefined || + isTraversableNavigable(request.traversableForUserPrompts) + )) { + // 2. If request’s body is non-null, then: + if (request.body != null) { + // 1. If request’s body’s source is null, then return a network error. + if (request.body.source == null) { + // Note: In Node.js, this code path should not be reached because + // isTraversableNavigable() returns false for non-navigable contexts. + // However, we handle it gracefully by returning the response instead of + // a network error, as we won't actually retry the request. + // This aligns with the Fetch spec discussion in whatwg/fetch#1132, + // which allows implementations flexibility when credentials can't be obtained. + return response + } + + // 2. Set request’s body to the body of the result of safely extracting + // request’s body’s source. + request.body = safelyExtractBody(request.body.source)[0] + } + + // 3. If request’s use-URL-credentials flag is unset or isAuthenticationFetch is + // true, then: + if (request.useURLCredentials === undefined || isAuthenticationFetch) { + // 1. If fetchParams is canceled, then return the appropriate network error + // for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 2. Let username and password be the result of prompting the end user for a + // username and password, respectively, in request’s traversable for user prompts. + // TODO + + // 3. Set the username given request’s current URL and username. + // requestCurrentURL(request).username = TODO + + // 4. Set the password given request’s current URL and password. + // requestCurrentURL(request).password = TODO + + // In browsers, the user will be prompted to enter a username/password before the request + // is re-sent. To prevent an infinite 401 loop, return the response for now. + // https://github.com/nodejs/undici/pull/4756 + return response + } + + // 4. Set response to the result of running HTTP-network-or-cache fetch given + // fetchParams and true. + fetchParams.controller.connection.destroy() + + response = await httpNetworkOrCacheFetch(fetchParams, true) + } // 15. If response’s status is 407, then: if (response.status === 407) { @@ -1909,15 +2003,11 @@ async function httpNetworkFetch ( // cancelAlgorithm set to cancelAlgorithm. const stream = new ReadableStream( { - async start (controller) { + start (controller) { fetchParams.controller.controller = controller }, - async pull (controller) { - await pullAlgorithm(controller) - }, - async cancel (reason) { - await cancelAlgorithm(reason) - }, + pull: pullAlgorithm, + cancel: cancelAlgorithm, type: 'bytes' } ) @@ -2055,12 +2145,15 @@ async function httpNetworkFetch ( function dispatch ({ body }) { const url = requestCurrentURL(request) - /** @type {import('../..').Agent} */ + /** @type {import('../../..').Agent} */ const agent = fetchParams.controller.dispatcher + const path = url.pathname + url.search + const hasTrailingQuestionMark = url.search.length === 0 && url.href[url.href.length - url.hash.length - 1] === '?' + return new Promise((resolve, reject) => agent.dispatch( { - path: url.pathname + url.search, + path: hasTrailingQuestionMark ? `${path}?` : path, origin: url.origin, method: request.method, body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body, @@ -2104,37 +2197,49 @@ async function httpNetworkFetch ( onHeaders (status, rawHeaders, resume, statusText) { if (status < 200) { - return + return false } - /** @type {string[]} */ - let codings = [] - let location = '' - const headersList = new HeadersList() for (let i = 0; i < rawHeaders.length; i += 2) { - headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true) - } - const contentEncoding = headersList.get('content-encoding', true) - if (contentEncoding) { - // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 - // "All content-coding values are case-insensitive..." - codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()) + const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i]) + const value = rawHeaders[i + 1] + if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) { + for (const val of value) { + headersList.append(nameStr, val.toString('latin1'), true) + } + } else { + headersList.append(nameStr, value.toString('latin1'), true) + } } - location = headersList.get('location', true) + const location = headersList.get('location', true) this.body = new Readable({ read: resume }) - const decoders = [] - const willFollow = location && request.redirect === 'follow' && redirectStatusSet.has(status) + const decoders = [] + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding - if (codings.length !== 0 && request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { + if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + const contentEncoding = headersList.get('content-encoding', true) + // "All content-coding values are case-insensitive..." + /** @type {string[]} */ + const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : [] + + // Limit the number of content-encodings to prevent resource exhaustion. + // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). + const maxContentEncodings = 5 + if (codings.length > maxContentEncodings) { + reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`)) + return true + } + for (let i = codings.length - 1; i >= 0; --i) { - const coding = codings[i] + const coding = codings[i].trim() // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 if (coding === 'x-gzip' || coding === 'gzip') { decoders.push(zlib.createGunzip({ @@ -2155,8 +2260,7 @@ async function httpNetworkFetch ( flush: zlib.constants.BROTLI_OPERATION_FLUSH, finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH })) - } else if (coding === 'zstd' && typeof zlib.createZstdDecompress === 'function') { - // Node.js v23.8.0+ and v22.15.0+ supports Zstandard + } else if (coding === 'zstd' && hasZstd) { decoders.push(zlib.createZstdDecompress({ flush: zlib.constants.ZSTD_e_continue, finishFlush: zlib.constants.ZSTD_e_end @@ -2231,15 +2335,60 @@ async function httpNetworkFetch ( reject(error) }, + onRequestUpgrade (_controller, status, headers, socket) { + // We need to support 200 for websocket over h2 as per RFC-8441 + // Absence of session means H1 + if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) { + return false + } + + const headersList = new HeadersList() + + for (const [name, value] of Object.entries(headers)) { + if (value == null) { + continue + } + + const headerName = name.toLowerCase() + + if (Array.isArray(value)) { + for (const entry of value) { + headersList.append(headerName, String(entry), true) + } + } else { + headersList.append(headerName, String(value), true) + } + } + + resolve({ + status, + statusText: STATUS_CODES[status], + headersList, + socket + }) + + return true + }, + onUpgrade (status, rawHeaders, socket) { - if (status !== 101) { - return + // We need to support 200 for websocket over h2 as per RFC-8441 + // Absence of session means H1 + if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) { + return false } const headersList = new HeadersList() for (let i = 0; i < rawHeaders.length; i += 2) { - headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true) + const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i]) + const value = rawHeaders[i + 1] + if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) { + for (const val of value) { + headersList.append(nameStr, val.toString('latin1'), true) + } + } else { + headersList.append(nameStr, value.toString('latin1'), true) + } } resolve({ diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index e2191b12dc5..6ef40f99920 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -4,7 +4,6 @@ const { extractBody, mixinBody, cloneBody, bodyUnusable } = require('./body') const { Headers, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require('./headers') -const { FinalizationRegistry } = require('./dispatcher-weakref')() const util = require('../../core/util') const nodeUtil = require('node:util') const { @@ -109,8 +108,8 @@ class Request { const prefix = 'Request constructor' webidl.argumentLengthCheck(arguments, 1, prefix) - input = webidl.converters.RequestInfo(input, prefix, 'input') - init = webidl.converters.RequestInit(init, prefix, 'init') + input = webidl.converters.RequestInfo(input) + init = webidl.converters.RequestInit(init) // 1. Let request be null. let request = null @@ -919,6 +918,8 @@ function makeRequest (init) { preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false, done: init.done ?? false, timingAllowFailed: init.timingAllowFailed ?? false, + useURLCredentials: init.useURLCredentials ?? undefined, + traversableForUserPrompts: init.traversableForUserPrompts ?? 'client', urlList: init.urlList, url: init.urlList[0], headersList: init.headersList @@ -937,7 +938,7 @@ function cloneRequest (request) { // 2. If request’s body is non-null, set newRequest’s body to the // result of cloning request’s body. if (request.body != null) { - newRequest.body = cloneBody(newRequest, request.body) + newRequest.body = cloneBody(request.body) } // 3. Return newRequest. @@ -993,8 +994,13 @@ Object.defineProperties(Request.prototype, { webidl.is.Request = webidl.util.MakeTypeAssertion(Request) -// https://fetch.spec.whatwg.org/#requestinfo -webidl.converters.RequestInfo = function (V, prefix, argument) { +/** + * @param {*} V + * @returns {import('../../../types/fetch').Request|string} + * + * @see https://fetch.spec.whatwg.org/#requestinfo + */ +webidl.converters.RequestInfo = function (V) { if (typeof V === 'string') { return webidl.converters.USVString(V) } @@ -1006,7 +1012,11 @@ webidl.converters.RequestInfo = function (V, prefix, argument) { return webidl.converters.USVString(V) } -// https://fetch.spec.whatwg.org/#requestinit +/** + * @param {*} V + * @returns {import('../../../types/fetch').RequestInit} + * @see https://fetch.spec.whatwg.org/#requestinit + */ webidl.converters.RequestInit = webidl.dictionaryConverter([ { key: 'method', @@ -1086,6 +1096,12 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([ { key: 'dispatcher', // undici specific option converter: webidl.converters.any + }, + { + key: 'priority', + converter: webidl.converters.DOMString, + allowedValues: ['high', 'low', 'auto'], + defaultValue: () => 'auto' } ]) diff --git a/lib/web/fetch/response.js b/lib/web/fetch/response.js index 9f2eec401b8..ffb7ce14074 100644 --- a/lib/web/fetch/response.js +++ b/lib/web/fetch/response.js @@ -1,7 +1,7 @@ 'use strict' const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers') -const { extractBody, cloneBody, mixinBody, hasFinalizationRegistry, streamRegistry, bodyUnusable } = require('./body') +const { extractBody, cloneBody, mixinBody, streamRegistry, bodyUnusable } = require('./body') const util = require('../../core/util') const nodeUtil = require('node:util') const { kEnumerableProperty } = util @@ -9,9 +9,7 @@ const { isValidReasonPhrase, isCancelled, isAborted, - serializeJavascriptValueToJSONString, isErrorLike, - isomorphicEncode, environmentSettingsObject: relevantRealm } = require('./util') const { @@ -22,7 +20,7 @@ const { webidl } = require('../webidl') const { URLSerializer } = require('./data-url') const { kConstruct } = require('../../core/symbols') const assert = require('node:assert') -const { types } = require('node:util') +const { isomorphicEncode, serializeJavascriptValueToJSONString } = require('../infra') const textEncoder = new TextEncoder('utf-8') @@ -119,7 +117,7 @@ class Response { } if (body !== null) { - body = webidl.converters.BodyInit(body) + body = webidl.converters.BodyInit(body, 'Response', 'body') } init = webidl.converters.ResponseInit(init) @@ -243,6 +241,12 @@ class Response { // 2. Let clonedResponse be the result of cloning this’s response. const clonedResponse = cloneResponse(this.#state) + // Note: To re-register because of a new stream. + // Don't set finalizers other than for fetch responses. + if (this.#state.urlList.length !== 0 && this.#state.body?.stream) { + streamRegistry.register(this, new WeakRef(this.#state.body.stream)) + } + // 3. Return the result of creating a Response object, given // clonedResponse, this’s headers’s guard, and this’s relevant Realm. return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers)) @@ -352,7 +356,7 @@ function cloneResponse (response) { // 3. If response’s body is non-null, then set newResponse’s body to the // result of cloning response’s body. if (response.body != null) { - newResponse.body = cloneBody(newResponse, response.body) + newResponse.body = cloneBody(response.body) } // 4. Return newResponse. @@ -450,7 +454,7 @@ function filterResponse (response, type) { return makeFilteredResponse(response, { type: 'opaque', - urlList: Object.freeze([]), + urlList: [], status: 0, statusText: '', body: null @@ -552,7 +556,8 @@ function fromInnerResponse (innerResponse, guard) { setHeadersList(headers, innerResponse.headersList) setHeadersGuard(headers, guard) - if (hasFinalizationRegistry && innerResponse.body?.stream) { + // Note: If innerResponse's urlList contains a URL, it is a fetch response. + if (innerResponse.urlList.length !== 0 && innerResponse.body?.stream) { // If the target (response) is reclaimed, the cleanup callback may be called at some point with // the held value provided for it (innerResponse.body.stream). The held value can be any value: // a primitive or an object, even undefined. If the held value is an object, the registry keeps @@ -574,7 +579,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) { return V } - if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + if (webidl.is.BufferSource(V)) { return V } diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index f229a5540da..b77be74f5d4 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -4,26 +4,13 @@ const { Transform } = require('node:stream') const zlib = require('node:zlib') const { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require('./constants') const { getGlobalOrigin } = require('./global') -const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./data-url') +const { collectAnHTTPQuotedString, parseMIMEType } = require('./data-url') const { performance } = require('node:perf_hooks') const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util') const assert = require('node:assert') const { isUint8Array } = require('node:util/types') const { webidl } = require('../webidl') - -let supportedHashes = [] - -// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable -/** @type {import('crypto')} */ -let crypto -try { - crypto = require('node:crypto') - const possibleRelevantHashes = ['sha256', 'sha384', 'sha512'] - supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)) -/* c8 ignore next 3 */ -} catch { - -} +const { isomorphicEncode, collectASequenceOfCodePoints, removeChars } = require('../infra') function responseURL (response) { // https://fetch.spec.whatwg.org/#responses @@ -516,8 +503,8 @@ function determineRequestsReferrer (request) { if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { return 'no-referrer' } - // 2. Return referrerOrigin - return referrerOrigin + // 2. Return referrerURL. + return referrerURL } } } @@ -568,17 +555,11 @@ function stripURLForReferrer (url, originOnly = false) { return url } -const potentialleTrustworthyIPv4RegExp = new RegExp('^(?:' + - '(?:127\\.)' + - '(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){2}' + - '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])' + -')$') +const isPotentialleTrustworthyIPv4 = RegExp.prototype.test + .bind(/^127\.(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){2}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$/) -const potentialleTrustworthyIPv6RegExp = new RegExp('^(?:' + - '(?:(?:0{1,4}):){7}(?:(?:0{0,3}1))|' + - '(?:(?:0{1,4}):){1,6}(?::(?:0{0,3}1))|' + - '(?:::(?:0{0,3}1))|' + -')$') +const isPotentiallyTrustworthyIPv6 = RegExp.prototype.test + .bind(/^(?:(?:0{1,4}:){7}|(?:0{1,4}:){1,6}:|::)0{0,3}1$/) /** * Check if host matches one of the CIDR notations 127.0.0.0/8 or ::1/128. @@ -593,11 +574,11 @@ function isOriginIPPotentiallyTrustworthy (origin) { if (origin[0] === '[' && origin[origin.length - 1] === ']') { origin = origin.slice(1, -1) } - return potentialleTrustworthyIPv6RegExp.test(origin) + return isPotentiallyTrustworthyIPv6(origin) } // IPv4 - return potentialleTrustworthyIPv4RegExp.test(origin) + return isPotentialleTrustworthyIPv4(origin) } /** @@ -698,206 +679,6 @@ function isURLPotentiallyTrustworthy (url) { return isOriginPotentiallyTrustworthy(url.origin) } -/** - * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist - * @param {Uint8Array} bytes - * @param {string} metadataList - */ -function bytesMatch (bytes, metadataList) { - // If node is not built with OpenSSL support, we cannot check - // a request's integrity, so allow it by default (the spec will - // allow requests if an invalid hash is given, as precedence). - /* istanbul ignore if: only if node is built with --without-ssl */ - if (crypto === undefined) { - return true - } - - // 1. Let parsedMetadata be the result of parsing metadataList. - const parsedMetadata = parseMetadata(metadataList) - - // 2. If parsedMetadata is no metadata, return true. - if (parsedMetadata === 'no metadata') { - 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) { - return true - } - - // 5. Let metadata be the result of getting the strongest - // metadata from parsedMetadata. - const strongest = getStrongestMetadata(parsedMetadata) - const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest) - - // 6. For each item in metadata: - for (const item of metadata) { - // 1. Let algorithm be the alg component of item. - const algorithm = item.algo - - // 2. Let expectedValue be the val component of item. - 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. - let 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, - // return true. - if (compareBase64Mixed(actualValue, expectedValue)) { - return true - } - } - - // 7. Return false. - return false -} - -// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options -// https://www.w3.org/TR/CSP2/#source-list-syntax -// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 -const parseHashWithOptions = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i - -/** - * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata - * @param {string} metadata - */ -function parseMetadata (metadata) { - // 1. Let result be the empty set. - /** @type {{ algo: string, hash: string }[]} */ - const result = [] - - // 2. Let empty be equal to true. - let empty = true - - // 3. For each token returned by splitting metadata on spaces: - for (const token of metadata.split(' ')) { - // 1. Set empty to false. - empty = false - - // 2. Parse token as a hash-with-options. - const parsedToken = parseHashWithOptions.exec(token) - - // 3. If token does not parse, continue to the next token. - if ( - parsedToken === null || - parsedToken.groups === undefined || - parsedToken.groups.algo === undefined - ) { - // Note: Chromium blocks the request at this point, but Firefox - // gives a warning that an invalid integrity was given. The - // correct behavior is to ignore these, and subsequently not - // check the integrity of the resource. - continue - } - - // 4. Let algorithm be the hash-algo component of token. - const algorithm = parsedToken.groups.algo.toLowerCase() - - // 5. If algorithm is a hash function recognized by the user - // agent, add the parsed token to result. - if (supportedHashes.includes(algorithm)) { - result.push(parsedToken.groups) - } - } - - // 4. Return no metadata if empty is true, otherwise return result. - if (empty === true) { - return 'no metadata' - } - - 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 function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { // TODO @@ -924,17 +705,6 @@ function sameOrigin (A, B) { return false } -function createDeferredPromise () { - let res - let rej - const promise = new Promise((resolve, reject) => { - res = resolve - rej = reject - }) - - return { promise, resolve: res, reject: rej } -} - function isAborted (fetchParams) { return fetchParams.controller.state === 'aborted' } @@ -952,23 +722,6 @@ function normalizeMethod (method) { return normalizedMethodRecordsBase[method.toLowerCase()] ?? method } -// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string -function serializeJavascriptValueToJSONString (value) { - // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). - const result = JSON.stringify(value) - - // 2. If result is undefined, then throw a TypeError. - if (result === undefined) { - throw new TypeError('Value is not JSON serializable') - } - - // 3. Assert: result is a string. - assert(typeof result === 'string') - - // 4. Return result. - return result -} - // https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) @@ -1177,6 +930,11 @@ function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueInde } /** + * @param {import('./body').ExtractBodyResult} body + * @param {(bytes: Uint8Array) => void} processBody + * @param {(error: Error) => void} processBodyError + * @returns {void} + * * @see https://fetch.spec.whatwg.org/#body-fully-read */ function fullyReadBody (body, processBody, processBodyError) { @@ -1191,20 +949,17 @@ function fullyReadBody (body, processBody, processBodyError) { // with taskDestination. const errorSteps = processBodyError + try { // 4. Let reader be the result of getting a reader for body’s stream. // If that threw an exception, then run errorSteps with that // exception and return. - let reader + const reader = body.stream.getReader() - try { - reader = body.stream.getReader() + // 5. Read all bytes from reader, given successSteps and errorSteps. + readAllBytes(reader, successSteps, errorSteps) } catch (e) { errorSteps(e) - return } - - // 5. Read all bytes from reader, given successSteps and errorSteps. - readAllBytes(reader, successSteps, errorSteps) } /** @@ -1222,34 +977,19 @@ function readableStreamClose (controller) { } } -const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line - -/** - * @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. - assert(!invalidIsomorphicEncodeValueRegex.test(input)) - - // 2. Return a byte sequence whose length is equal to input’s code - // point length and whose bytes have the same values as the - // values of input’s 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 + * @param {ReadableStream>} reader * @param {(bytes: Uint8Array) => void} successSteps * @param {(error: Error) => void} failureSteps + * @returns {Promise} */ async function readAllBytes (reader, successSteps, failureSteps) { - const bytes = [] - let byteLength = 0 - try { + const bytes = [] + let byteLength = 0 + do { const { done, value: chunk } = await reader.read() @@ -1324,10 +1064,17 @@ function urlIsHttpHttpsScheme (url) { return protocol === 'http:' || protocol === 'https:' } +/** + * @typedef {Object} RangeHeaderValue + * @property {number|null} rangeStartValue + * @property {number|null} rangeEndValue + */ + /** * @see https://fetch.spec.whatwg.org/#simple-range-header-value * @param {string} value * @param {boolean} allowWhitespace + * @return {RangeHeaderValue|'failure'} */ function simpleRangeHeaderValue (value, allowWhitespace) { // 1. Let data be the isomorphic decoding of value. @@ -1682,32 +1429,28 @@ function getDecodeSplit (name, list) { return gettingDecodingSplitting(value) } -const textDecoder = new TextDecoder() +function hasAuthenticationEntry (request) { + return false +} /** - * @see https://encoding.spec.whatwg.org/#utf-8-decode - * @param {Buffer} buffer + * @see https://url.spec.whatwg.org/#include-credentials + * @param {URL} url */ -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-8’s - // decoder, ioQueue, output, and "replacement". - const output = textDecoder.decode(buffer) +function includesCredentials (url) { + // A URL includes credentials if its username or password is not the empty string. + return !!(url.username || url.password) +} - // 4. Return output. - return output +/** + * @see https://html.spec.whatwg.org/multipage/document-sequences.html#traversable-navigable + * @param {object|string} navigable + */ +function isTraversableNavigable (navigable) { + // Returns true only if we have an actual traversable navigable object + // that can prompt the user for credentials. In Node.js, this will always + // be false since there's no Window object or navigable. + return navigable != null && navigable !== 'client' && navigable !== 'no-traversable' } class EnvironmentSettingsObjectBase { @@ -1732,7 +1475,6 @@ module.exports = { isAborted, isCancelled, isValidEncodedURL, - createDeferredPromise, ReadableStreamFrom, tryUpgradeRequestToAPotentiallyTrustworthyURL, clampAndCoarsenConnectionTimingInfo, @@ -1756,27 +1498,25 @@ module.exports = { isValidReasonPhrase, sameOrigin, normalizeMethod, - serializeJavascriptValueToJSONString, iteratorMixin, createIterator, isValidHeaderName, isValidHeaderValue, isErrorLike, fullyReadBody, - bytesMatch, readableStreamClose, - isomorphicEncode, urlIsLocal, urlHasHttpsScheme, urlIsHttpHttpsScheme, readAllBytes, simpleRangeHeaderValue, buildContentRange, - parseMetadata, createInflate, extractMimeType, getDecodeSplit, - utf8DecodeBytes, environmentSettingsObject, - isOriginIPPotentiallyTrustworthy + isOriginIPPotentiallyTrustworthy, + hasAuthenticationEntry, + includesCredentials, + isTraversableNavigable } diff --git a/lib/web/infra/index.js b/lib/web/infra/index.js new file mode 100644 index 00000000000..391bb2e3d7d --- /dev/null +++ b/lib/web/infra/index.js @@ -0,0 +1,229 @@ +'use strict' + +const assert = require('node:assert') +const { utf8DecodeBytes } = require('../../encoding') + +/** + * @param {(char: string) => boolean} condition + * @param {string} input + * @param {{ position: number }} position + * @returns {string} + * + * @see https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points + */ +function collectASequenceOfCodePoints (condition, input, position) { + // 1. Let result be the empty string. + let result = '' + + // 2. While position doesn’t point past the end of input and the + // code point at position within input meets the condition condition: + while (position.position < input.length && condition(input[position.position])) { + // 1. Append that code point to the end of result. + result += input[position.position] + + // 2. Advance position by 1. + position.position++ + } + + // 3. 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 + * @returns {string} + * + * @see https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points + */ +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) +} + +const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line no-control-regex + +/** + * @param {string} data + * @returns {Uint8Array | 'failure'} + * + * @see https://infra.spec.whatwg.org/#forgiving-base64-decode + */ +function forgivingBase64 (data) { + // 1. Remove all ASCII whitespace from data. + data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, '') + + let dataLength = data.length + // 2. If data’s code point length divides by 4 leaving + // no remainder, then: + if (dataLength % 4 === 0) { + // 1. If data ends with one or two U+003D (=) code points, + // then remove them from data. + if (data.charCodeAt(dataLength - 1) === 0x003D) { + --dataLength + if (data.charCodeAt(dataLength - 1) === 0x003D) { + --dataLength + } + } + } + + // 3. If data’s code point length divides by 4 leaving + // a remainder of 1, then return failure. + if (dataLength % 4 === 1) { + return 'failure' + } + + // 4. If data contains a code point that is not one of + // U+002B (+) + // U+002F (/) + // ASCII alphanumeric + // then return failure. + if (/[^+/0-9A-Za-z]/.test(data.length === dataLength ? data : data.substring(0, dataLength))) { + return 'failure' + } + + const buffer = Buffer.from(data, 'base64') + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) +} + +/** + * @param {number} char + * @returns {boolean} + * + * @see https://infra.spec.whatwg.org/#ascii-whitespace + */ +function isASCIIWhitespace (char) { + return ( + char === 0x09 || // \t + char === 0x0a || // \n + char === 0x0c || // \f + char === 0x0d || // \r + char === 0x20 // space + ) +} + +/** + * @param {Uint8Array} input + * @returns {string} + * + * @see https://infra.spec.whatwg.org/#isomorphic-decode + */ +function isomorphicDecode (input) { + // 1. To isomorphic decode a byte sequence input, return a string whose code point + // length is equal to input’s length and whose code points have the same values + // as the values of input’s bytes, in the same order. + const length = input.length + if ((2 << 15) - 1 > length) { + return String.fromCharCode.apply(null, input) + } + let result = '' + let i = 0 + let addition = (2 << 15) - 1 + while (i < length) { + if (i + addition > length) { + addition = length - i + } + result += String.fromCharCode.apply(null, input.subarray(i, i += addition)) + } + return result +} + +const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line no-control-regex + +/** + * @param {string} input + * @returns {string} + * + * @see https://infra.spec.whatwg.org/#isomorphic-encode + */ +function isomorphicEncode (input) { + // 1. Assert: input contains no code points greater than U+00FF. + assert(!invalidIsomorphicEncodeValueRegex.test(input)) + + // 2. Return a byte sequence whose length is equal to input’s code + // point length and whose bytes have the same values as the + // values of input’s code points, in the same order + return input +} + +/** + * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value + * @param {Uint8Array} bytes + */ +function parseJSONFromBytes (bytes) { + return JSON.parse(utf8DecodeBytes(bytes)) +} + +/** + * @param {string} str + * @param {boolean} [leading=true] + * @param {boolean} [trailing=true] + * @returns {string} + * + * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace + */ +function removeASCIIWhitespace (str, leading = true, trailing = true) { + return removeChars(str, leading, trailing, isASCIIWhitespace) +} + +/** + * @param {string} str + * @param {boolean} leading + * @param {boolean} trailing + * @param {(charCode: number) => boolean} predicate + * @returns {string} + */ +function removeChars (str, leading, trailing, predicate) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + while (lead < str.length && predicate(str.charCodeAt(lead))) lead++ + } + + if (trailing) { + while (trail > 0 && predicate(str.charCodeAt(trail))) trail-- + } + + return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1) +} + +// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string +function serializeJavascriptValueToJSONString (value) { + // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). + const result = JSON.stringify(value) + + // 2. If result is undefined, then throw a TypeError. + if (result === undefined) { + throw new TypeError('Value is not JSON serializable') + } + + // 3. Assert: result is a string. + assert(typeof result === 'string') + + // 4. Return result. + return result +} + +module.exports = { + collectASequenceOfCodePoints, + collectASequenceOfCodePointsFast, + forgivingBase64, + isASCIIWhitespace, + isomorphicDecode, + isomorphicEncode, + parseJSONFromBytes, + removeASCIIWhitespace, + removeChars, + serializeJavascriptValueToJSONString +} diff --git a/lib/web/subresource-integrity/Readme.md b/lib/web/subresource-integrity/Readme.md new file mode 100644 index 00000000000..289a2b84d46 --- /dev/null +++ b/lib/web/subresource-integrity/Readme.md @@ -0,0 +1,9 @@ +# Subresource Integrity + +based on Editor’s Draft, 12 June 2025 + +This module provides support for Subresource Integrity (SRI) in the context of web fetch operations. SRI is a security feature that allows clients to verify that fetched resources are delivered without unexpected manipulation. + +## Links + +- [Subresource Integrity](https://w3c.github.io/webappsec-subresource-integrity/) \ No newline at end of file diff --git a/lib/web/subresource-integrity/subresource-integrity.js b/lib/web/subresource-integrity/subresource-integrity.js new file mode 100644 index 00000000000..8c8f6c4d88f --- /dev/null +++ b/lib/web/subresource-integrity/subresource-integrity.js @@ -0,0 +1,307 @@ +'use strict' + +const assert = require('node:assert') +const { runtimeFeatures } = require('../../util/runtime-features.js') + +/** + * @typedef {object} Metadata + * @property {SRIHashAlgorithm} alg - The algorithm used for the hash. + * @property {string} val - The base64-encoded hash value. + */ + +/** + * @typedef {Metadata[]} MetadataList + */ + +/** + * @typedef {('sha256' | 'sha384' | 'sha512')} SRIHashAlgorithm + */ + +/** + * @type {Map} + * + * The valid SRI hash algorithm token set is the ordered set « "sha256", + * "sha384", "sha512" » (corresponding to SHA-256, SHA-384, and SHA-512 + * respectively). The ordering of this set is meaningful, with stronger + * algorithms appearing later in the set. + * + * @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token-set + */ +const validSRIHashAlgorithmTokenSet = new Map([['sha256', 0], ['sha384', 1], ['sha512', 2]]) + +// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable +/** @type {import('node:crypto')} */ +let crypto + +if (runtimeFeatures.has('crypto')) { + crypto = require('node:crypto') + const cryptoHashes = crypto.getHashes() + + // If no hashes are available, we cannot support SRI. + if (cryptoHashes.length === 0) { + validSRIHashAlgorithmTokenSet.clear() + } + + for (const algorithm of validSRIHashAlgorithmTokenSet.keys()) { + // If the algorithm is not supported, remove it from the list. + if (cryptoHashes.includes(algorithm) === false) { + validSRIHashAlgorithmTokenSet.delete(algorithm) + } + } +} else { + // If crypto is not available, we cannot support SRI. + validSRIHashAlgorithmTokenSet.clear() +} + +/** + * @typedef GetSRIHashAlgorithmIndex + * @type {(algorithm: SRIHashAlgorithm) => number} + * @param {SRIHashAlgorithm} algorithm + * @returns {number} The index of the algorithm in the valid SRI hash algorithm + * token set. + */ + +const getSRIHashAlgorithmIndex = /** @type {GetSRIHashAlgorithmIndex} */ (Map.prototype.get.bind( + validSRIHashAlgorithmTokenSet)) + +/** + * @typedef IsValidSRIHashAlgorithm + * @type {(algorithm: string) => algorithm is SRIHashAlgorithm} + * @param {*} algorithm + * @returns {algorithm is SRIHashAlgorithm} + */ + +const isValidSRIHashAlgorithm = /** @type {IsValidSRIHashAlgorithm} */ ( + Map.prototype.has.bind(validSRIHashAlgorithmTokenSet) +) + +/** + * @param {Uint8Array} bytes + * @param {string} metadataList + * @returns {boolean} + * + * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist + */ +const bytesMatch = runtimeFeatures.has('crypto') === false || validSRIHashAlgorithmTokenSet.size === 0 + // If node is not built with OpenSSL support, we cannot check + // a request's integrity, so allow it by default (the spec will + // allow requests if an invalid hash is given, as precedence). + ? () => true + : (bytes, metadataList) => { + // 1. Let parsedMetadata be the result of parsing metadataList. + const parsedMetadata = parseMetadata(metadataList) + + // 2. If parsedMetadata is empty set, return true. + if (parsedMetadata.length === 0) { + return true + } + + // 3. Let metadata be the result of getting the strongest + // metadata from parsedMetadata. + const metadata = getStrongestMetadata(parsedMetadata) + + // 4. For each item in metadata: + for (const item of metadata) { + // 1. Let algorithm be the item["alg"]. + const algorithm = item.alg + + // 2. Let expectedValue be the item["val"]. + const expectedValue = item.val + + // 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 . + const actualValue = applyAlgorithmToBytes(algorithm, bytes) + + // 4. If actualValue is a case-sensitive match for expectedValue, + // return true. + if (caseSensitiveMatch(actualValue, expectedValue)) { + return true + } + } + + // 5. Return false. + return false + } + +/** + * @param {MetadataList} metadataList + * @returns {MetadataList} The strongest hash algorithm from the metadata list. + */ +function getStrongestMetadata (metadataList) { + // 1. Let result be the empty set and strongest be the empty string. + const result = [] + /** @type {Metadata|null} */ + let strongest = null + + // 2. For each item in set: + for (const item of metadataList) { + // 1. Assert: item["alg"] is a valid SRI hash algorithm token. + assert(isValidSRIHashAlgorithm(item.alg), 'Invalid SRI hash algorithm token') + + // 2. If result is the empty set, then: + if (result.length === 0) { + // 1. Append item to result. + result.push(item) + + // 2. Set strongest to item. + strongest = item + + // 3. Continue. + continue + } + + // 3. Let currentAlgorithm be strongest["alg"], and currentAlgorithmIndex be + // the index of currentAlgorithm in the valid SRI hash algorithm token set. + const currentAlgorithm = /** @type {Metadata} */ (strongest).alg + const currentAlgorithmIndex = getSRIHashAlgorithmIndex(currentAlgorithm) + + // 4. Let newAlgorithm be the item["alg"], and newAlgorithmIndex be the + // index of newAlgorithm in the valid SRI hash algorithm token set. + const newAlgorithm = item.alg + const newAlgorithmIndex = getSRIHashAlgorithmIndex(newAlgorithm) + + // 5. If newAlgorithmIndex is less than currentAlgorithmIndex, then continue. + if (newAlgorithmIndex < currentAlgorithmIndex) { + continue + + // 6. Otherwise, if newAlgorithmIndex is greater than + // currentAlgorithmIndex: + } else if (newAlgorithmIndex > currentAlgorithmIndex) { + // 1. Set strongest to item. + strongest = item + + // 2. Set result to « item ». + result[0] = item + result.length = 1 + + // 7. Otherwise, newAlgorithmIndex and currentAlgorithmIndex are the same + // value. Append item to result. + } else { + result.push(item) + } + } + + // 3. Return result. + return result +} + +/** + * @param {string} metadata + * @returns {MetadataList} + * + * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata + */ +function parseMetadata (metadata) { + // 1. Let result be the empty set. + /** @type {MetadataList} */ + const result = [] + + // 2. For each item returned by splitting metadata on spaces: + for (const item of metadata.split(' ')) { + // 1. Let expression-and-options be the result of splitting item on U+003F (?). + const expressionAndOptions = item.split('?', 1) + + // 2. Let algorithm-expression be expression-and-options[0]. + const algorithmExpression = expressionAndOptions[0] + + // 3. Let base64-value be the empty string. + let base64Value = '' + + // 4. Let algorithm-and-value be the result of splitting algorithm-expression on U+002D (-). + const algorithmAndValue = [algorithmExpression.slice(0, 6), algorithmExpression.slice(7)] + + // 5. Let algorithm be algorithm-and-value[0]. + const algorithm = algorithmAndValue[0] + + // 6. If algorithm is not a valid SRI hash algorithm token, then continue. + if (!isValidSRIHashAlgorithm(algorithm)) { + continue + } + + // 7. If algorithm-and-value[1] exists, set base64-value to + // algorithm-and-value[1]. + if (algorithmAndValue[1]) { + base64Value = algorithmAndValue[1] + } + + // 8. Let metadata be the ordered map + // «["alg" → algorithm, "val" → base64-value]». + const metadata = { + alg: algorithm, + val: base64Value + } + + // 9. Append metadata to result. + result.push(metadata) + } + + // 3. Return result. + return result +} + +/** + * Applies the specified hash algorithm to the given bytes + * + * @typedef {(algorithm: SRIHashAlgorithm, bytes: Uint8Array) => string} ApplyAlgorithmToBytes + * @param {SRIHashAlgorithm} algorithm + * @param {Uint8Array} bytes + * @returns {string} + */ +const applyAlgorithmToBytes = (algorithm, bytes) => { + return crypto.hash(algorithm, bytes, 'base64') +} + +/** + * Compares two base64 strings, allowing for base64url + * in the second string. + * + * @param {string} actualValue base64 encoded string + * @param {string} expectedValue base64 or base64url encoded string + * @returns {boolean} + */ +function caseSensitiveMatch (actualValue, expectedValue) { + // Ignore padding characters from the end of the strings by + // decreasing the length by 1 or 2 if the last characters are `=`. + let actualValueLength = actualValue.length + if (actualValueLength !== 0 && actualValue[actualValueLength - 1] === '=') { + actualValueLength -= 1 + } + if (actualValueLength !== 0 && actualValue[actualValueLength - 1] === '=') { + actualValueLength -= 1 + } + let expectedValueLength = expectedValue.length + if (expectedValueLength !== 0 && expectedValue[expectedValueLength - 1] === '=') { + expectedValueLength -= 1 + } + if (expectedValueLength !== 0 && expectedValue[expectedValueLength - 1] === '=') { + expectedValueLength -= 1 + } + + if (actualValueLength !== expectedValueLength) { + return false + } + + for (let i = 0; i < actualValueLength; ++i) { + if ( + actualValue[i] === expectedValue[i] || + (actualValue[i] === '+' && expectedValue[i] === '-') || + (actualValue[i] === '/' && expectedValue[i] === '_') + ) { + continue + } + return false + } + + return true +} + +module.exports = { + applyAlgorithmToBytes, + bytesMatch, + caseSensitiveMatch, + isValidSRIHashAlgorithm, + getStrongestMetadata, + parseMetadata +} diff --git a/lib/web/webidl/index.js b/lib/web/webidl/index.js index fc82e3cc1f3..58664859dc9 100644 --- a/lib/web/webidl/index.js +++ b/lib/web/webidl/index.js @@ -1,7 +1,8 @@ 'use strict' +const assert = require('node:assert') const { types, inspect } = require('node:util') -const { markAsUncloneable } = require('node:worker_threads') +const { runtimeFeatures } = require('../../util/runtime-features') const UNDEFINED = 1 const BOOLEAN = 2 @@ -157,10 +158,12 @@ webidl.util.TypeValueToString = function (o) { } } -webidl.util.markAsUncloneable = markAsUncloneable || (() => {}) +webidl.util.markAsUncloneable = runtimeFeatures.has('markAsUncloneable') + ? require('node:worker_threads').markAsUncloneable + : () => {} // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint -webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) { +webidl.util.ConvertToInt = function (V, bitLength, signedness, flags) { let upperBound let lowerBound @@ -187,10 +190,10 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) { } else { // 3. Otherwise: - // 1. Let lowerBound be -2^bitLength − 1. - lowerBound = Math.pow(-2, bitLength) - 1 + // 1. Let lowerBound be -2^(bitLength − 1). + lowerBound = -Math.pow(2, bitLength - 1) - // 2. Let upperBound be 2^bitLength − 1 − 1. + // 2. Let upperBound be 2^(bitLength − 1) − 1. upperBound = Math.pow(2, bitLength - 1) - 1 } @@ -204,7 +207,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) { // 6. If the conversion is to an IDL type associated // with the [EnforceRange] extended attribute, then: - if (opts?.enforceRange === true) { + if (webidl.util.HasFlag(flags, webidl.attributes.EnforceRange)) { // 1. If x is NaN, +∞, or −∞, then throw a TypeError. if ( Number.isNaN(x) || @@ -236,7 +239,7 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) { // 7. If x is not NaN and the conversion is to an IDL // type associated with the [Clamp] extended // attribute, then: - if (!Number.isNaN(x) && opts?.clamp === true) { + if (!Number.isNaN(x) && webidl.util.HasFlag(flags, webidl.attributes.Clamp)) { // 1. Set x to min(max(x, lowerBound), upperBound). x = Math.min(Math.max(x, lowerBound), upperBound) @@ -269,9 +272,9 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, opts) { // 10. Set x to x modulo 2^bitLength. x = x % Math.pow(2, bitLength) - // 11. If signedness is "signed" and x ≥ 2^bitLength − 1, + // 11. If signedness is "signed" and x ≥ 2^(bitLength − 1), // then return x − 2^bitLength. - if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) { + if (signedness === 'signed' && x >= Math.pow(2, bitLength - 1)) { return x - Math.pow(2, bitLength) } @@ -310,6 +313,25 @@ webidl.util.Stringify = function (V) { } } +webidl.util.IsResizableArrayBuffer = function (V) { + if (types.isArrayBuffer(V)) { + return V.resizable + } + + if (types.isSharedArrayBuffer(V)) { + return V.growable + } + + throw webidl.errors.exception({ + header: 'IsResizableArrayBuffer', + message: `"${webidl.util.Stringify(V)}" is not an array buffer.` + }) +} + +webidl.util.HasFlag = function (flags, attributes) { + return typeof flags === 'number' && (flags & attributes) === attributes +} + // https://webidl.spec.whatwg.org/#es-sequence webidl.sequenceConverter = function (converter) { return (V, prefix, argument, Iterable) => { @@ -430,6 +452,9 @@ webidl.interfaceConverter = function (TypeCheck, name) { } webidl.dictionaryConverter = function (converters) { + // "For each dictionary member member declared on dictionary, in lexicographical order:" + converters.sort((a, b) => (a.key > b.key) - (a.key < b.key)) + return (dictionary, prefix, argument) => { const dict = {} @@ -509,18 +534,76 @@ webidl.is.USVString = function (value) { webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream) webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob) webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams) -webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require('node:buffer').File) +webidl.is.File = webidl.util.MakeTypeAssertion(File) webidl.is.URL = webidl.util.MakeTypeAssertion(URL) webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal) webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort) +webidl.is.BufferSource = function (V) { + return types.isArrayBuffer(V) || ( + ArrayBuffer.isView(V) && + types.isArrayBuffer(V.buffer) + ) +} + +// https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy +webidl.util.getCopyOfBytesHeldByBufferSource = function (bufferSource) { + // 1. Let jsBufferSource be the result of converting bufferSource to a JavaScript value. + const jsBufferSource = bufferSource + + // 2. Let jsArrayBuffer be jsBufferSource. + let jsArrayBuffer = jsBufferSource + + // 3. Let offset be 0. + let offset = 0 + + // 4. Let length be 0. + let length = 0 + + // 5. If jsBufferSource has a [[ViewedArrayBuffer]] internal slot, then: + if (types.isTypedArray(jsBufferSource) || types.isDataView(jsBufferSource)) { + // 5.1. Set jsArrayBuffer to jsBufferSource.[[ViewedArrayBuffer]]. + jsArrayBuffer = jsBufferSource.buffer + + // 5.2. Set offset to jsBufferSource.[[ByteOffset]]. + offset = jsBufferSource.byteOffset + + // 5.3. Set length to jsBufferSource.[[ByteLength]]. + length = jsBufferSource.byteLength + } else { + // 6. Otherwise: + + // 6.1. Assert: jsBufferSource is an ArrayBuffer or SharedArrayBuffer object. + assert(types.isAnyArrayBuffer(jsBufferSource)) + + // 6.2. Set length to jsBufferSource.[[ArrayBufferByteLength]]. + length = jsBufferSource.byteLength + } + + // 7. If IsDetachedBuffer(jsArrayBuffer) is true, then return the empty byte sequence. + if (jsArrayBuffer.detached) { + return new Uint8Array(0) + } + + // 8. Let bytes be a new byte sequence of length equal to length. + const bytes = new Uint8Array(length) + + // 9. For i in the range offset to offset + length − 1, inclusive, + // set bytes[i − offset] to GetValueFromBuffer(jsArrayBuffer, i, Uint8, true, Unordered). + const view = new Uint8Array(jsArrayBuffer, offset, length) + bytes.set(view) + + // 10. Return bytes. + return bytes +} + // https://webidl.spec.whatwg.org/#es-DOMString -webidl.converters.DOMString = function (V, prefix, argument, opts) { +webidl.converters.DOMString = function (V, prefix, argument, flags) { // 1. If V is null and the conversion is to an IDL type // associated with the [LegacyNullToEmptyString] // extended attribute, then return the DOMString value // that represents the empty string. - if (V === null && opts?.legacyNullToEmptyString) { + if (V === null && webidl.util.HasFlag(flags, webidl.attributes.LegacyNullToEmptyString)) { return '' } @@ -599,7 +682,7 @@ webidl.converters.any = function (V) { // https://webidl.spec.whatwg.org/#es-long-long webidl.converters['long long'] = function (V, prefix, argument) { // 1. Let x be ? ConvertToInt(V, 64, "signed"). - const x = webidl.util.ConvertToInt(V, 64, 'signed', undefined, prefix, argument) + const x = webidl.util.ConvertToInt(V, 64, 'signed', 0, prefix, argument) // 2. Return the IDL long long value that represents // the same numeric value as x. @@ -609,7 +692,7 @@ webidl.converters['long long'] = function (V, prefix, argument) { // https://webidl.spec.whatwg.org/#es-unsigned-long-long webidl.converters['unsigned long long'] = function (V, prefix, argument) { // 1. Let x be ? ConvertToInt(V, 64, "unsigned"). - const x = webidl.util.ConvertToInt(V, 64, 'unsigned', undefined, prefix, argument) + const x = webidl.util.ConvertToInt(V, 64, 'unsigned', 0, prefix, argument) // 2. Return the IDL unsigned long long value that // represents the same numeric value as x. @@ -619,7 +702,7 @@ webidl.converters['unsigned long long'] = function (V, prefix, argument) { // https://webidl.spec.whatwg.org/#es-unsigned-long webidl.converters['unsigned long'] = function (V, prefix, argument) { // 1. Let x be ? ConvertToInt(V, 32, "unsigned"). - const x = webidl.util.ConvertToInt(V, 32, 'unsigned', undefined, prefix, argument) + const x = webidl.util.ConvertToInt(V, 32, 'unsigned', 0, prefix, argument) // 2. Return the IDL unsigned long value that // represents the same numeric value as x. @@ -627,9 +710,9 @@ webidl.converters['unsigned long'] = function (V, prefix, argument) { } // https://webidl.spec.whatwg.org/#es-unsigned-short -webidl.converters['unsigned short'] = function (V, prefix, argument, opts) { +webidl.converters['unsigned short'] = function (V, prefix, argument, flags) { // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). - const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts, prefix, argument) + const x = webidl.util.ConvertToInt(V, 16, 'unsigned', flags, prefix, argument) // 2. Return the IDL unsigned short value that represents // the same numeric value as x. @@ -637,15 +720,16 @@ webidl.converters['unsigned short'] = function (V, prefix, argument, opts) { } // https://webidl.spec.whatwg.org/#idl-ArrayBuffer -webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) { - // 1. If Type(V) is not Object, or V does not have an +webidl.converters.ArrayBuffer = function (V, prefix, argument, flags) { + // 1. If V is not an Object, or V does not have an // [[ArrayBufferData]] internal slot, then throw a // TypeError. + // 2. If IsSharedArrayBuffer(V) is true, then throw a + // TypeError. // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances - // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances if ( webidl.util.Type(V) !== OBJECT || - !types.isAnyArrayBuffer(V) + !types.isArrayBuffer(V) ) { throw webidl.errors.conversionFailed({ prefix, @@ -654,14 +738,38 @@ webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) { }) } - // 2. If the conversion is not to an IDL type associated - // with the [AllowShared] extended attribute, and - // IsSharedArrayBuffer(V) is true, then throw a + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V) is true, then throw a // TypeError. - if (opts?.allowShared === false && types.isSharedArrayBuffer(V)) { + if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V)) { throw webidl.errors.exception({ - header: 'ArrayBuffer', - message: 'SharedArrayBuffer is not allowed.' + header: prefix, + message: `${argument} cannot be a resizable ArrayBuffer.` + }) + } + + // 4. Return the IDL ArrayBuffer value that is a + // reference to the same object as V. + return V +} + +// https://webidl.spec.whatwg.org/#idl-SharedArrayBuffer +webidl.converters.SharedArrayBuffer = function (V, prefix, argument, flags) { + // 1. If V is not an Object, or V does not have an + // [[ArrayBufferData]] internal slot, then throw a + // TypeError. + // 2. If IsSharedArrayBuffer(V) is false, then throw a + // TypeError. + // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances + if ( + webidl.util.Type(V) !== OBJECT || + !types.isSharedArrayBuffer(V) + ) { + throw webidl.errors.conversionFailed({ + prefix, + argument: `${argument} ("${webidl.util.Stringify(V)}")`, + types: ['SharedArrayBuffer'] }) } @@ -669,19 +777,20 @@ webidl.converters.ArrayBuffer = function (V, prefix, argument, opts) { // with the [AllowResizable] extended attribute, and // IsResizableArrayBuffer(V) is true, then throw a // TypeError. - if (V.resizable || V.growable) { + if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V)) { throw webidl.errors.exception({ - header: 'ArrayBuffer', - message: 'Received a resizable ArrayBuffer.' + header: prefix, + message: `${argument} cannot be a resizable SharedArrayBuffer.` }) } - // 4. Return the IDL ArrayBuffer value that is a + // 4. Return the IDL SharedArrayBuffer value that is a // reference to the same object as V. return V } -webidl.converters.TypedArray = function (V, T, prefix, name, opts) { +// https://webidl.spec.whatwg.org/#dfn-typed-array-type +webidl.converters.TypedArray = function (V, T, prefix, argument, flags) { // 1. Let T be the IDL type V is being converted to. // 2. If Type(V) is not Object, or V does not have a @@ -694,7 +803,7 @@ webidl.converters.TypedArray = function (V, T, prefix, name, opts) { ) { throw webidl.errors.conversionFailed({ prefix, - argument: `${name} ("${webidl.util.Stringify(V)}")`, + argument: `${argument} ("${webidl.util.Stringify(V)}")`, types: [T.name] }) } @@ -703,10 +812,10 @@ webidl.converters.TypedArray = function (V, T, prefix, name, opts) { // with the [AllowShared] extended attribute, and // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is // true, then throw a TypeError. - if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) { throw webidl.errors.exception({ - header: 'ArrayBuffer', - message: 'SharedArrayBuffer is not allowed.' + header: prefix, + message: `${argument} cannot be a view on a shared array buffer.` }) } @@ -714,10 +823,10 @@ webidl.converters.TypedArray = function (V, T, prefix, name, opts) { // with the [AllowResizable] extended attribute, and // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is // true, then throw a TypeError. - if (V.buffer.resizable || V.buffer.growable) { + if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) { throw webidl.errors.exception({ - header: 'ArrayBuffer', - message: 'Received a resizable ArrayBuffer.' + header: prefix, + message: `${argument} cannot be a view on a resizable array buffer.` }) } @@ -726,13 +835,15 @@ webidl.converters.TypedArray = function (V, T, prefix, name, opts) { return V } -webidl.converters.DataView = function (V, prefix, name, opts) { +// https://webidl.spec.whatwg.org/#idl-DataView +webidl.converters.DataView = function (V, prefix, argument, flags) { // 1. If Type(V) is not Object, or V does not have a // [[DataView]] internal slot, then throw a TypeError. if (webidl.util.Type(V) !== OBJECT || !types.isDataView(V)) { - throw webidl.errors.exception({ - header: prefix, - message: `${name} is not a DataView.` + throw webidl.errors.conversionFailed({ + prefix, + argument: `${argument} ("${webidl.util.Stringify(V)}")`, + types: ['DataView'] }) } @@ -740,10 +851,10 @@ webidl.converters.DataView = function (V, prefix, name, opts) { // with the [AllowShared] extended attribute, and // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, // then throw a TypeError. - if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) { throw webidl.errors.exception({ - header: 'ArrayBuffer', - message: 'SharedArrayBuffer is not allowed.' + header: prefix, + message: `${argument} cannot be a view on a shared array buffer.` }) } @@ -751,10 +862,10 @@ webidl.converters.DataView = function (V, prefix, name, opts) { // with the [AllowResizable] extended attribute, and // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is // true, then throw a TypeError. - if (V.buffer.resizable || V.buffer.growable) { + if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) { throw webidl.errors.exception({ - header: 'ArrayBuffer', - message: 'Received a resizable ArrayBuffer.' + header: prefix, + message: `${argument} cannot be a view on a resizable array buffer.` }) } @@ -763,6 +874,85 @@ webidl.converters.DataView = function (V, prefix, name, opts) { return V } +// https://webidl.spec.whatwg.org/#ArrayBufferView +webidl.converters.ArrayBufferView = function (V, prefix, argument, flags) { + if ( + webidl.util.Type(V) !== OBJECT || + !types.isArrayBufferView(V) + ) { + throw webidl.errors.conversionFailed({ + prefix, + argument: `${argument} ("${webidl.util.Stringify(V)}")`, + types: ['ArrayBufferView'] + }) + } + + if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: prefix, + message: `${argument} cannot be a view on a shared array buffer.` + }) + } + + if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: prefix, + message: `${argument} cannot be a view on a resizable array buffer.` + }) + } + + return V +} + +// https://webidl.spec.whatwg.org/#BufferSource +webidl.converters.BufferSource = function (V, prefix, argument, flags) { + if (types.isArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, prefix, argument, flags) + } + + if (types.isArrayBufferView(V)) { + flags &= ~webidl.attributes.AllowShared + + return webidl.converters.ArrayBufferView(V, prefix, argument, flags) + } + + // Make this explicit for easier debugging + if (types.isSharedArrayBuffer(V)) { + throw webidl.errors.exception({ + header: prefix, + message: `${argument} cannot be a SharedArrayBuffer.` + }) + } + + throw webidl.errors.conversionFailed({ + prefix, + argument: `${argument} ("${webidl.util.Stringify(V)}")`, + types: ['ArrayBuffer', 'ArrayBufferView'] + }) +} + +// https://webidl.spec.whatwg.org/#AllowSharedBufferSource +webidl.converters.AllowSharedBufferSource = function (V, prefix, argument, flags) { + if (types.isArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, prefix, argument, flags) + } + + if (types.isSharedArrayBuffer(V)) { + return webidl.converters.SharedArrayBuffer(V, prefix, argument, flags) + } + + if (types.isArrayBufferView(V)) { + flags |= webidl.attributes.AllowShared + return webidl.converters.ArrayBufferView(V, prefix, argument, flags) + } + + throw webidl.errors.conversionFailed({ + prefix, + argument: `${argument} ("${webidl.util.Stringify(V)}")`, + types: ['ArrayBuffer', 'SharedArrayBuffer', 'ArrayBufferView'] + }) +} + webidl.converters['sequence'] = webidl.sequenceConverter( webidl.converters.ByteString ) @@ -783,6 +973,34 @@ webidl.converters.AbortSignal = webidl.interfaceConverter( 'AbortSignal' ) +/** + * [LegacyTreatNonObjectAsNull] + * callback EventHandlerNonNull = any (Event event); + * typedef EventHandlerNonNull? EventHandler; + * @param {*} V + */ +webidl.converters.EventHandlerNonNull = function (V) { + if (webidl.util.Type(V) !== OBJECT) { + return null + } + + // [I]f the value is not an object, it will be converted to null, and if the value is not callable, + // it will be converted to a callback function value that does nothing when called. + if (typeof V === 'function') { + return V + } + + return () => {} +} + +webidl.attributes = { + Clamp: 1 << 0, + EnforceRange: 1 << 1, + AllowShared: 1 << 2, + AllowResizable: 1 << 3, + LegacyNullToEmptyString: 1 << 4 +} + module.exports = { webidl } diff --git a/lib/web/websocket/connection.js b/lib/web/websocket/connection.js index 2e34bea1e04..4ecc8a195fc 100644 --- a/lib/web/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -1,23 +1,20 @@ 'use strict' const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants') -const { parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util') -const { channels } = require('../../core/diagnostics') +const { parseExtensions, isClosed, isClosing, isEstablished, isConnecting, validateCloseCodeAndReason } = require('./util') const { makeRequest } = require('../fetch/request') const { fetching } = require('../fetch/index') const { Headers, getHeadersList } = require('../fetch/headers') const { getDecodeSplit } = require('../fetch/util') const { WebsocketFrameSend } = require('./frame') const assert = require('node:assert') +const { runtimeFeatures } = require('../../util/runtime-features') -/** @type {import('crypto')} */ -let crypto -try { - crypto = require('node:crypto') -/* c8 ignore next 3 */ -} catch { +const crypto = runtimeFeatures.has('crypto') + ? require('node:crypto') + : null -} +let warningEmitted = false /** * @see https://websockets.spec.whatwg.org/#concept-websocket-establish @@ -36,7 +33,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // 2. Let request be a new request, whose URL is requestURL, client is client, // service-workers mode is "none", referrer is "no-referrer", mode is // "websocket", credentials mode is "include", cache mode is "no-store" , - // and redirect mode is "error". + // redirect mode is "error", and use-URL-credentials flag is set. const request = makeRequest({ urlList: [requestURL], client, @@ -45,7 +42,8 @@ function establishWebSocketConnection (url, protocols, client, handler, options) mode: 'websocket', credentials: 'include', cache: 'no-store', - redirect: 'error' + redirect: 'error', + useURLCredentials: true }) // Note: undici extension, allow setting custom headers. @@ -96,17 +94,27 @@ function establishWebSocketConnection (url, protocols, client, handler, options) useParallelQueue: true, dispatcher: options.dispatcher, processResponse (response) { - if (response.type === 'error') { - // If the WebSocket connection could not be established, it is also said - // that _The WebSocket Connection is Closed_, but not _cleanly_. - handler.readyState = states.CLOSED - } - // 1. If response is a network error or its status is not 101, // fail the WebSocket connection. + // if (response.type === 'error' || ((response.socket?.session != null && response.status !== 200) && response.status !== 101)) { if (response.type === 'error' || response.status !== 101) { - failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error) - return + // The presence of a session property on the socket indicates HTTP2 + // HTTP1 + if (response.socket?.session == null) { + failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error) + return + } + + // HTTP2 + if (response.status !== 200) { + failWebsocketConnection(handler, 1002, 'Received network error or non-200 status code.', response.error) + return + } + } + + if (warningEmitted === false && response.socket?.session != null) { + process.emitWarning('WebSocket over HTTP2 is experimental, and subject to change.', 'ExperimentalWarning') + warningEmitted = true } // 2. If protocols is not the empty list and extracting header @@ -128,7 +136,8 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // header field contains a value that is not an ASCII case- // insensitive match for the value "websocket", the client MUST // _Fail the WebSocket Connection_. - if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { + // For H2, no upgrade header is expected. + if (response.socket.session == null && response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".') return } @@ -137,7 +146,8 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // |Connection| header field doesn't contain a token that is an // ASCII case-insensitive match for the value "Upgrade", the client // MUST _Fail the WebSocket Connection_. - if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { + // For H2, no connection header is expected. + if (response.socket.session == null && response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".') return } @@ -150,7 +160,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // trailing whitespace, the client MUST _Fail the WebSocket // Connection_. const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') - const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64') + const digest = crypto.hash('sha1', keyValue + uid, 'base64') if (secWSAccept !== digest) { failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.') return @@ -200,14 +210,6 @@ function establishWebSocketConnection (url, protocols, client, handler, options) response.socket.on('close', handler.onSocketClose) response.socket.on('error', handler.onSocketError) - if (channels.open.hasSubscribers) { - channels.open.publish({ - address: response.socket.address(), - protocol: secProtocol, - extensions: secExtension - }) - } - handler.wasEverConnected = true handler.onConnectionEstablished(response, extensions) } @@ -312,11 +314,12 @@ function failWebsocketConnection (handler, code, reason, cause) { handler.controller.abort() - if (handler.socket?.destroyed === false) { + if (isConnecting(handler.readyState)) { + // If the connection was not established, we must still emit an 'error' and 'close' events + handler.onSocketClose() + } else if (handler.socket?.destroyed === false) { handler.socket.destroy() } - - handler.onFail(code, reason, cause) } module.exports = { diff --git a/lib/web/websocket/events.js b/lib/web/websocket/events.js index 3f2cf61ada9..7ac9566be46 100644 --- a/lib/web/websocket/events.js +++ b/lib/web/websocket/events.js @@ -272,7 +272,7 @@ webidl.converters.MessageEventInit = webidl.dictionaryConverter([ { key: 'ports', converter: webidl.converters['sequence'], - defaultValue: () => new Array(0) + defaultValue: () => [] } ]) diff --git a/lib/web/websocket/frame.js b/lib/web/websocket/frame.js index 68f31ebab9f..e397c878a4f 100644 --- a/lib/web/websocket/frame.js +++ b/lib/web/websocket/frame.js @@ -1,33 +1,27 @@ 'use strict' +const { runtimeFeatures } = require('../../util/runtime-features') const { maxUnsigned16Bit, opcodes } = require('./constants') const BUFFER_SIZE = 8 * 1024 -/** @type {import('crypto')} */ -let crypto let buffer = null let bufIdx = BUFFER_SIZE -try { - crypto = require('node:crypto') -/* c8 ignore next 3 */ -} catch { - crypto = { - // not full compatibility, but minimum. - randomFillSync: function randomFillSync (buffer, _offset, _size) { - for (let i = 0; i < buffer.length; ++i) { - buffer[i] = Math.random() * 255 | 0 - } - return buffer +const randomFillSync = runtimeFeatures.has('crypto') + ? require('node:crypto').randomFillSync + // not full compatibility, but minimum. + : function randomFillSync (buffer, _offset, _size) { + for (let i = 0; i < buffer.length; ++i) { + buffer[i] = Math.random() * 255 | 0 } + return buffer } -} function generateMask () { if (bufIdx === BUFFER_SIZE) { bufIdx = 0 - crypto.randomFillSync((buffer ??= Buffer.allocUnsafeSlow(BUFFER_SIZE)), 0, BUFFER_SIZE) + randomFillSync((buffer ??= Buffer.allocUnsafeSlow(BUFFER_SIZE)), 0, BUFFER_SIZE) } return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]] } diff --git a/lib/web/websocket/permessage-deflate.js b/lib/web/websocket/permessage-deflate.js index 76cb366d5e5..6a6e43899c5 100644 --- a/lib/web/websocket/permessage-deflate.js +++ b/lib/web/websocket/permessage-deflate.js @@ -2,6 +2,7 @@ const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib') const { isValidClientWindowBits } = require('./util') +const { MessageSizeExceededError } = require('../../core/errors') const tail = Buffer.from([0x00, 0x00, 0xff, 0xff]) const kBuffer = Symbol('kBuffer') @@ -13,17 +14,29 @@ class PerMessageDeflate { #options = {} - constructor (extensions) { + #maxPayloadSize = 0 + + /** + * @param {Map} extensions + */ + constructor (extensions, options) { this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover') this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits') + + this.#maxPayloadSize = options.maxPayloadSize } + /** + * Decompress a compressed payload. + * @param {Buffer} chunk Compressed data + * @param {boolean} fin Final fragment flag + * @param {Function} callback Callback function + */ decompress (chunk, fin, callback) { // An endpoint uses the following algorithm to decompress a message. // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the // payload of the message. // 2. Decompress the resulting data using DEFLATE. - if (!this.#inflate) { let windowBits = Z_DEFAULT_WINDOWBITS @@ -36,13 +49,26 @@ class PerMessageDeflate { windowBits = Number.parseInt(this.#options.serverMaxWindowBits) } - this.#inflate = createInflateRaw({ windowBits }) + try { + this.#inflate = createInflateRaw({ windowBits }) + } catch (err) { + callback(err) + return + } this.#inflate[kBuffer] = [] this.#inflate[kLength] = 0 this.#inflate.on('data', (data) => { - this.#inflate[kBuffer].push(data) this.#inflate[kLength] += data.length + + if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { + callback(new MessageSizeExceededError()) + this.#inflate.removeAllListeners() + this.#inflate = null + return + } + + this.#inflate[kBuffer].push(data) }) this.#inflate.on('error', (err) => { @@ -57,6 +83,10 @@ class PerMessageDeflate { } this.#inflate.flush(() => { + if (!this.#inflate) { + return + } + const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength]) this.#inflate[kBuffer].length = 0 diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 3ea603e4858..a37bd22265a 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -3,7 +3,6 @@ const { Writable } = require('node:stream') const assert = require('node:assert') const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants') -const { channels } = require('../../core/diagnostics') const { isValidStatusCode, isValidOpcode, @@ -16,6 +15,7 @@ const { const { failWebsocketConnection } = require('./connection') const { WebsocketFrameSend } = require('./frame') const { PerMessageDeflate } = require('./permessage-deflate') +const { MessageSizeExceededError } = require('../../core/errors') // This code was influenced by ws released under the MIT license. // Copyright (c) 2011 Einar Otto Stangvik @@ -39,14 +39,27 @@ class ByteParser extends Writable { /** @type {import('./websocket').Handler} */ #handler - constructor (handler, extensions) { + /** @type {number} */ + #maxFragments + + /** @type {number} */ + #maxPayloadSize + + /** + * @param {import('./websocket').Handler} handler + * @param {Map|null} extensions + * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options] + */ + constructor (handler, extensions, options = {}) { super() this.#handler = handler this.#extensions = extensions == null ? new Map() : extensions + this.#maxFragments = options.maxFragments ?? 0 + this.#maxPayloadSize = options.maxPayloadSize ?? 0 if (this.#extensions.has('permessage-deflate')) { - this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions)) + this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options)) } } @@ -62,6 +75,19 @@ class ByteParser extends Writable { this.run(callback) } + #validatePayloadLength () { + if ( + this.#maxPayloadSize > 0 && + !isControlFrame(this.#info.opcode) && + this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize + ) { + failWebsocketConnection(this.#handler, 1009, 'Payload size exceeds maximum allowed size') + return false + } + + return true + } + /** * Runs whenever a new chunk is received. * Callback is called whenever there are no more chunks buffering, @@ -150,6 +176,10 @@ class ByteParser extends Writable { if (payloadLength <= 125) { this.#info.payloadLength = payloadLength this.#state = parserStates.READ_DATA + + if (!this.#validatePayloadLength()) { + return + } } else if (payloadLength === 126) { this.#state = parserStates.PAYLOADLENGTH_16 } else if (payloadLength === 127) { @@ -174,6 +204,10 @@ class ByteParser extends Writable { this.#info.payloadLength = buffer.readUInt16BE(0) this.#state = parserStates.READ_DATA + + if (!this.#validatePayloadLength()) { + return + } } else if (this.#state === parserStates.PAYLOADLENGTH_64) { if (this.#byteOffset < 8) { return callback() @@ -181,6 +215,7 @@ class ByteParser extends Writable { const buffer = this.consume(8) const upper = buffer.readUInt32BE(0) + const lower = buffer.readUInt32BE(4) // 2^31 is the maximum bytes an arraybuffer can contain // on 32-bit systems. Although, on 64-bit systems, this is @@ -188,15 +223,17 @@ class ByteParser extends Writable { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e - if (upper > 2 ** 31 - 1) { + if (upper !== 0 || lower > 2 ** 31 - 1) { failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.') return } - const lower = buffer.readUInt32BE(4) - - this.#info.payloadLength = (upper << 8) + lower + this.#info.payloadLength = lower this.#state = parserStates.READ_DATA + + if (!this.#validatePayloadLength()) { + return + } } else if (this.#state === parserStates.READ_DATA) { if (this.#byteOffset < this.#info.payloadLength) { return callback() @@ -209,7 +246,9 @@ class ByteParser extends Writable { this.#state = parserStates.INFO } else { if (!this.#info.compressed) { - this.writeFragments(body) + if (!this.writeFragments(body)) { + return + } // If the frame is not fragmented, a message has been received. // If the frame is fragmented, it will terminate with a fin bit set @@ -221,27 +260,41 @@ class ByteParser extends Writable { this.#state = parserStates.INFO } else { - this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => { - if (error) { - failWebsocketConnection(this.#handler, 1007, error.message) - return - } - - this.writeFragments(data) + this.#extensions.get('permessage-deflate').decompress( + body, + this.#info.fin, + (error, data) => { + if (error) { + const code = error instanceof MessageSizeExceededError ? 1009 : 1007 + failWebsocketConnection(this.#handler, code, error.message) + return + } + + if (!this.writeFragments(data)) { + return + } + + // Check cumulative fragment size + if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { + failWebsocketConnection(this.#handler, 1009, new MessageSizeExceededError().message) + return + } + + if (!this.#info.fin) { + this.#state = parserStates.INFO + this.#loop = true + this.run(callback) + return + } + + websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments()) - if (!this.#info.fin) { - this.#state = parserStates.INFO this.#loop = true + this.#state = parserStates.INFO this.run(callback) - return - } - - websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments()) - - this.#loop = true - this.#state = parserStates.INFO - this.run(callback) - }) + }, + this.#fragmentsBytes + ) this.#loop = false break @@ -300,8 +353,17 @@ class ByteParser extends Writable { } writeFragments (fragment) { + if ( + this.#maxFragments > 0 && + this.#fragments.length === this.#maxFragments + ) { + failWebsocketConnection(this.#handler, 1008, 'Too many message fragments') + return false + } + this.#fragmentsBytes += fragment.length this.#fragments.push(fragment) + return true } consumeFragments () { @@ -423,22 +485,13 @@ class ByteParser extends Writable { this.#handler.socket.write(frame.createFrame(opcodes.PONG)) - if (channels.ping.hasSubscribers) { - channels.ping.publish({ - payload: body - }) - } + this.#handler.onPing(body) } } else if (opcode === opcodes.PONG) { // A Pong frame MAY be sent unsolicited. This serves as a // unidirectional heartbeat. A response to an unsolicited Pong frame is // not expected. - - if (channels.pong.hasSubscribers) { - channels.pong.publish({ - payload: body - }) - } + this.#handler.onPong(body) } return true diff --git a/lib/web/websocket/stream/websocketerror.js b/lib/web/websocket/stream/websocketerror.js index e57b0697d60..a34c5213a39 100644 --- a/lib/web/websocket/stream/websocketerror.js +++ b/lib/web/websocket/stream/websocketerror.js @@ -5,7 +5,28 @@ const { validateCloseCodeAndReason } = require('../util') const { kConstruct } = require('../../../core/symbols') const { kEnumerableProperty } = require('../../../core/util') -class WebSocketError extends DOMException { +function createInheritableDOMException () { + // https://github.com/nodejs/node/issues/59677 + class Test extends DOMException { + get reason () { + return '' + } + } + + if (new Test().reason !== undefined) { + return DOMException + } + + return new Proxy(DOMException, { + construct (target, args, newTarget) { + const instance = Reflect.construct(target, args, target) + Object.setPrototypeOf(instance, newTarget.prototype) + return instance + } + }) +} + +class WebSocketError extends createInheritableDOMException() { #closeCode #reason diff --git a/lib/web/websocket/stream/websocketstream.js b/lib/web/websocket/stream/websocketstream.js index 6b4e8285cfb..1da0292b465 100644 --- a/lib/web/websocket/stream/websocketstream.js +++ b/lib/web/websocket/stream/websocketstream.js @@ -1,17 +1,17 @@ 'use strict' -const { createDeferredPromise, environmentSettingsObject } = require('../../fetch/util') +const { createDeferredPromise } = require('../../../util/promise') +const { environmentSettingsObject } = require('../../fetch/util') const { states, opcodes, sentCloseFrameState } = require('../constants') const { webidl } = require('../../webidl') const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util') const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection') -const { types } = require('node:util') const { channels } = require('../../../core/diagnostics') const { WebsocketFrameSend } = require('../frame') const { ByteParser } = require('../receiver') const { WebSocketError, createUnvalidatedWebSocketError } = require('./websocketerror') -const { utf8DecodeBytes } = require('../../fetch/util') const { kEnumerableProperty } = require('../../../core/util') +const { utf8DecodeBytes } = require('../../../encoding') let emittedExperimentalWarning = false @@ -21,11 +21,11 @@ class WebSocketStream { #url // Each WebSocketStream object has an associated opened promise , which is a promise. - /** @type {ReturnType} */ + /** @type {import('../../../util/promise').DeferredPromise} */ #openedPromise // Each WebSocketStream object has an associated closed promise , which is a promise. - /** @type {ReturnType} */ + /** @type {import('../../../util/promise').DeferredPromise} */ #closedPromise // Each WebSocketStream object has an associated readable stream , which is a ReadableStream . @@ -45,7 +45,6 @@ class WebSocketStream { #handler = { // https://whatpr.org/websockets/48/7b748d3...d5570f3.html#feedback-to-websocket-stream-from-the-protocol onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions), - onFail: (_code, _reason) => {}, onMessage: (opcode, data) => this.#onMessage(opcode, data), onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message), onParserDrain: () => this.#handler.socket.resume(), @@ -64,6 +63,8 @@ class WebSocketStream { this.#handler.socket.destroy() }, onSocketClose: () => this.#onSocketClose(), + onPing: () => {}, + onPong: () => {}, readyState: states.CONNECTING, socket: null, @@ -197,6 +198,9 @@ class WebSocketStream { } #write (chunk) { + // See /websockets/stream/tentative/write.any.html + chunk = webidl.converters.WebSocketStreamWrite(chunk) + // 1. Let promise be a new promise created in stream ’s relevant realm . const promise = createDeferredPromise() @@ -207,9 +211,9 @@ class WebSocketStream { let opcode = null // 4. If chunk is a BufferSource , - if (ArrayBuffer.isView(chunk) || types.isArrayBuffer(chunk)) { + if (webidl.is.BufferSource(chunk)) { // 4.1. Set data to a copy of the bytes given chunk . - data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk) + data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk.slice()) // 4.2. Set opcode to a binary frame opcode. opcode = opcodes.BINARY @@ -224,7 +228,7 @@ class WebSocketStream { string = webidl.converters.DOMString(chunk) } catch (e) { promise.reject(e) - return + return promise.promise } // 5.2. Set data to the result of UTF-8 encoding string . @@ -247,14 +251,21 @@ class WebSocketStream { } // 6.3. Queue a global task on the WebSocket task source given stream ’s relevant global object to resolve promise with undefined. - return promise + return promise.promise } /** @type {import('../websocket').Handler['onConnectionEstablished']} */ #onConnectionEstablished (response, parsedExtensions) { this.#handler.socket = response.socket - const parser = new ByteParser(this.#handler, parsedExtensions) + // Get options from dispatcher options + const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments + const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize + + const parser = new ByteParser(this.#handler, parsedExtensions, { + maxFragments, + maxPayloadSize + }) parser.on('drain', () => this.#handler.onParserDrain()) parser.on('error', (err) => this.#handler.onParserError(err)) @@ -280,12 +291,6 @@ class WebSocketStream { start: (controller) => { this.#readableStreamController = controller }, - pull (controller) { - let chunk - while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) { - controller.enqueue(chunk) - } - }, cancel: (reason) => this.#cancel(reason) }) @@ -334,7 +339,7 @@ class WebSocketStream { try { chunk = utf8Decode(data) } catch { - failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.') + failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.') return } } else if (type === opcodes.BINARY) { @@ -366,7 +371,7 @@ class WebSocketStream { this.#openedPromise.reject(new WebSocketError('Socket never opened')) } - const result = this.#parser.closingInfo + const result = this.#parser?.closingInfo // 4. Let code be the WebSocket connection close code . // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 @@ -388,7 +393,7 @@ class WebSocketStream { // 6. If the connection was closed cleanly , if (wasClean) { // 6.1. Close stream ’s readable stream . - this.#readableStream.cancel().catch(() => {}) + this.#readableStreamController.close() // 6.2. Error stream ’s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to. if (!this.#writableStream.locked) { @@ -407,10 +412,10 @@ class WebSocketStream { const error = createUnvalidatedWebSocketError('unclean close', code, reason) // 7.2. Error stream ’s readable stream with error . - this.#readableStreamController.error(error) + this.#readableStreamController?.error(error) // 7.3. Error stream ’s writable stream with error . - this.#writableStream.abort(error) + this.#writableStream?.abort(error) // 7.4. Reject stream ’s closed promise with error . this.#closedPromise.reject(error) @@ -473,7 +478,7 @@ webidl.converters.WebSocketStreamOptions = webidl.dictionaryConverter([ webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([ { key: 'closeCode', - converter: (V) => webidl.converters['unsigned short'](V, { enforceRange: true }) + converter: (V) => webidl.converters['unsigned short'](V, webidl.attributes.EnforceRange) }, { key: 'reason', @@ -482,4 +487,12 @@ webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([ } ]) +webidl.converters.WebSocketStreamWrite = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + return webidl.converters.BufferSource(V) +} + module.exports = { WebSocketStream } diff --git a/lib/web/websocket/util.js b/lib/web/websocket/util.js index ae8f076c0fb..f9cbf912d09 100644 --- a/lib/web/websocket/util.js +++ b/lib/web/websocket/util.js @@ -2,7 +2,8 @@ const { states, opcodes } = require('./constants') const { isUtf8 } = require('node:buffer') -const { collectASequenceOfCodePointsFast, removeHTTPWhitespace } = require('../fetch/data-url') +const { removeHTTPWhitespace } = require('../fetch/data-url') +const { collectASequenceOfCodePointsFast } = require('../infra') /** * @param {number} readyState @@ -226,6 +227,12 @@ function parseExtensions (extensions) { * @returns {boolean} */ function isValidClientWindowBits (value) { + // Must have at least one character + if (value.length === 0) { + return false + } + + // Check all characters are ASCII digits for (let i = 0; i < value.length; i++) { const byte = value.charCodeAt(i) @@ -234,7 +241,9 @@ function isValidClientWindowBits (value) { } } - return true + // Check numeric range: zlib requires windowBits in range 8-15 + const num = Number.parseInt(value, 10) + return num >= 8 && num <= 15 } /** diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index c2915933408..4f816ef17a7 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -1,5 +1,6 @@ 'use strict' +const { isArrayBuffer } = require('node:util/types') const { webidl } = require('../webidl') const { URLSerializer } = require('../fetch/data-url') const { environmentSettingsObject } = require('../fetch/util') @@ -8,6 +9,7 @@ const { isConnecting, isEstablished, isClosing, + isClosed, isValidSubprotocol, fireEvent, utf8Decode, @@ -18,21 +20,34 @@ const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketCon const { ByteParser } = require('./receiver') const { kEnumerableProperty } = require('../../core/util') const { getGlobalDispatcher } = require('../../global') -const { types } = require('node:util') const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events') const { SendQueue } = require('./sender') +const { WebsocketFrameSend } = require('./frame') const { channels } = require('../../core/diagnostics') +function getSocketAddress (socket) { + if (typeof socket?.address === 'function') { + return socket.address() + } + + if (typeof socket?.session?.socket?.address === 'function') { + return socket.session.socket.address() + } + + return null +} + /** * @typedef {object} Handler * @property {(response: any, extensions?: string[]) => void} onConnectionEstablished - * @property {(code: number, reason: any) => void} onFail * @property {(opcode: number, data: Buffer) => void} onMessage * @property {(error: Error) => void} onParserError * @property {() => void} onParserDrain * @property {(chunk: Buffer) => void} onSocketData * @property {(err: Error) => void} onSocketError * @property {() => void} onSocketClose + * @property {(body: Buffer) => void} onPing + * @property {(body: Buffer) => void} onPong * * @property {number} readyState * @property {import('stream').Duplex} socket @@ -60,7 +75,6 @@ class WebSocket extends EventTarget { /** @type {Handler} */ #handler = { onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions), - onFail: (code, reason, cause) => this.#onFail(code, reason, cause), onMessage: (opcode, data) => this.#onMessage(opcode, data), onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message), onParserDrain: () => this.#onParserDrain(), @@ -79,6 +93,22 @@ class WebSocket extends EventTarget { this.#handler.socket.destroy() }, onSocketClose: () => this.#onSocketClose(), + onPing: (body) => { + if (channels.ping.hasSubscribers) { + channels.ping.publish({ + payload: body, + websocket: this + }) + } + }, + onPong: (body) => { + if (channels.pong.hasSubscribers) { + channels.pong.publish({ + payload: body, + websocket: this + }) + } + }, readyState: states.CONNECTING, socket: null, @@ -175,7 +205,7 @@ class WebSocket extends EventTarget { const prefix = 'WebSocket.close' if (code !== undefined) { - code = webidl.converters['unsigned short'](code, prefix, 'code', { clamp: true }) + code = webidl.converters['unsigned short'](code, prefix, 'code', webidl.attributes.Clamp) } if (reason !== undefined) { @@ -237,7 +267,7 @@ class WebSocket extends EventTarget { this.#sendQueue.add(buffer, () => { this.#bufferedAmount -= buffer.byteLength }, sendHints.text) - } else if (types.isArrayBuffer(data)) { + } else if (isArrayBuffer(data)) { // If the WebSocket connection is established, and the WebSocket // closing handshake has not yet started, then the user agent must // send a WebSocket Message comprised of data using a binary frame @@ -335,9 +365,11 @@ class WebSocket extends EventTarget { this.removeEventListener('open', this.#events.open) } - if (typeof fn === 'function') { + const listener = webidl.converters.EventHandlerNonNull(fn) + + if (listener !== null) { + this.addEventListener('open', listener) this.#events.open = fn - this.addEventListener('open', fn) } else { this.#events.open = null } @@ -356,9 +388,11 @@ class WebSocket extends EventTarget { this.removeEventListener('error', this.#events.error) } - if (typeof fn === 'function') { + const listener = webidl.converters.EventHandlerNonNull(fn) + + if (listener !== null) { + this.addEventListener('error', listener) this.#events.error = fn - this.addEventListener('error', fn) } else { this.#events.error = null } @@ -377,9 +411,11 @@ class WebSocket extends EventTarget { this.removeEventListener('close', this.#events.close) } - if (typeof fn === 'function') { + const listener = webidl.converters.EventHandlerNonNull(fn) + + if (listener !== null) { + this.addEventListener('close', listener) this.#events.close = fn - this.addEventListener('close', fn) } else { this.#events.close = null } @@ -398,9 +434,11 @@ class WebSocket extends EventTarget { this.removeEventListener('message', this.#events.message) } - if (typeof fn === 'function') { + const listener = webidl.converters.EventHandlerNonNull(fn) + + if (listener !== null) { + this.addEventListener('message', listener) this.#events.message = fn - this.addEventListener('message', fn) } else { this.#events.message = null } @@ -426,11 +464,18 @@ class WebSocket extends EventTarget { * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol */ #onConnectionEstablished (response, parsedExtensions) { - // processResponse is called when the "response’s header list has been received and initialized." + // processResponse is called when the "response's header list has been received and initialized." // once this happens, the connection is open this.#handler.socket = response.socket - const parser = new ByteParser(this.#handler, parsedExtensions) + const webSocketOptions = this.#handler.controller.dispatcher?.webSocketOptions + const maxFragments = webSocketOptions?.maxFragments + const maxPayloadSize = webSocketOptions?.maxPayloadSize + + const parser = new ByteParser(this.#handler, parsedExtensions, { + maxFragments, + maxPayloadSize + }) parser.on('drain', () => this.#handler.onParserDrain()) parser.on('error', (err) => this.#handler.onParserError(err)) @@ -460,24 +505,20 @@ class WebSocket extends EventTarget { // 4. Fire an event named open at the WebSocket object. fireEvent('open', this) - } - - #onFail (code, reason, cause) { - if (reason) { - // TODO: process.nextTick - fireEvent('error', this, (type, init) => new ErrorEvent(type, init), { - error: new Error(reason, cause ? { cause } : undefined), - message: reason - }) - } - - if (!this.#handler.wasEverConnected) { - this.#handler.readyState = states.CLOSED - // If the WebSocket connection could not be established, it is also said - // that _The WebSocket Connection is Closed_, but not _cleanly_. - fireEvent('close', this, (type, init) => new CloseEvent(type, init), { - wasClean: false, code, reason + if (channels.open.hasSubscribers) { + // Convert headers to a plain object for the event + const headers = response.headersList.entries + channels.open.publish({ + address: getSocketAddress(response.socket), + protocol: this.#protocol, + extensions: this.#extensions, + websocket: this, + handshakeResponse: { + status: response.status, + statusText: response.statusText, + headers + } }) } } @@ -542,18 +583,11 @@ class WebSocket extends EventTarget { let code = 1005 let reason = '' - const result = this.#parser.closingInfo + const result = this.#parser?.closingInfo if (result && !result.error) { code = result.code ?? 1005 reason = result.reason - } else if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) { - // If _The WebSocket - // Connection is Closed_ and no Close control frame was received by the - // endpoint (such as could occur if the underlying transport connection - // is lost), _The WebSocket Connection Close Code_ is considered to be - // 1006. - code = 1006 } // 1. Change the ready state to CLOSED (3). @@ -563,7 +597,18 @@ class WebSocket extends EventTarget { // connection, or if the WebSocket connection was closed // after being flagged as full, fire an event named error // at the WebSocket object. - // TODO + if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) { + // If _The WebSocket + // Connection is Closed_ and no Close control frame was received by the + // endpoint (such as could occur if the underlying transport connection + // is lost), _The WebSocket Connection Close Code_ is considered to be + // 1006. + code = 1006 + + fireEvent('error', this, (type, init) => new ErrorEvent(type, init), { + error: new TypeError(reason) + }) + } // 3. Fire an event named close at the WebSocket object, // using CloseEvent, with the wasClean attribute @@ -586,8 +631,34 @@ class WebSocket extends EventTarget { }) } } + + /** + * @param {WebSocket} ws + * @param {Buffer|undefined} buffer + */ + static ping (ws, buffer) { + if (Buffer.isBuffer(buffer)) { + if (buffer.length > 125) { + throw new TypeError('A PING frame cannot have a body larger than 125 bytes.') + } + } else if (buffer !== undefined) { + throw new TypeError('Expected buffer payload') + } + + // An endpoint MAY send a Ping frame any time after the connection is + // established and before the connection is closed. + const readyState = ws.#handler.readyState + + if (isEstablished(readyState) && !isClosing(readyState) && !isClosed(readyState)) { + const frame = new WebsocketFrameSend(buffer) + ws.#handler.socket.write(frame.createFrame(opcodes.PING)) + } + } } +const { ping } = WebSocket +Reflect.deleteProperty(WebSocket, 'ping') + // https://websockets.spec.whatwg.org/#dom-websocket-connecting WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING // https://websockets.spec.whatwg.org/#dom-websocket-open @@ -646,7 +717,7 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([ { key: 'protocols', converter: webidl.converters['DOMString or sequence'], - defaultValue: () => new Array(0) + defaultValue: () => [] }, { key: 'dispatcher', @@ -673,7 +744,7 @@ webidl.converters.WebSocketSendData = function (V) { return V } - if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + if (webidl.is.BufferSource(V)) { return V } } @@ -682,5 +753,6 @@ webidl.converters.WebSocketSendData = function (V) { } module.exports = { - WebSocket + WebSocket, + ping } diff --git a/package.json b/package.json index 4d9ae38bac9..25a191b25d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "7.11.0", + "version": "7.28.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { @@ -67,35 +67,37 @@ "generate-pem": "node scripts/generate-pem.js", "lint": "eslint --cache", "lint:fix": "eslint --fix --cache", - "test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript", + "test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript", "test:javascript": "npm run test:javascript:no-jest && npm run test:jest", - "test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:cache-tests", + "test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:fetch && npm run test:node-fetch && npm run test:infra && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:cookies && npm run test:eventsource && npm run test:subresource-integrity && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:cache-tests", "test:javascript:without-intl": "npm run test:javascript:no-jest", - "test:busboy": "borp -p \"test/busboy/*.js\"", - "test:cache": "borp -p \"test/cache/*.js\"", - "test:sqlite": "NODE_OPTIONS=--experimental-sqlite borp -p \"test/cache-interceptor/*.js\"", - "test:cache-interceptor": "borp -p \"test/cache-interceptor/*.js\"", - "test:cookies": "borp -p \"test/cookie/*.js\"", - "test:eventsource": "npm run build:node && borp --expose-gc -p \"test/eventsource/*.js\"", + "test:busboy": "borp --timeout 180000 -p \"test/busboy/*.js\"", + "test:cache": "borp --timeout 180000 -p \"test/cache/*.js\"", + "test:cache-interceptor": "borp --timeout 180000 -p \"test/cache-interceptor/*.js\"", + "test:cache-interceptor:sqlite": "cross-env NODE_OPTIONS=--experimental-sqlite npm run test:cache-interceptor", + "test:cookies": "borp --timeout 180000 -p \"test/cookie/*.js\"", + "test:eventsource": "npm run build:node && borp --timeout 180000 --expose-gc -p \"test/eventsource/*.js\"", "test:fuzzing": "node test/fuzzing/fuzzing.test.js", "test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy", + "test:subresource-integrity": "borp --timeout 180000 -p \"test/subresource-integrity/*.js\"", "test:h2": "npm run test:h2:core && npm run test:h2:fetch", - "test:h2:core": "borp -p \"test/+(http2|h2)*.js\"", - "test:h2:fetch": "npm run build:node && borp -p \"test/fetch/http2*.js\"", - "test:interceptors": "borp -p \"test/interceptors/*.js\"", + "test:h2:core": "borp --timeout 180000 -p \"test/+(http2|h2)*.js\"", + "test:h2:fetch": "npm run build:node && borp --timeout 180000 -p \"test/fetch/http2*.js\"", + "test:infra": "borp --timeout 180000 -p \"test/infra/*.js\"", + "test:interceptors": "borp --timeout 180000 -p \"test/interceptors/*.js\"", "test:jest": "cross-env NODE_V8_COVERAGE= jest", - "test:unit": "borp --expose-gc -p \"test/*.js\"", - "test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"", - "test:node-test": "borp -p \"test/node-test/**/*.js\"", - "test:tdd": "borp --expose-gc -p \"test/*.js\"", - "test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w", + "test:unit": "borp --timeout 180000 --expose-gc -p \"test/*.js\"", + "test:node-fetch": "borp --timeout 180000 -p \"test/node-fetch/**/*.js\"", + "test:node-test": "borp --timeout 180000 -p \"test/node-test/**/*.js\"", + "test:tdd": "borp --timeout 180000 --expose-gc -p \"test/*.js\"", + "test:tdd:node-test": "borp --timeout 180000 -p \"test/node-test/**/*.js\" -w", "test:typescript": "tsd && tsc test/imports/undici-import.ts --typeRoots ./types --noEmit && tsc ./types/*.d.ts --noEmit --typeRoots ./types", - "test:webidl": "borp -p \"test/webidl/*.js\"", - "test:websocket": "borp -p \"test/websocket/*.js\"", + "test:webidl": "borp --timeout 180000 -p \"test/webidl/*.js\"", + "test:websocket": "borp --timeout 180000 -p \"test/websocket/**/*.js\"", "test:websocket:autobahn": "node test/autobahn/client.js", "test:websocket:autobahn:report": "node test/autobahn/report.js", - "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs", - "test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs", + "test:wpt:setup": "node test/web-platform-tests/wpt-runner.mjs setup", + "test:wpt": "npm run test:wpt:setup && node test/web-platform-tests/wpt-runner.mjs run /fetch /mimesniff /xhr /websockets /serviceWorkers /eventsource", "test:cache-tests": "node test/cache-interceptor/cache-tests.mjs --ci", "coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report", "coverage:ci": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report:ci", @@ -107,26 +109,27 @@ "prepare": "husky && node ./scripts/platform-shell.js" }, "devDependencies": { - "@fastify/busboy": "3.1.1", + "@fastify/busboy": "3.2.0", "@matteo.collina/tspl": "^0.2.0", + "@metcoder95/https-pem": "^1.0.0", "@sinonjs/fake-timers": "^12.0.0", - "@types/node": "^18.19.50", + "@types/node": "^20.19.22", "abort-controller": "^3.0.0", "borp": "^0.20.0", "c8": "^10.0.0", - "cross-env": "^7.0.3", + "cross-env": "^10.0.0", "dns-packet": "^5.4.0", - "esbuild": "^0.25.2", + "esbuild": "^0.27.3", "eslint": "^9.9.0", "fast-check": "^4.1.1", - "https-pem": "^3.0.0", "husky": "^9.0.7", - "jest": "^29.0.2", + "jest": "^30.0.5", + "jsondiffpatch": "^0.7.3", "neostandard": "^0.12.0", "node-forge": "^1.3.1", "proxy": "^2.1.1", - "tsd": "^0.32.0", - "typescript": "^5.6.2", + "tsd": "^0.33.0", + "typescript": "^6.0.2", "ws": "^8.11.0" }, "engines": { diff --git a/scripts/generate-pem.js b/scripts/generate-pem.js index 88ac5c8392f..3e403020a9d 100644 --- a/scripts/generate-pem.js +++ b/scripts/generate-pem.js @@ -1,4 +1,3 @@ 'use strict' -/* istanbul ignore file */ -require('https-pem/install') +require('@metcoder95/https-pem/install') diff --git a/scripts/release.js b/scripts/release.js index dd3e86eb5dc..25922f46010 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -2,7 +2,7 @@ // Called from .github/workflows -const generateReleaseNotes = async ({ github, owner, repo, versionTag, defaultBranch }) => { +const generateReleaseNotes = async ({ github, owner, repo, versionTag, releaseBranch, commitHash }) => { const { data: releases } = await github.rest.repos.listReleases({ owner, repo @@ -14,7 +14,7 @@ const generateReleaseNotes = async ({ github, owner, repo, versionTag, defaultBr owner, repo, tag_name: versionTag, - target_commitish: defaultBranch, + target_commitish: commitHash ?? releaseBranch, previous_tag_name: previousRelease?.tag_name }) @@ -25,33 +25,35 @@ const generateReleaseNotes = async ({ github, owner, repo, versionTag, defaultBr return bodyWithoutReleasePr } -const generatePr = async ({ github, context, defaultBranch, versionTag }) => { +const generatePr = async ({ github, context, releaseBranch, versionTag, commitHash }) => { const { owner, repo } = context.repo - const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, defaultBranch }) + const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, releaseBranch, commitHash }) await github.rest.pulls.create({ owner, repo, head: `release/${versionTag}`, - base: defaultBranch, + base: releaseBranch, title: `[Release] ${versionTag}`, body: releaseNotes }) } -const release = async ({ github, context, defaultBranch, versionTag }) => { +const release = async ({ github, context, releaseBranch, versionTag, commitHash }) => { const { owner, repo } = context.repo - const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, defaultBranch }) + const releaseNotes = await generateReleaseNotes({ github, owner, repo, versionTag, releaseBranch, commitHash }) + const makeLatest = releaseBranch === 'v7.x' ? 'false' : 'legacy' await github.rest.repos.createRelease({ owner, repo, tag_name: versionTag, - target_commitish: defaultBranch, + target_commitish: commitHash ?? releaseBranch, name: versionTag, body: releaseNotes, draft: false, prerelease: false, + make_latest: makeLatest, generate_release_notes: false }) diff --git a/scripts/verifyVersion.js b/scripts/verifyVersion.js deleted file mode 100644 index dbaafa245d5..00000000000 --- a/scripts/verifyVersion.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict' - -/* istanbul ignore file */ - -const [major, minor, patch] = process.versions.node.split('.').map(v => Number(v)) -const required = process.argv.pop().split('.').map(v => Number(v)) - -const badMajor = major < required[0] -const badMinor = major === required[0] && minor < required[1] -const badPatch = major === required[0] && minor === required[1] && patch < required[2] - -if (badMajor || badMinor || badPatch) { - console.log(`Required Node.js >=${required.join('.')}, got ${process.versions.node}`) - console.log('Skipping') -} else { - process.exit(1) -} diff --git a/test/autobahn/client.js b/test/autobahn/client.js index 3f013922839..485b7e6742f 100644 --- a/test/autobahn/client.js +++ b/test/autobahn/client.js @@ -48,5 +48,5 @@ ws.addEventListener('close', () => { }) ws.addEventListener('error', (e) => { console.error(e.error) - process.exit(1) + process.exitCode = 1 }) diff --git a/test/autobahn/report.js b/test/autobahn/report.js index 7dcae5fed7e..9ba1922f4bc 100644 --- a/test/autobahn/report.js +++ b/test/autobahn/report.js @@ -103,4 +103,4 @@ console.log('Details:') console.table(reorderedResult) -process.exit(runFailed ? 1 : 0) +process.exitCode = runFailed ? 1 : 0 diff --git a/test/busboy/formdata-test.js b/test/busboy/formdata-test.js new file mode 100644 index 00000000000..78aef46709b --- /dev/null +++ b/test/busboy/formdata-test.js @@ -0,0 +1,387 @@ +'use strict' + +// Copyright 2009 The Go Authors. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google LLC nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// https://github.com/golang/go/blob/16e5d24480dca7ddcbdffb78a8ed5de3e5155dec/src/mime/multipart/formdata_test.go + +const { test } = require('node:test') +const { Response } = require('../..') + +const fileaContents = 'This is a test file.' +const filebContents = 'Another test file.' +const textaValue = 'foo' +const textbValue = 'bar' +const boundary = 'MyBoundary' + +const message = [ + '--MyBoundary', + 'Content-Disposition: form-data; name="filea"; filename="filea.txt"', + 'Content-Type: text/plain', + '', + fileaContents, + '--MyBoundary', + 'Content-Disposition: form-data; name="fileb"; filename="fileb.txt"', + 'Content-Type: text/plain', + '', + filebContents, + '--MyBoundary', + 'Content-Disposition: form-data; name="texta"', + '', + textaValue, + '--MyBoundary', + 'Content-Disposition: form-data; name="textb"', + '', + textbValue, + '--MyBoundary--' +].join('\r\n') + +const messageWithFileWithoutName = [ + '--MyBoundary', + 'Content-Disposition: form-data; name="hiddenfile"; filename=""', + 'Content-Type: text/plain', + '', + filebContents, + '--MyBoundary--' +].join('\r\n') + +const messageWithFileName = [ + '--MyBoundary', + 'Content-Disposition: form-data; name="filea"; filename="filea.txt"', + 'Content-Type: text/plain', + '', + fileaContents, + '--MyBoundary--' +].join('\r\n') + +const messageWithTextContentType = [ + '--MyBoundary', + 'Content-Disposition: form-data; name="texta"', + 'Content-Type: text/plain', + '', + textaValue, + '--MyBoundary--' +].join('\r\n') + +function makeResponse (body, bnd) { + return new Response(body, { + headers: { + 'content-type': `multipart/form-data; boundary=${bnd}` + } + }) +} + +test('ReadForm - fields and files', async (t) => { + const response = makeResponse(message, boundary) + const fd = await response.formData() + + t.assert.strictEqual(fd.get('texta'), textaValue, 'texta value mismatch') + t.assert.strictEqual(fd.get('textb'), textbValue, 'textb value mismatch') + + const filea = fd.get('filea') + t.assert.strictEqual(filea.name, 'filea.txt', 'filea filename mismatch') + t.assert.strictEqual(filea.size, fileaContents.length, 'filea size mismatch') + t.assert.strictEqual(await filea.text(), fileaContents, 'filea contents mismatch') + + const fileb = fd.get('fileb') + t.assert.strictEqual(fileb.name, 'fileb.txt', 'fileb filename mismatch') + t.assert.strictEqual(fileb.size, filebContents.length, 'fileb size mismatch') + t.assert.strictEqual(await fileb.text(), filebContents, 'fileb contents mismatch') +}) + +test('ReadForm - file without name', async (t) => { + const response = makeResponse(messageWithFileWithoutName, boundary) + const fd = await response.formData() + + // A file with an empty filename is treated as a field in the web platform FormData + const value = fd.get('hiddenfile') + if (typeof value === 'string') { + t.assert.strictEqual(value, filebContents, 'hiddenfile value mismatch') + } else { + t.assert.strictEqual(await value.text(), filebContents, 'hiddenfile contents mismatch') + } +}) + +test('ReadForm - file with filename', async (t) => { + const response = makeResponse(messageWithFileName, boundary) + const fd = await response.formData() + + const filea = fd.get('filea') + t.assert.strictEqual(filea.name, 'filea.txt', 'filea filename mismatch') + t.assert.strictEqual(await filea.text(), fileaContents, 'filea contents mismatch') +}) + +test('ReadForm - text content type', async (t) => { + const response = makeResponse(messageWithTextContentType, boundary) + const fd = await response.formData() + + t.assert.strictEqual(fd.get('texta'), textaValue, 'texta value mismatch') +}) + +test('ReadForm - no read after EOF', async (t) => { + const eofBoundary = '---------------------------8d345eef0d38dc9' + const body = [ + '-----------------------------8d345eef0d38dc9', + 'Content-Disposition: form-data; name="version"', + '', + '171', + '-----------------------------8d345eef0d38dc9--' + ].join('\r\n') + + const response = makeResponse(body, eofBoundary) + const fd = await response.formData() + + t.assert.strictEqual(fd.get('version'), '171', 'version value mismatch') +}) + +test('ReadForm - non-file max memory (large text value)', async (t) => { + const n = 10 << 20 // 10 MB + const largeTextValue = '1'.repeat(n) + const body = [ + '--MyBoundary', + 'Content-Disposition: form-data; name="largetext"', + '', + largeTextValue, + '--MyBoundary--' + ].join('\r\n') + + const response = makeResponse(body, boundary) + const fd = await response.formData() + + t.assert.strictEqual(fd.get('largetext'), largeTextValue, 'largetext value mismatch') +}) + +test('ReadForm - many files', async (t) => { + const numFiles = 10 + const parts = [] + const fileBoundary = 'TestBoundary' + + for (let i = 0; i < numFiles; i++) { + parts.push( + `--${fileBoundary}`, + `Content-Disposition: form-data; name="${i}"; filename="${i}"`, + 'Content-Type: application/octet-stream', + '', + `${i}` + ) + } + parts.push(`--${fileBoundary}--`) + const body = parts.join('\r\n') + + const response = makeResponse(body, fileBoundary) + const fd = await response.formData() + + for (let i = 0; i < numFiles; i++) { + const file = fd.get(`${i}`) + t.assert.ok(file, `file "${i}" should exist`) + t.assert.strictEqual(file.name, `${i}`, `file "${i}" filename mismatch`) + const text = await file.text() + t.assert.strictEqual(text, `${i}`, `file "${i}" contents mismatch`) + } +}) + +test('ReadForm - limits with many values', async (t) => { + const numValues = 100 + const parts = [] + const limitBoundary = 'LimitBoundary' + + for (let i = 0; i < numValues; i++) { + parts.push( + `--${limitBoundary}`, + `Content-Disposition: form-data; name="field${i}"`, + '', + `value ${i}` + ) + } + parts.push(`--${limitBoundary}--`) + const body = parts.join('\r\n') + + const response = makeResponse(body, limitBoundary) + const fd = await response.formData() + + for (let i = 0; i < numValues; i++) { + t.assert.strictEqual( + fd.get(`field${i}`), + `value ${i}`, + `field${i} value mismatch` + ) + } +}) + +test('ReadForm - limits with values and files', async (t) => { + const numValues = 50 + const numFiles = 50 + const parts = [] + const limitBoundary = 'LimitBoundary' + + for (let i = 0; i < numValues; i++) { + parts.push( + `--${limitBoundary}`, + `Content-Disposition: form-data; name="field${i}"`, + '', + `value ${i}` + ) + } + for (let i = 0; i < numFiles; i++) { + parts.push( + `--${limitBoundary}`, + `Content-Disposition: form-data; name="file${i}"; filename="file${i}"`, + 'Content-Type: application/octet-stream', + '', + `value ${i}` + ) + } + parts.push(`--${limitBoundary}--`) + const body = parts.join('\r\n') + + const response = makeResponse(body, limitBoundary) + const fd = await response.formData() + + for (let i = 0; i < numValues; i++) { + t.assert.strictEqual( + fd.get(`field${i}`), + `value ${i}`, + `field${i} value mismatch` + ) + } + for (let i = 0; i < numFiles; i++) { + const file = fd.get(`file${i}`) + t.assert.ok(file, `file${i} should exist`) + const text = await file.text() + t.assert.strictEqual(text, `value ${i}`, `file${i} contents mismatch`) + } +}) + +test('ReadForm - metadata too large (large field name)', async (t) => { + const largeName = 'a'.repeat(10 << 20) + const body = [ + '--MyBoundary', + `Content-Disposition: form-data; name="${largeName}"`, + '', + 'value', + '--MyBoundary--' + ].join('\r\n') + + const response = makeResponse(body, boundary) + + // Expect parsing to either succeed (implementation dependent) or throw + try { + const fd = await response.formData() + // If it succeeds, the value should be correct + t.assert.strictEqual(fd.get(largeName), 'value') + } catch (err) { + // Implementation may reject overly large metadata + t.assert.ok(err, 'error thrown for large metadata') + } +}) + +test('ReadForm - metadata too large (large MIME header)', async (t) => { + const largeHeaderValue = 'a'.repeat(10 << 20) + const body = [ + '--MyBoundary', + 'Content-Disposition: form-data; name="a"', + `X-Foo: ${largeHeaderValue}`, + '', + 'value', + '--MyBoundary--' + ].join('\r\n') + + const response = makeResponse(body, boundary) + + try { + const fd = await response.formData() + t.assert.strictEqual(fd.get('a'), 'value') + } catch (err) { + t.assert.ok(err, 'error thrown for large MIME header') + } +}) + +test('ReadForm - metadata too large (many parts)', async (t) => { + const parts = [] + const numParts = 110000 + + for (let i = 0; i < numParts; i++) { + parts.push( + '--MyBoundary', + 'Content-Disposition: form-data; name="f"', + '', + 'v' + ) + } + parts.push('--MyBoundary--') + const body = parts.join('\r\n') + + const response = makeResponse(body, boundary) + + try { + const fd = await response.formData() + // If it succeeds, check that values are present + t.assert.ok(fd.getAll('f').length > 0) + } catch (err) { + // Implementation may reject too many parts + t.assert.ok(err, 'error thrown for too many parts') + } +}) + +test('ReadForm - endless header line (name)', async (t) => { + // Create a body with an extremely long header name that never ends + const longPrefix = 'X-' + 'X'.repeat(1 << 20) + const body = [ + '--MyBoundary', + 'Content-Disposition: form-data; name="a"', + 'Content-Type: text/plain', + longPrefix + ].join('\r\n') + + const response = makeResponse(body, boundary) + + try { + await response.formData() + // If parsing succeeds (truncated or ignored), that's acceptable + } catch (err) { + t.assert.ok(err, 'error thrown for endless header line') + } +}) + +test('ReadForm - endless header line (value)', async (t) => { + const longValue = 'X-Header: ' + 'X'.repeat(1 << 20) + const body = [ + '--MyBoundary', + 'Content-Disposition: form-data; name="a"', + 'Content-Type: text/plain', + longValue + ].join('\r\n') + + const response = makeResponse(body, boundary) + + try { + await response.formData() + } catch (err) { + t.assert.ok(err, 'error thrown for endless header value') + } +}) diff --git a/test/busboy/issue-3676.js b/test/busboy/issue-3676.js index 4b74af88767..a73de7265e1 100644 --- a/test/busboy/issue-3676.js +++ b/test/busboy/issue-3676.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { Response } = require('../..') // https://github.com/nodejs/undici/issues/3676 @@ -20,5 +19,5 @@ test('Leading and trailing CRLFs are ignored', async (t) => { } }) - await assert.doesNotReject(response.formData()) + await t.assert.doesNotReject(response.formData()) }) diff --git a/test/busboy/issue-3760.js b/test/busboy/issue-3760.js index d37852c8fac..f7e1d8290c1 100644 --- a/test/busboy/issue-3760.js +++ b/test/busboy/issue-3760.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { Response } = require('../..') // https://github.com/nodejs/undici/issues/3760 @@ -21,10 +20,10 @@ test('filename* parameter is parsed properly', async (t) => { }) const fd = await response.formData() - assert.deepEqual(fd.get('file').name, '€ rates') + t.assert.deepEqual(fd.get('file').name, '€ rates') }) -test('whitespace after filename[*]= is ignored', async () => { +test('whitespace after filename[*]= is ignored', async (t) => { for (const response of [ new Response([ '--83d82e0d-9ced-44c0-ac79-4e66a827415b\r\n' + @@ -54,6 +53,6 @@ test('whitespace after filename[*]= is ignored', async () => { }) ]) { const fd = await response.formData() - assert.deepEqual(fd.get('file').name, 'hello') + t.assert.deepEqual(fd.get('file').name, 'hello') } }) diff --git a/test/busboy/issue-4660.js b/test/busboy/issue-4660.js new file mode 100644 index 00000000000..cba98ef3468 --- /dev/null +++ b/test/busboy/issue-4660.js @@ -0,0 +1,62 @@ +'use strict' + +const { test } = require('node:test') +const { Request } = require('../..') + +// https://github.com/nodejs/undici/issues/4660 +test('unquoted attributes are parsed correctly', async (t) => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data; boundary=7427efb8-6ce7-4740-9198-d90399842641' + }, + body: + '--7427efb8-6ce7-4740-9198-d90399842641\r\n' + + 'Content-Type: text/plain; charset=utf-8\r\n' + + 'Content-Disposition: form-data; name=test\r\n' + + '\r\n' + + 'abc\r\n' + + '--7427efb8-6ce7-4740-9198-d90399842641--' + }) + + const fd = await request.formData() + t.assert.deepEqual(fd.get('test'), 'abc') +}) + +test('leading spaces are allowed with quoted strings', async (t) => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data; boundary=7427efb8-6ce7-4740-9198-d90399842641' + }, + body: + '--7427efb8-6ce7-4740-9198-d90399842641\r\n' + + 'Content-Type: text/plain; charset=utf-8\r\n' + + 'Content-Disposition: form-data; name= "test"\r\n' + // <-- space between attribute name and value + '\r\n' + + 'abc\r\n' + + '--7427efb8-6ce7-4740-9198-d90399842641--' + }) + + const fd = await request.formData() + t.assert.deepEqual(fd.get('test'), 'abc') +}) + +test('leading spaces are allowed & ignored in unquoted strings', async (t) => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data; boundary=7427efb8-6ce7-4740-9198-d90399842641' + }, + body: + '--7427efb8-6ce7-4740-9198-d90399842641\r\n' + + 'Content-Type: text/plain; charset=utf-8\r\n' + + 'Content-Disposition: form-data; name= test\r\n' + // <-- space between attribute name and value + '\r\n' + + 'abc\r\n' + + '--7427efb8-6ce7-4740-9198-d90399842641--' + }) + + const fd = await request.formData() + t.assert.deepEqual(fd.get('test'), 'abc') +}) diff --git a/test/busboy/issue-4661.js b/test/busboy/issue-4661.js new file mode 100644 index 00000000000..112edcb79c4 --- /dev/null +++ b/test/busboy/issue-4661.js @@ -0,0 +1,102 @@ +'use strict' + +const { test } = require('node:test') +const { Request } = require('../..') + +const boundary = '1df6c75e-c5a7-486c-af47-67b632b19522' +const contentType = `multipart/form-data; boundary=${boundary}` + +// https://github.com/nodejs/undici/issues/4661 +test('content-disposition allows both filename and filename* parameters', async (t) => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': contentType }, + body: + `--${boundary}\r\n` + + 'Content-Disposition: form-data; name="Abc"; filename="E 1962 029 1342-0003.JPG"; filename*=utf-8\'\'E%201962%20029%201342-0003.JPG\r\n' + + '\r\n' + + 'Hello\r\n' + + `--${boundary}--` + }) + + const fd = await request.formData() + const file = fd.get('Abc') + t.assert.ok(file instanceof File) + t.assert.strictEqual(file.name, 'E 1962 029 1342-0003.JPG') + t.assert.strictEqual(await file.text(), 'Hello') +}) + +test('filename* (RFC 5987) without legacy filename is parsed correctly', async (t) => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': contentType }, + body: + `--${boundary}\r\n` + + 'Content-Disposition: form-data; name="Abc"; filename*=utf-8\'\'only-extended.txt\r\n' + + '\r\n' + + 'Hello\r\n' + + `--${boundary}--` + }) + + const fd = await request.formData() + const file = fd.get('Abc') + t.assert.ok(file instanceof File) + t.assert.strictEqual(file.name, 'only-extended.txt') +}) + +test('filename* takes precedence over filename when both are present', async (t) => { + // Per RFC 5987 §4.1, when both extended and non-extended forms of the + // same parameter appear, the extended form is preferred regardless of order. + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': contentType }, + body: + `--${boundary}\r\n` + + 'Content-Disposition: form-data; name="Abc"; filename*=utf-8\'\'extended.txt; filename="legacy.txt"\r\n' + + '\r\n' + + 'Hello\r\n' + + `--${boundary}--` + }) + + const fd = await request.formData() + const file = fd.get('Abc') + t.assert.ok(file instanceof File) + t.assert.strictEqual(file.name, 'extended.txt') +}) + +test('filename* takes precedence when filename appears first', async (t) => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': contentType }, + body: + `--${boundary}\r\n` + + 'Content-Disposition: form-data; name="Abc"; filename="legacy.txt"; filename*=utf-8\'\'extended.txt\r\n' + + '\r\n' + + 'Hello\r\n' + + `--${boundary}--` + }) + + const fd = await request.formData() + const file = fd.get('Abc') + t.assert.ok(file instanceof File) + t.assert.strictEqual(file.name, 'extended.txt') +}) + +test('filename* with percent-encoded UTF-8 bytes is decoded', async (t) => { + // %E2%82%AC = U+20AC (€), %20 = space + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': contentType }, + body: + `--${boundary}\r\n` + + 'Content-Disposition: form-data; name="Abc"; filename*=UTF-8\'\'%E2%82%AC%20rates.txt\r\n' + + '\r\n' + + 'Hello\r\n' + + `--${boundary}--` + }) + + const fd = await request.formData() + const file = fd.get('Abc') + t.assert.ok(file instanceof File) + t.assert.strictEqual(file.name, '\u20AC rates.txt') +}) diff --git a/test/busboy/issue-4671.js b/test/busboy/issue-4671.js new file mode 100644 index 00000000000..4786d44157b --- /dev/null +++ b/test/busboy/issue-4671.js @@ -0,0 +1,42 @@ +'use strict' + +const { test } = require('node:test') +const { Request, Response, FormData } = require('../..') + +// https://github.com/nodejs/undici/issues/4671 +test('preamble and epilogue is ignored', async (t) => { + const request = new Request('https://example.com', { + method: 'POST', + body: (function () { + const formData = new FormData() + formData.append('a', 'b') + return formData + })() + }) + + const contentType = request.headers.get('Content-Type') + let bytes = await request.bytes() + bytes = new Uint8Array([...bytes, ...Array(10).fill(0)]) + + await t.test('epilogue', async () => { + await new Response(bytes, { + headers: { + 'Content-Type': contentType + } + }).formData() + }) + + await t.test('preamble', async () => { + // preamble + bytes.set(bytes.subarray(0, -10), 10) + bytes.fill(0, 0, 8) + bytes[8] = 13 + bytes[9] = 10 + + await new Response(bytes, { + headers: { + 'Content-Type': contentType + } + }).formData() + }) +}) diff --git a/test/busboy/test-parser.js b/test/busboy/test-parser.js new file mode 100644 index 00000000000..b5f4b725f59 --- /dev/null +++ b/test/busboy/test-parser.js @@ -0,0 +1,712 @@ +'use strict' + +// Copyright (c) 2010-2025, Marcel Hellkamp +// +// 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. + +// https://github.com/defnull/multipart/blob/d390c3a98a619f4445d7f340d6022b0ccad2ca8e/test/test_push_parser.py + +const { test } = require('node:test') +const { Response } = require('../..') + +function b64decode (input) { + return Buffer.from(input, 'base64').toString() +} + +function makeResponse (body, bnd) { + return new Response(body, { + headers: { + 'content-type': `multipart/form-data; boundary=${bnd}` + } + }) +} + +const legacyTests = { + 'firefox3-2png1txt': [ + '-----------------------------186454651713519341951581030105\r\n', + 'Content-Disposition: form-data; name="file1"; filename="anchor.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file1-data]\r\n', + '-----------------------------186454651713519341951581030105\r\n', + 'Content-Disposition: form-data; name="file2"; filename="application_edit.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file2-data]\r\n', + '-----------------------------186454651713519341951581030105\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '-----------------------------186454651713519341951581030105--\r\n' + ], + 'firefox3-2pnglongtext': [ + '-----------------------------14904044739787191031754711748\r\n', + 'Content-Disposition: form-data; name="file1"; filename="accept.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file1-data]\r\n', + '-----------------------------14904044739787191031754711748\r\n', + 'Content-Disposition: form-data; name="file2"; filename="add.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file2-data]\r\n', + '-----------------------------14904044739787191031754711748\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '-----------------------------14904044739787191031754711748--\r\n' + ], + 'opera8-2png1txt': [ + '------------zEO9jQKmLc2Cq88c23Dx19\r\n', + 'Content-Disposition: form-data; name="file1"; filename="arrow_branch.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file1-data]\r\n', + '------------zEO9jQKmLc2Cq88c23Dx19\r\n', + 'Content-Disposition: form-data; name="file2"; filename="award_star_bronze_1.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file2-data]\r\n', + '------------zEO9jQKmLc2Cq88c23Dx19\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '------------zEO9jQKmLc2Cq88c23Dx19--\r\n' + ], + 'webkit3-2png1txt': [ + '------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n', + 'Content-Disposition: form-data; name="file1"; filename="gtk-apply.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file1-data]\r\n', + '------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n', + 'Content-Disposition: form-data; name="file2"; filename="gtk-no.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file2-data]\r\n', + '------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '------WebKitFormBoundaryjdSFhcARk8fyGNy6--\r\n' + ], + 'ie6-2png1txt': [ + '-----------------------------7d91b03a20128\r\n', + 'Content-Disposition: form-data; name="file1"; filename="C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file1.png"\r\n', + 'Content-Type: image/x-png\r\n', + '\r\n', + '[file1-data]\r\n', + '-----------------------------7d91b03a20128\r\n', + 'Content-Disposition: form-data; name="file2"; filename="C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file2.png"\r\n', + 'Content-Type: image/x-png\r\n', + '\r\n', + '[file2-data]\r\n', + '-----------------------------7d91b03a20128\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '-----------------------------7d91b03a20128--\r\n' + ] +} + +const browserTestCases = { + 'firefox3-2png1txt': { + data: b64decode( + ` +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xODY0NTQ2NTE3MTM1MTkzNDE5NTE1ODEwMzAx +MDUNCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0iZmlsZTEiOyBmaWxlbmFt +ZT0iYW5jaG9yLnBuZyINCkNvbnRlbnQtVHlwZTogaW1hZ2UvcG5nDQoNColQTkcNChoKAAAADUlI +RFIAAAAQAAAAEAgGAAAAH/P/YQAAAARnQU1BAACvyDcFiukAAAAZdEVYdFNvZnR3YXJlAEFkb2Jl +IEltYWdlUmVhZHlxyWU8AAABnUlEQVQ4y6VTMWvCQBS+qwEFB10KGaS1P6FDpw7SrVvzAwRRx04V +Ck4K6iAoDhLXdhFcW9qhZCk4FQoW0gp2U4lQRDAUS4hJmn5Xgg2lsQ198PHu3b3vu5d3L9S2bfIf +47wOer1ewzTNtGEYBP48kUjkfsrb8BIAMb1cLovwRfi07wrYzcCr4/1/Am4FzzhzBGZeefR7E7vd +7j0Iu4wYjUYDBMfD0dBiMUQfstns3toKkHgF6EgmqqruW6bFiHcsxr70awVu63Q6NiOmUinquwfM +dF1f28CVgCRJx0jMAQ1BEFquRn7CbYVCYZVbr9dbnJMohoIh9kViu90WEW9nMpmxu4JyubyF/VEs +FiNcgCPyoyxiu7XhCPBzdU4s652VnUccbDabPLyN2C6VSmwdhFgel5DB84AJb64mEUlvmqadTKcv +40gkUkUsg1DjeZ7iRsrWgByP71T7/afxYrHIYry/eoBD9mxsaK4VRamFw2EBQknMAWGvRClNTpQJ +AfkCxFNgBmiez1ipVA4hdgQcOD/TLfylKIo3vubgL/YBnIw+ioOMLtwAAAAASUVORK5CYIINCi0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tMTg2NDU0NjUxNzEzNTE5MzQxOTUxNTgxMDMwMTA1 +DQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUyIjsgZmlsZW5hbWU9 +ImFwcGxpY2F0aW9uX2VkaXQucG5nIg0KQ29udGVudC1UeXBlOiBpbWFnZS9wbmcNCg0KiVBORw0K +GgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdh +cmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJRSURBVBgZpcHda81xHMDx9+d3fudYzuYw2RaZ5yTW +olEiuZpCSjGJFEktUUr8A6ZxQZGHmDtqdrGUXHgoeZqSp1F2bLFWjtkOB8PZzvmd7+djv5XaBRfL +6yVmxv+QjQeu7l25uuZYJmtxM0AVU8Wpw9RQU8w51AxzDqfKhFjwq6Mjdbj1RN0Zv2ZFzaloUdwr +L2Is4r+y7hRwxs8G5mUzPxmrwcA8hvnmjIZtcxmr3Y09hHwzJZQvOAwwNZyCYqgaThVXMFzBCD7f +Jfv8MpHiKvaV3ePV2f07fMwIiSeIGeYJJoao4HmCiIeIQzPXifY+paJqO4lZi/nWPZ/krabjvlNH +yANMBAQiBiqgakQMCunbxHJviM9bQeZdBzHJUzKhguLJlQnf1BghAmZ4gImAgAjk++8jP56QmL2G +XG8zsfFCz8skA1mQXKbaU3X8ISIgQsgDcun7FL7cJjFnLUMfLyLRr0SLS4hbhiup5Szd19rpFYKA +ESKICCERoS95neyHmyTmbmAodQ4vGpAfmEn6YTtTahv4ODiRkGdOCUUAAUSE/uQNfqTaKFu4jvyn +JiIxIzcwg/SjF1RsOk9R+QJMlZCvqvwhQFdbM4XvrynIVHpfn2ZSWYyhzHS+PUtSueUC0cQ0QmpG +yE9197TUnwzq1DnUKbXSxOb6S7xtPkjngzbGVVbzvS/FjaGt9DU8xlRRJdTCMDEzRjuyZ1FwaFe9 +j+d4eecaPd1dPxNTSlfWHm1v5y/EzBitblXp4JLZ5f6yBbOwaK5tsD+9c33jq/f8w2+mRSjOllPh +kAAAAABJRU5ErkJggg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xODY0NTQ2NTE3MTM1 +MTkzNDE5NTE1ODEwMzAxMDUNCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0i +dGV4dCINCg0KZXhhbXBsZSB0ZXh0DQotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTE4NjQ1 +NDY1MTcxMzUxOTM0MTk1MTU4MTAzMDEwNS0tDQo=` + ), + boundary: '---------------------------186454651713519341951581030105', + files: { + file1: [ + 'anchor.png', + 'image/png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 +U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGdSURBVDjLpVMxa8JAFL6rAQUHXQoZpLU/ +oUOnDtKtW/MDBFHHThUKTgrqICgOEtd2EVxb2qFkKTgVChbSCnZTiVBEMBRLiEmafleCDaWxDX3w +8e7dve+7l3cv1LZt8h/jvA56vV7DNM20YRgE/jyRSOR+ytvwEgAxvVwui/BF+LTvCtjNwKvj/X8C +bgXPOHMEZl559HsTu93uPQi7jBiNRgMEx8PR0GIxRB+y2eze2gqQeAXoSCaqqu5bpsWIdyzGvvRr +BW7rdDo2I6ZSKeq7B8x0XV/bwJWAJEnHSMwBDUEQWq5GfsJthUJhlVuv11uckyiGgiH2RWK73RYR +b2cymbG7gnK5vIX9USwWI1yAI/KjLGK7teEI8HN1TizrnZWdRxxsNps8vI3YLpVKbB2EWB6XkMHz +gAlvriYRSW+app1Mpy/jSCRSRSyDUON5nuJGytaAHI/vVPv9p/FischivL96gEP2bGxorhVFqYXD +YQFCScwBYa9EKU1OlAkB+QLEU2AGaJ7PWKlUDiF2BBw4P9Mt/KUoije+5uAv9gGcjD6Kg4wu3AAA +AABJRU5ErkJggg==` + ) + ], + file2: [ + 'application_edit.png', + 'image/png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 +U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJRSURBVBgZpcHda81xHMDx9+d3fudYzuYw +2RaZ5yTWolEiuZpCSjGJFEktUUr8A6ZxQZGHmDtqdrGUXHgoeZqSp1F2bLFWjtkOB8PZzvmd7+dj +v5XaBRfL6yVmxv+QjQeu7l25uuZYJmtxM0AVU8Wpw9RQU8w51AxzDqfKhFjwq6Mjdbj1RN0Zv2ZF +zaloUdwrL2Is4r+y7hRwxs8G5mUzPxmrwcA8hvnmjIZtcxmr3Y09hHwzJZQvOAwwNZyCYqgaThVX +MFzBCD7fJfv8MpHiKvaV3ePV2f07fMwIiSeIGeYJJoao4HmCiIeIQzPXifY+paJqO4lZi/nWPZ/k +rabjvlNHyANMBAQiBiqgakQMCunbxHJviM9bQeZdBzHJUzKhguLJlQnf1BghAmZ4gImAgAjk++8j +P56QmL2GXG8zsfFCz8skA1mQXKbaU3X8ISIgQsgDcun7FL7cJjFnLUMfLyLRr0SLS4hbhiup5Szd +19rpFYKAESKICCERoS95neyHmyTmbmAodQ4vGpAfmEn6YTtTahv4ODiRkGdOCUUAAUSE/uQNfqTa +KFu4jvynJiIxIzcwg/SjF1RsOk9R+QJMlZCvqvwhQFdbM4XvrynIVHpfn2ZSWYyhzHS+PUtSueUC +0cQ0QmpGyE9197TUnwzq1DnUKbXSxOb6S7xtPkjngzbGVVbzvS/FjaGt9DU8xlRRJdTCMDEzRjuy +Z1FwaFe9j+d4eecaPd1dPxNTSlfWHm1v5y/EzBitblXp4JLZ5f6yBbOwaK5tsD+9c33jq/f8w2+m +RSjOllPhkAAAAABJRU5ErkJggg==` + ) + ] + }, + forms: { text: 'example text' } + }, + + 'firefox3-2pnglongtext': { + data: b64decode( + ` +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xNDkwNDA0NDczOTc4NzE5MTAzMTc1NDcxMTc0 +OA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJmaWxlMSI7IGZpbGVuYW1l +PSJhY2NlcHQucG5nIg0KQ29udGVudC1UeXBlOiBpbWFnZS9wbmcNCg0KiVBORw0KGgoAAAANSUhE +UgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUg +SW1hZ2VSZWFkeXHJZTwAAAKfSURBVDjLpZPrS1NhHMf9O3bOdmwDCWREIYKEUHsVJBI7mg3FvCxL +09290jZj2EyLMnJexkgpLbPUanNOberU5taUMnHZUULMvelCtWF0sW/n7MVMEiN64AsPD8/n83uu +cQDi/id/DBT4Dolypw/qsz0pTMbj/WHpiDgsdSUyUmeiPt2+V7SrIM+bSss8ySGdR4abQQv6lrui +6VxsRonrGCS9VEjSQ9E7CtiqdOZ4UuTqnBHO1X7YXl6Daa4yGq7vWO1D40wVDtj4kWQbn94myPGk +CDPdSesczE2sCZShwl8CzcwZ6NiUs6n2nYX99T1cnKqA2EKui6+TwphA5k4yqMayopU5mANV3lNQ +TBdCMVUA9VQh3GuDMHiVcLCS3J4jSLhCGmKCjBEx0xlshjXYhApfMZRP5CyYD+UkG08+xt+4wLVQ +ZA1tzxthm2tEfD3JxARH7QkbD1ZuozaggdZbxK5kAIsf5qGaKMTY2lAU/rH5HW3PLsEwUYy+YCcE +RmIjJpDcpzb6l7th9KtQ69fi09ePUej9l7cx2DJbD7UrG3r3afQHOyCo+V3QQzE35pvQvnAZukk5 +zL5qRL59jsKbPzdheXoBZc4saFhBS6AO7V4zqCpiawuptwQG+UAa7Ct3UT0hh9p9EnXT5Vh6t4C2 +2QaUDh6HwnECOmcO7K+6kW49DKqS2DrEZCtfuI+9GrNHg4fMHVSO5kE7nAPVkAxKBxcOzsajpS4Y +h4ohUPPWKTUh3PaQEptIOr6BiJjcZXCwktaAGfrRIpwblqOV3YKdhfXOIvBLeREWpnd8ynsaSJoy +ESFphwTtfjN6X1jRO2+FxWtCWksqBApeiFIR9K6fiTpPiigDoadqCEag5YUFKl6Yrciw0VOlhOiv +v/Ff8wtn0KzlebrUYwAAAABJRU5ErkJggg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0x +NDkwNDA0NDczOTc4NzE5MTAzMTc1NDcxMTc0OA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1k +YXRhOyBuYW1lPSJmaWxlMiI7IGZpbGVuYW1lPSJhZGQucG5nIg0KQ29udGVudC1UeXBlOiBpbWFn +ZS9wbmcNCg0KiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK +6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9 +W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61Ci +jSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI +0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsm +ahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bA +fWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM +0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/ +HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8 +BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3 +WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH6 +2kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64 +TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJg +gg0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xNDkwNDA0NDczOTc4NzE5MTAzMTc1NDcx +MTc0OA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJ0ZXh0Ig0KDQotLWxv +bmcgdGV4dA0KLS13aXRoIGJvdW5kYXJ5DQotLWxvb2thbGlrZXMtLQ0KLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0xNDkwNDA0NDczOTc4NzE5MTAzMTc1NDcxMTc0OC0tDQo=` + ), + boundary: '---------------------------14904044739787191031754711748', + files: { + file1: [ + 'accept.png', + 'image/png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 +U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAKfSURBVDjLpZPrS1NhHMf9O3bOdmwDCWRE +IYKEUHsVJBI7mg3FvCxL09290jZj2EyLMnJexkgpLbPUanNOberU5taUMnHZUULMvelCtWF0sW/n +7MVMEiN64AsPD8/n83uucQDi/id/DBT4Dolypw/qsz0pTMbj/WHpiDgsdSUyUmeiPt2+V7SrIM+b +Sss8ySGdR4abQQv6lrui6VxsRonrGCS9VEjSQ9E7CtiqdOZ4UuTqnBHO1X7YXl6Daa4yGq7vWO1D +40wVDtj4kWQbn94myPGkCDPdSesczE2sCZShwl8CzcwZ6NiUs6n2nYX99T1cnKqA2EKui6+TwphA +5k4yqMayopU5mANV3lNQTBdCMVUA9VQh3GuDMHiVcLCS3J4jSLhCGmKCjBEx0xlshjXYhApfMZRP +5CyYD+UkG08+xt+4wLVQZA1tzxthm2tEfD3JxARH7QkbD1ZuozaggdZbxK5kAIsf5qGaKMTY2lAU +/rH5HW3PLsEwUYy+YCcERmIjJpDcpzb6l7th9KtQ69fi09ePUej9l7cx2DJbD7UrG3r3afQHOyCo ++V3QQzE35pvQvnAZukk5zL5qRL59jsKbPzdheXoBZc4saFhBS6AO7V4zqCpiawuptwQG+UAa7Ct3 +UT0hh9p9EnXT5Vh6t4C22QaUDh6HwnECOmcO7K+6kW49DKqS2DrEZCtfuI+9GrNHg4fMHVSO5kE7 +nAPVkAxKBxcOzsajpS4Yh4ohUPPWKTUh3PaQEptIOr6BiJjcZXCwktaAGfrRIpwblqOV3YKdhfXO +IvBLeREWpnd8ynsaSJoyESFphwTtfjN6X1jRO2+FxWtCWksqBApeiFIR9K6fiTpPiigDoadqCEag +5YUFKl6Yrciw0VOlhOivv/Ff8wtn0KzlebrUYwAAAABJRU5ErkJggg==` + ) + ], + file2: [ + 'add.png', + 'image/png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 +U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlko +qCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94V +TUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpK +tE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw +9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0Hgl +WNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1V +MzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8 +w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9 +XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVP +C4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnj +ahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQy +u/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg==` + ) + ] + }, + forms: { text: '--long text\r\n--with boundary\r\n--lookalikes--' } + }, + + 'opera8-2png1txt': { + data: b64decode( + ` +LS0tLS0tLS0tLS0tekVPOWpRS21MYzJDcTg4YzIzRHgxOQ0KQ29udGVudC1EaXNwb3NpdGlvbjog +Zm9ybS1kYXRhOyBuYW1lPSJmaWxlMSI7IGZpbGVuYW1lPSJhcnJvd19icmFuY2gucG5nIg0KQ29u +dGVudC1UeXBlOiBpbWFnZS9wbmcNCg0KiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9h +AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHY +SURBVDjLlVLPS1RxHJynpVu7KEn0Vt+2l6IO5qGCIsIwCPwD6hTUaSk6REoUHeoQ0qVAMrp0COpY +0SUIPVRgSl7ScCUTst6zIoqg0y7lvpnPt8MWKuuu29w+hxnmx8dzzmE5+l7mxk1u/a3Dd/ejDjSs +II/m3vjJ9MF0yt93ZuTkdD0CnnMO/WOnmsxsJp3yd2zfvA3mHOa+zuHTjy/zojrvHX1YqunAZE9M +lpUcZAaZQBNIZUg9XdPBP5wePuEO7eyGQXg29QL3jz3y1oqwbvkhCuYEOQMp/HeJohCbICMUVwr0 +DvZcOnK9u7GmQNmBQLJCgORxkneqRmAs0BFmDi0bW9E72PPda/BikwWi0OEHkNR14MrewsTAZF+l +AAWZEH6LUCwUkUlntrS1tiG5IYlEc6LcjYjSYuncngtdhakbM5dXlhgTNEMYLqB9q49MKgsPjTBX +ntVgkDNIgmI1VY2Q7QzgJ9rx++ci3ofziBYiiELQEUAyhB/D29M3Zy+uIkDIhGYvgeKvIkbHxz6T +evzq6ut+ANh9fldetMn80OzZVVdgLFjBQ0tpEz68jcB4ifx3pQeictVXIEETnBPCKMLEwBIZAPJD +767V/ETGwsjzYYiC6vzEP9asLo3SGuQvAAAAAElFTkSuQmCCDQotLS0tLS0tLS0tLS16RU85alFL +bUxjMkNxODhjMjNEeDE5DQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZp +bGUyIjsgZmlsZW5hbWU9ImF3YXJkX3N0YXJfYnJvbnplXzEucG5nIg0KQ29udGVudC1UeXBlOiBp +bWFnZS9wbmcNCg0KiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/I +NwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLhZNNSFRR +FIC/N++9eWMzhkl/ZJqFMQMRFvTvImkXSdKiVRAURBRRW1eZA9EqaNOiFlZEtQxKyrJwUS0K+qEQ +zaTE/AtLHR3HmffuvafFNINDWGdz7z2c7+Nyzr2WiFAIffaMBDW1+B0diAgYgxiDiCDG4DU1QfcL +os+fWAXGYUGIUsXiAliUFER+sBAhVCIIVB7QGtEat1oTbcwVz2LMfwR+gPg+oY0bEa3x6sHdUoVd +niMUj0M2i/j+PwVJa2QUu7YWp34D7mqNWdNApD6Ks24dpvcL4gfJRQXevbutjI4lGRzCS9iYukPo +5dvxVqWQvn6k/2uyoudd60LGEhG43VBGyI4j2ADZ7vDJ8DZ9Img4hw4cvO/3UZ1vH3p7lrWRLwGV +neD4y6G84NaOYSoTVYIFIiAGvXI3OWctJv0TW03jZb5gZSfzl9YBpMcIzUwdzQsuVR9EyR3TeCqm +6w5jZiZQMz8xsxOYzDTi50AMVngJNgrnUweRbwMPiLpHrOJDOl9Vh6HD7GyO52qa0VPj6MwUJpNC +5mYQS/DUJLH3zzRp1cqN8YulTUyODBBzt4X6Ou870z2I8ZHsHJLLYNQ8jusQ6+2exJf9BfivKdAy +mKZiaVdodhBRAagAjIbgzxp20lwb6Vp0jADYkQO6IpHfuoqInSJUVoE2HrpyRQ1tic2LC9p3lSHW +Ph2rJfL1MeVP2weWvHp8s3ziNZ49i1q6HrR1YHGBNnt1dG2Z++gC4TdvrqNkK1eHj7ljQ/ujHx6N +yPw8BFIiKPmNpKar7P7xb/zyT9P+o7OYvzzYSUt8U+TzxytodixEfgN3CFlQMNAcMgAAAABJRU5E +rkJggg0KLS0tLS0tLS0tLS0tekVPOWpRS21MYzJDcTg4YzIzRHgxOQ0KQ29udGVudC1EaXNwb3Np +dGlvbjogZm9ybS1kYXRhOyBuYW1lPSJ0ZXh0Ig0KDQpibGFmYXNlbCDDtsOkw7wNCi0tLS0tLS0t +LS0tLXpFTzlqUUttTGMyQ3E4OGMyM0R4MTktLQ0K` + ), + boundary: '----------zEO9jQKmLc2Cq88c23Dx19', + files: { + file1: [ + 'arrow_branch.png', + 'image/png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 +U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHYSURBVDjLlVLPS1RxHJynpVu7KEn0Vt+2 +l6IO5qGCIsIwCPwD6hTUaSk6REoUHeoQ0qVAMrp0COpY0SUIPVRgSl7ScCUTst6zIoqg0y7lvpnP +t8MWKuuu29w+hxnmx8dzzmE5+l7mxk1u/a3Dd/ejDjSsII/m3vjJ9MF0yt93ZuTkdD0CnnMO/WOn +msxsJp3yd2zfvA3mHOa+zuHTjy/zojrvHX1YqunAZE9MlpUcZAaZQBNIZUg9XdPBP5wePuEO7eyG +QXg29QL3jz3y1oqwbvkhCuYEOQMp/HeJohCbICMUVwr0DvZcOnK9u7GmQNmBQLJCgORxkneqRmAs +0BFmDi0bW9E72PPda/BikwWi0OEHkNR14MrewsTAZF+lAAWZEH6LUCwUkUlntrS1tiG5IYlEc6Lc +jYjSYuncngtdhakbM5dXlhgTNEMYLqB9q49MKgsPjTBXntVgkDNIgmI1VY2Q7QzgJ9rx++ci3ofz +iBYiiELQEUAyhB/D29M3Zy+uIkDIhGYvgeKvIkbHxz6Tevzq6ut+ANh9fldetMn80OzZVVdgLFjB +Q0tpEz68jcB4ifx3pQeictVXIEETnBPCKMLEwBIZAPJD767V/ETGwsjzYYiC6vzEP9asLo3SGuQv +AAAAAElFTkSuQmCC` + ) + ], + file2: [ + 'award_star_bronze_1.png', + 'image/png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 +U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLhZNNSFRRFIC/N++9eWMzhkl/ +ZJqFMQMRFvTvImkXSdKiVRAURBRRW1eZA9EqaNOiFlZEtQxKyrJwUS0K+qEQzaTE/AtLHR3Hmffu +vafFNINDWGdz7z2c7+Nyzr2WiFAIffaMBDW1+B0diAgYgxiDiCDG4DU1QfcLos+fWAXGYUGIUsXi +AliUFER+sBAhVCIIVB7QGtEat1oTbcwVz2LMfwR+gPg+oY0bEa3x6sHdUoVdniMUj0M2i/j+PwVJ +a2QUu7YWp34D7mqNWdNApD6Ks24dpvcL4gfJRQXevbutjI4lGRzCS9iYukPo5dvxVqWQvn6k/2uy +oudd60LGEhG43VBGyI4j2ADZ7vDJ8DZ9Img4hw4cvO/3UZ1vH3p7lrWRLwGVneD4y6G84NaOYSoT +VYIFIiAGvXI3OWctJv0TW03jZb5gZSfzl9YBpMcIzUwdzQsuVR9EyR3TeCqm6w5jZiZQMz8xsxOY +zDTi50AMVngJNgrnUweRbwMPiLpHrOJDOl9Vh6HD7GyO52qa0VPj6MwUJpNC5mYQS/DUJLH3zzRp +1cqN8YulTUyODBBzt4X6Ou870z2I8ZHsHJLLYNQ8jusQ6+2exJf9BfivKdAymKZiaVdodhBRAagA +jIbgzxp20lwb6Vp0jADYkQO6IpHfuoqInSJUVoE2HrpyRQ1tic2LC9p3lSHWPh2rJfL1MeVP2weW +vHp8s3ziNZ49i1q6HrR1YHGBNnt1dG2Z++gC4TdvrqNkK1eHj7ljQ/ujHx6NyPw8BFIiKPmNpKar +7P7xb/zyT9P+o7OYvzzYSUt8U+TzxytodixEfgN3CFlQMNAcMgAAAABJRU5ErkJggg==` + ) + ] + }, + forms: { text: 'blafasel öäü' } + }, + + 'webkit3-2png1txt': { + data: b64decode( + ` +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 +melp14H1cYUZ3J31fZyTE1zA7fVw+n0cERSg8v2RUS5pPqeArNtlZmGBwqtjY+skwYig80lXBCff +5OvANFeSxzIRojge5+j8Uu9dXOD5Pt6o41jAz1W69uznMQ8wgOf79LpdNNTHwBT22r1ebDwPt0h8 +DbQAFTADGGvp9PtxCntjYAa7zW43wVpca3HyZZsJaAF0C/k+4vs0wzDJYHcMfCSyHyfJzq/n50NT +raKVwhl1H3cCpAsphVut8tvz58M4SXaKn8X4pFzB1lG/P2gOBuhaDYxBJhqR5e8Yg56f53gwoNHr +Da9gq+CMz7JSauoz+HgFvr1trX+vXPZKUYSbJCMTA+K6xMYw8Dx+7Pfjw+Fw+Dt8/h38ALwQkeg6 +cAaoLcLyp/BlVam1dz3PWdDaqbkjdwVpymmaZn9FUXouUn8M3zyDJvAC+PclYA6dBmpA5SO4dxM+ +mIf3fVgCGMLfz+CPf+CXPfgZCIFz4ExEkpeWfH0opZzcKYUsI38nIy5D4BK4kgnAfwLblOaQdQsS +AAAAAElFTkSuQmCCDQotLS0tLS1XZWJLaXRGb3JtQm91bmRhcnlqZFNGaGNBUms4ZnlHTnk2DQpD +b250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9InRleHQiDQoNCnRoaXMgaXMgYW5v +dGhlciB0ZXh0IHdpdGggw7xtbMOkw7x0cw0KLS0tLS0tV2ViS2l0Rm9ybUJvdW5kYXJ5amRTRmhj +QVJrOGZ5R055Ni0tDQo=` + ), + boundary: '----WebKitFormBoundaryjdSFhcARk8fyGNy6', + files: { + file1: [ + 'gtk-apply.png', + 'image/png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz +AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANnSURB +VDiNldJ9aJVVHAfw7znPuS/PvW4405WbLWfbsBuNbramq5Tp7mLqIFPXINlwpAitaCAPjWKgBdXz +R2TBpEZoadAyCVGndttCFNxqLXORK7x3y704NlzXzfs8d89znuf0R/fKk03xHvjCOZxzPpzzO4cI +IZBuC6nsGYmRrwFMWVw0hxV+PDVH0gVDKvNSRgZfrm5+QCISOi58pY1MXhm1uHg+rPDfabqnoxJp +KQ2snf/gwgKY3ut4pfodX/lTGwokRt4AgLTAkMoK3cz7enVJg/fyTCdGE/3gwsTo+LBu2+J82qDE +6IEXyrd7YvYwbpgjyPOtQHTikvhz+NKgsNGWFhhSWU3uwqWPBx9aRwfjPTCFgXx5JY50tumWKbaF +FS7uGQypLINKZH/tukb/kN6DSSOCFfO3oqu/3biZiH0ZVvjF1Np7AiVG31sdXO/P8GfhqtaLbE8B +qOlBZ++xuMXFbudaljxBDnNJHbZlFwF407bFh6krhFRW7Jcztlc9Uee5HD+DaWsCTy/YgbaOvZpl +2Y1hhU87QVLxvpQpMfpzfeXuZfmLA/Rw1wdaZOS3Pm7aNQDGJUZ/qatqKs5etIj03TiKQv8aaFOW +OHRm30+nm4zS229DmVs6Ulm6OW/50iD9G1Hsqnrbt2lNwyoXYwMAPnk4N1D4aO4qEtW6wagHeZ4S +fNP1mW6Zdt1c5WEE8Lll5qKCQbdiGIh/h+JlK6WixcHM4z2fb9tUtkOO6hdw3Yzi2axdON33xaxu +zLSGFf7HXCA1Dav+5Nn2Kyd7DyYK5bXw0QWIJM4j7rqGmvKd8gwZw5D+I3K8jyGhmzj366lpi4uW +Oz0gEUIgpDKPxGjr/VlLanZubJknXLMYiH8PjccwK26C27Oouu8tfHysWbs6HnkxrPATdwVTLaSy +zW63+8BLzzX6H1lSSrtjBzFpRPBkZi0mrk3Z7Z2tP5xqMiruhP0PTKL5EqMnSgKr87eUvSqPGf3I +psux53CDpie0QFjhf90NhBDiVlJ1LaqmcqXq2l/7aU7826E94rWjQb3iXbYXgAzAC8ADwI1//zF1 +OkQIAUIIBSAlc6tfpkjr52XTj4SFi937eP3MmDAB2I5YyaT63AmyuVDHmAAQt0FOzARg/aeGhBCS +3EjnCBygMwKAnXL+AdDkiZ/xYgR3AAAAAElFTkSuQmCC` + ) + ], + file2: [ + 'gtk-no.png', + 'image/png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz +AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAM1SURB +VDiNrZTPaxtHFMc/M7uzs7KobckyDjQxtoPbnlJyMBRKgsGFHkwvvfbuU/+BQCilBHrof+BT7zmV +4lsNxqUUXBo1tIdSsCy3uI7jrKSVJUveX68H7QpHSW4d+O6PeW8/vJn3nVUiwv853DcFHip1X8OG +A+sp3AVwoJ7CXga7j0T2X/edmqzwgVIVC9teqbS5tLxsZqtV9625OQAugoBOu500G404Ggx2rmDr +a5H2G4FfKPUh8P3KysrUe3fuWA14c3NoYxARsjgmbrXIlOLPp0+vGo3GJfDJVyI/vQJ8oFTFwOHa +2lqlVqmgSyXM9PQoSwQRgVxxGCJpStBuc3Bw0I7hdlGpLsgWtpdu3ZqqlssMTk6QJCFqtYiC4BVl +cczl0RGVcpmlxcUpC9sFRxcNMNZurq6u2rBex7GWtNUiDQIkCMiKe6tF2mqRBAHKdQmfPOGd1VVr +rN18qNT9cZc1bLy9sGCS01MIQ3QYohwHnS9RFfssQiaCZBk6SZAwJDo95eaNG+bw+HgD2HdzO6zP +lkqu6nSwgDo7Q/s+OstQ14CZCGTZiB1Fo9xOh5npadeB9XGFGdyd9X2ckxNcwO31cPp9HBEUoPL9 +kVEuaT6ngKzbZWZhgcKrY2PrJMGIoPNJVwQn3+TrwDRXkscyEaI4Hufo/FLvXVzg+T7eqONYwM9V +uvbs5zEPMIDn+/S6XTTUx8AU9tq9Xmw8D7dIfA20ABUwAxhr6fT7cQp7Y2AGu81uN8FaXGtx8mWb +CWgBdAv5PuL7NMMwyWB3DHwksh8nyc6v5+dDU62ilcIZdR93AqQLKYVbrfLb8+fDOEl2ip/F+KRc +wdZRvz9oDgboWg2MQSYakeXvGIOen+d4MKDR6w2vYKvgjM+yUmrqM/h4Bb69ba1/r1z2SlGEmyQj +EwPiusTGMPA8fuz348PhcPg7fP4d/AC8EJHoOnAGqC3C8qfwZVWptXc9z1nQ2qm5I3cFacppmmZ/ +RVF6LlJ/DN88gybwAvj3JWAOnQZqQOUjuHcTPpiH931YAhjC38/gj3/glz34GQiBc+BMRJKXlnx9 +KKWc3CmFLCN/JyMuQ+ASuJIJwH8C25TmkHULEgAAAABJRU5ErkJggg==` + ) + ] + }, + forms: { text: 'this is another text with ümläüts' } + }, + + 'ie6-2png1txt': { + data: b64decode( + ` +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS03ZDkxYjAzYTIwMTI4DQpDb250ZW50LURpc3Bv +c2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUxIjsgZmlsZW5hbWU9IkM6XFB5dGhvbjI1XHd6 +dGVzdFx3ZXJremV1Zy1tYWluXHRlc3RzXG11bHRpcGFydFxmaXJlZm94My0ycG5nMXR4dFxmaWxl +MS5wbmciDQpDb250ZW50LVR5cGU6IGltYWdlL3gtcG5nDQoNColQTkcNChoKAAAADUlIRFIAAAAQ +AAAAEAgGAAAAH/P/YQAAAARnQU1BAACvyDcFiukAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdl +UmVhZHlxyWU8AAABnUlEQVQ4y6VTMWvCQBS+qwEFB10KGaS1P6FDpw7SrVvzAwRRx04VCk4K6iAo +DhLXdhFcW9qhZCk4FQoW0gp2U4lQRDAUS4hJmn5Xgg2lsQ198PHu3b3vu5d3L9S2bfIf47wOer1e +wzTNtGEYBP48kUjkfsrb8BIAMb1cLovwRfi07wrYzcCr4/1/Am4FzzhzBGZeefR7E7vd7j0Iu4wY +jUYDBMfD0dBiMUQfstns3toKkHgF6EgmqqruW6bFiHcsxr70awVu63Q6NiOmUinquwfMdF1f28CV +gCRJx0jMAQ1BEFquRn7CbYVCYZVbr9dbnJMohoIh9kViu90WEW9nMpmxu4JyubyF/VEsFiNcgCPy +oyxiu7XhCPBzdU4s652VnUccbDabPLyN2C6VSmwdhFgel5DB84AJb64mEUlvmqadTKcv40gkUkUs +g1DjeZ7iRsrWgByP71T7/afxYrHIYry/eoBD9mxsaK4VRamFw2EBQknMAWGvRClNTpQJAfkCxFNg +Bmiez1ipVA4hdgQcOD/TLfylKIo3vubgL/YBnIw+ioOMLtwAAAAASUVORK5CYIINCi0tLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tN2Q5MWIwM2EyMDEyOA0KQ29udGVudC1EaXNwb3NpdGlvbjog +Zm9ybS1kYXRhOyBuYW1lPSJmaWxlMiI7IGZpbGVuYW1lPSJDOlxQeXRob24yNVx3enRlc3Rcd2Vy +a3pldWctbWFpblx0ZXN0c1xtdWx0aXBhcnRcZmlyZWZveDMtMnBuZzF0eHRcZmlsZTIucG5nIg0K +Q29udGVudC1UeXBlOiBpbWFnZS94LXBuZw0KDQqJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAA +AB/z/2EAAAAEZ0FNQQAAr8g3BYrpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccll +PAAAAlFJREFUGBmlwd1rzXEcwPH353d+51jO5jDZFpnnJNaiUSK5mkJKMYkUSS1RSvwDpnFBkYeY +O2p2sZRceCh5mpKnUXZssVaO2Q4Hw9nO+Z3v52O/ldoFF8vrJWbG/5CNB67uXbm65lgma3EzQBVT +xanD1FBTzDnUDHMOp8qEWPCroyN1uPVE3Rm/ZkXNqWhR3CsvYiziv7LuFHDGzwbmZTM/GavBwDyG ++eaMhm1zGavdjT2EfDMllC84DDA1nIJiqBpOFVcwXMEIPt8l+/wykeIq9pXd49XZ/Tt8zAiJJ4gZ +5gkmhqjgeYKIh4hDM9eJ9j6lomo7iVmL+dY9n+StpuO+U0fIA0wEBCIGKqBqRAwK6dvEcm+Iz1tB +5l0HMclTMqGC4smVCd/UGCECZniAiYCACOT77yM/npCYvYZcbzOx8ULPyyQDWZBcptpTdfwhIiBC +yANy6fsUvtwmMWctQx8vItGvRItLiFuGK6nlLN3X2ukVgoARIogIIRGhL3md7IebJOZuYCh1Di8a +kB+YSfphO1NqG/g4OJGQZ04JRQABRIT+5A1+pNooW7iO/KcmIjEjNzCD9KMXVGw6T1H5AkyVkK+q +/CFAV1szhe+vKchUel+fZlJZjKHMdL49S1K55QLRxDRCakbIT3X3tNSfDOrUOdQptdLE5vpLvG0+ +SOeDNsZVVvO9L8WNoa30NTzGVFEl1MIwMTNGO7JnUXBoV72P53h55xo93V0/E1NKV9YebW/nL8TM +GK1uVengktnl/rIFs7Borm2wP71zfeOr9/zDb6ZFKM6WU+GQAAAAAElFTkSuQmCCDQotLS0tLS0t +LS0tLS0tLS0tLS0tLS0tLS0tLS0tLTdkOTFiMDNhMjAxMjgNCkNvbnRlbnQtRGlzcG9zaXRpb246 +IGZvcm0tZGF0YTsgbmFtZT0idGV4dCINCg0KaWU2IHN1Y2tzIDotLw0KLS0tLS0tLS0tLS0tLS0t +LS0tLS0tLS0tLS0tLS03ZDkxYjAzYTIwMTI4LS0NCg==` + ), + boundary: '---------------------------7d91b03a20128', + files: { + file1: [ + 'C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file1.png', + 'image/x-png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 +U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGdSURBVDjLpVMxa8JAFL6rAQUHXQoZpLU/ +oUOnDtKtW/MDBFHHThUKTgrqICgOEtd2EVxb2qFkKTgVChbSCnZTiVBEMBRLiEmafleCDaWxDX3w +8e7dve+7l3cv1LZt8h/jvA56vV7DNM20YRgE/jyRSOR+ytvwEgAxvVwui/BF+LTvCtjNwKvj/X8C +bgXPOHMEZl559HsTu93uPQi7jBiNRgMEx8PR0GIxRB+y2eze2gqQeAXoSCaqqu5bpsWIdyzGvvRr +BW7rdDo2I6ZSKeq7B8x0XV/bwJWAJEnHSMwBDUEQWq5GfsJthUJhlVuv11uckyiGgiH2RWK73RYR +b2cymbG7gnK5vIX9USwWI1yAI/KjLGK7teEI8HN1TizrnZWdRxxsNps8vI3YLpVKbB2EWB6XkMHz +gAlvriYRSW+app1Mpy/jSCRSRSyDUON5nuJGytaAHI/vVPv9p/FischivL96gEP2bGxorhVFqYXD +YQFCScwBYa9EKU1OlAkB+QLEU2AGaJ7PWKlUDiF2BBw4P9Mt/KUoije+5uAv9gGcjD6Kg4wu3AAA +AABJRU5ErkJggg==` + ) + ], + file2: [ + 'C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file2.png', + 'image/x-png', + b64decode( + ` +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0 +U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJRSURBVBgZpcHda81xHMDx9+d3fudYzuYw +2RaZ5yTWolEiuZpCSjGJFEktUUr8A6ZxQZGHmDtqdrGUXHgoeZqSp1F2bLFWjtkOB8PZzvmd7+dj +v5XaBRfL6yVmxv+QjQeu7l25uuZYJmtxM0AVU8Wpw9RQU8w51AxzDqfKhFjwq6Mjdbj1RN0Zv2ZF +zaloUdwrL2Is4r+y7hRwxs8G5mUzPxmrwcA8hvnmjIZtcxmr3Y09hHwzJZQvOAwwNZyCYqgaThVX +MFzBCD7fJfv8MpHiKvaV3ePV2f07fMwIiSeIGeYJJoao4HmCiIeIQzPXifY+paJqO4lZi/nWPZ/k +rabjvlNHyANMBAQiBiqgakQMCunbxHJviM9bQeZdBzHJUzKhguLJlQnf1BghAmZ4gImAgAjk++8j +P56QmL2GXG8zsfFCz8skA1mQXKbaU3X8ISIgQsgDcun7FL7cJjFnLUMfLyLRr0SLS4hbhiup5Szd +19rpFYKAESKICCERoS95neyHmyTmbmAodQ4vGpAfmEn6YTtTahv4ODiRkGdOCUUAAUSE/uQNfqTa +KFu4jvynJiIxIzcwg/SjF1RsOk9R+QJMlZCvqvwhQFdbM4XvrynIVHpfn2ZSWYyhzHS+PUtSueUC +0cQ0QmpGyE9197TUnwzq1DnUKbXSxOb6S7xtPkjngzbGVVbzvS/FjaGt9DU8xlRRJdTCMDEzRjuy +Z1FwaFe9j+d4eecaPd1dPxNTSlfWHm1v5y/EzBitblXp4JLZ5f6yBbOwaK5tsD+9c33jq/f8w2+m +RSjOllPhkAAAAABJRU5ErkJggg==` + ) + ] + }, + forms: { text: 'ie6 sucks :-/' } + } +} + +const legacyBrowserTestCases = { + 'firefox3-2png1txt': { + data: [ + '-----------------------------186454651713519341951581030105\r\n', + 'Content-Disposition: form-data; name="file1"; filename="anchor.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file1-data]\r\n', + '-----------------------------186454651713519341951581030105\r\n', + 'Content-Disposition: form-data; name="file2"; filename="application_edit.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file2-data]\r\n', + '-----------------------------186454651713519341951581030105\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '-----------------------------186454651713519341951581030105--\r\n' + ].join(''), + boundary: '---------------------------186454651713519341951581030105', + files: { + file1: ['anchor.png', 'image/png', '[file1-data]'], + file2: ['application_edit.png', 'image/png', '[file2-data]'] + }, + forms: { text: '[Text]' } + }, + 'opera8-2png1txt': { + data: [ + '------------zEO9jQKmLc2Cq88c23Dx19\r\n', + 'Content-Disposition: form-data; name="file1"; filename="arrow_branch.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file1-data]\r\n', + '------------zEO9jQKmLc2Cq88c23Dx19\r\n', + 'Content-Disposition: form-data; name="file2"; filename="award_star_bronze_1.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file2-data]\r\n', + '------------zEO9jQKmLc2Cq88c23Dx19\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '------------zEO9jQKmLc2Cq88c23Dx19--\r\n' + ].join(''), + boundary: '----------zEO9jQKmLc2Cq88c23Dx19', + files: { + file1: ['arrow_branch.png', 'image/png', '[file1-data]'], + file2: ['award_star_bronze_1.png', 'image/png', '[file2-data]'] + }, + forms: { text: '[Text]' } + }, + 'webkit3-2png1txt': { + data: [ + '------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n', + 'Content-Disposition: form-data; name="file1"; filename="gtk-apply.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file1-data]\r\n', + '------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n', + 'Content-Disposition: form-data; name="file2"; filename="gtk-no.png"\r\n', + 'Content-Type: image/png\r\n', + '\r\n', + '[file2-data]\r\n', + '------WebKitFormBoundaryjdSFhcARk8fyGNy6\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '------WebKitFormBoundaryjdSFhcARk8fyGNy6--\r\n' + ].join(''), + boundary: '----WebKitFormBoundaryjdSFhcARk8fyGNy6', + files: { + file1: ['gtk-apply.png', 'image/png', '[file1-data]'], + file2: ['gtk-no.png', 'image/png', '[file2-data]'] + }, + forms: { text: '[Text]' } + }, + 'ie6-2png1txt': { + data: [ + '-----------------------------7d91b03a20128\r\n', + 'Content-Disposition: form-data; name="file1"; filename="C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file1.png"\r\n', + 'Content-Type: image/x-png\r\n', + '\r\n', + '[file1-data]\r\n', + '-----------------------------7d91b03a20128\r\n', + 'Content-Disposition: form-data; name="file2"; filename="C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file2.png"\r\n', + 'Content-Type: image/x-png\r\n', + '\r\n', + '[file2-data]\r\n', + '-----------------------------7d91b03a20128\r\n', + 'Content-Disposition: form-data; name="text"\r\n', + '\r\n', + '[Text]\r\n', + '-----------------------------7d91b03a20128--\r\n' + ].join(''), + boundary: '---------------------------7d91b03a20128', + files: { + file1: ['C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file1.png', 'image/x-png', '[file1-data]'], + file2: ['C:\\Python25\\wztest\\werkzeug-main\\tests\\multipart\\firefox3-2png1txt\\file2.png', 'image/x-png', '[file2-data]'] + }, + forms: { text: '[Text]' } + } +} + +test('legacy tests', async (t) => { + for (const [name, body] of Object.entries(legacyTests)) { + await t.test(`parsing ${name}`, async (t) => { + const response = makeResponse(body.join(''), body[0].slice(2).trim()) + + await t.assert.doesNotReject(response.formData()) + }) + } +}) + +test('browser tests', async (t) => { + for (const [name, value] of Object.entries(browserTestCases)) { + await t.test(`parsing ${name}`, async (t) => { + const response = makeResponse(value.data, value.boundary) + const fd = await response.formData() + + for (const [key, val] of fd.entries()) { + if (key in value.files) { + const [fileName, mimeType, body] = value.files[key] + + t.assert.ok(val instanceof File) + t.assert.deepStrictEqual(fileName, val.name) + t.assert.deepStrictEqual(mimeType, val.type) + t.assert.deepStrictEqual(body, await val.text()) + } else { + const expected = value.forms[key] + t.assert.deepStrictEqual(val, expected) + } + } + }) + } +}) + +test('legacy browser tests', async (t) => { + for (const [name, value] of Object.entries(legacyBrowserTestCases)) { + await t.test(`parsing ${name}`, async (t) => { + const response = makeResponse(value.data, value.boundary) + const fd = await response.formData() + + for (const [key, val] of fd.entries()) { + if (key in value.files) { + const [fileName, mimeType, body] = value.files[key] + + t.assert.ok(val instanceof File) + t.assert.deepStrictEqual(fileName, val.name) + t.assert.deepStrictEqual(mimeType, val.type) + t.assert.deepStrictEqual(body, await val.text()) + } else { + const expected = value.forms[key] + t.assert.deepStrictEqual(val, expected) + } + } + }) + } +}) diff --git a/test/busboy/test-types-multipart-charsets.js b/test/busboy/test-types-multipart-charsets.js index b162024438d..1544101f986 100644 --- a/test/busboy/test-types-multipart-charsets.js +++ b/test/busboy/test-types-multipart-charsets.js @@ -1,6 +1,5 @@ 'use strict' -const assert = require('node:assert') const { inspect } = require('node:util') const { test } = require('node:test') const { Response } = require('../..') @@ -63,7 +62,7 @@ test('unicode filename', async (t) => { } } - assert.deepStrictEqual( + t.assert.deepStrictEqual( results, expected, 'Results mismatch.\n' + diff --git a/test/busboy/test-types-multipart.js b/test/busboy/test-types-multipart.js index 73960f2ccac..2a463572c7a 100644 --- a/test/busboy/test-types-multipart.js +++ b/test/busboy/test-types-multipart.js @@ -1,8 +1,7 @@ 'use strict' -const assert = require('node:assert') const { inspect } = require('node:util') -const { describe } = require('node:test') +const { test } = require('node:test') const { Response } = require('../..') const active = new Map() @@ -563,7 +562,7 @@ const tests = [ } ] -describe('FormData parsing tests', async (t) => { +test('FormData parsing tests', async (t) => { for (const test of tests) { active.set(test, 1) @@ -618,7 +617,7 @@ describe('FormData parsing tests', async (t) => { active.delete(test) - assert.deepStrictEqual( + t.assert.deepStrictEqual( results, test.expected, `[${what}] Results mismatch.\n` + diff --git a/test/cache-interceptor/cache-store-test-utils.js b/test/cache-interceptor/cache-store-test-utils.js index b97fe01f8d5..54616b89744 100644 --- a/test/cache-interceptor/cache-store-test-utils.js +++ b/test/cache-interceptor/cache-store-test-utils.js @@ -10,16 +10,18 @@ const FakeTimers = require('@sinonjs/fake-timers') * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore * * @param {{ new(...any): CacheStore }} CacheStore + * @param {object} [options] + * @param {boolean} [options.skip] */ -function cacheStoreTests (CacheStore) { +function cacheStoreTests (CacheStore, options) { describe(CacheStore.prototype.constructor.name, () => { - test('matches interface', () => { + test('matches interface', options, () => { equal(typeof CacheStore.prototype.get, 'function') equal(typeof CacheStore.prototype.createWriteStream, 'function') equal(typeof CacheStore.prototype.delete, 'function') }) - test('caches request', async () => { + test('caches request', options, async () => { /** * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} */ @@ -106,7 +108,7 @@ function cacheStoreTests (CacheStore) { } }) - test('returns stale response before deleteAt', async () => { + test('returns stale response before deleteAt', options, async () => { const clock = FakeTimers.install({ shouldClearNativeTimers: true }) @@ -164,7 +166,7 @@ function cacheStoreTests (CacheStore) { equal(await store.get(key), undefined) }) - test('a stale request is overwritten', async () => { + test('a stale request is overwritten', options, async () => { const clock = FakeTimers.install({ shouldClearNativeTimers: true }) @@ -245,7 +247,7 @@ function cacheStoreTests (CacheStore) { } }) - test('vary directives used to decide which response to use', async () => { + test('vary directives used to decide which response to use', options, async () => { /** * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} */ diff --git a/test/cache-interceptor/cache-tests-worker.mjs b/test/cache-interceptor/cache-tests-worker.mjs index b9b3308ecbf..2ef4de8bef7 100644 --- a/test/cache-interceptor/cache-tests-worker.mjs +++ b/test/cache-interceptor/cache-tests-worker.mjs @@ -6,6 +6,7 @@ import { getResults, runTests as runTestSuite } from '../fixtures/cache-tests/te import { determineTestResult, testLookup } from '../fixtures/cache-tests/test-engine/lib/results.mjs' import tests from '../fixtures/cache-tests/tests/index.mjs' import { Agent, fetch, interceptors, setGlobalDispatcher } from '../../index.js' +import { runtimeFeatures } from '../../lib/util/runtime-features.js' import MemoryCacheStore from '../../lib/cache/memory-cache-store.js' if (!process.env.TEST_ENVIRONMENT) { @@ -72,13 +73,9 @@ async function makeCacheStore (type) { MemoryCacheStore } - try { - await import('node:sqlite') - + if (runtimeFeatures.has('sqlite')) { const { default: SqliteCacheStore } = await import('../../lib/cache/sqlite-cache-store.js') stores.SqliteCacheStore = SqliteCacheStore - } catch (err) { - // Do nothing } const Store = stores[type] diff --git a/test/cache-interceptor/cache-tests.mjs b/test/cache-interceptor/cache-tests.mjs index 8844e0a3c52..350103d7353 100644 --- a/test/cache-interceptor/cache-tests.mjs +++ b/test/cache-interceptor/cache-tests.mjs @@ -5,6 +5,7 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import { exit } from 'node:process' import { fork } from 'node:child_process' +import { runtimeFeatures } from '../../lib/util/runtime-features.js' /** * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheOptions} CacheOptions @@ -106,10 +107,9 @@ const CACHE_STORES = [ { opts: {}, cacheStore: 'MemoryCacheStore' } ] -try { - await import('node:sqlite') +if (runtimeFeatures.has('sqlite')) { CACHE_STORES.push({ opts: {}, cacheStore: 'SqliteCacheStore' }) -} catch (err) { +} else { console.warn('Skipping SqliteCacheStore, node:sqlite not present') } @@ -135,9 +135,11 @@ for (let i = 0; i < testEnvironments.length; i++) { const port = PORT + i const promise = new Promise((resolve) => { - const process = fork(join(import.meta.dirname, 'cache-tests-worker.mjs'), { + const cacheTestsWorkerProcess = fork(join(import.meta.dirname, 'cache-tests-worker.mjs'), { stdio: 'pipe', env: { + NODE_OPTIONS: process.env.NODE_OPTIONS, + NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE, TEST_ENVIRONMENT: JSON.stringify(environment), BASE_URL: `${PROTOCOL}://localhost:${port}`, CI: CLI_OPTIONS.values.ci ? 'true' : undefined, @@ -148,15 +150,15 @@ for (let i = 0; i < testEnvironments.length; i++) { }) const stdout = [] - process.stdout.on('data', chunk => { + cacheTestsWorkerProcess.stdout.on('data', chunk => { stdout.push(chunk) }) - process.stderr.on('error', chunk => { + cacheTestsWorkerProcess.stderr.on('error', chunk => { stdout.push(chunk) }) - process.on('close', code => { + cacheTestsWorkerProcess.on('close', code => { resolve([code, stdout]) }) }) diff --git a/test/cache-interceptor/cache-utils.js b/test/cache-interceptor/cache-utils.js new file mode 100644 index 00000000000..7b66e66d3aa --- /dev/null +++ b/test/cache-interceptor/cache-utils.js @@ -0,0 +1,44 @@ +'use strict' + +const { tspl } = require('@matteo.collina/tspl') +const { test } = require('node:test') +const { normalizeHeaders } = require('../../lib/util/cache') + +test('normalizeHeaders handles plain object headers with polluted Object.prototype[Symbol.iterator]', (t) => { + const { strictEqual } = tspl(t, { plan: 2 }) + + const originalIterator = Object.prototype[Symbol.iterator] + // eslint-disable-next-line no-extend-native + Object.prototype[Symbol.iterator] = function * () {} + + try { + const headers = normalizeHeaders({ + headers: { + Authorization: 'Bearer token', + 'X-Test': 'ok' + } + }) + + strictEqual(headers.authorization, 'Bearer token') + strictEqual(headers['x-test'], 'ok') + } finally { + if (originalIterator === undefined) { + delete Object.prototype[Symbol.iterator] + } else { + // eslint-disable-next-line no-extend-native + Object.prototype[Symbol.iterator] = originalIterator + } + } +}) + +test('normalizeHeaders handles headers from Map', (t) => { + const { strictEqual } = tspl(t, { plan: 1 }) + + const headers = normalizeHeaders({ + headers: new Map([ + ['X-Test', 'ok'] + ]) + }) + + strictEqual(headers['x-test'], 'ok') +}) diff --git a/test/cache-interceptor/issue-4209.js b/test/cache-interceptor/issue-4209.js new file mode 100644 index 00000000000..e207a0525be --- /dev/null +++ b/test/cache-interceptor/issue-4209.js @@ -0,0 +1,144 @@ +'use strict' + +const { test, after } = require('node:test') +const { equal, notEqual, deepStrictEqual } = require('node:assert') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { request, Agent, interceptors } = require('../../') +const { makeCacheKey } = require('../../lib/util/cache') + +// Regression test for https://github.com/nodejs/undici/issues/4209 +// The cache interceptor must treat requests with different `query` params +// (or query strings) as distinct cache entries, and must not throw when +// `opts.path` is absent while `opts.query` is provided. + +test('issue #4209 - makeCacheKey includes query in key.path', () => { + const k1 = makeCacheKey({ + origin: 'http://example.com', + method: 'GET', + path: '/', + query: { i: 1 }, + headers: {} + }) + const k2 = makeCacheKey({ + origin: 'http://example.com', + method: 'GET', + path: '/', + query: { i: 2 }, + headers: {} + }) + equal(k1.path, '/?i=1') + equal(k2.path, '/?i=2') + notEqual(k1.path, k2.path) +}) + +test('issue #4209 - makeCacheKey does not throw when opts.path is undefined', () => { + // Previously `pathHasQueryOrFragment(opts.path)` threw when opts.path was + // undefined, because undefined has no `.includes`. The cache key must be + // derivable even when only `query` is supplied. + const key = makeCacheKey({ + origin: 'http://example.com', + method: 'GET', + query: { i: 42 }, + headers: {} + }) + deepStrictEqual(key, { + origin: 'http://example.com', + method: 'GET', + path: '/?i=42', + headers: {} + }) +}) + +test('issue #4209 - different query strings do not collide in the cache', async () => { + let requestCount = 0 + const server = createServer((req, res) => { + requestCount++ + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'public, max-age=100' + }) + // Each response embeds the received URL and a per-request counter so + // that a cache collision is immediately observable: a cached reply for + // a different query would carry the wrong URL/counter. + res.end(`req=${requestCount};url=${req.url}`) + }) + + server.listen(0) + await once(server, 'listening') + + const dispatcher = new Agent().compose(interceptors.cache()) + + after(async () => { + server.close() + await dispatcher.close() + }) + + const origin = `http://localhost:${server.address().port}` + + // First request with ?i=0 – hits origin + const r0 = await request(`${origin}/?i=0`, { dispatcher }) + const b0 = await r0.body.text() + equal(requestCount, 1, 'first distinct query hits the origin') + equal(b0, 'req=1;url=/?i=0') + + // Same query repeated – must be served from cache + const r0b = await request(`${origin}/?i=0`, { dispatcher }) + const b0b = await r0b.body.text() + equal(requestCount, 1, 'repeated query is served from cache') + equal(b0b, b0, 'cached body matches original') + + // Different query – must NOT collide with the previous cache entry + const r1 = await request(`${origin}/?i=1`, { dispatcher }) + const b1 = await r1.body.text() + equal(requestCount, 2, 'different query goes to origin, not the cache') + equal(b1, 'req=2;url=/?i=1') + notEqual(b1, b0, 'different query must produce a different response') + + // And the first one still comes from cache. + const r0c = await request(`${origin}/?i=0`, { dispatcher }) + const b0c = await r0c.body.text() + equal(requestCount, 2, 'original query still cached after the new one') + equal(b0c, b0) +}) + +test('issue #4209 - query object form also keyed distinctly', async () => { + let requestCount = 0 + const server = createServer((req, res) => { + requestCount++ + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'public, max-age=100' + }) + res.end(`req=${requestCount};url=${req.url}`) + }) + + server.listen(0) + await once(server, 'listening') + + const dispatcher = new Agent().compose(interceptors.cache()) + + after(async () => { + server.close() + await dispatcher.close() + }) + + const origin = `http://localhost:${server.address().port}` + + const r0 = await request(origin, { dispatcher, query: { i: 0 } }) + const b0 = await r0.body.text() + equal(requestCount, 1) + equal(b0, 'req=1;url=/?i=0') + + const r1 = await request(origin, { dispatcher, query: { i: 1 } }) + const b1 = await r1.body.text() + equal(requestCount, 2, 'different query option must bypass cache') + equal(b1, 'req=2;url=/?i=1') + notEqual(b0, b1) + + // Re-request first query – served from cache, counter unchanged. + const r0b = await request(origin, { dispatcher, query: { i: 0 } }) + const b0b = await r0b.body.text() + equal(requestCount, 2, 'first query still served from cache') + equal(b0b, b0) +}) diff --git a/test/cache-interceptor/sqlite-cache-store-tests.js b/test/cache-interceptor/sqlite-cache-store-tests.js index 43573714d1b..f5eb18d0116 100644 --- a/test/cache-interceptor/sqlite-cache-store-tests.js +++ b/test/cache-interceptor/sqlite-cache-store-tests.js @@ -1,31 +1,15 @@ 'use strict' -const { test, skip } = require('node:test') +const { test } = require('node:test') const { notEqual, strictEqual, deepStrictEqual } = require('node:assert') const { rm } = require('node:fs/promises') const { cacheStoreTests, writeBody, compareGetResults } = require('./cache-store-test-utils.js') +const { runtimeFeatures } = require('../../lib/util/runtime-features.js') -let hasSqlite = false -try { - require('node:sqlite') - - const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') - cacheStoreTests(SqliteCacheStore) - hasSqlite = true -} catch (err) { - if (err.code === 'ERR_UNKNOWN_BUILTIN_MODULE') { - skip('`node:sqlite` not present') - } else { - throw err - } -} - -test('SqliteCacheStore works nicely with multiple stores', async (t) => { - if (!hasSqlite) { - t.skip() - return - } +const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') +cacheStoreTests(SqliteCacheStore, { skip: runtimeFeatures.has('sqlite') === false }) +test('SqliteCacheStore works nicely with multiple stores', { skip: runtimeFeatures.has('sqlite') === false }, async (t) => { const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') const sqliteLocation = 'cache-interceptor.sqlite' @@ -88,17 +72,11 @@ test('SqliteCacheStore works nicely with multiple stores', async (t) => { } }) -test('SqliteCacheStore maxEntries', async (t) => { - if (!hasSqlite) { - t.skip() - return - } - +test('SqliteCacheStore maxEntries', { skip: runtimeFeatures.has('sqlite') === false }, async () => { const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') - const store = new SqliteCacheStore({ - maxCount: 10 - }) + const maxCount = 10 + const store = new SqliteCacheStore({ maxCount }) for (let i = 0; i < 20; i++) { /** @@ -130,15 +108,46 @@ test('SqliteCacheStore maxEntries', async (t) => { writeBody(writable, body) } - strictEqual(store.size <= 11, true) + strictEqual(store.size <= maxCount, true) }) -test('SqliteCacheStore two writes', async (t) => { - if (!hasSqlite) { - t.skip() - return - } +test('SqliteCacheStore enforces maxCount after insert', { skip: runtimeFeatures.has('sqlite') === false }, () => { + const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') + + const store = new SqliteCacheStore({ maxCount: 1 }) + + store.set( + { origin: 'localhost', path: '/a', method: 'GET', headers: {} }, + { + statusCode: 200, + statusMessage: '', + headers: {}, + cachedAt: Date.now(), + staleAt: Date.now() + 10_000, + deleteAt: Date.now() + 20_000, + body: Buffer.from('a') + } + ) + + store.set( + { origin: 'localhost', path: '/b', method: 'GET', headers: {} }, + { + statusCode: 200, + statusMessage: '', + headers: {}, + cachedAt: Date.now() + 1, + staleAt: Date.now() + 10_001, + deleteAt: Date.now() + 20_001, + body: Buffer.from('b') + } + ) + + strictEqual(store.size, 1) + strictEqual(store.get({ origin: 'localhost', path: '/a', method: 'GET', headers: {} }), undefined) + notEqual(store.get({ origin: 'localhost', path: '/b', method: 'GET', headers: {} }), undefined) +}) +test('SqliteCacheStore two writes', { skip: runtimeFeatures.has('sqlite') === false }, async () => { const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') const store = new SqliteCacheStore({ @@ -182,12 +191,45 @@ test('SqliteCacheStore two writes', async (t) => { } }) -test('SqliteCacheStore write & read', async (t) => { - if (!hasSqlite) { - t.skip() - return +test('SqliteCacheStore prune evicts oldest entries first', { skip: runtimeFeatures.has('sqlite') === false }, async () => { + const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') + + const maxCount = 10 + const store = new SqliteCacheStore({ maxCount }) + + const baseTime = Date.now() + + for (let i = 0; i < 20; i++) { + const key = { + origin: 'localhost', + path: '/' + i, + method: 'GET', + headers: {} + } + + const value = { + statusCode: 200, + statusMessage: '', + headers: { foo: 'bar' }, + cachedAt: baseTime + i * 1000, + staleAt: baseTime + i * 1000 + 60_000, + deleteAt: baseTime + i * 1000 + 120_000, + body: Buffer.from('x') + } + + store.set(key, value) } + // The most recently cached entry must still be present; + // the oldest entry must have been evicted. + const newest = store.get({ origin: 'localhost', path: '/19', method: 'GET', headers: {} }) + notEqual(newest, undefined) + + const oldest = store.get({ origin: 'localhost', path: '/0', method: 'GET', headers: {} }) + strictEqual(oldest, undefined) +}) + +test('SqliteCacheStore write & read', { skip: runtimeFeatures.has('sqlite') === false }, async () => { const SqliteCacheStore = require('../../lib/cache/sqlite-cache-store.js') const store = new SqliteCacheStore({ @@ -224,3 +266,59 @@ test('SqliteCacheStore write & read', async (t) => { deepStrictEqual(store.get(key), value) }) + +test('SqliteCacheStore ignores expired Vary variants when a later one is still valid', { skip: runtimeFeatures.has('sqlite') === false }, () => { + const store = new SqliteCacheStore() + const now = Date.now() + const baseKey = { + origin: 'localhost', + path: '/', + method: 'GET' + } + + store.set({ + ...baseKey, + headers: { + 'accept-encoding': 'gzip' + } + }, { + statusCode: 200, + statusMessage: '', + headers: {}, + vary: { + 'accept-encoding': 'gzip' + }, + cachedAt: now - 2000, + staleAt: now - 1000, + deleteAt: now - 1, + body: Buffer.from('expired gzip') + }) + + store.set({ + ...baseKey, + headers: { + 'accept-encoding': 'br' + } + }, { + statusCode: 200, + statusMessage: '', + headers: {}, + vary: { + 'accept-encoding': 'br' + }, + cachedAt: now, + staleAt: now + 1000, + deleteAt: now + 2000, + body: Buffer.from('valid br') + }) + + const result = store.get({ + ...baseKey, + headers: { + 'accept-encoding': 'br' + } + }) + + notEqual(result, undefined) + strictEqual(result.body.toString(), 'valid br') +}) diff --git a/test/cache-interceptor/utils.js b/test/cache-interceptor/utils.js index 2ae75d15489..103ee569449 100644 --- a/test/cache-interceptor/utils.js +++ b/test/cache-interceptor/utils.js @@ -95,6 +95,37 @@ describe('parseCacheControlHeader', () => { }) }) + test('trims qualified no-cache and private field names', () => { + let directives = parseCacheControlHeader('private=" authorization"') + deepStrictEqual(directives, { + private: [ + 'authorization' + ] + }) + + directives = parseCacheControlHeader('no-cache="\tauthorization"') + deepStrictEqual(directives, { + 'no-cache': [ + 'authorization' + ] + }) + + directives = parseCacheControlHeader('no-cache=authorization\t') + deepStrictEqual(directives, { + 'no-cache': [ + 'authorization' + ] + }) + + directives = parseCacheControlHeader('private=" authorization, x-user\t"') + deepStrictEqual(directives, { + private: [ + 'authorization', + 'x-user' + ] + }) + }) + test('private with headers', () => { let directives = parseCacheControlHeader('max-age=10, private=some-header, only-if-cached') deepStrictEqual(directives, { diff --git a/test/cache/cache.js b/test/cache/cache.js index 4a6b1ba960f..0a3d438ea3a 100644 --- a/test/cache/cache.js +++ b/test/cache/cache.js @@ -1,12 +1,53 @@ 'use strict' -const { throws } = require('node:assert') const { test } = require('node:test') const { Cache } = require('../../lib/web/cache/cache') +const { caches, Response } = require('../../') -test('constructor', () => { - throws(() => new Cache(null), { +test('constructor', (t) => { + t.assert.throws(() => new Cache(null), { name: 'TypeError', message: 'TypeError: Illegal constructor' }) }) + +// https://github.com/nodejs/undici/issues/4710 +test('cache.match should work after garbage collection', async (t) => { + const cache = await caches.open('test-gc-cache') + + t.after(async () => { + await caches.delete('test-gc-cache') + }) + + const url = 'https://example.com/test-gc' + const testData = { answer: 42 } + + await cache.put(url, Response.json(testData)) + + // Call match multiple times with GC pressure between calls + // The bug manifests when the temporary Response object from fromInnerResponse() + // is garbage collected, which triggers the FinalizationRegistry to cancel + // the cached stream. + for (let i = 0; i < 20; i++) { + // Create significant memory pressure to trigger GC + // eslint-disable-next-line no-unused-vars + const garbage = Array.from({ length: 30000 }, () => ({ value: Math.random() })) + + // Force GC if available (run with --expose-gc) + if (global.gc) { + global.gc() + } + + // Delay to allow FinalizationRegistry callbacks to run + // The bug requires time for the GC to collect the temporary Response + // and for the finalization callback to cancel the stream + await new Promise((resolve) => setTimeout(resolve, 10)) + + // This should not throw "Body has already been consumed" + const match = await cache.match(url) + t.assert.ok(match, `Iteration ${i}: match should return a response`) + + const result = await match.json() + t.assert.deepStrictEqual(result, testData, `Iteration ${i}: response body should match`) + } +}) diff --git a/test/cache/cachestorage.js b/test/cache/cachestorage.js index ba648f1197f..9a89cee3502 100644 --- a/test/cache/cachestorage.js +++ b/test/cache/cachestorage.js @@ -1,11 +1,10 @@ 'use strict' -const { throws } = require('node:assert') const { test } = require('node:test') const { CacheStorage } = require('../../lib/web/cache/cachestorage') -test('constructor', () => { - throws(() => new CacheStorage(null), { +test('constructor', (t) => { + t.assert.throws(() => new CacheStorage(null), { name: 'TypeError', message: 'TypeError: Illegal constructor' }) diff --git a/test/cache/get-field-values.js b/test/cache/get-field-values.js index b6a2f60dbbc..53ed6ecd8fa 100644 --- a/test/cache/get-field-values.js +++ b/test/cache/get-field-values.js @@ -1,19 +1,18 @@ 'use strict' -const { deepStrictEqual, throws } = require('node:assert') const { test } = require('node:test') const { getFieldValues } = require('../../lib/web/cache/util') -test('getFieldValues', () => { - throws(() => getFieldValues(null), { +test('getFieldValues', (t) => { + t.assert.throws(() => getFieldValues(null), { name: 'AssertionError', message: 'The expression evaluated to a falsy value:\n\n assert(header !== null)\n' }) - deepStrictEqual(getFieldValues(''), []) - deepStrictEqual(getFieldValues('foo'), ['foo']) - deepStrictEqual(getFieldValues('invälid'), []) - deepStrictEqual(getFieldValues('foo, bar'), ['foo', 'bar']) - deepStrictEqual(getFieldValues('foo, bar, baz'), ['foo', 'bar', 'baz']) - deepStrictEqual(getFieldValues('foo, bar, baz, '), ['foo', 'bar', 'baz']) - deepStrictEqual(getFieldValues('foo, bar, baz, , '), ['foo', 'bar', 'baz']) + t.assert.deepStrictEqual(getFieldValues(''), []) + t.assert.deepStrictEqual(getFieldValues('foo'), ['foo']) + t.assert.deepStrictEqual(getFieldValues('invälid'), []) + t.assert.deepStrictEqual(getFieldValues('foo, bar'), ['foo', 'bar']) + t.assert.deepStrictEqual(getFieldValues('foo, bar, baz'), ['foo', 'bar', 'baz']) + t.assert.deepStrictEqual(getFieldValues('foo, bar, baz, '), ['foo', 'bar', 'baz']) + t.assert.deepStrictEqual(getFieldValues('foo, bar, baz, , '), ['foo', 'bar', 'baz']) }) diff --git a/test/client-connect.js b/test/client-connect.js index 7d9e561f242..da155bc3d05 100644 --- a/test/client-connect.js +++ b/test/client-connect.js @@ -29,6 +29,11 @@ test('connect aborted after connect', async (t) => { pipelining: 3 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.connect({ path: '/', diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index e91f2ae0a0e..e6088de2049 100644 --- a/test/client-keep-alive.js +++ b/test/client-keep-alive.js @@ -4,7 +4,6 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') -const timers = require('../lib/util/timers') const { kConnect } = require('../lib/core/symbols') const { createServer } = require('node:net') const http = require('node:http') @@ -21,10 +20,8 @@ test('keep-alive header', async (t) => { socket.write('\r\n\r\n') }) after(() => server.close()) + await once(server.listen(0), 'listening') - server.listen(0) - - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) @@ -52,10 +49,6 @@ test('keep-alive header 0', async (t) => { const clock = FakeTimers.install() after(() => clock.uninstall()) - const orgTimers = { ...timers } - Object.assign(timers, { setTimeout, clearTimeout }) - after(() => { Object.assign(timers, orgTimers) }) - const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 0\r\n') @@ -64,10 +57,8 @@ test('keep-alive header 0', async (t) => { socket.write('\r\n\r\n') }) after(() => server.close()) + await once(server.listen(0), 'listening') - server.listen(0) - - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeoutThreshold: 500 }) @@ -99,10 +90,8 @@ test('keep-alive header 1', async (t) => { socket.write('\r\n\r\n') }) after(() => server.close()) + await once(server.listen(0), 'listening') - server.listen(0) - - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) @@ -135,10 +124,8 @@ test('keep-alive header no postfix', async (t) => { socket.write('\r\n\r\n') }) after(() => server.close()) + await once(server.listen(0), 'listening') - server.listen(0) - - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) @@ -163,6 +150,11 @@ test('keep-alive header no postfix', async (t) => { test('keep-alive not timeout', async (t) => { t = tspl(t, { plan: 2 }) + const clock = FakeTimers.install({ + apis: ['setTimeout'] + }) + after(() => clock.uninstall()) + const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 0\r\n') @@ -172,9 +164,8 @@ test('keep-alive not timeout', async (t) => { }) after(() => server.close()) - server.listen(0) + await once(server.listen(0), 'listening') - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 1e3 }) @@ -186,21 +177,26 @@ test('keep-alive not timeout', async (t) => { }, (err, { body }) => { t.ifError(err) body.on('end', () => { - const timeout = setTimeout(() => { - t.fail() - }, 3e3) + const timeout = setTimeout(t.fail, 3e3) client.on('disconnect', () => { t.ok(true, 'pass') clearTimeout(timeout) }) + clock.tick(1000) }).resume() }) + await t.completed }) test('keep-alive threshold', async (t) => { t = tspl(t, { plan: 2 }) + const clock = FakeTimers.install({ + apis: ['setTimeout'] + }) + after(() => clock.uninstall()) + const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 0\r\n') @@ -209,10 +205,8 @@ test('keep-alive threshold', async (t) => { socket.write('\r\n\r\n') }) after(() => server.close()) + await once(server.listen(0), 'listening') - server.listen(0) - - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 30e3, keepAliveTimeoutThreshold: 29e3 @@ -232,6 +226,7 @@ test('keep-alive threshold', async (t) => { t.ok(true, 'pass') clearTimeout(timeout) }) + clock.tick(1000) }).resume() }) await t.completed @@ -240,6 +235,11 @@ test('keep-alive threshold', async (t) => { test('keep-alive max keepalive', async (t) => { t = tspl(t, { plan: 2 }) + const clock = FakeTimers.install({ + apis: ['setTimeout'] + }) + after(() => clock.uninstall()) + const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 0\r\n') @@ -248,10 +248,8 @@ test('keep-alive max keepalive', async (t) => { socket.write('\r\n\r\n') }) after(() => server.close()) + await once(server.listen(0), 'listening') - server.listen(0) - - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`, { keepAliveTimeout: 30e3, keepAliveMaxTimeout: 1e3 @@ -271,6 +269,7 @@ test('keep-alive max keepalive', async (t) => { t.ok(true, 'pass') clearTimeout(timeout) }) + clock.tick(1000) }).resume() }) await t.completed @@ -291,10 +290,8 @@ test('connection close', async (t) => { socket.write('\r\n\r\n') }) after(() => server.close()) + await once(server.listen(0), 'listening') - server.listen(0) - - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 2 }) @@ -349,10 +346,8 @@ test('Disable keep alive', async (t) => { res.end() }) after(() => server.close()) + await once(server.listen(0), 'listening') - server.listen(0) - - await once(server, 'listening') const client = new Client(`http://localhost:${server.address().port}`, { pipelining: 0 }) after(() => client.close()) diff --git a/test/client-node-max-header-size.js b/test/client-node-max-header-size.js index d4461c35a1e..9d5ab0ed90d 100644 --- a/test/client-node-max-header-size.js +++ b/test/client-node-max-header-size.js @@ -21,7 +21,10 @@ describe("Node.js' --max-http-header-size cli option", () => { await once(server, 'listening') }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) test("respect Node.js' --max-http-header-size", async (t) => { t = tspl(t, { plan: 6 }) @@ -36,7 +39,12 @@ describe("Node.js' --max-http-header-size cli option", () => { exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => { t.ifError(err) t.strictEqual(stdout, '') - t.strictEqual(stderr, '', 'default max-http-header-size should not throw') + // Filter out debugger messages that may appear when running with --inspect + const filteredStderr = stderr.replace(/Debugger listening on ws:\/\/.*?\n/g, '') + .replace(/For help, see:.*?\n/g, '') + .replace(/Debugger attached\.\n/g, '') + .replace(/Waiting for the debugger to disconnect\.\.\.\n/g, '') + t.strictEqual(filteredStderr, '', 'default max-http-header-size should not throw') }) await t.completed @@ -55,7 +63,12 @@ describe("Node.js' --max-http-header-size cli option", () => { exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => { t.ifError(err) t.strictEqual(stdout, '') - t.strictEqual(stderr, '', 'default max-http-header-size should not throw') + // Filter out debugger messages that may appear when running with --inspect + const filteredStderr = stderr.replace(/Debugger listening on ws:\/\/.*?\n/g, '') + .replace(/For help, see:.*?\n/g, '') + .replace(/Debugger attached\.\n/g, '') + .replace(/Waiting for the debugger to disconnect\.\.\.\n/g, '') + t.strictEqual(filteredStderr, '', 'default max-http-header-size should not throw') }) await t.completed diff --git a/test/client-pipelining.js b/test/client-pipelining.js index 9e1281b53a3..e21a7c2f6e0 100644 --- a/test/client-pipelining.js +++ b/test/client-pipelining.js @@ -41,6 +41,11 @@ test('20 times GET with pipelining 10', async (t) => { pipelining: 10 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) for (let i = 0; i < num; i++) { makeRequest(i) @@ -98,6 +103,11 @@ test('A client should enqueue as much as twice its pipelining factor', async (t) pipelining: 2 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) for (; sent < 2;) { t.ok(client[kSize] <= client.pipelining, 'client is not full') @@ -148,6 +158,11 @@ test('pipeline 1 is 1 active request', async (t) => { pipelining: 1 }) after(() => client.destroy()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET' @@ -201,6 +216,11 @@ test('pipelined chunked POST stream', async (t) => { pipelining: 2 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -270,6 +290,11 @@ test('pipelined chunked POST iterator', async (t) => { pipelining: 2 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -393,6 +418,11 @@ test('pipelining non-idempotent', async (t) => { pipelining: 2 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let ended = false client.request({ @@ -439,6 +469,11 @@ function pipeliningNonIdempotentWithBody (bodyType) { pipelining: 2 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let ended = false let reading = false @@ -747,6 +782,11 @@ test('pipelining blocked', async (t) => { pipelining: 10 }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', method: 'GET', diff --git a/test/client-post.js b/test/client-post.js index 375f9c54b61..10350e031ee 100644 --- a/test/client-post.js +++ b/test/client-post.js @@ -5,9 +5,8 @@ const { test, after } = require('node:test') const { once } = require('node:events') const { Client } = require('..') const { createServer } = require('node:http') -const { Blob } = require('node:buffer') -test('request post blob', { skip: !Blob }, async (t) => { +test('request post blob', async (t) => { t = tspl(t, { plan: 3 }) const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => { @@ -27,6 +26,11 @@ test('request post blob', { skip: !Blob }, async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.request({ path: '/', @@ -43,7 +47,7 @@ test('request post blob', { skip: !Blob }, async (t) => { await t.completed }) -test('request post arrayBuffer', { skip: !Blob }, async (t) => { +test('request post arrayBuffer', async (t) => { t = tspl(t, { plan: 3 }) const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => { @@ -63,6 +67,11 @@ test('request post arrayBuffer', { skip: !Blob }, async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const buf = Buffer.from('asd') const dst = new ArrayBuffer(buf.byteLength) diff --git a/test/client-request.js b/test/client-request.js index 295d953ba7f..d367435eeff 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -11,7 +11,7 @@ const { kConnect } = require('../lib/core/symbols') const { Readable } = require('node:stream') const net = require('node:net') const { promisify } = require('node:util') -const { NotSupportedError, InvalidArgumentError } = require('../lib/core/errors') +const { NotSupportedError, InvalidArgumentError, AbortError } = require('../lib/core/errors') const { parseFormDataString } = require('./utils/formdata') test('request dump head', async (t) => { @@ -22,7 +22,10 @@ test('request dump head', async (t) => { res.flushHeaders() res.write('hello'.repeat(100)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -56,7 +59,10 @@ test('request dump big', async (t) => { // Do nothing... } }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -90,7 +96,10 @@ test('request dump', async (t) => { res.setHeader('content-length', 5) res.end('hello') }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -116,11 +125,14 @@ test('request dump', async (t) => { }) test('request dump with abort signal', async (t) => { - t = tspl(t, { plan: 2 }) + t = tspl(t, { plan: 10 }) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.write('hello') }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -134,6 +146,95 @@ test('request dump with abort signal', async (t) => { const ac = new AbortController() body.dump({ signal: ac.signal }).catch((err) => { t.strictEqual(err.name, 'AbortError') + t.strictEqual(err.message, 'This operation was aborted') + const stackLines = err.stack.split('\n').map((l) => l.trim()) + + t.ok(stackLines[0].startsWith('AbortError: This operation was aborted')) + t.ok(stackLines[1].startsWith('at new DOMException')) + t.ok(stackLines[2].startsWith('at AbortController.abort')) + t.ok(/client-request.js/.test(stackLines[3])) + t.ok(stackLines[4].startsWith('at RequestHandler.runInAsyncScope')) + t.ok(stackLines[5].startsWith('at RequestHandler.onHeaders')) + t.ok(stackLines[6].startsWith('at Request.onHeaders')) + server.close() + }) + ac.abort() + }) + }) + + await t.completed +}) + +test('request dump with POJO as invalid signal', async (t) => { + t = tspl(t, { plan: 9 }) + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.write('hello') + }) + after(() => { + server.closeAllConnections?.() + server.close() + }) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.destroy()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + body.dump({ signal: {} }).catch((err) => { + t.strictEqual(err.name, 'InvalidArgumentError') + t.strictEqual(err.message, 'signal must be an AbortSignal') + const stackLines = err.stack.split('\n').map((l) => l.trim()) + + t.ok(stackLines[0].startsWith('InvalidArgumentError: signal must be an AbortSignal')) + t.ok(stackLines[1].startsWith('at BodyReadable.dump')) + t.ok(/client-request.js/.test(stackLines[2])) + t.ok(stackLines[3].startsWith('at RequestHandler.runInAsyncScope')) + t.ok(stackLines[4].startsWith('at RequestHandler.onHeaders')) + t.ok(stackLines[5].startsWith('at Request.onHeaders')) + server.close() + }) + }) + }) + + await t.completed +}) + +test('request dump with aborted signal', async (t) => { + t = tspl(t, { plan: 8 }) + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.write('hello') + }) + after(() => { + server.closeAllConnections?.() + server.close() + }) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.destroy()) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.ifError(err) + const ac = new AbortController() + ac.abort(new AbortError('This operation was with purpose aborted')) + + body.dump({ signal: ac.signal }).catch((err) => { + t.strictEqual(err.name, 'AbortError') + t.strictEqual(err.message, 'This operation was with purpose aborted') + const stackLines = err.stack.split('\n').map((l) => l.trim()) + + t.ok(stackLines[0].startsWith('AbortError: This operation was with purpose aborted')) + t.ok(/client-request.js/.test(stackLines[1])) + t.ok(stackLines[2].startsWith('at RequestHandler.runInAsyncScope')) + t.ok(stackLines[3].startsWith('at RequestHandler.onHeaders')) + t.ok(stackLines[4].startsWith('at Request.onHeaders')) server.close() }) ac.abort() @@ -148,7 +249,10 @@ test('request hwm', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.write('hello') }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -176,7 +280,10 @@ test('request abort before headers', async (t) => { res.end('hello') signal.emit('abort') }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -213,7 +320,10 @@ test('request body destroyed on invalid callback', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -244,7 +354,10 @@ test('trailers', async (t) => { res.addTrailers({ 'Content-MD5': 'test' }) res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -282,7 +395,10 @@ test('destroy socket abruptly', async (t) => { // therefore we delay it to the next event loop run. setImmediate(socket.destroy.bind(socket)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) @@ -324,7 +440,10 @@ test('destroy socket abruptly with keep-alive', async (t) => { // therefore we delay it to the next event loop run. setImmediate(socket.destroy.bind(socket)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) await promisify(server.listen.bind(server))(0) const client = new Client(`http://localhost:${server.address().port}`) @@ -358,7 +477,10 @@ test('request json', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -381,7 +503,10 @@ test('request long multibyte json', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -404,7 +529,10 @@ test('request text', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -494,7 +622,10 @@ describe('headers', () => { serverAddress = `localhost:${server.address().port}` }) - after(() => server.close()) + after(() => { + server.closeAllConnections() + server.close() + }) test('empty host header', async (t) => { t = tspl(t, { plan: 4 }) @@ -534,7 +665,10 @@ describe('headers', () => { serverAddress = `localhost:${server.address().port}` }) - after(() => server.close()) + after(() => { + server.closeAllConnections() + server.close() + }) test('invalid host header', async (t) => { t = tspl(t, { plan: 1 }) @@ -586,7 +720,10 @@ test('request long multibyte text', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -610,7 +747,10 @@ test('request blob', async (t) => { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -636,7 +776,10 @@ test('request arrayBuffer', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -662,7 +805,10 @@ test('request bytes', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -688,7 +834,10 @@ test('request body', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -720,7 +869,10 @@ test('request post body no missing data', async (t) => { t.strictEqual(ret, 'asd') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -734,8 +886,7 @@ test('request post body no missing data', async (t) => { this.push('asd') this.push(null) } - }), - maxRedirections: 2 + }) }) await body.text() t.ok(true, 'pass') @@ -755,7 +906,10 @@ test('request post body no extra data handler', async (t) => { t.strictEqual(ret, 'asd') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -773,8 +927,7 @@ test('request post body no extra data handler', async (t) => { const { body } = await client.request({ path: '/', method: 'GET', - body: reqBody, - maxRedirections: 0 + body: reqBody }) await body.text() t.ok(true, 'pass') @@ -791,7 +944,10 @@ test('request with onInfo callback', async (t) => { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ foo: 'bar' })) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -818,7 +974,10 @@ test('request with onInfo callback but socket is destroyed before end of respons response = res res.writeProcessing() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -862,7 +1021,10 @@ test('request onInfo callback headers parsing', async (t) => { ] socket.end(lines.join('\r\n')) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) await promisify(server.listen.bind(server))(0) @@ -898,7 +1060,10 @@ test('request raw responseHeaders', async (t) => { ] socket.end(lines.join('\r\n')) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) await promisify(server.listen.bind(server))(0) @@ -925,7 +1090,10 @@ test('request formData', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -954,7 +1122,10 @@ test('request text2', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(JSON.stringify(obj)) }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -979,7 +1150,6 @@ test('request text2', async (t) => { test('request with FormData body', async (t) => { const { FormData } = require('../') - const { Blob } = require('node:buffer') const fd = new FormData() fd.set('key', 'value') @@ -1012,7 +1182,10 @@ test('request with FormData body', async (t) => { return res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1042,7 +1215,10 @@ test('request post body Buffer from string', async (t) => { t.strictEqual(ret, 'abcdefghijklmnopqrstuvwxyz') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1051,8 +1227,7 @@ test('request post body Buffer from string', async (t) => { const { body } = await client.request({ path: '/', method: 'POST', - body: requestBody, - maxRedirections: 2 + body: requestBody }) await body.text() t.ok(true, 'pass') @@ -1074,7 +1249,10 @@ test('request post body Buffer from buffer', async (t) => { t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1083,8 +1261,7 @@ test('request post body Buffer from buffer', async (t) => { const { body } = await client.request({ path: '/', method: 'POST', - body: requestBody, - maxRedirections: 2 + body: requestBody }) await body.text() t.ok(true, 'pass') @@ -1106,7 +1283,10 @@ test('request post body Uint8Array', async (t) => { t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1115,8 +1295,7 @@ test('request post body Uint8Array', async (t) => { const { body } = await client.request({ path: '/', method: 'POST', - body: requestBody, - maxRedirections: 2 + body: requestBody }) await body.text() t.ok(true, 'pass') @@ -1138,7 +1317,10 @@ test('request post body Uint32Array', async (t) => { t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1147,8 +1329,7 @@ test('request post body Uint32Array', async (t) => { const { body } = await client.request({ path: '/', method: 'POST', - body: requestBody, - maxRedirections: 2 + body: requestBody }) await body.text() t.ok(true, 'pass') @@ -1170,7 +1351,10 @@ test('request post body Float64Array', async (t) => { t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1179,8 +1363,7 @@ test('request post body Float64Array', async (t) => { const { body } = await client.request({ path: '/', method: 'POST', - body: requestBody, - maxRedirections: 2 + body: requestBody }) await body.text() t.ok(true, 'pass') @@ -1202,7 +1385,10 @@ test('request post body BigUint64Array', async (t) => { t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1211,8 +1397,7 @@ test('request post body BigUint64Array', async (t) => { const { body } = await client.request({ path: '/', method: 'POST', - body: requestBody, - maxRedirections: 2 + body: requestBody }) await body.text() t.ok(true, 'pass') @@ -1234,7 +1419,10 @@ test('request post body DataView', async (t) => { t.strictEqual(ret, 'ijklmnopqrstuvwx') res.end() }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) @@ -1243,8 +1431,7 @@ test('request post body DataView', async (t) => { const { body } = await client.request({ path: '/', method: 'POST', - body: requestBody, - maxRedirections: 2 + body: requestBody }) await body.text() t.ok(true, 'pass') diff --git a/test/client-stream.js b/test/client-stream.js index 8428ae990db..24d64bf822c 100644 --- a/test/client-stream.js +++ b/test/client-stream.js @@ -22,6 +22,11 @@ test('stream get', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const signal = new EE() client.stream({ @@ -65,6 +70,11 @@ test('stream promise get', async (t) => { server.listen(0, async () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) await client.stream({ path: '/', @@ -254,6 +264,11 @@ test('stream waits only for writable side', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const pt = new PassThrough({ autoDestroy: false }) client.stream({ @@ -321,6 +336,11 @@ test('stream destroy if not readable', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.stream({ path: '/', @@ -397,6 +417,11 @@ test('stream body without destroy', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.stream({ path: '/', @@ -520,6 +545,11 @@ test('stream abort after complete', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(client.destroy.bind(client)) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const pt = new PassThrough() const signal = new EE() @@ -580,6 +610,11 @@ test('trailers', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) client.stream({ path: '/', @@ -605,6 +640,11 @@ test('stream ignore 1xx', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let buf = '' client.stream({ @@ -637,6 +677,11 @@ test('stream ignore 1xx and use onInfo', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let buf = '' client.stream({ @@ -675,6 +720,11 @@ test('stream backpressure', async (t) => { server.listen(0, () => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) let buf = '' client.stream({ @@ -736,6 +786,11 @@ test('stream needDrain', async (t) => { after(() => { client.destroy() }) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const dst = new PassThrough() dst.pause() @@ -792,6 +847,11 @@ test('stream legacy needDrain', async (t) => { after(() => { client.destroy() }) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) const dst = new PassThrough() dst.pause() diff --git a/test/client-timeout.js b/test/client-timeout.js index 26cf21913c2..7ec8fdec6ee 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -3,10 +3,67 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { Client, errors } = require('..') +const EventEmitter = require('node:events') const { createServer } = require('node:http') const { Readable } = require('node:stream') const FakeTimers = require('@sinonjs/fake-timers') const timers = require('../lib/util/timers') +const connectH1 = require('../lib/dispatcher/client-h1') +const { + kMaxHeadersSize, + kMaxResponseSize, + kParser, + kQueue, + kRunningIdx +} = require('../lib/core/symbols') + +class DummySocket extends EventEmitter { + constructor () { + super() + this.destroyed = false + this.errored = null + } + + read () { + return null + } +} + +test('parser reuses WeakRef when replacing timeout callbacks', async (t) => { + const OriginalWeakRef = global.WeakRef + t.after(() => { + global.WeakRef = OriginalWeakRef + }) + + t = tspl(t, { plan: 1 }) + + let weakRefCount = 0 + + global.WeakRef = class CountingWeakRef extends OriginalWeakRef { + constructor (target) { + weakRefCount++ + super(target) + } + } + + const socket = new DummySocket() + const client = { + [kMaxHeadersSize]: 1024, + [kMaxResponseSize]: 1024, + [kQueue]: [], + [kRunningIdx]: 0 + } + + await connectH1(client, socket) + const parser = socket[kParser] + + parser.setTimeout(200, 0) + parser.setTimeout(300, 0) + parser.setTimeout(400, 1) + parser.destroy() + + t.strictEqual(weakRefCount, 1) +}) test('refresh timeout on pause', async (t) => { t = tspl(t, { plan: 1 }) @@ -180,6 +237,12 @@ test('parser resume with no body timeout', async (t) => { }) after(() => client.destroy()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.dispatch({ path: '/', method: 'GET' diff --git a/test/client-wasm.js b/test/client-wasm.js index 065729e26df..a7daeed7b9a 100644 --- a/test/client-wasm.js +++ b/test/client-wasm.js @@ -43,11 +43,8 @@ const { describe, test } = require('node:test') } }) - test('has the right amount of exports', async (t) => { - t = tspl(t, { plan: 2 }) - - t.ok(instance.exports, 'exports are present') - t.deepStrictEqual(Object.keys(instance.exports), [ + test('has the required exports', async (t) => { + const requiredExports = [ 'memory', '_initialize', '__indirect_function_table', @@ -88,7 +85,12 @@ const { describe, test } = require('node:test') 'llhttp_set_lenient_optional_cr_before_lf', 'llhttp_set_lenient_spaces_after_chunk_size', 'llhttp_message_needs_eof' - ]) + ] + t = tspl(t, { plan: requiredExports.length }) + + for (const key of requiredExports) { + t.ok(key in instance.exports, `${key} is exported`) + } await t.completed }) diff --git a/test/client-write-max-listeners.js b/test/client-write-max-listeners.js index 09ab13cafb8..abfe204db74 100644 --- a/test/client-write-max-listeners.js +++ b/test/client-write-max-listeners.js @@ -48,6 +48,12 @@ test('socket close listener does not leak', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.destroy()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + for (let n = 0; n < 16; ++n) { client.request({ path: '/', method: 'GET', body: makeBody() }, onRequest) } diff --git a/test/client.js b/test/client.js index 99a972c510e..0627f8f650d 100644 --- a/test/client.js +++ b/test/client.js @@ -15,7 +15,7 @@ const hasIPv6 = (() => { const iFaces = require('node:os').networkInterfaces() const re = process.platform === 'win32' ? /Loopback Pseudo-Interface/ : /lo/ return Object.keys(iFaces).some( - (name) => re.test(name) && iFaces[name].some(({ family }) => family === 6) + (name) => re.test(name) && iFaces[name].some(({ family }) => family === 'IPv6') ) })() @@ -91,6 +91,30 @@ test('basic get', async (t) => { await t.completed }) +test('passes socketPath to custom connect function', async (t) => { + t = tspl(t, { plan: 2 }) + + const connectError = new Error('custom connect error') + const socketPath = '/var/run/test.sock' + const client = new Client('http://localhost', { + socketPath, + connect (opts, cb) { + t.strictEqual(opts.socketPath, socketPath) + cb(connectError, null) + } + }) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET' + }, (err) => { + t.strictEqual(err, connectError) + }) + + await t.completed +}) + test('basic get with custom request.reset=true', async (t) => { t = tspl(t, { plan: 26 }) @@ -427,7 +451,7 @@ test('basic head', async (t) => { }) test('basic head (IPv6)', { skip: !hasIPv6 }, async (t) => { - t = tspl(t, { plan: 15 }) + t = tspl(t, { plan: 10 }) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { t.strictEqual('/123', req.url) @@ -2189,7 +2213,7 @@ test('stats', async (t) => { }, (err, data) => { t.ifError(err) t.strictEqual(client.stats.connected, true) - t.strictEqual(client.stats.pending, 1) + t.strictEqual(client.stats.pending, 0) t.strictEqual(client.stats.running, 1) }) }) diff --git a/test/connect-pre-shared-session.js b/test/connect-pre-shared-session.js index c10e3c82a3d..5bc811ff7c5 100644 --- a/test/connect-pre-shared-session.js +++ b/test/connect-pre-shared-session.js @@ -4,11 +4,11 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after, mock } = require('node:test') const { Client } = require('..') const { createServer } = require('node:https') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const tls = require('node:tls') test('custom session passed to client will be used in tls connect call', async (t) => { - t = tspl(t, { plan: 4 }) + t = tspl(t, { plan: 6 }) const mockConnect = mock.method(tls, 'connect') @@ -31,6 +31,12 @@ test('custom session passed to client will be used in tls connect call', async ( }) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + const { statusCode, headers, body } = await client.request({ path: '/', method: 'GET' diff --git a/test/content-length.js b/test/content-length.js index 6d8e7799c91..92386b93b67 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -209,6 +209,12 @@ test('request streaming no body data when content-length=0', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', @@ -278,6 +284,12 @@ test('request streaming with Readable.from(buf)', async (t) => { const client = new Client(`http://localhost:${server.address().port}`) after(() => client.close()) + client.on('disconnect', () => { + if (!client.closed && !client.destroyed) { + t.fail('unexpected disconnect') + } + }) + client.request({ path: '/', method: 'PUT', diff --git a/test/cookie/cookies.js b/test/cookie/cookies.js index 9e2d3cf5fff..27f3708e5cc 100644 --- a/test/cookie/cookies.js +++ b/test/cookie/cookies.js @@ -28,6 +28,7 @@ const { deleteCookie, getCookies, getSetCookies, + parseCookie, setCookie, Headers } = require('../..') @@ -600,6 +601,41 @@ test('Set-Cookie parser', () => { assert.deepEqual(getSetCookies(headers), []) }) +test('Set-Cookie parser does not percent-decode cookie values', () => { + assert.deepEqual( + parseCookie( + 'token=legit%0d%0aSet-Cookie:%20evil=injected%3B%20Path%3D/' + ), + { + name: 'token', + value: 'legit%0d%0aSet-Cookie:%20evil=injected%3B%20Path%3D/' + } + ) + + assert.deepEqual(parseCookie('data=prefix%00suffix'), { + name: 'data', + value: 'prefix%00suffix' + }) +}) + +test('Set-Cookie parser only accepts exact SameSite values', () => { + assert.deepEqual(parseCookie('a=b; SameSite=none'), { + name: 'a', + value: 'b', + sameSite: 'None' + }) + + assert.deepEqual(parseCookie('a=b; SameSite=StrictLax'), { + name: 'a', + value: 'b' + }) + + assert.deepEqual(parseCookie('a=b; SameSite=NoneOfYourBusiness'), { + name: 'a', + value: 'b' + }) +}) + test('Cookie setCookie throws if headers is not of type Headers', () => { class Headers { [Symbol.toStringTag] = 'CustomHeaders' diff --git a/test/cookie/npm-cookie.js b/test/cookie/npm-cookie.js index 28e86c1d279..0475b52d6ad 100644 --- a/test/cookie/npm-cookie.js +++ b/test/cookie/npm-cookie.js @@ -51,13 +51,16 @@ describe('parseCookie(str)', function () { assert.deepStrictEqual(parseCookie('f=;b='), { name: 'f', value: '', unparsed: ['b='] }) }) - it('should URL-decode values', function () { + it('should preserve encoded values', function () { assert.deepStrictEqual(parseCookie('foo="bar=123456789&name=Magic+Mouse"'), { name: 'foo', value: '"bar=123456789&name=Magic+Mouse"' }) - assert.deepStrictEqual(parseCookie('email=%20%22%2c%3b%2f'), { name: 'email', value: ' ",;/' }) + assert.deepStrictEqual(parseCookie('email=%20%22%2c%3b%2f'), { + name: 'email', + value: '%20%22%2c%3b%2f' + }) }) it('should trim whitespace around key and value', function () { diff --git a/test/env-http-proxy-agent-nodejs-bundle.js b/test/env-http-proxy-agent-nodejs-bundle.js index f9f8d526d10..47c2649ff42 100644 --- a/test/env-http-proxy-agent-nodejs-bundle.js +++ b/test/env-http-proxy-agent-nodejs-bundle.js @@ -20,7 +20,7 @@ describe('EnvHttpProxyAgent and setGlobalDispatcher', () => { process.env = { ...env } }) - test('should work together from the Node.js bundle', async (t) => { + test('should work with global fetch from undici bundled with Node.js', async (t) => { const { strictEqual } = tspl(t, { plan: 3 }) // Instead of using mocks, start a real server and a minimal proxy server @@ -30,7 +30,10 @@ describe('EnvHttpProxyAgent and setGlobalDispatcher', () => { server.on('error', err => { console.log('Server error', err) }) server.listen(0) await once(server, 'listening') - t.after(() => { server.close() }) + t.after(() => { + server.closeAllConnections?.() + server.close() + }) const proxy = http.createServer({ joinDuplicateHeaders: true }) proxy.on('connect', (req, clientSocket, head) => { @@ -60,7 +63,10 @@ describe('EnvHttpProxyAgent and setGlobalDispatcher', () => { proxy.listen(0) await once(proxy, 'listening') - t.after(() => { proxy.close() }) + t.after(() => { + proxy.closeAllConnections?.() + proxy.close() + }) // Use setGlobalDispatcher and EnvHttpProxyAgent from Node.js // and make sure that they work together. @@ -69,6 +75,7 @@ describe('EnvHttpProxyAgent and setGlobalDispatcher', () => { process.env.http_proxy = proxyAddress setGlobalDispatcher(new EnvHttpProxyAgent()) + // eslint-disable-next-line no-restricted-globals const res = await fetch(serverAddress) strictEqual(await res.text(), 'Hello world') }) diff --git a/test/env-http-proxy-agent.js b/test/env-http-proxy-agent.js index 1a707fad538..97969cfaf20 100644 --- a/test/env-http-proxy-agent.js +++ b/test/env-http-proxy-agent.js @@ -258,10 +258,10 @@ describe('no_proxy', () => { t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:0')) t.ok(await doesNotProxy('http://example:1337')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await doesNotProxy('http://sub.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) + t.ok(await doesNotProxy('http://a.b.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example')) return dispatcher.close() }) @@ -273,10 +273,10 @@ describe('no_proxy', () => { t.ok(await doesNotProxy('http://example:80')) t.ok(await doesNotProxy('http://example:0')) t.ok(await doesNotProxy('http://example:1337')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await doesNotProxy('http://sub.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) + t.ok(await doesNotProxy('http://a.b.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example')) return dispatcher.close() }) @@ -290,24 +290,25 @@ describe('no_proxy', () => { t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:0')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) t.ok(await doesNotProxy('http://sub.example')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://no.sub.example')) + t.ok(await doesNotProxy('http://no.sub.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub-example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.sub')) return dispatcher.close() }) test('host + port', async (t) => { - t = tspl(t, { plan: 12 }) + t = tspl(t, { plan: 13 }) process.env.no_proxy = 'example:80, localhost:3000' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(12) + const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(13) t.ok(await doesNotProxy('http://example')) t.ok(await doesNotProxy('http://example:80')) + t.ok(await doesNotProxy('http://sub.example:80')) t.ok(await doesNotProxy('http://example:0')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await doesNotProxy('http://sub.example')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) + t.ok(await doesNotProxy('http://a.b.example')) t.ok(await doesNotProxy('http://localhost:3000/')) t.ok(await doesNotProxy('https://localhost:3000/')) t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://localhost:3001/')) @@ -315,13 +316,13 @@ describe('no_proxy', () => { return dispatcher.close() }) - test('host suffix', async (t) => { + test('host suffix - leading dot stripped', async (t) => { t = tspl(t, { plan: 9 }) process.env.no_proxy = '.example' const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) + t.ok(await doesNotProxy('http://example')) + t.ok(await doesNotProxy('http://example:80')) + t.ok(await doesNotProxy('http://example:1337')) t.ok(await doesNotProxy('http://sub.example')) t.ok(await doesNotProxy('http://sub.example:80')) t.ok(await doesNotProxy('http://sub.example:1337')) @@ -331,13 +332,13 @@ describe('no_proxy', () => { return dispatcher.close() }) - test('host suffix with *.', async (t) => { + test('host suffix with *. - leading dot with asterisk stripped', async (t) => { t = tspl(t, { plan: 9 }) process.env.no_proxy = '*.example' const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(9) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:80')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example:1337')) + t.ok(await doesNotProxy('http://example')) + t.ok(await doesNotProxy('http://example:80')) + t.ok(await doesNotProxy('http://example:1337')) t.ok(await doesNotProxy('http://sub.example')) t.ok(await doesNotProxy('http://sub.example:80')) t.ok(await doesNotProxy('http://sub.example:1337')) @@ -347,20 +348,16 @@ describe('no_proxy', () => { return dispatcher.close() }) - test('substring suffix', async (t) => { - t = tspl(t, { plan: 10 }) + test('substring suffix are NOT supported', async (t) => { + t = tspl(t, { plan: 6 }) process.env.no_proxy = '*example' - const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(10) - t.ok(await doesNotProxy('http://example')) - t.ok(await doesNotProxy('http://example:80')) - t.ok(await doesNotProxy('http://example:1337')) - t.ok(await doesNotProxy('http://sub.example')) - t.ok(await doesNotProxy('http://sub.example:80')) - t.ok(await doesNotProxy('http://sub.example:1337')) - t.ok(await doesNotProxy('http://prefexample')) - t.ok(await doesNotProxy('http://a.b.example')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.no')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://host/example')) + const { dispatcher, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://x.prefexample')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://a.b.example')) return dispatcher.close() }) @@ -442,11 +439,11 @@ describe('no_proxy', () => { test('prefers lowercase over uppercase', async (t) => { t = tspl(t, { plan: 2 }) - process.env.NO_PROXY = 'sub.example.com' + process.env.NO_PROXY = 'another.com' process.env.no_proxy = 'example.com' const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(6) t.ok(await doesNotProxy('http://example.com')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://another.com')) return dispatcher.close() }) @@ -464,10 +461,10 @@ describe('no_proxy', () => { process.env.no_proxy = 'example.com' const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(4) t.ok(await doesNotProxy('http://example.com')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) - process.env.no_proxy = 'sub.example.com' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://another.com')) + process.env.no_proxy = 'another.com' t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://example.com')) - t.ok(await doesNotProxy('http://sub.example.com')) + t.ok(await doesNotProxy('http://another.com')) return dispatcher.close() }) @@ -475,10 +472,10 @@ describe('no_proxy', () => { t = tspl(t, { plan: 4 }) const { dispatcher, doesNotProxy, usesProxyAgent } = createEnvHttpProxyAgentWithMocks(4, { noProxy: 'example.com' }) t.ok(await doesNotProxy('http://example.com')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) - process.env.no_proxy = 'sub.example.com' + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://another.com')) + process.env.no_proxy = 'another.com' t.ok(await doesNotProxy('http://example.com')) - t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://sub.example.com')) + t.ok(await usesProxyAgent(kHttpProxyAgent, 'http://another.com')) return dispatcher.close() }) }) diff --git a/test/eventsource/eventsource-attributes.js b/test/eventsource/eventsource-attributes.js index 42465c912c7..4050c023278 100644 --- a/test/eventsource/eventsource-attributes.js +++ b/test/eventsource/eventsource-attributes.js @@ -1,56 +1,57 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') -const { test, describe } = require('node:test') +const { test, describe, before, after } = require('node:test') const { EventSource } = require('../../lib/web/eventsource/eventsource') -describe('EventSource - eventhandler idl', async () => { - const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { - res.writeHead(200, 'dummy') +describe('EventSource - eventhandler idl', () => { + let server + let port + + before(async () => { + server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.writeHead(200, 'dummy') + }) + + await once(server.listen(0), 'listening') + port = server.address().port }) - server.listen(0) - await events.once(server, 'listening') - const port = server.address().port + after(() => { server.close() }) - let done = 0 const eventhandlerIdl = ['onmessage', 'onerror', 'onopen'] eventhandlerIdl.forEach((type) => { - test(`Should properly configure the ${type} eventhandler idl`, () => { + test(`Should properly configure the ${type} eventhandler idl`, (t) => { const eventSourceInstance = new EventSource(`http://localhost:${port}`) // Eventsource eventhandler idl is by default null, - assert.strictEqual(eventSourceInstance[type], null) + t.assert.strictEqual(eventSourceInstance[type], null) // The eventhandler idl is by default not enumerable. - assert.strictEqual(Object.prototype.propertyIsEnumerable.call(eventSourceInstance, type), false) + t.assert.strictEqual(Object.prototype.propertyIsEnumerable.call(eventSourceInstance, type), false) // The eventhandler idl ignores non-functions. eventSourceInstance[type] = 7 - assert.strictEqual(EventSource[type], undefined) + t.assert.strictEqual(EventSource[type], undefined) // The eventhandler idl accepts functions. function fn () { - assert.fail('Should not have called the eventhandler') + t.assert.fail('Should not have called the eventhandler') } eventSourceInstance[type] = fn - assert.strictEqual(eventSourceInstance[type], fn) + t.assert.strictEqual(eventSourceInstance[type], fn) // The eventhandler idl can be set to another function. function fn2 () { } eventSourceInstance[type] = fn2 - assert.strictEqual(eventSourceInstance[type], fn2) + t.assert.strictEqual(eventSourceInstance[type], fn2) // The eventhandler idl overrides the previous function. eventSourceInstance.dispatchEvent(new Event(type)) eventSourceInstance.close() - done++ - - if (done === eventhandlerIdl.length) server.close() }) }) }) @@ -61,31 +62,31 @@ describe('EventSource - constants', () => { ['OPEN', 1], ['CLOSED', 2] ].forEach((config) => { - test(`Should expose the ${config[0]} constant`, () => { + test(`Should expose the ${config[0]} constant`, (t) => { const [constant, value] = config // EventSource exposes the constant. - assert.strictEqual(Object.hasOwn(EventSource, constant), true) + t.assert.strictEqual(Object.hasOwn(EventSource, constant), true) // The value is properly set. - assert.strictEqual(EventSource[constant], value) + t.assert.strictEqual(EventSource[constant], value) // The constant is enumerable. - assert.strictEqual(Object.prototype.propertyIsEnumerable.call(EventSource, constant), true) + t.assert.strictEqual(Object.prototype.propertyIsEnumerable.call(EventSource, constant), true) // The constant is not writable. try { EventSource[constant] = 666 } catch (e) { - assert.strictEqual(e instanceof TypeError, true) + t.assert.strictEqual(e instanceof TypeError, true) } // The constant is not configurable. try { delete EventSource[constant] } catch (e) { - assert.strictEqual(e instanceof TypeError, true) + t.assert.strictEqual(e instanceof TypeError, true) } - assert.strictEqual(EventSource[constant], value) + t.assert.strictEqual(EventSource[constant], value) }) }) }) diff --git a/test/eventsource/eventsource-close.js b/test/eventsource/eventsource-close.js index aef42e95185..0eb3b7fe878 100644 --- a/test/eventsource/eventsource-close.js +++ b/test/eventsource/eventsource-close.js @@ -1,59 +1,61 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') -const { setTimeout } = require('node:timers/promises') -const { test, describe } = require('node:test') +const { test, describe, after } = require('node:test') const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - close', () => { - test('should not emit error when closing the EventSource Instance', async () => { + test('should not emit error when closing the EventSource Instance', async (t) => { + t.plan(1) + const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.headers.connection, 'keep-alive') + t.assert.strictEqual(req.headers.connection, 'keep-alive') res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('data: hello\n\n') + + res.on('close', () => { + server.close() + }) }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = async () => { + eventSourceInstance.onopen = () => { eventSourceInstance.close() - await setTimeout(1000, { ref: false }) - server.close() } eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + t.assert.fail('Should not have errored') } + + await once(server, 'close') }) - test('should set readyState to CLOSED', async () => { + test('should set readyState to CLOSED', async (t) => { + t.plan(3) const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.headers.connection, 'keep-alive') + t.assert.strictEqual(req.headers.connection, 'keep-alive') res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('data: hello\n\n') }) - - server.listen(0) - await events.once(server, 'listening') + after(() => server.close()) + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) eventSourceInstance.onopen = () => { - assert.strictEqual(eventSourceInstance.readyState, EventSource.OPEN) + t.assert.strictEqual(eventSourceInstance.readyState, EventSource.OPEN) eventSourceInstance.close() - assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED) + t.assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED) + server.close() } eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + t.assert.fail('Should not have errored') } - - await setTimeout(2000, { ref: false }) - server.close() + await once(server, 'close') }) }) diff --git a/test/eventsource/eventsource-connect.js b/test/eventsource/eventsource-connect.js index 41879ef37e4..1b9967550b2 100644 --- a/test/eventsource/eventsource-connect.js +++ b/test/eventsource/eventsource-connect.js @@ -1,189 +1,185 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') -const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/web/eventsource/eventsource') +const { test, describe, after } = require('node:test') +const FakeTimers = require('@sinonjs/fake-timers') +const { EventSource, defaultReconnectionTime } = require('../../lib/web/eventsource/eventsource') const { randomInt } = require('node:crypto') describe('EventSource - sending correct request headers', () => { - test('should send request with connection keep-alive', async () => { + test('should send request with connection keep-alive', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.headers.connection, 'keep-alive') + t.assert.strictEqual(req.headers.connection, 'keep-alive') res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = () => { + eventSourceInstance.onopen = (t) => { eventSourceInstance.close() server.close() } - eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + eventSourceInstance.onerror = (t) => { + t.assert.fail('Should not have errored') } }) - test('should send request with sec-fetch-mode set to cors', async () => { + test('should send request with sec-fetch-mode set to cors', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.headers['sec-fetch-mode'], 'cors') + t.assert.strictEqual(req.headers['sec-fetch-mode'], 'cors') res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = () => { + eventSourceInstance.onopen = (t) => { eventSourceInstance.close() server.close() } - eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + eventSourceInstance.onerror = (t) => { + t.assert.fail('Should not have errored') } }) - test('should send request with pragma and cache-control set to no-cache', async () => { + test('should send request with pragma and cache-control set to no-cache', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.headers['cache-control'], 'no-cache') - assert.strictEqual(req.headers.pragma, 'no-cache') + t.assert.strictEqual(req.headers['cache-control'], 'no-cache') + t.assert.strictEqual(req.headers.pragma, 'no-cache') res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = () => { + eventSourceInstance.onopen = (t) => { eventSourceInstance.close() server.close() } - eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + eventSourceInstance.onerror = (t) => { + t.assert.fail('Should not have errored') } }) - test('should send request with accept text/event-stream', async () => { + test('should send request with accept text/event-stream', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.headers.accept, 'text/event-stream') + t.assert.strictEqual(req.headers.accept, 'text/event-stream') res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = () => { + eventSourceInstance.onopen = (t) => { eventSourceInstance.close() server.close() } - eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + eventSourceInstance.onerror = (t) => { + t.assert.fail('Should not have errored') } }) }) describe('EventSource - received response must have content-type to be text/event-stream', () => { - test('should send request with accept text/event-stream', async () => { + test('should send request with accept text/event-stream', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = () => { + eventSourceInstance.onopen = (t) => { eventSourceInstance.close() server.close() } - eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + eventSourceInstance.onerror = (t) => { + t.assert.fail('Should not have errored') } }) - test('should send request with accept text/event-stream;', async () => { + test('should send request with accept text/event-stream;', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream;' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = () => { + eventSourceInstance.onopen = (t) => { eventSourceInstance.close() server.close() } - eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + eventSourceInstance.onerror = (t) => { + t.assert.fail('Should not have errored') } }) - test('should handle content-type text/event-stream;charset=UTF-8 properly', async () => { + test('should handle content-type text/event-stream;charset=UTF-8 properly', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream;charset=UTF-8' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = () => { + eventSourceInstance.onopen = (t) => { eventSourceInstance.close() server.close() } - eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + eventSourceInstance.onerror = (t) => { + t.assert.fail('Should not have errored') } }) - test('should throw if content-type is text/html properly', async () => { + test('should throw if content-type is text/html properly', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/html' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = () => { - assert.fail('Should not have opened') + eventSourceInstance.onopen = (t) => { + t.assert.fail('Should not have opened') } - eventSourceInstance.onerror = () => { + eventSourceInstance.onerror = (t) => { eventSourceInstance.close() server.close() } }) - test('should try to connect again if server is unreachable', async () => { + test('should try to connect again if server is unreachable', async (t) => { + const clock = FakeTimers.install() + + after(() => clock.uninstall()) + const reconnectionTime = defaultReconnectionTime const domain = 'bad.n' + randomInt(1e10).toString(36) + '.proxy' const eventSourceInstance = new EventSource(`http://${domain}`) @@ -192,11 +188,57 @@ describe('EventSource - received response must have content-type to be text/even eventSourceInstance.onerror = (error) => { onerrorCalls.push(error) } + clock.tick(reconnectionTime) + + await once(eventSourceInstance, 'error') + + const start = Date.now() + clock.tick(reconnectionTime) + await once(eventSourceInstance, 'error') + clock.tick(reconnectionTime) + await once(eventSourceInstance, 'error') + clock.tick(reconnectionTime) + await once(eventSourceInstance, 'error') + const end = Date.now() + + eventSourceInstance.close() + + t.assert.strictEqual(onerrorCalls.length, 4, 'Expected 4 error events') + t.assert.strictEqual(end - start, 3 * reconnectionTime, `Expected reconnection to happen after ${3 * reconnectionTime}ms, but took ${end - start}ms`) + }) + + test('should try to connect again if server is unreachable, configure reconnectionTime', async (t) => { + const reconnectionTime = 1000 + const clock = FakeTimers.install() + after(() => clock.uninstall()) + + const domain = 'bad.n' + randomInt(1e10).toString(36) + '.proxy' + + const eventSourceInstance = new EventSource(`http://${domain}`, { + node: { + reconnectionTime + } + }) + + const onerrorCalls = [] + eventSourceInstance.onerror = (error) => { + onerrorCalls.push(error) + } + + await once(eventSourceInstance, 'error') - await new Promise(resolve => setTimeout(resolve, 8000)) + const start = Date.now() + clock.tick(reconnectionTime) + await once(eventSourceInstance, 'error') + clock.tick(reconnectionTime) + await once(eventSourceInstance, 'error') + clock.tick(reconnectionTime) + await once(eventSourceInstance, 'error') + const end = Date.now() eventSourceInstance.close() - assert.strictEqual(onerrorCalls.length, 3) + t.assert.strictEqual(onerrorCalls.length, 4, 'Expected 4 error events') + t.assert.strictEqual(end - start, 3 * reconnectionTime, `Expected reconnection to happen after ${3 * reconnectionTime}ms, but took ${end - start}ms`) }) }) diff --git a/test/eventsource/eventsource-constructor-stringify.js b/test/eventsource/eventsource-constructor-stringify.js index a8cd6b14986..3061a9712d5 100644 --- a/test/eventsource/eventsource-constructor-stringify.js +++ b/test/eventsource/eventsource-constructor-stringify.js @@ -1,21 +1,19 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - constructor stringify', () => { - test('should stringify argument', async () => { + test('should stringify argument', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.headers.connection, 'keep-alive') + t.assert.strictEqual(req.headers.connection, 'keep-alive') res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource({ toString: function () { return `http://localhost:${port}` } }) @@ -25,7 +23,7 @@ describe('EventSource - constructor stringify', () => { } eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + t.assert.fail('Should not have errored') } }) }) diff --git a/test/eventsource/eventsource-constructor.js b/test/eventsource/eventsource-constructor.js index 49a818e4cce..00a33965ed6 100644 --- a/test/eventsource/eventsource-constructor.js +++ b/test/eventsource/eventsource-constructor.js @@ -1,53 +1,50 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - withCredentials', () => { - test('withCredentials should be false by default', async () => { + test('withCredentials should be false by default', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) eventSourceInstance.onopen = () => { - assert.strictEqual(eventSourceInstance.withCredentials, false) + t.assert.strictEqual(eventSourceInstance.withCredentials, false) eventSourceInstance.close() server.close() } eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + t.assert.fail('Should not have errored') } }) - test('withCredentials can be set to true', async () => { + test('withCredentials can be set to true', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`, { withCredentials: true }) eventSourceInstance.onopen = () => { - assert.strictEqual(eventSourceInstance.withCredentials, true) + t.assert.strictEqual(eventSourceInstance.withCredentials, true) eventSourceInstance.close() server.close() } eventSourceInstance.onerror = () => { - assert.fail('Should not have errored') + t.assert.fail('Should not have errored') } }) }) diff --git a/test/eventsource/eventsource-custom-dispatcher.js b/test/eventsource/eventsource-custom-dispatcher.js index 4a852a56e7b..591626beb0a 100644 --- a/test/eventsource/eventsource-custom-dispatcher.js +++ b/test/eventsource/eventsource-custom-dispatcher.js @@ -1,38 +1,72 @@ 'use strict' const { createServer } = require('node:http') -const { once } = require('node:events') const { Agent, EventSource } = require('../..') -const { tspl } = require('@matteo.collina/tspl') const { test } = require('node:test') -test('EventSource allows setting custom dispatcher.', async (t) => { - const { completed, deepStrictEqual } = tspl(t, { plan: 1 }) +test('EventSource allows setting custom dispatcher.', (t, done) => { + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) - deepStrictEqual(req.headers['x-customer-header'], 'hello world') + t.assert.deepStrictEqual(req.headers['x-customer-header'], 'hello world') res.end() - }).listen(0) + done() + }) t.after(() => { server.close() - eventSourceInstance.close() }) - await once(server, 'listening') - - class CustomHeaderAgent extends Agent { - dispatch (opts) { - opts.headers['x-customer-header'] = 'hello world' - return super.dispatch(...arguments) + server.listen(0, () => { + class CustomHeaderAgent extends Agent { + dispatch (opts) { + opts.headers['x-customer-header'] = 'hello world' + return super.dispatch(...arguments) + } } - } - const eventSourceInstance = new EventSource(`http://localhost:${server.address().port}`, { - dispatcher: new CustomHeaderAgent() + const eventSourceInstance = new EventSource(`http://localhost:${server.address().port}`, { + dispatcher: new CustomHeaderAgent() + }) + t.after(() => { + eventSourceInstance.close() + }) + }) +}) + +test('EventSource allows setting custom dispatcher in EventSourceDict.', (t, done) => { + t.plan(1) + + const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => { + res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) + t.assert.deepStrictEqual(req.headers['x-customer-header'], 'hello world') + + res.end() + + done() }) - await completed + t.after(() => { + server.close() + }) + + server.listen(0, () => { + class CustomHeaderAgent extends Agent { + dispatch (opts) { + opts.headers['x-customer-header'] = 'hello world' + return super.dispatch(...arguments) + } + } + + const eventSourceInstance = new EventSource(`http://localhost:${server.address().port}`, { + node: { + dispatcher: new CustomHeaderAgent() + } + }) + t.after(() => { + eventSourceInstance.close() + }) + }) }) diff --git a/test/eventsource/eventsource-message.js b/test/eventsource/eventsource-message.js index 9de1eae4b43..f76503752c7 100644 --- a/test/eventsource/eventsource-message.js +++ b/test/eventsource/eventsource-message.js @@ -1,365 +1,244 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') -const { setTimeout } = require('node:timers/promises') -const { test, describe } = require('node:test') -const { EventSource } = require('../../lib/web/eventsource/eventsource') +const { test, describe, after } = require('node:test') +const { EventSource, defaultReconnectionTime } = require('../../lib/web/eventsource/eventsource') +const FakeTimers = require('@sinonjs/fake-timers') describe('EventSource - message', () => { - test('Should not emit a message if only retry field was sent', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should not emit a message if only retry field was sent', (t, done) => { + t.plan(2) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('retry: 100\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) - await events.once(server, 'listening') - const port = server.address().port + after(() => server.close()) - const start = Date.now() - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = async () => { + server.listen(0, () => { + const port = server.address().port + + const start = Date.now() + let connectionCount = 0 + const eventSourceInstance = new EventSource(`http://localhost:${port}`) eventSourceInstance.onopen = () => { - assert.ok(Date.now() - start >= 100) - assert.ok(Date.now() - start < 1000) - server.close() + if (++connectionCount === 2) { + t.assert.ok(Date.now() - start >= 100) + t.assert.ok(Date.now() - start < 1000) + eventSourceInstance.close() + done() + } + } + eventSourceInstance.onmessage = () => { + t.assert.fail('Should not have received a message') eventSourceInstance.close() - finishedPromise.resolve() } - } - eventSourceInstance.onmessage = () => { - finishedPromise.reject('Should not have received a message') - eventSourceInstance.close() - server.close() - } - - await setTimeout(500) - - await finishedPromise.promise + }) }) - test('Should not emit a message if no data is provided', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should not emit a message if no data is provided', async (t) => { + t.plan(1) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('event:message\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) - await events.once(server, 'listening') + after(() => server.close()) + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) eventSourceInstance.onmessage = () => { - finishedPromise.reject('Should not have received a message') + t.assert.fail('Should not have received a message') eventSourceInstance.close() - server.close() } - await setTimeout(500) - server.close() eventSourceInstance.close() - finishedPromise.resolve() - - await finishedPromise.promise + t.assert.ok('Should not have received a message') }) - test('Should emit a custom type message if data is provided', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should emit a custom type message if data is provided', (t, done) => { + t.plan(1) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('event:custom\ndata:test\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) + after(() => server.close()) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, () => { + const port = server.address().port - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.addEventListener('custom', () => { - finishedPromise.resolve() - eventSourceInstance.close() - server.close() + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + eventSourceInstance.addEventListener('custom', () => { + t.assert.ok(true) + done() + eventSourceInstance.close() + }) }) - - await setTimeout(500) - - await finishedPromise.promise }) - test('Should emit a message event if data is provided', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should emit a message event if data is provided', (t, done) => { + t.plan(1) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('data:test\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) + after(() => server.close()) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, () => { + const port = server.address().port - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.addEventListener('message', () => { - finishedPromise.resolve() - eventSourceInstance.close() - server.close() + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + eventSourceInstance.addEventListener('message', () => { + t.assert.ok(true) + eventSourceInstance.close() + done() + }) }) - - await setTimeout(500) - - await finishedPromise.promise }) - test('Should emit a message event if data as a field is provided', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should emit a message event if data as a field is provided', (t, done) => { + t.plan(1) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('data\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) + after(() => server.close()) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, () => { + const port = server.address().port - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.addEventListener('message', () => { - finishedPromise.resolve() - eventSourceInstance.close() - server.close() + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + eventSourceInstance.addEventListener('message', () => { + t.assert.ok(true) + eventSourceInstance.close() + done() + }) }) - - await setTimeout(500) - - await finishedPromise.promise }) - test('Should emit a custom message event if data is empty', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should emit a custom message event if data is empty', (t, done) => { + t.plan(1) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('event:custom\ndata:\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) + after(() => server.close()) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, () => { + const port = server.address().port - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.addEventListener('custom', () => { - finishedPromise.resolve() - eventSourceInstance.close() - server.close() + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + eventSourceInstance.addEventListener('custom', () => { + t.assert.ok(true) + eventSourceInstance.close() + done() + }) }) - - await setTimeout(500) - - await finishedPromise.promise }) - test('Should emit a message event if data is empty', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should emit a message event if data is empty', (t, done) => { + t.plan(1) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('data:\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) + after(() => server.close()) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, () => { + const port = server.address().port - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.addEventListener('message', () => { - finishedPromise.resolve() - eventSourceInstance.close() - server.close() + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + eventSourceInstance.addEventListener('message', () => { + t.assert.ok(true) + eventSourceInstance.close() + done() + }) }) - - await setTimeout(500) - - await finishedPromise.promise }) - test('Should emit a custom message event if data only as a field is provided', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should emit a custom message event if data only as a field is provided', (t, done) => { + t.plan(1) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('event:custom\ndata\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) + after(() => server.close()) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, () => { + const port = server.address().port - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.addEventListener('custom', () => { - finishedPromise.resolve() - eventSourceInstance.close() - server.close() + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + eventSourceInstance.addEventListener('custom', () => { + t.assert.ok(true) + eventSourceInstance.close() + done() + }) }) - - await setTimeout(500) - - await finishedPromise.promise }) - test('Should not emit a custom type message if no data is provided', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } + test('Should not emit a custom type message if no data is provided', (t, done) => { + const clock = FakeTimers.install() + after(() => clock.uninstall()) - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + t.plan(1) const server = http.createServer({ joinDuplicateHeaders: true }, async (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('event:custom\n\n') - await setTimeout(100) - - res.end() + setTimeout(() => res.end(), 100) }) - server.listen(0) - let reconnectionCount = 0 - await events.once(server, 'listening') - const port = server.address().port + after(() => server.close()) - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = async () => { + server.listen(0, async () => { + const port = server.address().port + + const eventSourceInstance = new EventSource(`http://localhost:${port}`) eventSourceInstance.onopen = () => { if (++reconnectionCount === 2) { - server.close() + t.assert.ok(true) eventSourceInstance.close() - finishedPromise.resolve() + done() } } - } - eventSourceInstance.addEventListener('custom', () => { - finishedPromise.reject('Should not have received a message') - eventSourceInstance.close() - server.close() - }) + eventSourceInstance.addEventListener('custom', () => { + t.assert.fail('Should not have received a message') + eventSourceInstance.close() + }) - await setTimeout(500) + await once(eventSourceInstance, 'open') + clock.tick(defaultReconnectionTime) + await once(eventSourceInstance, 'error') - await finishedPromise.promise + clock.tick(defaultReconnectionTime) + await once(eventSourceInstance, 'open') + clock.tick(defaultReconnectionTime) + }) }) }) diff --git a/test/eventsource/eventsource-properties.js b/test/eventsource/eventsource-properties.js index 58a02a91614..deeaf5d679c 100644 --- a/test/eventsource/eventsource-properties.js +++ b/test/eventsource/eventsource-properties.js @@ -1,15 +1,14 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { EventSource } = require('../..') // assuming the test is in test/eventsource/ -test('EventSource.prototype properties are configured correctly', () => { +test('EventSource.prototype properties are configured correctly', (t) => { const props = Object.entries(Object.getOwnPropertyDescriptors(EventSource.prototype)) for (const [key, value] of props) { if (key !== 'constructor') { - assert(value.enumerable, `${key} is not enumerable`) + t.assert.ok(value.enumerable, `${key} is not enumerable`) } } }) diff --git a/test/eventsource/eventsource-reconnect.js b/test/eventsource/eventsource-reconnect.js index 8459bb00d69..08c43758e94 100644 --- a/test/eventsource/eventsource-reconnect.js +++ b/test/eventsource/eventsource-reconnect.js @@ -1,155 +1,149 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') -const { test, describe } = require('node:test') +const { test, describe, after } = require('node:test') +const FakeTimers = require('@sinonjs/fake-timers') const { EventSource, defaultReconnectionTime } = require('../../lib/web/eventsource/eventsource') describe('EventSource - reconnect', () => { - test('Should reconnect on connection close', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject - }) + test('Should reconnect on connection closed by server', (t, done) => { + t.plan(1) + + const clock = FakeTimers.install() + after(() => clock.uninstall()) const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) + after(() => server.close()) - server.listen(0) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, async () => { + const port = server.address().port - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = async () => { + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + let connectionCount = 0 eventSourceInstance.onopen = () => { - server.close() - eventSourceInstance.close() - finishedPromise.resolve() + if (++connectionCount === 2) { + eventSourceInstance.close() + t.assert.ok(true) + done() + } } - } - await finishedPromise.promise - }) + await once(eventSourceInstance, 'open') - test('Should reconnect on with reconnection timeout', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } + clock.tick(10) + await once(eventSourceInstance, 'error') - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject + clock.tick(defaultReconnectionTime) }) + }) + + test('Should reconnect on with reconnection timeout', (t, done) => { + t.plan(2) + const clock = FakeTimers.install() + after(() => clock.uninstall()) const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.end() }) + after(() => server.close()) - server.listen(0) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, async () => { + const port = server.address().port - const start = Date.now() - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = async () => { + const start = Date.now() + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + + let connectionCount = 0 eventSourceInstance.onopen = () => { - assert.ok(Date.now() - start >= defaultReconnectionTime) - server.close() - eventSourceInstance.close() - finishedPromise.resolve() + if (++connectionCount === 2) { + t.assert.ok(Date.now() - start >= defaultReconnectionTime) + eventSourceInstance.close() + t.assert.ok(true) + + done() + } } - } - await finishedPromise.promise - }) + await once(eventSourceInstance, 'open') - test('Should reconnect on with modified reconnection timeout', async () => { - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } + clock.tick(10) + await once(eventSourceInstance, 'error') - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject + clock.tick(defaultReconnectionTime) }) + }) + + test('Should reconnect on with modified reconnection timeout', (t, done) => { + t.plan(3) + const clock = FakeTimers.install() + after(() => clock.uninstall()) const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('retry: 100\n\n') res.end() }) + after(() => server.close()) - server.listen(0) - await events.once(server, 'listening') - const port = server.address().port + server.listen(0, async () => { + const port = server.address().port - const start = Date.now() - const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = async () => { + const start = Date.now() + const eventSourceInstance = new EventSource(`http://localhost:${port}`) + + let connectionCount = 0 eventSourceInstance.onopen = () => { - assert.ok(Date.now() - start >= 100) - assert.ok(Date.now() - start < 1000) - server.close() - eventSourceInstance.close() - finishedPromise.resolve() + if (++connectionCount === 2) { + t.assert.ok(Date.now() - start >= 100) + t.assert.ok(Date.now() - start < 1000) + eventSourceInstance.close() + t.assert.ok(true) + + done() + } } - } - await finishedPromise.promise - }) + await once(eventSourceInstance, 'open') - test('Should reconnect and send lastEventId', async () => { - let requestCount = 0 + clock.tick(10) + await once(eventSourceInstance, 'error') - const finishedPromise = { - promise: undefined, - resolve: undefined, - reject: undefined - } - - finishedPromise.promise = new Promise((resolve, reject) => { - finishedPromise.resolve = resolve - finishedPromise.reject = reject + clock.tick(100) }) + }) + + test('Should reconnect and send lastEventId', async (t) => { + t.plan(1) + const clock = FakeTimers.install() + after(() => clock.uninstall()) + + let requestCount = 0 const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, 'OK', { 'Content-Type': 'text/event-stream' }) res.write('id: 1337\n\n') - if (requestCount++ !== 0) { - assert.strictEqual(req.headers['last-event-id'], '1337') + if (++requestCount === 2) { + t.assert.strictEqual(req.headers['last-event-id'], '1337') } res.end() }) - - server.listen(0) - await events.once(server, 'listening') + after(() => server.close()) + await once(server.listen(0), 'listening') const port = server.address().port - const start = Date.now() const eventSourceInstance = new EventSource(`http://localhost:${port}`) - eventSourceInstance.onopen = async () => { - eventSourceInstance.onopen = () => { - assert.ok(Date.now() - start >= 3000) - server.close() - eventSourceInstance.close() - finishedPromise.resolve() - } - } - await finishedPromise.promise + await once(eventSourceInstance, 'open') + + clock.tick(10) + await once(eventSourceInstance, 'error') + + clock.tick(defaultReconnectionTime) + await once(eventSourceInstance, 'open') }) }) diff --git a/test/eventsource/eventsource-redirecting.js b/test/eventsource/eventsource-redirecting.js index 20bfa983994..8b7cb95c6c8 100644 --- a/test/eventsource/eventsource-redirecting.js +++ b/test/eventsource/eventsource-redirecting.js @@ -1,14 +1,13 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - redirecting', () => { [301, 302, 307, 308].forEach((statusCode) => { - test(`Should redirect on ${statusCode} status code`, async () => { + test(`Should redirect on ${statusCode} status code`, async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { if (res.req.url === '/redirect') { res.writeHead(statusCode, undefined, { Location: '/target' }) @@ -19,24 +18,23 @@ describe('EventSource - redirecting', () => { } }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`) eventSourceInstance.onerror = (e) => { - assert.fail('Should not have errored') + t.assert.fail('Should not have errored') } eventSourceInstance.onopen = () => { - assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`) + t.assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`) eventSourceInstance.close() server.close() } }) }) - test('Stop trying to connect when getting a 204 response', async () => { + test('Stop trying to connect when getting a 204 response', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { if (res.req.url === '/redirect') { res.writeHead(301, undefined, { Location: '/target' }) @@ -47,22 +45,21 @@ describe('EventSource - redirecting', () => { } }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`) eventSourceInstance.onerror = (event) => { - assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`) - assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED) + t.assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`) + t.assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED) server.close() } eventSourceInstance.onopen = () => { - assert.fail('Should not have opened') + t.assert.fail('Should not have opened') } }) - test('Throw when missing a Location header', async () => { + test('Throw when missing a Location header', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { if (res.req.url === '/redirect') { res.writeHead(301, undefined) @@ -73,47 +70,45 @@ describe('EventSource - redirecting', () => { } }) - server.listen(0) - await events.once(server, 'listening') + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`) eventSourceInstance.onerror = () => { - assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`) - assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED) + t.assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`) + t.assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED) server.close() } }) - test('Should set origin attribute of messages after redirecting', async () => { + test('Should set origin attribute of messages after redirecting', async (t) => { const targetServer = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { if (res.req.url === '/target') { res.writeHead(200, undefined, { 'Content-Type': 'text/event-stream' }) res.write('event: message\ndata: test\n\n') } }) - targetServer.listen(0) - await events.once(targetServer, 'listening') + + await once(targetServer.listen(0), 'listening') const targetPort = targetServer.address().port const sourceServer = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(301, undefined, { Location: `http://127.0.0.1:${targetPort}/target` }) res.end() }) - sourceServer.listen(0) - await events.once(sourceServer, 'listening') + await once(sourceServer.listen(0), 'listening') const sourcePort = sourceServer.address().port const eventSourceInstance = new EventSource(`http://127.0.0.1:${sourcePort}/redirect`) eventSourceInstance.onmessage = (event) => { - assert.strictEqual(event.origin, `http://127.0.0.1:${targetPort}`) + t.assert.strictEqual(event.origin, `http://127.0.0.1:${targetPort}`) eventSourceInstance.close() targetServer.close() sourceServer.close() } eventSourceInstance.onerror = (e) => { - assert.fail('Should not have errored') + t.assert.fail('Should not have errored') } }) }) diff --git a/test/eventsource/eventsource-request-status-error.js b/test/eventsource/eventsource-request-status-error.js index 694ae160213..0ea0d43304a 100644 --- a/test/eventsource/eventsource-request-status-error.js +++ b/test/eventsource/eventsource-request-status-error.js @@ -1,35 +1,32 @@ 'use strict' -const assert = require('node:assert') -const events = require('node:events') +const { once } = require('node:events') const http = require('node:http') const { test, describe } = require('node:test') const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - status error', () => { [204, 205, 210, 299, 404, 410, 503].forEach((statusCode) => { - test(`Should error on ${statusCode} status code`, async () => { + test(`Should error on ${statusCode} status code`, async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(statusCode, 'dummy', { 'Content-Type': 'text/event-stream' }) res.end() }) - server.listen(0) - await events.once(server, 'listening') - + await once(server.listen(0), 'listening') const port = server.address().port const eventSourceInstance = new EventSource(`http://localhost:${port}`) eventSourceInstance.onerror = (e) => { - assert.strictEqual(this.readyState, this.CLOSED) + t.assert.strictEqual(this.readyState, this.CLOSED) eventSourceInstance.close() server.close() } eventSourceInstance.onmessage = () => { - assert.fail('Should not have received a message') + t.assert.fail('Should not have received a message') } eventSourceInstance.onopen = () => { - assert.fail('Should not have opened') + t.assert.fail('Should not have opened') } }) }) diff --git a/test/eventsource/eventsource-stream-bom.js b/test/eventsource/eventsource-stream-bom.js index b447832a124..c37d163fec4 100644 --- a/test/eventsource/eventsource-stream-bom.js +++ b/test/eventsource/eventsource-stream-bom.js @@ -1,19 +1,18 @@ 'use strict' -const assert = require('node:assert') const { test, describe } = require('node:test') const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream') describe('EventSourceStream - handle BOM', () => { - test('Remove BOM from the beginning of the stream. 1 byte chunks', () => { + test('Remove BOM from the beginning of the stream. 1 byte chunks', (t) => { const dataField = 'data: Hello' const content = Buffer.from(`\uFEFF${dataField}`, 'utf8') const stream = new EventSourceStream() stream.parseLine = function (line) { - assert.strictEqual(line.byteLength, dataField.length) - assert.strictEqual(line.toString(), dataField) + t.assert.strictEqual(line.byteLength, dataField.length) + t.assert.strictEqual(line.toString(), dataField) } for (let i = 0; i < content.length; i++) { @@ -21,15 +20,15 @@ describe('EventSourceStream - handle BOM', () => { } }) - test('Remove BOM from the beginning of the stream. 2 byte chunks', () => { + test('Remove BOM from the beginning of the stream. 2 byte chunks', (t) => { const dataField = 'data: Hello' const content = Buffer.from(`\uFEFF${dataField}`, 'utf8') const stream = new EventSourceStream() stream.parseLine = function (line) { - assert.strictEqual(line.byteLength, dataField.length) - assert.strictEqual(line.toString(), dataField) + t.assert.strictEqual(line.byteLength, dataField.length) + t.assert.strictEqual(line.toString(), dataField) } for (let i = 0; i < content.length; i += 2) { @@ -37,15 +36,15 @@ describe('EventSourceStream - handle BOM', () => { } }) - test('Remove BOM from the beginning of the stream. 3 byte chunks', () => { + test('Remove BOM from the beginning of the stream. 3 byte chunks', (t) => { const dataField = 'data: Hello' const content = Buffer.from(`\uFEFF${dataField}`, 'utf8') const stream = new EventSourceStream() stream.parseLine = function (line) { - assert.strictEqual(line.byteLength, dataField.length) - assert.strictEqual(line.toString(), dataField) + t.assert.strictEqual(line.byteLength, dataField.length) + t.assert.strictEqual(line.toString(), dataField) } for (let i = 0; i < content.length; i += 3) { @@ -53,15 +52,15 @@ describe('EventSourceStream - handle BOM', () => { } }) - test('Remove BOM from the beginning of the stream. 4 byte chunks', () => { + test('Remove BOM from the beginning of the stream. 4 byte chunks', (t) => { const dataField = 'data: Hello' const content = Buffer.from(`\uFEFF${dataField}`, 'utf8') const stream = new EventSourceStream() stream.parseLine = function (line) { - assert.strictEqual(line.byteLength, dataField.length) - assert.strictEqual(line.toString(), dataField) + t.assert.strictEqual(line.byteLength, dataField.length) + t.assert.strictEqual(line.toString(), dataField) } for (let i = 0; i < content.length; i += 4) { @@ -69,15 +68,15 @@ describe('EventSourceStream - handle BOM', () => { } }) - test('Not containing BOM from the beginning of the stream. 1 byte chunks', () => { + test('Not containing BOM from the beginning of the stream. 1 byte chunks', (t) => { const dataField = 'data: Hello' const content = Buffer.from(`${dataField}`, 'utf8') const stream = new EventSourceStream() stream.parseLine = function (line) { - assert.strictEqual(line.byteLength, dataField.length) - assert.strictEqual(line.toString(), dataField) + t.assert.strictEqual(line.byteLength, dataField.length) + t.assert.strictEqual(line.toString(), dataField) } for (let i = 0; i < content.length; i += 1) { @@ -85,15 +84,15 @@ describe('EventSourceStream - handle BOM', () => { } }) - test('Not containing BOM from the beginning of the stream. 2 byte chunks', () => { + test('Not containing BOM from the beginning of the stream. 2 byte chunks', (t) => { const dataField = 'data: Hello' const content = Buffer.from(`${dataField}`, 'utf8') const stream = new EventSourceStream() stream.parseLine = function (line) { - assert.strictEqual(line.byteLength, dataField.length) - assert.strictEqual(line.toString(), dataField) + t.assert.strictEqual(line.byteLength, dataField.length) + t.assert.strictEqual(line.toString(), dataField) } for (let i = 0; i < content.length; i += 2) { @@ -101,15 +100,15 @@ describe('EventSourceStream - handle BOM', () => { } }) - test('Not containing BOM from the beginning of the stream. 3 byte chunks', () => { + test('Not containing BOM from the beginning of the stream. 3 byte chunks', (t) => { const dataField = 'data: Hello' const content = Buffer.from(`${dataField}`, 'utf8') const stream = new EventSourceStream() stream.parseLine = function (line) { - assert.strictEqual(line.byteLength, dataField.length) - assert.strictEqual(line.toString(), dataField) + t.assert.strictEqual(line.byteLength, dataField.length) + t.assert.strictEqual(line.toString(), dataField) } for (let i = 0; i < content.length; i += 3) { @@ -117,15 +116,15 @@ describe('EventSourceStream - handle BOM', () => { } }) - test('Not containing BOM from the beginning of the stream. 4 byte chunks', () => { + test('Not containing BOM from the beginning of the stream. 4 byte chunks', (t) => { const dataField = 'data: Hello' const content = Buffer.from(`${dataField}`, 'utf8') const stream = new EventSourceStream() stream.parseLine = function (line) { - assert.strictEqual(line.byteLength, dataField.length) - assert.strictEqual(line.toString(), dataField) + t.assert.strictEqual(line.byteLength, dataField.length) + t.assert.strictEqual(line.toString(), dataField) } for (let i = 0; i < content.length; i += 4) { diff --git a/test/eventsource/eventsource-stream-parse-line.js b/test/eventsource/eventsource-stream-parse-line.js index 45069a2752d..cbe66bef286 100644 --- a/test/eventsource/eventsource-stream-parse-line.js +++ b/test/eventsource/eventsource-stream-parse-line.js @@ -1,6 +1,5 @@ 'use strict' -const assert = require('node:assert') const { test, describe } = require('node:test') const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream') @@ -10,7 +9,7 @@ describe('EventSourceStream - parseLine', () => { reconnectionTime: 1000 } - test('Should push an unmodified event when line is empty', () => { + test('Should push an unmodified event when line is empty', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -21,15 +20,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 0) - assert.strictEqual(event.data, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 0) + t.assert.strictEqual(event.data, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('Should set the data field with empty string if not containing data', () => { + test('Should set the data field with empty string if not containing data', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -40,15 +39,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('data:', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, '') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, '') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('Should set the data field with empty string if not containing data (containing space after colon)', () => { + test('Should set the data field with empty string if not containing data (containing space after colon)', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -59,15 +58,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('data: ', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, '') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, '') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('Should set the data field with a string containing space if having more than one space after colon', () => { + test('Should set the data field with a string containing space if having more than one space after colon', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -78,15 +77,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('data: ', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, ' ') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, ' ') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('Should set value properly, even if the line contains multiple colons', () => { + test('Should set value properly, even if the line contains multiple colons', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -97,15 +96,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('data: : ', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, ': ') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, ': ') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('Should set the data field when containing data', () => { + test('Should set the data field when containing data', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -116,15 +115,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('data: Hello', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, 'Hello') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, 'Hello') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('Should ignore comments', () => { + test('Should ignore comments', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -135,15 +134,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from(':comment', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 0) - assert.strictEqual(event.data, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 0) + t.assert.strictEqual(event.data, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('Should set retry field', () => { + test('Should set retry field', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -154,15 +153,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('retry: 1000', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, '1000') + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, '1000') }) - test('Should set id field', () => { + test('Should set id field', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -173,15 +172,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('id: 1234', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, undefined) - assert.strictEqual(event.id, '1234') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, undefined) + t.assert.strictEqual(event.id, '1234') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('Should set id field', () => { + test('Should set id field', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -192,15 +191,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('event: custom', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, 'custom') - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, 'custom') + t.assert.strictEqual(event.retry, undefined) }) - test('Should ignore invalid field', () => { + test('Should ignore invalid field', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -211,15 +210,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from('comment: invalid', 'utf8'), event) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 0) - assert.strictEqual(event.data, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 0) + t.assert.strictEqual(event.data, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('bogus retry', () => { + test('bogus retry', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -231,15 +230,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from(line, 'utf8'), event) }) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 2) - assert.strictEqual(event.data, 'x') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, '3000') + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 2) + t.assert.strictEqual(event.data, 'x') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, '3000') }) - test('bogus id', () => { + test('bogus id', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -251,15 +250,15 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from(line, 'utf8'), event) }) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 2) - assert.strictEqual(event.data, 'x') - assert.strictEqual(event.id, '3000') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 2) + t.assert.strictEqual(event.data, 'x') + t.assert.strictEqual(event.id, '3000') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) - test('empty event', () => { + test('empty event', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -271,11 +270,11 @@ describe('EventSourceStream - parseLine', () => { stream.parseLine(Buffer.from(line, 'utf8'), event) }) - assert.strictEqual(typeof event, 'object') - assert.strictEqual(Object.keys(event).length, 1) - assert.strictEqual(event.data, 'data') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(Object.keys(event).length, 1) + t.assert.strictEqual(event.data, 'data') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.retry, undefined) }) }) diff --git a/test/eventsource/eventsource-stream-process-event.js b/test/eventsource/eventsource-stream-process-event.js index a0452ad36dc..9a53ae4f9a5 100644 --- a/test/eventsource/eventsource-stream-process-event.js +++ b/test/eventsource/eventsource-stream-process-event.js @@ -1,6 +1,5 @@ 'use strict' -const assert = require('node:assert') const { test, describe } = require('node:test') const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream') @@ -10,7 +9,7 @@ describe('EventSourceStream - processEvent', () => { reconnectionTime: 1000 } - test('Should set the defined origin as the origin of the MessageEvent', () => { + test('Should set the defined origin as the origin of the MessageEvent', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -18,22 +17,22 @@ describe('EventSourceStream - processEvent', () => { }) stream.on('data', (event) => { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.type, 'message') - assert.strictEqual(event.options.data, null) - assert.strictEqual(event.options.lastEventId, undefined) - assert.strictEqual(event.options.origin, 'example.com') - assert.strictEqual(stream.state.reconnectionTime, 1000) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.type, 'message') + t.assert.strictEqual(event.options.data, null) + t.assert.strictEqual(event.options.lastEventId, undefined) + t.assert.strictEqual(event.options.origin, 'example.com') + t.assert.strictEqual(stream.state.reconnectionTime, 1000) }) stream.on('error', (error) => { - assert.fail(error) + t.assert.fail(error) }) stream.processEvent({}) }) - test('Should set reconnectionTime to 4000 if event contains retry field', () => { + test('Should set reconnectionTime to 4000 if event contains retry field', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -44,10 +43,10 @@ describe('EventSourceStream - processEvent', () => { retry: '4000' }) - assert.strictEqual(stream.state.reconnectionTime, 4000) + t.assert.strictEqual(stream.state.reconnectionTime, 4000) }) - test('Dispatches a MessageEvent with data', () => { + test('Dispatches a MessageEvent with data', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -55,16 +54,16 @@ describe('EventSourceStream - processEvent', () => { }) stream.on('data', (event) => { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.type, 'message') - assert.strictEqual(event.options.data, 'Hello') - assert.strictEqual(event.options.lastEventId, undefined) - assert.strictEqual(event.options.origin, 'example.com') - assert.strictEqual(stream.state.reconnectionTime, 1000) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.type, 'message') + t.assert.strictEqual(event.options.data, 'Hello') + t.assert.strictEqual(event.options.lastEventId, undefined) + t.assert.strictEqual(event.options.origin, 'example.com') + t.assert.strictEqual(stream.state.reconnectionTime, 1000) }) stream.on('error', (error) => { - assert.fail(error) + t.assert.fail(error) }) stream.processEvent({ @@ -72,7 +71,7 @@ describe('EventSourceStream - processEvent', () => { }) }) - test('Dispatches a MessageEvent with lastEventId, when event contains id field', () => { + test('Dispatches a MessageEvent with lastEventId, when event contains id field', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -80,12 +79,12 @@ describe('EventSourceStream - processEvent', () => { }) stream.on('data', (event) => { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.type, 'message') - assert.strictEqual(event.options.data, null) - assert.strictEqual(event.options.lastEventId, '1234') - assert.strictEqual(event.options.origin, 'example.com') - assert.strictEqual(stream.state.reconnectionTime, 1000) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.type, 'message') + t.assert.strictEqual(event.options.data, null) + t.assert.strictEqual(event.options.lastEventId, '1234') + t.assert.strictEqual(event.options.origin, 'example.com') + t.assert.strictEqual(stream.state.reconnectionTime, 1000) }) stream.processEvent({ @@ -93,7 +92,7 @@ describe('EventSourceStream - processEvent', () => { }) }) - test('Dispatches a MessageEvent with lastEventId, reusing the persisted', () => { + test('Dispatches a MessageEvent with lastEventId, reusing the persisted', (t) => { // lastEventId const stream = new EventSourceStream({ eventSourceSettings: { @@ -103,18 +102,18 @@ describe('EventSourceStream - processEvent', () => { }) stream.on('data', (event) => { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.type, 'message') - assert.strictEqual(event.options.data, null) - assert.strictEqual(event.options.lastEventId, '1234') - assert.strictEqual(event.options.origin, 'example.com') - assert.strictEqual(stream.state.reconnectionTime, 1000) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.type, 'message') + t.assert.strictEqual(event.options.data, null) + t.assert.strictEqual(event.options.lastEventId, '1234') + t.assert.strictEqual(event.options.origin, 'example.com') + t.assert.strictEqual(stream.state.reconnectionTime, 1000) }) stream.processEvent({}) }) - test('Dispatches a MessageEvent with type custom, when event contains type field', () => { + test('Dispatches a MessageEvent with type custom, when event contains type field', (t) => { const stream = new EventSourceStream({ eventSourceSettings: { ...defaultEventSourceSettings @@ -122,12 +121,12 @@ describe('EventSourceStream - processEvent', () => { }) stream.on('data', (event) => { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.type, 'custom') - assert.strictEqual(event.options.data, null) - assert.strictEqual(event.options.lastEventId, undefined) - assert.strictEqual(event.options.origin, 'example.com') - assert.strictEqual(stream.state.reconnectionTime, 1000) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.type, 'custom') + t.assert.strictEqual(event.options.data, null) + t.assert.strictEqual(event.options.lastEventId, undefined) + t.assert.strictEqual(event.options.origin, 'example.com') + t.assert.strictEqual(stream.state.reconnectionTime, 1000) }) stream.processEvent({ diff --git a/test/eventsource/eventsource-stream.js b/test/eventsource/eventsource-stream.js index 8a74f539d8e..7763f46ba29 100644 --- a/test/eventsource/eventsource-stream.js +++ b/test/eventsource/eventsource-stream.js @@ -1,30 +1,29 @@ 'use strict' -const assert = require('node:assert') const { test, describe } = require('node:test') const { EventSourceStream } = require('../../lib/web/eventsource/eventsource-stream') describe('EventSourceStream', () => { - test('ignore empty chunks', () => { + test('ignore empty chunks', (t) => { const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.fail() + t.assert.fail() } stream.write(Buffer.alloc(0)) }) - test('Simple event with data field.', () => { + test('Simple event with data field.', (t) => { const content = Buffer.from('data: Hello\n\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, 'Hello') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, 'Hello') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } for (let i = 0; i < content.length; i++) { @@ -32,17 +31,17 @@ describe('EventSourceStream', () => { } }) - test('Should also process CR as EOL.', () => { + test('Should also process CR as EOL.', (t) => { const content = Buffer.from('data: Hello\r\r', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, 'Hello') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, 'Hello') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } for (let i = 0; i < content.length; i++) { @@ -50,17 +49,17 @@ describe('EventSourceStream', () => { } }) - test('Should also process CRLF as EOL.', () => { + test('Should also process CRLF as EOL.', (t) => { const content = Buffer.from('data: Hello\r\n\r\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, 'Hello') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, 'Hello') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } for (let i = 0; i < content.length; i++) { @@ -68,17 +67,17 @@ describe('EventSourceStream', () => { } }) - test('Should also process mixed CR and CRLF as EOL.', () => { + test('Should also process mixed CR and CRLF as EOL.', (t) => { const content = Buffer.from('data: Hello\r\r\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, 'Hello') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, 'Hello') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } for (let i = 0; i < content.length; i++) { @@ -86,17 +85,17 @@ describe('EventSourceStream', () => { } }) - test('Should also process mixed LF and CRLF as EOL.', () => { + test('Should also process mixed LF and CRLF as EOL.', (t) => { const content = Buffer.from('data: Hello\n\r\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, 'Hello') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, 'Hello') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } for (let i = 0; i < content.length; i++) { @@ -104,17 +103,17 @@ describe('EventSourceStream', () => { } }) - test('Should ignore comments', () => { + test('Should ignore comments', (t) => { const content = Buffer.from(':data: Hello\n\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } for (let i = 0; i < content.length; i++) { @@ -122,7 +121,7 @@ describe('EventSourceStream', () => { } }) - test('Should fire two events.', () => { + test('Should fire two events.', (t) => { // @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation const content = Buffer.from('data\n\ndata\ndata\n\ndata:', 'utf8') const stream = new EventSourceStream() @@ -131,19 +130,19 @@ describe('EventSourceStream', () => { stream.processEvent = function (event) { switch (count) { case 0: { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, '') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, '') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) break } case 1: { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, '\n') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, '\n') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } } count++ @@ -154,17 +153,17 @@ describe('EventSourceStream', () => { } }) - test('Should fire two identical events.', () => { + test('Should fire two identical events.', (t) => { // @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation const content = Buffer.from('data:test\n\ndata: test\n\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, 'test') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, 'test') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } for (let i = 0; i < content.length; i++) { @@ -172,7 +171,7 @@ describe('EventSourceStream', () => { } }) - test('ignores empty comments', () => { + test('ignores empty comments', (t) => { const content = Buffer.from('data: Hello\n\n:\n\ndata: World\n\n', 'utf8') const stream = new EventSourceStream() @@ -181,23 +180,23 @@ describe('EventSourceStream', () => { stream.processEvent = function (event) { switch (count) { case 0: { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, 'Hello') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, 'Hello') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) break } case 1: { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, 'World') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, 'World') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) break } default: { - assert.fail() + t.assert.fail() } } count++ @@ -206,47 +205,47 @@ describe('EventSourceStream', () => { stream.write(content) }) - test('comment fest', () => { + test('comment fest', (t) => { const longstring = new Array(2 * 1024 + 1).join('x') const content = Buffer.from(`data:1\r:\0\n:\r\ndata:2\n:${longstring}\rdata:3\n:data:fail\r:${longstring}\ndata:4\n\n`, 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.data, '1\n2\n3\n4') - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.data, '1\n2\n3\n4') + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) } stream.write(content) }) - test('comment fest', () => { + test('comment fest', (t) => { const content = Buffer.from('data:\n\ndata\ndata\n\ndata:test\n\n', 'utf8') const stream = new EventSourceStream() let count = 0 stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) switch (count) { case 0: { - assert.strictEqual(event.data, '') + t.assert.strictEqual(event.data, '') break } case 1: { - assert.strictEqual(event.data, '\n') + t.assert.strictEqual(event.data, '\n') break } case 2: { - assert.strictEqual(event.data, 'test') + t.assert.strictEqual(event.data, 'test') break } default: { - assert.fail() + t.assert.fail() } } count++ @@ -254,44 +253,44 @@ describe('EventSourceStream', () => { stream.write(content) }) - test('newline test', () => { + test('newline test', (t) => { const content = Buffer.from('data:test\r\ndata\ndata:test\r\n\r\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) - assert.strictEqual(event.data, 'test\n\ntest') + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(event.data, 'test\n\ntest') } stream.write(content) }) - test('newline test', () => { + test('newline test', (t) => { const content = Buffer.from('data:test\n data\ndata\nfoobar:xxx\njustsometext\n:thisisacommentyay\ndata:test\n\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) - assert.strictEqual(event.data, 'test\n\ntest') + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(event.data, 'test\n\ntest') } stream.write(content) }) - test('newline test', () => { + test('newline test', (t) => { const content = Buffer.from('data:test\n data\ndata\nfoobar:xxx\njustsometext\n:thisisacommentyay\ndata:test\n\n', 'utf8') const stream = new EventSourceStream() stream.processEvent = function (event) { - assert.strictEqual(typeof event, 'object') - assert.strictEqual(event.event, undefined) - assert.strictEqual(event.id, undefined) - assert.strictEqual(event.retry, undefined) - assert.strictEqual(event.data, 'test\n\ntest') + t.assert.strictEqual(typeof event, 'object') + t.assert.strictEqual(event.event, undefined) + t.assert.strictEqual(event.id, undefined) + t.assert.strictEqual(event.retry, undefined) + t.assert.strictEqual(event.data, 'test\n\ntest') } stream.write(content) }) diff --git a/test/eventsource/eventsource.js b/test/eventsource/eventsource.js index f7b62831449..3a8ebc2d3a8 100644 --- a/test/eventsource/eventsource.js +++ b/test/eventsource/eventsource.js @@ -1,14 +1,13 @@ 'use strict' -const assert = require('node:assert') const { test, describe } = require('node:test') const { EventSource } = require('../../lib/web/eventsource/eventsource') describe('EventSource - constructor', () => { - test('Not providing url argument should throw', () => { - assert.throws(() => new EventSource(), TypeError) + test('Not providing url argument should throw', (t) => { + t.assert.throws(() => new EventSource(), TypeError) }) - test('Throw DOMException if URL is invalid', () => { - assert.throws(() => new EventSource('http:'), { message: /Invalid URL/ }) + test('Throw DOMException if URL is invalid', (t) => { + t.assert.throws(() => new EventSource('http:'), { message: /Invalid URL/ }) }) }) diff --git a/test/eventsource/util.js b/test/eventsource/util.js index fa6c854a43a..fdc4aeb691b 100644 --- a/test/eventsource/util.js +++ b/test/eventsource/util.js @@ -1,18 +1,17 @@ 'use strict' -const assert = require('node:assert') const { test } = require('node:test') const { isASCIINumber, isValidLastEventId } = require('../../lib/web/eventsource/util') -test('isValidLastEventId', () => { - assert.strictEqual(isValidLastEventId('valid'), true) - assert.strictEqual(isValidLastEventId('in\u0000valid'), false) - assert.strictEqual(isValidLastEventId('in\x00valid'), false) - assert.strictEqual(isValidLastEventId('…'), true) +test('isValidLastEventId', (t) => { + t.assert.strictEqual(isValidLastEventId('valid'), true) + t.assert.strictEqual(isValidLastEventId('in\u0000valid'), false) + t.assert.strictEqual(isValidLastEventId('in\x00valid'), false) + t.assert.strictEqual(isValidLastEventId('…'), true) }) -test('isASCIINumber', () => { - assert.strictEqual(isASCIINumber('123'), true) - assert.strictEqual(isASCIINumber(''), false) - assert.strictEqual(isASCIINumber('123a'), false) +test('isASCIINumber', (t) => { + t.assert.strictEqual(isASCIINumber('123'), true) + t.assert.strictEqual(isASCIINumber(''), false) + t.assert.strictEqual(isASCIINumber('123a'), false) }) diff --git a/test/fetch/401-statuscode-no-infinite-loop.js b/test/fetch/401-statuscode-no-infinite-loop.js new file mode 100644 index 00000000000..2a959e1e77b --- /dev/null +++ b/test/fetch/401-statuscode-no-infinite-loop.js @@ -0,0 +1,66 @@ +'use strict' + +const { fetch } = require('../..') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { test } = require('node:test') +const assert = require('node:assert') + +const { closeServerAsPromise } = require('../utils/node-http') + +test('Receiving a 401 status code should not cause infinite retry loop', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.statusCode = 401 + res.end('Unauthorized') + }).listen(0) + + t.after(closeServerAsPromise(server)) + await once(server, 'listening') + + const response = await fetch(`http://localhost:${server.address().port}`) + assert.strictEqual(response.status, 401) +}) + +test('Receiving a 401 status code should not fail for stream-backed request bodies', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.statusCode = 401 + res.end('Unauthorized') + }).listen(0) + + t.after(closeServerAsPromise(server)) + await once(server, 'listening') + + const response = await fetch(`http://localhost:${server.address().port}`, { + method: 'PUT', + duplex: 'half', + body: new ReadableStream({ + start (controller) { + controller.enqueue(Buffer.from('hello world')) + controller.close() + } + }) + }) + + assert.strictEqual(response.status, 401) +}) + +test('Receiving a 401 status code should work for POST with JSON body', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.statusCode = 401 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ error: 'unauthorized' })) + }).listen(0) + + t.after(closeServerAsPromise(server)) + await once(server, 'listening') + + const response = await fetch(`http://localhost:${server.address().port}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data: 'test' }) + }) + + assert.strictEqual(response.status, 401) + const body = await response.json() + assert.deepStrictEqual(body, { error: 'unauthorized' }) +}) diff --git a/test/fetch/407-statuscode-window-null.js b/test/fetch/407-statuscode-window-null.js index 26e763c59dc..5e87574d550 100644 --- a/test/fetch/407-statuscode-window-null.js +++ b/test/fetch/407-statuscode-window-null.js @@ -4,7 +4,6 @@ const { fetch } = require('../..') const { createServer } = require('node:http') const { once } = require('node:events') const { test } = require('node:test') -const assert = require('node:assert') const { closeServerAsPromise } = require('../utils/node-http') @@ -19,5 +18,5 @@ test('Receiving a 407 status code w/ a window option present should reject', asy // if init.window exists, the spec tells us to set request.window to 'no-window', // which later causes the request to be rejected if the status code is 407 - await assert.rejects(fetch(`http://localhost:${server.address().port}`, { window: null })) + await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, { window: null })) }) diff --git a/test/fetch/abort.js b/test/fetch/abort.js index 8f4a1f321b6..6a0162d3d4b 100644 --- a/test/fetch/abort.js +++ b/test/fetch/abort.js @@ -1,8 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') const { fetch } = require('../..') const { createServer } = require('node:http') const { once } = require('node:events') @@ -14,21 +12,21 @@ test('allows aborting with custom errors', async (t) => { t.after(closeServerAsPromise(server)) await once(server, 'listening') - await t.test('Using AbortSignal.timeout with cause', async () => { - const { strictEqual } = tspl(t, { plan: 2 }) + await t.test('Using AbortSignal.timeout with cause', async (t) => { + t.plan(2) try { await fetch(`http://localhost:${server.address().port}`, { signal: AbortSignal.timeout(50) }) - assert.fail('should throw') + t.assert.fail('should throw') } catch (err) { if (err.name === 'TypeError') { const cause = err.cause - strictEqual(cause.name, 'HeadersTimeoutError') - strictEqual(cause.code, 'UND_ERR_HEADERS_TIMEOUT') + t.assert.strictEqual(cause.name, 'HeadersTimeoutError') + t.assert.strictEqual(cause.code, 'UND_ERR_HEADERS_TIMEOUT') } else if (err.name === 'TimeoutError') { - strictEqual(err.code, DOMException.TIMEOUT_ERR) - strictEqual(err.cause, undefined) + t.assert.strictEqual(err.code, DOMException.TIMEOUT_ERR) + t.assert.strictEqual(err.cause, undefined) } else { throw err } @@ -39,7 +37,7 @@ test('allows aborting with custom errors', async (t) => { const ac = new AbortController() ac.abort() // no reason - await assert.rejects( + await t.assert.rejects( fetch(`http://localhost:${server.address().port}`, { signal: ac.signal }), diff --git a/test/fetch/abort2.js b/test/fetch/abort2.js index f62e5c8c49a..beffd3fd998 100644 --- a/test/fetch/abort2.js +++ b/test/fetch/abort2.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { fetch } = require('../..') const { createServer } = require('node:http') const { once } = require('node:events') @@ -52,9 +51,9 @@ test('parallel fetch with the same AbortController works as expected', async (t) return a }, { resolved: [], rejected: [] }) - assert.strictEqual(rejected.length, 9) // out of 10 requests, only 1 should succeed - assert.strictEqual(resolved.length, 1) + t.assert.strictEqual(rejected.length, 9) // out of 10 requests, only 1 should succeed + t.assert.strictEqual(resolved.length, 1) - assert.ok(rejected.every(rej => rej.reason?.code === DOMException.ABORT_ERR)) - assert.deepStrictEqual(resolved[0].value, body) + t.assert.ok(rejected.every(rej => rej.reason?.code === DOMException.ABORT_ERR)) + t.assert.deepStrictEqual(resolved[0].value, body) }) diff --git a/test/fetch/about-uri.js b/test/fetch/about-uri.js index fc8ef5f3455..643001f962e 100644 --- a/test/fetch/about-uri.js +++ b/test/fetch/about-uri.js @@ -1,20 +1,19 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { fetch } = require('../..') test('fetching about: uris', async (t) => { await t.test('about:blank', async () => { - await assert.rejects(fetch('about:blank')) + await t.assert.rejects(fetch('about:blank')) }) await t.test('All other about: urls should return an error', async () => { try { await fetch('about:config') - assert.fail('fetching about:config should fail') + t.assert.fail('fetching about:config should fail') } catch (e) { - assert.ok(e, 'this error was expected') + t.assert.ok(e, 'this error was expected') } }) }) diff --git a/test/fetch/blob-uri.js b/test/fetch/blob-uri.js index 5d3d3f4dfd3..1eb21352373 100644 --- a/test/fetch/blob-uri.js +++ b/test/fetch/blob-uri.js @@ -1,13 +1,11 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { fetch } = require('../..') -const { Blob } = require('node:buffer') test('fetching blob: uris', async (t) => { const blobContents = 'hello world' - /** @type {import('buffer').Blob} */ + /** @type {Blob} */ let blob /** @type {string} */ let objectURL @@ -20,9 +18,22 @@ test('fetching blob: uris', async (t) => { await t.test('a normal fetch request works', async () => { const res = await fetch(objectURL) - assert.strictEqual(blobContents, await res.text()) - assert.strictEqual(blob.type, res.headers.get('Content-Type')) - assert.strictEqual(`${blob.size}`, res.headers.get('Content-Length')) + t.assert.strictEqual(blobContents, await res.text()) + t.assert.strictEqual(blob.type, res.headers.get('Content-Type')) + t.assert.strictEqual(`${blob.size}`, res.headers.get('Content-Length')) + }) + + await t.test('a range fetch request returns the inclusive byte range', async () => { + const res = await fetch(objectURL, { + headers: { + Range: 'bytes=1-3' + } + }) + + t.assert.strictEqual(res.status, 206) + t.assert.strictEqual(await res.text(), blobContents.slice(1, 4)) + t.assert.strictEqual(res.headers.get('Content-Length'), '3') + t.assert.strictEqual(res.headers.get('Content-Range'), `bytes 1-3/${blob.size}`) }) await t.test('non-GET method to blob: fails', async () => { @@ -30,9 +41,9 @@ test('fetching blob: uris', async (t) => { await fetch(objectURL, { method: 'POST' }) - assert.fail('expected POST to blob: uri to fail') + t.assert.fail('expected POST to blob: uri to fail') } catch (e) { - assert.ok(e, 'Got the expected error') + t.assert.ok(e, 'Got the expected error') } }) @@ -42,9 +53,9 @@ test('fetching blob: uris', async (t) => { try { await fetch(objectURL) - assert.fail('expected revoked blob: url to fail') + t.assert.fail('expected revoked blob: url to fail') } catch (e) { - assert.ok(e, 'Got the expected error') + t.assert.ok(e, 'Got the expected error') } }) @@ -52,16 +63,16 @@ test('fetching blob: uris', async (t) => { await t.test('works with a fragment', async () => { const res = await fetch(objectURL + '#fragment') - assert.strictEqual(blobContents, await res.text()) + t.assert.strictEqual(blobContents, await res.text()) }) // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 await t.test('Appending a query string to blob: url should cause fetch to fail', async () => { try { await fetch(objectURL + '?querystring') - assert.fail('expected ?querystring blob: url to fail') + t.assert.fail('expected ?querystring blob: url to fail') } catch (e) { - assert.ok(e, 'Got the expected error') + t.assert.ok(e, 'Got the expected error') } }) @@ -69,9 +80,9 @@ test('fetching blob: uris', async (t) => { await t.test('Appending a path should cause fetch to fail', async () => { try { await fetch(objectURL + '/path') - assert.fail('expected /path blob: url to fail') + t.assert.fail('expected /path blob: url to fail') } catch (e) { - assert.ok(e, 'Got the expected error') + t.assert.ok(e, 'Got the expected error') } }) @@ -80,9 +91,9 @@ test('fetching blob: uris', async (t) => { for (const method of ['HEAD', 'POST', 'DELETE', 'OPTIONS', 'PUT', 'CUSTOM']) { try { await fetch(objectURL, { method }) - assert.fail(`${method} fetch should have failed`) + t.assert.fail(`${method} fetch should have failed`) } catch (e) { - assert.ok(e, `${method} blob url - test succeeded`) + t.assert.ok(e, `${method} blob url - test succeeded`) } } }) diff --git a/test/fetch/bundle.js b/test/fetch/bundle.js index f073e9e18f9..b39dd0d0b02 100644 --- a/test/fetch/bundle.js +++ b/test/fetch/bundle.js @@ -1,25 +1,24 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { Response, Request, FormData, Headers, MessageEvent, CloseEvent, ErrorEvent } = require('../../undici-fetch') -test('bundle sets constructor.name and .name properly', () => { - assert.strictEqual(new Response().constructor.name, 'Response') - assert.strictEqual(Response.name, 'Response') +test('bundle sets constructor.name and .name properly', (t) => { + t.assert.strictEqual(new Response().constructor.name, 'Response') + t.assert.strictEqual(Response.name, 'Response') - assert.strictEqual(new Request('http://a').constructor.name, 'Request') - assert.strictEqual(Request.name, 'Request') + t.assert.strictEqual(new Request('http://a').constructor.name, 'Request') + t.assert.strictEqual(Request.name, 'Request') - assert.strictEqual(new Headers().constructor.name, 'Headers') - assert.strictEqual(Headers.name, 'Headers') + t.assert.strictEqual(new Headers().constructor.name, 'Headers') + t.assert.strictEqual(Headers.name, 'Headers') - assert.strictEqual(new FormData().constructor.name, 'FormData') - assert.strictEqual(FormData.name, 'FormData') + t.assert.strictEqual(new FormData().constructor.name, 'FormData') + t.assert.strictEqual(FormData.name, 'FormData') }) -test('regression test for https://github.com/nodejs/node/issues/50263', () => { +test('regression test for https://github.com/nodejs/node/issues/50263', (t) => { const request = new Request('https://a', { headers: { test: 'abc' @@ -29,11 +28,11 @@ test('regression test for https://github.com/nodejs/node/issues/50263', () => { const request1 = new Request(request, { body: 'does not matter' }) - assert.strictEqual(request1.headers.get('test'), 'abc') + t.assert.strictEqual(request1.headers.get('test'), 'abc') }) test('WebSocket related events are exported', (t) => { - assert.deepStrictEqual(typeof CloseEvent, 'function') - assert.deepStrictEqual(typeof MessageEvent, 'function') - assert.deepStrictEqual(typeof ErrorEvent, 'function') + t.assert.deepStrictEqual(typeof CloseEvent, 'function') + t.assert.deepStrictEqual(typeof MessageEvent, 'function') + t.assert.deepStrictEqual(typeof ErrorEvent, 'function') }) diff --git a/test/fetch/client-error-stack-trace.js b/test/fetch/client-error-stack-trace.js index 146443bc077..b31e2830428 100644 --- a/test/fetch/client-error-stack-trace.js +++ b/test/fetch/client-error-stack-trace.js @@ -1,20 +1,31 @@ 'use strict' -const { test } = require('node:test') -const assert = require('node:assert') -const { fetch, setGlobalDispatcher, Agent } = require('../..') +const { test, after } = require('node:test') +const { sep, basename, join } = require('node:path') +const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('../..') + +const projectFolder = basename(join(__dirname, '..', '..')) const { fetch: fetchIndex } = require('../../index-fetch') +const previousDispatcher = getGlobalDispatcher() setGlobalDispatcher(new Agent({ headersTimeout: 500, connectTimeout: 500 })) +after(() => { + setGlobalDispatcher(previousDispatcher) +}) + test('FETCH: request errors and prints trimmed stack trace', async (t) => { try { await fetch('http://a.com') } catch (error) { - assert.ok(error.stack.includes(`at async TestContext. (${__filename}`)) + const stackLines = error.stack.split('\n') + t.assert.ok(stackLines[0].includes('TypeError: fetch failed')) + t.assert.ok(stackLines.some(line => line.includes(`lib${sep}web${sep}fetch${sep}index.js`))) + t.assert.ok(stackLines.some(line => line.includes(`${projectFolder}${sep}index.js`))) + t.assert.ok(stackLines.some(line => line.includes(__filename))) } }) @@ -22,6 +33,10 @@ test('FETCH-index: request errors and prints trimmed stack trace', async (t) => try { await fetchIndex('http://a.com') } catch (error) { - assert.ok(error.stack.includes(`at async TestContext. (${__filename}`)) + const stackLines = error.stack.split('\n') + t.assert.ok(stackLines[0].includes('TypeError: fetch failed')) + t.assert.ok(stackLines.some(line => line.includes(`lib${sep}web${sep}fetch${sep}index.js`))) + t.assert.ok(stackLines.some(line => line.includes(`${projectFolder}${sep}index-fetch.js`))) + t.assert.ok(stackLines.some(line => line.includes(__filename))) } }) diff --git a/test/fetch/client-fetch.js b/test/fetch/client-fetch.js index 353a72aacf9..81a8bb4028a 100644 --- a/test/fetch/client-fetch.js +++ b/test/fetch/client-fetch.js @@ -2,13 +2,10 @@ 'use strict' -const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') const { createServer } = require('node:http') -const { Blob, File } = require('node:buffer') const { fetch, Response, Request, FormData } = require('../..') -const { Client, setGlobalDispatcher, Agent } = require('../..') +const { Client, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('../..') const nodeFetch = require('../../index-fetch') const { once } = require('node:events') const { gzipSync } = require('node:zlib') @@ -17,27 +14,32 @@ const { randomFillSync, createHash } = require('node:crypto') const { closeServerAsPromise } = require('../utils/node-http') +const previousDispatcher = getGlobalDispatcher() setGlobalDispatcher(new Agent({ keepAliveTimeout: 1, keepAliveMaxTimeout: 1 })) +after(() => { + setGlobalDispatcher(previousDispatcher) +}) + test('function signature', (t) => { - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) - strictEqual(fetch.name, 'fetch') - strictEqual(fetch.length, 1) + t.assert.strictEqual(fetch.name, 'fetch') + t.assert.strictEqual(fetch.length, 1) }) test('args validation', async (t) => { - const { rejects } = tspl(t, { plan: 2 }) + t.plan(2) - await rejects(fetch(), TypeError) - await rejects(fetch('ftp://unsupported'), TypeError) + await t.assert.rejects(fetch(), TypeError) + await t.assert.rejects(fetch('ftp://unsupported'), TypeError) }) test('request json', (t, done) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const obj = { asd: true } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { @@ -47,13 +49,13 @@ test('request json', (t, done) => { server.listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}`) - deepStrictEqual(obj, await body.json()) + t.assert.deepStrictEqual(obj, await body.json()) done() }) }) test('request text', (t, done) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const obj = { asd: true } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { @@ -63,13 +65,13 @@ test('request text', (t, done) => { server.listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}`) - strictEqual(JSON.stringify(obj), await body.text()) + t.assert.strictEqual(JSON.stringify(obj), await body.text()) done() }) }) test('request arrayBuffer', (t, done) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const obj = { asd: true } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { @@ -79,13 +81,13 @@ test('request arrayBuffer', (t, done) => { server.listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}`) - deepStrictEqual(Buffer.from(JSON.stringify(obj)), Buffer.from(await body.arrayBuffer())) + t.assert.deepStrictEqual(Buffer.from(JSON.stringify(obj)), Buffer.from(await body.arrayBuffer())) done() }) }) test('should set type of blob object to the value of the `Content-Type` header from response', (t, done) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const obj = { asd: true } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { @@ -96,13 +98,13 @@ test('should set type of blob object to the value of the `Content-Type` header f server.listen(0, async () => { const response = await fetch(`http://localhost:${server.address().port}`) - strictEqual('application/json', (await response.blob()).type) + t.assert.strictEqual('application/json', (await response.blob()).type) done() }) }) test('pre aborted with readable request body', (t, done) => { - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { }) @@ -116,18 +118,18 @@ test('pre aborted with readable request body', (t, done) => { method: 'POST', body: new ReadableStream({ async cancel (reason) { - strictEqual(reason.name, 'AbortError') + t.assert.strictEqual(reason.name, 'AbortError') } }), duplex: 'half' }).catch(err => { - strictEqual(err.name, 'AbortError') + t.assert.strictEqual(err.name, 'AbortError') }).finally(done) }) }) test('pre aborted with closed readable request body', (t, done) => { - const { ok, strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { }) @@ -138,11 +140,11 @@ test('pre aborted with closed readable request body', (t, done) => { ac.abort() const body = new ReadableStream({ async start (c) { - ok(true) + t.assert.ok(true) c.close() }, async cancel (reason) { - assert.fail() + t.assert.fail() } }) queueMicrotask(() => { @@ -152,14 +154,14 @@ test('pre aborted with closed readable request body', (t, done) => { body, duplex: 'half' }).catch(err => { - strictEqual(err.name, 'AbortError') + t.assert.strictEqual(err.name, 'AbortError') }).finally(done) }) }) }) test('unsupported formData 1', (t, done) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.setHeader('content-type', 'asdasdsad') @@ -171,14 +173,14 @@ test('unsupported formData 1', (t, done) => { fetch(`http://localhost:${server.address().port}`) .then(res => res.formData()) .catch(err => { - strictEqual(err.name, 'TypeError') + t.assert.strictEqual(err.name, 'TypeError') }) .finally(done) }) }) test('multipart formdata not base64', async (t) => { - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) // Construct example form data, with text and blob fields const formData = new FormData() @@ -202,14 +204,14 @@ test('multipart formdata not base64', async (t) => { const res = await fetch(`http://localhost:${server.address().port}`) const form = await res.formData() - strictEqual(form.get('field1'), 'value1') + t.assert.strictEqual(form.get('field1'), 'value1') const text = await form.get('field2').text() - strictEqual(text, 'example\ntext file') + t.assert.strictEqual(text, 'example\ntext file') }) test('multipart formdata base64', (t, done) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) // Example form data with base64 encoding const data = randomFillSync(Buffer.alloc(256)) @@ -240,14 +242,14 @@ test('multipart formdata base64', (t, done) => { .then(form => form.get('file').arrayBuffer()) .then(buffer => createHash('sha256').update(Buffer.from(buffer)).digest('base64')) .then(digest => { - strictEqual(createHash('sha256').update(data).digest('base64'), digest) + t.assert.strictEqual(createHash('sha256').update(data).digest('base64'), digest) }) .finally(done) }) }) test('multipart fromdata non-ascii filed names', async (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const request = new Request('http://localhost', { method: 'POST', @@ -263,11 +265,11 @@ test('multipart fromdata non-ascii filed names', async (t) => { }) const form = await request.formData() - strictEqual(form.get('fiŝo'), 'value1') + t.assert.strictEqual(form.get('fiŝo'), 'value1') }) test('busboy emit error', async (t) => { - const { rejects } = tspl(t, { plan: 1 }) + t.plan(1) const formData = new FormData() formData.append('field1', 'value1') @@ -285,23 +287,23 @@ test('busboy emit error', async (t) => { await listen(0) const res = await fetch(`http://localhost:${server.address().port}`) - await rejects(res.formData(), 'Unexpected end of multipart data') + await t.assert.rejects(res.formData(), 'Unexpected end of multipart data') }) // https://github.com/nodejs/undici/issues/2244 test('parsing formData preserve full path on files', async (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const formData = new FormData() formData.append('field1', new File(['foo'], 'a/b/c/foo.txt')) const tempRes = new Response(formData) const form = await tempRes.formData() - strictEqual(form.get('field1').name, 'a/b/c/foo.txt') + t.assert.strictEqual(form.get('field1').name, 'a/b/c/foo.txt') }) test('urlencoded formData', (t, done) => { - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.setHeader('content-type', 'application/x-www-form-urlencoded') @@ -313,15 +315,15 @@ test('urlencoded formData', (t, done) => { fetch(`http://localhost:${server.address().port}`) .then(res => res.formData()) .then(formData => { - strictEqual(formData.get('field1'), 'value1') - strictEqual(formData.get('field2'), 'value2') + t.assert.strictEqual(formData.get('field1'), 'value1') + t.assert.strictEqual(formData.get('field2'), 'value2') }) .finally(done) }) }) test('text with BOM', (t, done) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.setHeader('content-type', 'application/x-www-form-urlencoded') @@ -333,14 +335,14 @@ test('text with BOM', (t, done) => { fetch(`http://localhost:${server.address().port}`) .then(res => res.text()) .then(text => { - strictEqual(text, 'test=\uFEFF') + t.assert.strictEqual(text, 'test=\uFEFF') }) .finally(done) }) }) test('formData with BOM', (t, done) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.setHeader('content-type', 'application/x-www-form-urlencoded') @@ -352,14 +354,14 @@ test('formData with BOM', (t, done) => { fetch(`http://localhost:${server.address().port}`) .then(res => res.formData()) .then(formData => { - strictEqual(formData.get('\uFEFFtest'), '\uFEFF') + t.assert.strictEqual(formData.get('\uFEFFtest'), '\uFEFF') }) .finally(done) }) }) test('locked blob body', (t, done) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() @@ -370,14 +372,14 @@ test('locked blob body', (t, done) => { const res = await fetch(`http://localhost:${server.address().port}`) const reader = res.body.getReader() res.blob().catch(err => { - strictEqual(err.message, 'Body is unusable: Body has already been read') + t.assert.strictEqual(err.message, 'Body is unusable: Body has already been read') reader.cancel() }).finally(done) }) }) test('disturbed blob body', (t, done) => { - const { ok, strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() @@ -387,17 +389,17 @@ test('disturbed blob body', (t, done) => { server.listen(0, async () => { const res = await fetch(`http://localhost:${server.address().port}`) await res.blob().then(() => { - ok(true) + t.assert.ok(true) }) await res.blob().catch(err => { - strictEqual(err.message, 'Body is unusable: Body has already been read') + t.assert.strictEqual(err.message, 'Body is unusable: Body has already been read') }) done() }) }) test('redirect with body', (t, done) => { - const { strictEqual } = tspl(t, { plan: 3 }) + t.plan(3) let count = 0 const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => { @@ -405,7 +407,7 @@ test('redirect with body', (t, done) => { for await (const chunk of req) { body += chunk } - strictEqual(body, 'asd') + t.assert.strictEqual(body, 'asd') if (count++ === 0) { res.setHeader('location', 'asd') res.statusCode = 302 @@ -421,13 +423,13 @@ test('redirect with body', (t, done) => { method: 'PUT', body: 'asd' }) - strictEqual(await res.text(), '2') + t.assert.strictEqual(await res.text(), '2') done() }) }) test('redirect with stream', (t, done) => { - const { strictEqual } = tspl(t, { plan: 3 }) + t.plan(3) const location = '/asd' const body = 'hello!' @@ -448,15 +450,15 @@ test('redirect with stream', (t, done) => { const res = await fetch(`http://localhost:${server.address().port}`, { redirect: 'manual' }) - strictEqual(res.status, 302) - strictEqual(res.headers.get('location'), location) - strictEqual(await res.text(), body) + t.assert.strictEqual(res.status, 302) + t.assert.strictEqual(res.headers.get('location'), location) + t.assert.strictEqual(await res.text(), body) done() }) }) test('fail to extract locked body', (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const stream = new ReadableStream({}) const reader = stream.getReader() @@ -464,13 +466,13 @@ test('fail to extract locked body', (t) => { // eslint-disable-next-line new Response(stream) } catch (err) { - strictEqual(err.name, 'TypeError') + t.assert.strictEqual(err.name, 'TypeError') } reader.cancel() }) test('fail to extract locked body', (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const stream = new ReadableStream({}) const reader = stream.getReader() @@ -482,13 +484,13 @@ test('fail to extract locked body', (t) => { keepalive: true }) } catch (err) { - strictEqual(err.message, 'keepalive') + t.assert.strictEqual(err.message, 'keepalive') } reader.cancel() }) test('post FormData with Blob', (t, done) => { - const { ok } = tspl(t, { plan: 1 }) + t.plan(1) const body = new FormData() body.append('field1', new Blob(['asd1'])) @@ -503,13 +505,13 @@ test('post FormData with Blob', (t, done) => { method: 'PUT', body }) - ok(/asd1/.test(await res.text())) + t.assert.ok(/asd1/.test(await res.text())) done() }) }) test('post FormData with File', (t, done) => { - const { ok } = tspl(t, { plan: 2 }) + t.plan(2) const body = new FormData() body.append('field1', new File(['asd1'], 'filename123')) @@ -525,24 +527,24 @@ test('post FormData with File', (t, done) => { body }) const result = await res.text() - ok(/asd1/.test(result)) - ok(/filename123/.test(result)) + t.assert.ok(/asd1/.test(result)) + t.assert.ok(/filename123/.test(result)) done() }) }) test('invalid url', async (t) => { - const { match } = tspl(t, { plan: 1 }) + t.plan(1) try { await fetch('http://invalid') } catch (e) { - match(e.cause.message, /invalid/) + t.assert.match(e.cause.message, /invalid/) } }) test('custom agent', (t, done) => { - const { ok, deepStrictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const obj = { asd: true } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { @@ -557,19 +559,19 @@ test('custom agent', (t, done) => { }) const oldDispatch = dispatcher.dispatch dispatcher.dispatch = function (options, handler) { - ok(true) + t.assert.ok(true) return oldDispatch.call(this, options, handler) } const body = await fetch(`http://localhost:${server.address().port}`, { dispatcher }) - deepStrictEqual(obj, await body.json()) + t.assert.deepStrictEqual(obj, await body.json()) done() }) }) test('custom agent node fetch', (t, done) => { - const { ok, deepStrictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const obj = { asd: true } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { @@ -584,13 +586,13 @@ test('custom agent node fetch', (t, done) => { }) const oldDispatch = dispatcher.dispatch dispatcher.dispatch = function (options, handler) { - ok(true) + t.assert.ok(true) return oldDispatch.call(this, options, handler) } const body = await nodeFetch.fetch(`http://localhost:${server.address().port}`, { dispatcher }) - deepStrictEqual(obj, await body.json()) + t.assert.deepStrictEqual(obj, await body.json()) done() }) }) @@ -607,7 +609,7 @@ test('error on redirect', (t, done) => { redirect: 'error' }).catch((e) => e.cause) - assert.strictEqual(errorCause.message, 'unexpected redirect') + t.assert.strictEqual(errorCause.message, 'unexpected redirect') done() }) }) @@ -615,7 +617,7 @@ test('error on redirect', (t, done) => { // https://github.com/nodejs/undici/issues/1527 test('fetching with Request object - issue #1527', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.ok(true) + t.assert.ok(true) res.end() }).listen(0) @@ -628,16 +630,16 @@ test('fetching with Request object - issue #1527', async (t) => { body }) - await assert.doesNotReject(fetch(request)) + await t.assert.doesNotReject(fetch(request)) }) test('do not decode redirect body', (t, done) => { - const { ok, strictEqual } = tspl(t, { plan: 3 }) + t.plan(3) const obj = { asd: true } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { if (req.url === '/resource') { - ok(true) + t.assert.ok(true) res.statusCode = 301 res.setHeader('location', '/resource/') // Some dumb http servers set the content-encoding gzip @@ -646,7 +648,7 @@ test('do not decode redirect body', (t, done) => { res.end() return } - ok(true) + t.assert.ok(true) res.setHeader('content-encoding', 'gzip') res.end(gzipSync(JSON.stringify(obj))) }) @@ -654,17 +656,17 @@ test('do not decode redirect body', (t, done) => { server.listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}/resource`) - strictEqual(JSON.stringify(obj), await body.text()) + t.assert.strictEqual(JSON.stringify(obj), await body.text()) done() }) }) test('decode non-redirect body with location header', (t, done) => { - const { ok, strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const obj = { asd: true } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - ok(true) + t.assert.ok(true) res.statusCode = 201 res.setHeader('location', '/resource/') res.setHeader('content-encoding', 'gzip') @@ -674,7 +676,7 @@ test('decode non-redirect body with location header', (t, done) => { server.listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}/resource`) - strictEqual(JSON.stringify(obj), await body.text()) + t.assert.strictEqual(JSON.stringify(obj), await body.text()) done() }) }) @@ -706,6 +708,6 @@ test('Receiving non-Latin1 headers', async (t) => { .map(([, v]) => v) const lengths = cdHeaders.map(h => h.length) - assert.deepStrictEqual(cdHeaders, ContentDisposition) - assert.deepStrictEqual(lengths, [30, 34, 94, 104, 90]) + t.assert.deepStrictEqual(cdHeaders, ContentDisposition) + t.assert.deepStrictEqual(lengths, [30, 34, 94, 104, 90]) }) diff --git a/test/fetch/client-node-max-header-size.js b/test/fetch/client-node-max-header-size.js index 533d2377737..744729b6371 100644 --- a/test/fetch/client-node-max-header-size.js +++ b/test/fetch/client-node-max-header-size.js @@ -1,6 +1,5 @@ 'use strict' -const { tspl } = require('@matteo.collina/tspl') const { exec } = require('node:child_process') const { once } = require('node:events') const { createServer } = require('node:http') @@ -21,25 +20,28 @@ describe('fetch respects --max-http-header-size', () => { await once(server, 'listening') }) - after(() => server.close()) + after(() => { + server.closeAllConnections?.() + server.close() + }) - test("respect Node.js' --max-http-header-size", async (t) => { - t = tspl(t, { plan: 6 }) + test("respect Node.js' --max-http-header-size", (t, done) => { + t.plan(6) const command = 'node -e "require(\'./undici-fetch.js\').fetch(\'http://localhost:' + server.address().port + '\')"' exec(`${command} --max-http-header-size=1`, { stdio: 'pipe' }, (err, stdout, stderr) => { - t.strictEqual(err.code, 1) - t.strictEqual(stdout, '') - t.match(stderr, /UND_ERR_HEADERS_OVERFLOW/, '--max-http-header-size=1 should throw') - }) + t.assert.strictEqual(err.code, 1) + t.assert.strictEqual(stdout, '') + t.assert.match(stderr, /UND_ERR_HEADERS_OVERFLOW/, '--max-http-header-size=1 should throw') - exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => { - t.ifError(err) - t.strictEqual(stdout, '') - t.strictEqual(stderr, '', 'default max-http-header-size should not throw') - }) + exec(command, { stdio: 'pipe' }, (err, stdout, stderr) => { + t.assert.ifError(err) + t.assert.strictEqual(stdout, '') + t.assert.strictEqual(stderr, '', 'default max-http-header-size should not throw') - await t.completed + done() + }) + }) }) }) diff --git a/test/fetch/content-length.js b/test/fetch/content-length.js index 496b4de1b26..807e0529cce 100644 --- a/test/fetch/content-length.js +++ b/test/fetch/content-length.js @@ -1,10 +1,8 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { createServer } = require('node:http') const { once } = require('node:events') -const { Blob } = require('node:buffer') const { fetch, FormData } = require('../..') const { closeServerAsPromise } = require('../utils/node-http') @@ -12,8 +10,8 @@ const { closeServerAsPromise } = require('../utils/node-http') test('Content-Length is set when using a FormData body with fetch', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { // TODO: check the length's value once the boundary has a fixed length - assert.ok('content-length' in req.headers) // request has content-length header - assert.ok(!Number.isNaN(Number(req.headers['content-length']))) + t.assert.ok('content-length' in req.headers) // request has content-length header + t.assert.ok(!Number.isNaN(Number(req.headers['content-length']))) res.end() }).listen(0) @@ -29,3 +27,23 @@ test('Content-Length is set when using a FormData body with fetch', async (t) => body: fd }) }) + +test('Content-Length is not duplicated when provided explicitly', async (t) => { + const body = 'a+b+c' + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + t.assert.strictEqual(req.headers['content-length'], `${Buffer.byteLength(body)}`) + res.end() + }).listen(0) + + await once(server, 'listening') + t.after(closeServerAsPromise(server)) + + await fetch(`http://localhost:${server.address().port}`, { + method: 'POST', + body, + headers: { + 'content-length': Buffer.byteLength(body) + } + }) +}) diff --git a/test/fetch/cookies.js b/test/fetch/cookies.js index ac30938dbad..43e5f927ef6 100644 --- a/test/fetch/cookies.js +++ b/test/fetch/cookies.js @@ -2,110 +2,120 @@ const { once } = require('node:events') const { createServer } = require('node:http') -const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') +const { test, describe, before, after } = require('node:test') +const { stringify: qsStringify } = require('node:querystring') const { Client, fetch, Headers } = require('../..') -const { closeServerAsPromise } = require('../utils/node-http') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { createSecureServer } = require('node:http2') -const { closeClientAndServerAsPromise } = require('../utils/node-http') -test('Can receive set-cookie headers from a server using fetch - issue #1262', async (t) => { - const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - res.setHeader('set-cookie', 'name=value; Domain=example.com') - res.end() - }).listen(0) +const isMacOSNode20CI = process.platform === 'darwin' && + Number(process.versions.node.split('.')[0]) === 20 && + process.env.CI - t.after(closeServerAsPromise(server)) - await once(server, 'listening') +describe('cookies', () => { + let server - const response = await fetch(`http://localhost:${server.address().port}`) - - assert.strictEqual(response.headers.get('set-cookie'), 'name=value; Domain=example.com') + before(() => { + server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + const searchParams = new URL(req.url, 'http://localhost').searchParams + if (searchParams.has('set-cookie')) { + res.setHeader('set-cookie', searchParams.get('set-cookie')) + } + res.end(req.headers.cookie) + }) - const response2 = await fetch(`http://localhost:${server.address().port}`, { - credentials: 'include' + return once(server.listen(0), 'listening') }) - assert.strictEqual(response2.headers.get('set-cookie'), 'name=value; Domain=example.com') -}) - -test('Can send cookies to a server with fetch - issue #1463', async (t) => { - const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.headers.cookie, 'value') - res.end() - }).listen(0) - - t.after(closeServerAsPromise(server)) - await once(server, 'listening') - - const headersInit = [ - new Headers([['cookie', 'value']]), - { cookie: 'value' }, - [['cookie', 'value']] - ] + after(() => { + server.close() + return once(server, 'close') + }) - for (const headers of headersInit) { - await fetch(`http://localhost:${server.address().port}`, { headers }) - } -}) + test('Can receive set-cookie headers from a server using fetch - issue #1262', { + skip: isMacOSNode20CI + }, async (t) => { + const query = qsStringify({ + 'set-cookie': 'name=value; Domain=example.com' + }) + const response = await fetch(`http://localhost:${server.address().port}?${query}`) -test('Cookie header is delimited with a semicolon rather than a comma - issue #1905', async (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.assert.strictEqual(response.headers.get('set-cookie'), 'name=value; Domain=example.com') + t.assert.strictEqual(await response.text(), '') - const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - strictEqual(req.headers.cookie, 'FOO=lorem-ipsum-dolor-sit-amet; BAR=the-quick-brown-fox') - res.end() - }).listen(0) + const response2 = await fetch(`http://localhost:${server.address().port}?${query}`, { + credentials: 'include' + }) - t.after(closeServerAsPromise(server)) - await once(server, 'listening') + t.assert.strictEqual(response2.headers.get('set-cookie'), 'name=value; Domain=example.com') + t.assert.strictEqual(await response2.text(), '') + }) - await fetch(`http://localhost:${server.address().port}`, { - headers: [ - ['cookie', 'FOO=lorem-ipsum-dolor-sit-amet'], - ['cookie', 'BAR=the-quick-brown-fox'] + test('Can send cookies to a server with fetch - issue #1463', async (t) => { + const headersInit = [ + new Headers([['cookie', 'value']]), + { cookie: 'value' }, + [['cookie', 'value']] ] + + for (const headers of headersInit) { + const response = await fetch(`http://localhost:${server.address().port}`, { headers }) + const text = await response.text() + t.assert.strictEqual(text, 'value') + } }) -}) -test('Can receive set-cookie headers from a http2 server using fetch - issue #2885', async (t) => { - const server = createSecureServer(pem) - server.on('stream', async (stream, headers) => { - stream.respond({ - 'content-type': 'text/plain; charset=utf-8', - 'x-method': headers[':method'], - 'set-cookie': 'Space=Cat; Secure; HttpOnly', - ':status': 200 + test('Cookie header is delimited with a semicolon rather than a comma - issue #1905', { + skip: isMacOSNode20CI + }, async (t) => { + const response = await fetch(`http://localhost:${server.address().port}`, { + headers: [ + ['cookie', 'FOO=lorem-ipsum-dolor-sit-amet'], + ['cookie', 'BAR=the-quick-brown-fox'] + ] }) - stream.end('test') + t.assert.strictEqual(await response.text(), 'FOO=lorem-ipsum-dolor-sit-amet; BAR=the-quick-brown-fox') }) - server.listen() - await once(server, 'listening') + test('Can receive set-cookie headers from a http2 server using fetch - issue #2885', async (t) => { + const server = createSecureServer(pem) + server.on('stream', (stream, headers) => { + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-method': headers[':method'], + 'set-cookie': 'Space=Cat; Secure; HttpOnly', + ':status': 200 + }) + + stream.end('test') + }) + + await once(server.listen(0), 'listening') - const client = new Client(`https://localhost:${server.address().port}`, { - connect: { - rejectUnauthorized: false - }, - allowH2: true - }) + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) - const response = await fetch( - `https://localhost:${server.address().port}/`, - // Needs to be passed to disable the reject unauthorized - { - method: 'GET', - dispatcher: client, - headers: { - 'content-type': 'text-plain' + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'GET', + dispatcher: client, + headers: { + 'content-type': 'text-plain' + } } - } - ) + ) - t.after(closeClientAndServerAsPromise(client, server)) + t.assert.deepStrictEqual(response.headers.getSetCookie(), ['Space=Cat; Secure; HttpOnly']) + t.assert.strictEqual(await response.text(), 'test') - assert.deepStrictEqual(response.headers.getSetCookie(), ['Space=Cat; Secure; HttpOnly']) + await client.close() + await new Promise((resolve, reject) => server.close(err => err ? reject(err) : resolve())) + }) }) diff --git a/test/fetch/data-uri.js b/test/fetch/data-uri.js index feab3408d90..e0370296bc9 100644 --- a/test/fetch/data-uri.js +++ b/test/fetch/data-uri.js @@ -1,11 +1,8 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') const { URLSerializer, - collectASequenceOfCodePoints, stringPercentDecode, parseMIMEType, collectAnHTTPQuotedString @@ -13,104 +10,91 @@ const { const { fetch } = require('../..') test('https://url.spec.whatwg.org/#concept-url-serializer', async (t) => { - await t.test('url scheme gets appended', () => { + await t.test('url scheme gets appended', (t) => { const url = new URL('https://www.google.com/') const serialized = URLSerializer(url) - assert.ok(serialized.startsWith(url.protocol)) + t.assert.ok(serialized.startsWith(url.protocol)) }) - await t.test('non-null url host with authentication', () => { + await t.test('non-null url host with authentication', (t) => { const url = new URL('https://username:password@google.com') const serialized = URLSerializer(url) - assert.ok(serialized.includes(`//${url.username}:${url.password}`)) - assert.ok(serialized.endsWith('@google.com/')) + t.assert.ok(serialized.includes(`//${url.username}:${url.password}`)) + t.assert.ok(serialized.endsWith('@google.com/')) }) - await t.test('null url host', () => { + await t.test('null url host', (t) => { for (const url of ['web+demo:/.//not-a-host/', 'web+demo:/path/..//not-a-host/']) { - assert.strictEqual( + t.assert.strictEqual( URLSerializer(new URL(url)), 'web+demo:/.//not-a-host/' ) } }) - await t.test('url with query works', () => { - assert.strictEqual( + await t.test('url with query works', (t) => { + t.assert.strictEqual( URLSerializer(new URL('https://www.google.com/?fetch=undici')), 'https://www.google.com/?fetch=undici' ) }) - await t.test('exclude fragment', () => { - assert.strictEqual( + await t.test('exclude fragment', (t) => { + t.assert.strictEqual( URLSerializer(new URL('https://www.google.com/#frag')), 'https://www.google.com/#frag' ) - assert.strictEqual( + t.assert.strictEqual( URLSerializer(new URL('https://www.google.com/#frag'), true), 'https://www.google.com/' ) }) }) -test('https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points', () => { - const input = 'text/plain;base64,' - const position = { position: 0 } - const result = collectASequenceOfCodePoints( - (char) => char !== ';', - input, - position - ) - - assert.strictEqual(result, 'text/plain') - assert.strictEqual(position.position, input.indexOf(';')) -}) - test('https://url.spec.whatwg.org/#string-percent-decode', async (t) => { - await t.test('encodes %{2} in range properly', () => { + await t.test('encodes %{2} in range properly', (t) => { const input = '%FF' const percentDecoded = stringPercentDecode(input) - assert.deepStrictEqual(percentDecoded, new Uint8Array([255])) + t.assert.deepStrictEqual(percentDecoded, new Uint8Array([255])) }) - await t.test('encodes %{2} not in range properly', () => { + await t.test('encodes %{2} not in range properly', (t) => { const input = 'Hello %XD World' const percentDecoded = stringPercentDecode(input) const expected = [...input].map(c => c.charCodeAt(0)) - assert.deepStrictEqual(percentDecoded, new Uint8Array(expected)) + t.assert.deepStrictEqual(percentDecoded, new Uint8Array(expected)) }) - await t.test('normal string works', () => { + await t.test('normal string works', (t) => { const input = 'Hello world' const percentDecoded = stringPercentDecode(input) const expected = [...input].map(c => c.charCodeAt(0)) - assert.deepStrictEqual(percentDecoded, Uint8Array.from(expected)) + t.assert.deepStrictEqual(percentDecoded, Uint8Array.from(expected)) }) }) -test('https://mimesniff.spec.whatwg.org/#parse-a-mime-type', () => { - assert.deepStrictEqual(parseMIMEType('text/plain'), { +test('https://mimesniff.spec.whatwg.org/#parse-a-mime-type', (t) => { + t.assert.deepStrictEqual(parseMIMEType('text/plain'), { type: 'text', subtype: 'plain', parameters: new Map(), essence: 'text/plain' }) - assert.deepStrictEqual(parseMIMEType('text/html;charset="shift_jis"iso-2022-jp'), { + t.assert.deepStrictEqual(parseMIMEType('text/html;charset="shift_jis"iso-2022-jp'), { type: 'text', subtype: 'html', parameters: new Map([['charset', 'shift_jis']]), essence: 'text/html' }) - assert.deepStrictEqual(parseMIMEType('application/javascript'), { + t.assert.deepStrictEqual(parseMIMEType('application/javascript'), { type: 'application', subtype: 'javascript', parameters: new Map(), @@ -120,30 +104,30 @@ test('https://mimesniff.spec.whatwg.org/#parse-a-mime-type', () => { test('https://fetch.spec.whatwg.org/#collect-an-http-quoted-string', async (t) => { // https://fetch.spec.whatwg.org/#example-http-quoted-string - await t.test('first', () => { + await t.test('first', (t) => { const position = { position: 0 } - assert.strictEqual(collectAnHTTPQuotedString('"\\', { + t.assert.strictEqual(collectAnHTTPQuotedString('"\\', { position: 0 }), '"\\') - assert.strictEqual(collectAnHTTPQuotedString('"\\', position, true), '\\') - assert.strictEqual(position.position, 2) + t.assert.strictEqual(collectAnHTTPQuotedString('"\\', position, true), '\\') + t.assert.strictEqual(position.position, 2) }) - await t.test('second', () => { + await t.test('second', (t) => { const position = { position: 0 } const input = '"Hello" World' - assert.strictEqual(collectAnHTTPQuotedString(input, { + t.assert.strictEqual(collectAnHTTPQuotedString(input, { position: 0 }), '"Hello"') - assert.strictEqual(collectAnHTTPQuotedString(input, position, true), 'Hello') - assert.strictEqual(position.position, 7) + t.assert.strictEqual(collectAnHTTPQuotedString(input, position, true), 'Hello') + t.assert.strictEqual(position.position, 7) }) }) // https://github.com/nodejs/undici/issues/1574 -test('too long base64 url', async () => { +test('too long base64 url', async (t) => { const inputStr = 'a'.repeat(1 << 20) const base64 = Buffer.from(inputStr).toString('base64') const dataURIPrefix = 'data:application/octet-stream;base64,' @@ -152,43 +136,43 @@ test('too long base64 url', async () => { const res = await fetch(dataURL) const buf = await res.arrayBuffer() const outputStr = Buffer.from(buf).toString('ascii') - assert.strictEqual(outputStr, inputStr) + t.assert.strictEqual(outputStr, inputStr) } catch (e) { - assert.fail(`failed to fetch ${dataURL}`) + t.assert.fail(`failed to fetch ${dataURL}`) } }) test('https://domain.com/#', (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const domain = 'https://domain.com/#a' const serialized = URLSerializer(new URL(domain)) - strictEqual(serialized, domain) + t.assert.strictEqual(serialized, domain) }) test('https://domain.com/?', (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const domain = 'https://domain.com/?a=b' const serialized = URLSerializer(new URL(domain)) - strictEqual(serialized, domain) + t.assert.strictEqual(serialized, domain) }) // https://github.com/nodejs/undici/issues/2474 test('hash url', (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const domain = 'https://domain.com/#a#b' const url = new URL(domain) const serialized = URLSerializer(url, true) - strictEqual(serialized, url.href.substring(0, url.href.length - url.hash.length)) + t.assert.strictEqual(serialized, url.href.substring(0, url.href.length - url.hash.length)) }) // https://github.com/nodejs/undici/issues/2474 test('data url that includes the hash', async (t) => { - const { strictEqual, fail } = tspl(t, { plan: 1 }) + t.plan(1) const dataURL = 'data:,node#js#' try { const res = await fetch(dataURL) - strictEqual(await res.text(), 'node') + t.assert.strictEqual(await res.text(), 'node') } catch (error) { - fail(`failed to fetch ${dataURL}`) + t.assert.fail(`failed to fetch ${dataURL}`) } }) diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js index e34ece0df5a..4f6c16ed687 100644 --- a/test/fetch/encoding.js +++ b/test/fetch/encoding.js @@ -1,87 +1,170 @@ 'use strict' -const { test } = require('node:test') -const assert = require('node:assert') -const { createServer } = require('node:http') const { once } = require('node:events') -const { fetch } = require('../..') -const zlib = require('node:zlib') -const { closeServerAsPromise } = require('../utils/node-http') - -test('content-encoding header is case-iNsENsITIve', async (t) => { - const contentCodings = 'GZiP, bR' - const text = 'Hello, World!' - - const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - const gzip = zlib.createGzip() - const brotli = zlib.createBrotliCompress() +const { createServer } = require('node:http') +const { test, before, after, describe } = require('node:test') +const { fetch, Client } = require('../..') + +describe('content-encoding handling', () => { + const gzipDeflateText = Buffer.from('H4sIAAAAAAAAA6uY89nj7MmT1wM5zuuf8gxkYZCfx5IFACQ8u/wVAAAA', 'base64') + const zstdText = Buffer.from('KLUv/QBYaQAASGVsbG8sIFdvcmxkIQ==', 'base64') + + let server + before(async () => { + server = createServer({ + noDelay: true + }, (req, res) => { + res.socket.setNoDelay(true) + if ( + req.headers['accept-encoding'] === 'deflate, gzip' || + req.headers['accept-encoding'] === 'DeFlAtE, GzIp' + ) { + res.writeHead(200, + { + 'Content-Encoding': 'deflate, gzip', + 'Content-Type': 'text/plain' + } + ) + res.flushHeaders() + res.end(gzipDeflateText) + } else if (req.headers['accept-encoding'] === 'zstd') { + res.writeHead(200, + { + 'Content-Encoding': 'zstd', + 'Content-Type': 'text/plain' + } + ) + res.flushHeaders() + res.end(zstdText) + } else { + res.writeHead(200, + { + 'Content-Type': 'text/plain' + } + ) + res.flushHeaders() + res.end('Hello, World!') + } + }) + await once(server.listen(0), 'listening') + }) - res.setHeader('Content-Encoding', contentCodings) - res.setHeader('Content-Type', 'text/plain') + after(() => { + server.closeAllConnections?.() + server.close() + }) - gzip.pipe(brotli).pipe(res) + test('content-encoding header', async (t) => { + const response = await fetch(`http://localhost:${server.address().port}`, { + keepalive: false, + headers: { 'accept-encoding': 'deflate, gzip' } + }) - gzip.write(text) - gzip.end() - }).listen(0) + t.assert.strictEqual(response.headers.get('content-encoding'), 'deflate, gzip') + t.assert.strictEqual(response.headers.get('content-type'), 'text/plain') + t.assert.strictEqual(await response.text(), 'Hello, World!') + }) - t.after(closeServerAsPromise(server)) - await once(server, 'listening') + test('content-encoding header is case-iNsENsITIve', async (t) => { + const response = await fetch(`http://localhost:${server.address().port}`, { + keepalive: false, + headers: { 'accept-encoding': 'DeFlAtE, GzIp' } + }) - const response = await fetch(`http://localhost:${server.address().port}`) + t.assert.strictEqual(response.headers.get('content-encoding'), 'deflate, gzip') + t.assert.strictEqual(response.headers.get('content-type'), 'text/plain') + t.assert.strictEqual(await response.text(), 'Hello, World!') + }) - assert.strictEqual(await response.text(), text) - assert.strictEqual(response.headers.get('content-encoding'), contentCodings) + test('should decompress zstandard response', + { skip: typeof require('node:zlib').createZstdDecompress !== 'function' }, + async (t) => { + const response = await fetch(`http://localhost:${server.address().port}`, { + keepalive: false, + headers: { 'accept-encoding': 'zstd' } + }) + + t.assert.strictEqual(response.headers.get('content-encoding'), 'zstd') + t.assert.strictEqual(response.headers.get('content-type'), 'text/plain') + t.assert.strictEqual(await response.text(), 'Hello, World!') + }) }) -test('response decompression according to content-encoding should be handled in a correct order', async (t) => { - const contentCodings = 'deflate, gzip' - const text = 'Hello, World!' - - const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - const gzip = zlib.createGzip() - const deflate = zlib.createDeflate() - - res.setHeader('Content-Encoding', contentCodings) - res.setHeader('Content-Type', 'text/plain') +describe('content-encoding chain limit', () => { + // CVE fix: Limit the number of content-encodings to prevent resource exhaustion + // Similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206) + const MAX_CONTENT_ENCODINGS = 5 + + let server + before(async () => { + server = createServer({ + noDelay: true + }, (req, res) => { + res.socket.setNoDelay(true) + const encodingCount = parseInt(req.headers['x-encoding-count'] || '1', 10) + const encodings = Array(encodingCount).fill('identity').join(', ') + + res.writeHead(200, { + 'Content-Encoding': encodings, + 'Content-Type': 'text/plain' + }) + res.flushHeaders() + res.end('test') + }) + await once(server.listen(0, '127.0.0.1'), 'listening') + }) - deflate.pipe(gzip).pipe(res) + after(() => { + server.closeAllConnections?.() + server.close() + }) - deflate.write(text) - deflate.end() - }).listen(0) + test(`should allow exactly ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { + const client = new Client(`http://127.0.0.1:${server.address().port}`) + t.after(() => client.close()) - t.after(closeServerAsPromise(server)) - await once(server, 'listening') + const response = await fetch(`http://127.0.0.1:${server.address().port}`, { + dispatcher: client, + keepalive: false, + headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS) } + }) - const response = await fetch(`http://localhost:${server.address().port}`) + t.assert.strictEqual(response.status, 200) + // identity encoding is a no-op, so the body should be passed through + t.assert.strictEqual(await response.text(), 'test') + }) - assert.strictEqual(await response.text(), text) -}) + test(`should reject more than ${MAX_CONTENT_ENCODINGS} content-encodings`, async (t) => { + const client = new Client(`http://127.0.0.1:${server.address().port}`) + t.after(() => client.close()) + + await t.assert.rejects( + fetch(`http://127.0.0.1:${server.address().port}`, { + dispatcher: client, + keepalive: false, + headers: { 'x-encoding-count': String(MAX_CONTENT_ENCODINGS + 1) } + }), + (err) => { + t.assert.ok(err.cause?.message.includes('content-encoding')) + return true + } + ) + }) -test('should decompress zstandard response', - { skip: typeof zlib.createZstdDecompress !== 'function' }, - async (t) => { - const contentCodings = 'zstd' - const text = 'Hello, World!' - const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - const zstd = zlib.createZstdCompress() - - res.setHeader('Content-Encoding', contentCodings) - res.setHeader('Content-Type', 'text/plain') - - zstd.pipe(res) - zstd.write(text) - zstd.end() - } - ).listen(0) - t.after(closeServerAsPromise(server)) - - await once(server, 'listening') - const url = `http://localhost:${server.address().port}` - - const response = await fetch(url) - assert.strictEqual(await response.text(), text) - assert.strictEqual(response.headers.get('content-encoding'), contentCodings) - assert.strictEqual(response.headers.get('content-type'), 'text/plain') + test('should reject excessive content-encoding chains', async (t) => { + const client = new Client(`http://127.0.0.1:${server.address().port}`) + t.after(() => client.close()) + + await t.assert.rejects( + fetch(`http://127.0.0.1:${server.address().port}`, { + dispatcher: client, + keepalive: false, + headers: { 'x-encoding-count': '100' } + }), + (err) => { + t.assert.ok(err.cause?.message.includes('content-encoding')) + return true + } + ) }) +}) diff --git a/test/fetch/exiting.js b/test/fetch/exiting.js index 746a7fd97ee..223af588c59 100644 --- a/test/fetch/exiting.js +++ b/test/fetch/exiting.js @@ -4,23 +4,26 @@ const { test } = require('node:test') const { fetch } = require('../..') const { createServer } = require('node:http') const { closeServerAsPromise } = require('../utils/node-http') -const tspl = require('@matteo.collina/tspl') +const { once } = require('node:events') +const { createDeferredPromise } = require('../../lib/util/promise') test('abort the request on the other side if the stream is canceled', async (t) => { - const p = tspl(t, { plan: 1 }) + t.plan(1) + + const promise = createDeferredPromise() + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200) res.write('hello') req.on('aborted', () => { - p.ok('aborted') + t.assert.ok('aborted') + promise.resolve() }) // Let's not end the response on purpose }) t.after(closeServerAsPromise(server)) - await new Promise((resolve) => { - server.listen(0, resolve) - }) + await once(server.listen(0), 'listening') const url = new URL(`http://127.0.0.1:${server.address().port}`) @@ -35,5 +38,5 @@ test('abort the request on the other side if the stream is canceled', async (t) await response.body.cancel() } - await p.completed + await promise.promise }) diff --git a/test/fetch/export-env-proxy-agent.js b/test/fetch/export-env-proxy-agent.js index 933a6bb530d..e43b974c793 100644 --- a/test/fetch/export-env-proxy-agent.js +++ b/test/fetch/export-env-proxy-agent.js @@ -1,15 +1,18 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const undiciFetch = require('../../undici-fetch') -test('EnvHttpProxyAgent should be part of Node.js bundle', () => { - assert.strictEqual(typeof undiciFetch.EnvHttpProxyAgent, 'function') - assert.strictEqual(typeof undiciFetch.getGlobalDispatcher, 'function') - assert.strictEqual(typeof undiciFetch.setGlobalDispatcher, 'function') +test('EnvHttpProxyAgent should be part of Node.js bundle', (t) => { + t.assert.strictEqual(typeof undiciFetch.EnvHttpProxyAgent, 'function') + t.assert.strictEqual(typeof undiciFetch.getGlobalDispatcher, 'function') + t.assert.strictEqual(typeof undiciFetch.setGlobalDispatcher, 'function') const agent = new undiciFetch.EnvHttpProxyAgent() + const previousDispatcher = undiciFetch.getGlobalDispatcher() undiciFetch.setGlobalDispatcher(agent) - assert.strictEqual(undiciFetch.getGlobalDispatcher(), agent) + t.after(() => { + undiciFetch.setGlobalDispatcher(previousDispatcher) + }) + t.assert.strictEqual(undiciFetch.getGlobalDispatcher(), agent) }) diff --git a/test/fetch/fetch-leak.js b/test/fetch/fetch-leak.js index 761a63f9700..aa467763a24 100644 --- a/test/fetch/fetch-leak.js +++ b/test/fetch/fetch-leak.js @@ -1,8 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') const { fetch } = require('../..') const { createServer } = require('node:http') const { closeServerAsPromise } = require('../utils/node-http') @@ -13,7 +11,7 @@ test('do not leak', (t, done) => { if (!hasGC) { throw new Error('gc is not available. Run with \'--expose-gc\'.') } - const { ok } = tspl(t, { plan: 1 }) + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() }) @@ -40,10 +38,10 @@ test('do not leak', (t, done) => { global.gc() const next = process.memoryUsage().heapUsed if (next <= prev) { - ok(true) + t.assert.ok(true) done() } else if (count++ > 20) { - assert.fail() + t.assert.fail() } else { prev = next } diff --git a/test/fetch/fetch-timeouts.js b/test/fetch/fetch-timeouts.js index 41f9484e01d..83eb6bbeba2 100644 --- a/test/fetch/fetch-timeouts.js +++ b/test/fetch/fetch-timeouts.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') const { fetch, Agent } = require('../..') const timers = require('../../lib/util/timers') @@ -13,7 +12,7 @@ test('Fetch very long request, timeout overridden so no error', (t, done) => { const minutes = 6 const msToDelay = 1000 * 60 * minutes - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const clock = FakeTimers.install() t.after(clock.uninstall.bind(clock)) @@ -44,7 +43,7 @@ test('Fetch very long request, timeout overridden so no error', (t, done) => { }) .then((response) => response.text()) .then((response) => { - strictEqual('hello', response) + t.assert.strictEqual('hello', response) done() }) .catch((err) => { diff --git a/test/fetch/fetch-url-after-redirect.js b/test/fetch/fetch-url-after-redirect.js index c24c092e15b..0500c3cc492 100644 --- a/test/fetch/fetch-url-after-redirect.js +++ b/test/fetch/fetch-url-after-redirect.js @@ -1,6 +1,5 @@ 'use strict' -const assert = require('node:assert') const { test } = require('node:test') const { createServer } = require('node:http') const { fetch } = require('../..') @@ -36,7 +35,7 @@ test('after redirecting the url of the response is set to the target url', async const { port } = server.address() const response = await fetch(`http://127.0.0.1:${port}/redirect-1`) - assert.strictEqual(response.url, `http://127.0.0.1:${port}/target`) + t.assert.strictEqual(response.url, `http://127.0.0.1:${port}/target`) }) test('location header with non-ASCII character redirects to a properly encoded url', async (t) => { @@ -57,5 +56,5 @@ test('location header with non-ASCII character redirects to a properly encoded u const { port } = server.address() const response = await fetch(`http://127.0.0.1:${port}/redirect`) - assert.strictEqual(response.url, `http://127.0.0.1:${port}/${encodeURIComponent('안녕')}`) + t.assert.strictEqual(response.url, `http://127.0.0.1:${port}/${encodeURIComponent('안녕')}`) }) diff --git a/test/fetch/fire-and-forget.js b/test/fetch/fire-and-forget.js index 04125bad46c..34bf6c80b2f 100644 --- a/test/fetch/fire-and-forget.js +++ b/test/fetch/fire-and-forget.js @@ -1,9 +1,9 @@ 'use strict' const { randomFillSync } = require('node:crypto') -const { setTimeout: sleep } = require('node:timers/promises') +const { setTimeout: sleep, setImmediate: nextTick } = require('node:timers/promises') const { test } = require('node:test') -const { fetch, Agent, setGlobalDispatcher } = require('../..') +const { fetch, Request, Response, Agent, setGlobalDispatcher, getGlobalDispatcher } = require('../..') const { createServer } = require('node:http') const { closeServerAsPromise } = require('../utils/node-http') @@ -11,6 +11,77 @@ const blob = randomFillSync(new Uint8Array(1024 * 512)) const hasGC = typeof global.gc !== 'undefined' +// https://github.com/nodejs/undici/issues/4150 +test('test finalizer cloned request', async () => { + if (!hasGC) { + throw new Error('gc is not available. Run with \'--expose-gc\'.') + } + + const request = new Request('http://localhost', { method: 'POST', body: 'Hello' }) + + request.clone() + + await nextTick() + // eslint-disable-next-line no-undef + gc() + + await nextTick() + await request.arrayBuffer() // check consume body +}) + +test('test finalizer cloned response', async () => { + if (!hasGC) { + throw new Error('gc is not available. Run with \'--expose-gc\'.') + } + + const response = new Response('Hello') + + response.clone() + + await nextTick() + // eslint-disable-next-line no-undef + gc() + + await nextTick() + await response.arrayBuffer() // check consume body +}) + +// https://github.com/nodejs/undici/pull/4803 +test('should not call cancel() during GC (new Response)', async () => { + if (!hasGC) { + throw new Error('gc is not available. Run with \'--expose-gc\'.') + } + + let response = new Response(new ReadableStream({ + start () {}, + + pull (ctrl) { + ctrl.enqueue(new Uint8Array([72, 101, 108, 108, 111])) // Hello + }, + + cancel () { + throw new Error('should be unreachable') + } + })) + + let cloned = response.clone() + + const body = response.body + + await nextTick() + cloned.body.cancel() + + cloned = null + response = null + + await nextTick() + // eslint-disable-next-line no-undef + gc() + + await nextTick(); // handle 'uncaughtException' event + (function () {})(body) // save a reference without triggering linter warnings +}) + test('does not need the body to be consumed to continue', { timeout: 180_000 }, async (t) => { if (!hasGC) { throw new Error('gc is not available. Run with \'--expose-gc\'.') @@ -19,7 +90,11 @@ test('does not need the body to be consumed to continue', { timeout: 180_000 }, keepAliveMaxTimeout: 10, keepAliveTimeoutThreshold: 10 }) + const previousDispatcher = getGlobalDispatcher() setGlobalDispatcher(agent) + t.after(() => { + setGlobalDispatcher(previousDispatcher) + }) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200) res.end(blob) diff --git a/test/fetch/formdata-inspect-custom.js b/test/fetch/formdata-inspect-custom.js index 4fb70069439..fcf67175bf1 100644 --- a/test/fetch/formdata-inspect-custom.js +++ b/test/fetch/formdata-inspect-custom.js @@ -3,14 +3,13 @@ const { FormData } = require('../../') const { inspect } = require('node:util') const { test } = require('node:test') -const assert = require('node:assert') -test('FormData class custom inspection', () => { +test('FormData class custom inspection', (t) => { const formData = new FormData() formData.append('username', 'john_doe') formData.append('email', 'john@example.com') const expectedOutput = "FormData {\n username: 'john_doe',\n email: 'john@example.com'\n}" - assert.deepStrictEqual(inspect(formData), expectedOutput) + t.assert.deepStrictEqual(inspect(formData), expectedOutput) }) diff --git a/test/fetch/formdata.js b/test/fetch/formdata.js index fb828c229e0..3ebfea5d6de 100644 --- a/test/fetch/formdata.js +++ b/test/fetch/formdata.js @@ -1,164 +1,161 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') const { FormData, Response, Request } = require('../../') -const { Blob, File } = require('node:buffer') const { isFormDataLike } = require('../../lib/core/util') -test('arg validation', () => { +test('arg validation', (t) => { const form = new FormData() // constructor - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new FormData('asd') }, TypeError) // append - assert.throws(() => { + t.assert.throws(() => { FormData.prototype.append.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { form.append() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { form.append('k', 'not usv', '') }, TypeError) // delete - assert.throws(() => { + t.assert.throws(() => { FormData.prototype.delete.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { form.delete() }, TypeError) // get - assert.throws(() => { + t.assert.throws(() => { FormData.prototype.get.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { form.get() }, TypeError) // getAll - assert.throws(() => { + t.assert.throws(() => { FormData.prototype.getAll.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { form.getAll() }, TypeError) // has - assert.throws(() => { + t.assert.throws(() => { FormData.prototype.has.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { form.has() }, TypeError) // set - assert.throws(() => { + t.assert.throws(() => { FormData.prototype.set.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { form.set('k') }, TypeError) - assert.throws(() => { + t.assert.throws(() => { form.set('k', 'not usv', '') }, TypeError) // iterator - assert.throws(() => { + t.assert.throws(() => { Reflect.apply(FormData.prototype[Symbol.iterator], null) }, TypeError) // toStringTag - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { FormData.prototype[Symbol.toStringTag].charAt(0) }) }) -test('set blob', () => { +test('set blob', (t) => { const form = new FormData() form.set('key', new Blob([]), undefined) - assert.strictEqual(form.get('key').name, 'blob') + t.assert.strictEqual(form.get('key').name, 'blob') form.set('key1', new Blob([]), null) - assert.strictEqual(form.get('key1').name, 'null') + t.assert.strictEqual(form.get('key1').name, 'null') }) -test('append file', () => { +test('append file', (t) => { const form = new FormData() form.set('asd', new File([], 'asd1', { type: 'text/plain' }), 'asd2') form.append('asd2', new File([], 'asd1'), 'asd2') - assert.strictEqual(form.has('asd'), true) - assert.strictEqual(form.has('asd2'), true) - assert.strictEqual(form.get('asd').name, 'asd2') - assert.strictEqual(form.get('asd2').name, 'asd2') - assert.strictEqual(form.get('asd').type, 'text/plain') + t.assert.strictEqual(form.has('asd'), true) + t.assert.strictEqual(form.has('asd2'), true) + t.assert.strictEqual(form.get('asd').name, 'asd2') + t.assert.strictEqual(form.get('asd2').name, 'asd2') + t.assert.strictEqual(form.get('asd').type, 'text/plain') form.delete('asd') - assert.strictEqual(form.get('asd'), null) - assert.strictEqual(form.has('asd2'), true) - assert.strictEqual(form.has('asd'), false) + t.assert.strictEqual(form.get('asd'), null) + t.assert.strictEqual(form.has('asd2'), true) + t.assert.strictEqual(form.has('asd'), false) }) -test('append blob', async () => { +test('append blob', async (t) => { const form = new FormData() form.set('asd', new Blob(['asd1'], { type: 'text/plain' })) - assert.strictEqual(form.has('asd'), true) - assert.strictEqual(form.get('asd').type, 'text/plain') - assert.strictEqual(await form.get('asd').text(), 'asd1') + t.assert.strictEqual(form.has('asd'), true) + t.assert.strictEqual(form.get('asd').type, 'text/plain') + t.assert.strictEqual(await form.get('asd').text(), 'asd1') form.delete('asd') - assert.strictEqual(form.get('asd'), null) + t.assert.strictEqual(form.get('asd'), null) form.append('key', new Blob([]), undefined) - assert.strictEqual(form.get('key').name, 'blob') + t.assert.strictEqual(form.get('key').name, 'blob') form.append('key1', new Blob([]), null) - assert.strictEqual(form.get('key1').name, 'null') + t.assert.strictEqual(form.get('key1').name, 'null') }) -test('append string', () => { +test('append string', (t) => { const form = new FormData() form.set('k1', 'v1') form.set('k2', 'v2') - assert.deepStrictEqual([...form], [['k1', 'v1'], ['k2', 'v2']]) - assert.strictEqual(form.has('k1'), true) - assert.strictEqual(form.get('k1'), 'v1') + t.assert.deepStrictEqual([...form], [['k1', 'v1'], ['k2', 'v2']]) + t.assert.strictEqual(form.has('k1'), true) + t.assert.strictEqual(form.get('k1'), 'v1') form.append('k1', 'v1+') - assert.deepStrictEqual(form.getAll('k1'), ['v1', 'v1+']) + t.assert.deepStrictEqual(form.getAll('k1'), ['v1', 'v1+']) form.set('k2', 'v1++') - assert.strictEqual(form.get('k2'), 'v1++') + t.assert.strictEqual(form.get('k2'), 'v1++') form.delete('asd') - assert.strictEqual(form.get('asd'), null) + t.assert.strictEqual(form.get('asd'), null) }) test('formData.entries', async (t) => { const form = new FormData() await t.test('with 0 entries', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const entries = [...form.entries()] - deepStrictEqual(entries, []) + t.assert.deepStrictEqual(entries, []) }) await t.test('with 1+ entries', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 2 }) + t.plan(2) form.set('k1', 'v1') form.set('k2', 'v2') const entries = [...form.entries()] const entries2 = [...form.entries()] - deepStrictEqual(entries, [['k1', 'v1'], ['k2', 'v2']]) - deepStrictEqual(entries, entries2) + t.assert.deepStrictEqual(entries, [['k1', 'v1'], ['k2', 'v2']]) + t.assert.deepStrictEqual(entries, entries2) }) }) @@ -166,22 +163,22 @@ test('formData.keys', async (t) => { const form = new FormData() await t.test('with 0 keys', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const keys = [...form.entries()] - deepStrictEqual(keys, []) + t.assert.deepStrictEqual(keys, []) }) await t.test('with 1+ keys', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 2 }) + t.plan(2) form.set('k1', 'v1') form.set('k2', 'v2') const keys = [...form.keys()] const keys2 = [...form.keys()] - deepStrictEqual(keys, ['k1', 'k2']) - deepStrictEqual(keys, keys2) + t.assert.deepStrictEqual(keys, ['k1', 'k2']) + t.assert.deepStrictEqual(keys, keys2) }) }) @@ -189,39 +186,39 @@ test('formData.values', async (t) => { const form = new FormData() await t.test('with 0 values', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const values = [...form.values()] - deepStrictEqual(values, []) + t.assert.deepStrictEqual(values, []) }) await t.test('with 1+ values', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 2 }) + t.plan(2) form.set('k1', 'v1') form.set('k2', 'v2') const values = [...form.values()] const values2 = [...form.values()] - deepStrictEqual(values, ['v1', 'v2']) - deepStrictEqual(values, values2) + t.assert.deepStrictEqual(values, ['v1', 'v2']) + t.assert.deepStrictEqual(values, values2) }) }) test('formData forEach', async (t) => { - await t.test('invalid arguments', () => { - assert.throws(() => { + await t.test('invalid arguments', (t) => { + t.assert.throws(() => { FormData.prototype.forEach.call({}) }, TypeError('Illegal invocation')) - assert.throws(() => { + t.assert.throws(() => { const fd = new FormData() fd.forEach({}) }, TypeError) }) - await t.test('with a callback', () => { + await t.test('with a callback', (t) => { const fd = new FormData() fd.set('a', 'b') @@ -230,48 +227,48 @@ test('formData forEach', async (t) => { let i = 0 fd.forEach((value, key, self) => { if (i++ === 0) { - assert.strictEqual(value, 'b') - assert.strictEqual(key, 'a') + t.assert.strictEqual(value, 'b') + t.assert.strictEqual(key, 'a') } else { - assert.strictEqual(value, 'd') - assert.strictEqual(key, 'c') + t.assert.strictEqual(value, 'd') + t.assert.strictEqual(key, 'c') } - assert.strictEqual(fd, self) + t.assert.strictEqual(fd, self) }) }) - await t.test('with a thisArg', () => { + await t.test('with a thisArg', (t) => { const fd = new FormData() fd.set('b', 'a') fd.forEach(function (value, key, self) { - assert.strictEqual(this, globalThis) - assert.strictEqual(fd, self) - assert.strictEqual(key, 'b') - assert.strictEqual(value, 'a') + t.assert.strictEqual(this, globalThis) + t.assert.strictEqual(fd, self) + t.assert.strictEqual(key, 'b') + t.assert.strictEqual(value, 'a') }) const thisArg = Symbol('thisArg') fd.forEach(function () { - assert.strictEqual(this, thisArg) + t.assert.strictEqual(this, thisArg) }, thisArg) }) }) -test('formData toStringTag', () => { +test('formData toStringTag', (t) => { const form = new FormData() - assert.strictEqual(form[Symbol.toStringTag], 'FormData') - assert.strictEqual(FormData.prototype[Symbol.toStringTag], 'FormData') + t.assert.strictEqual(form[Symbol.toStringTag], 'FormData') + t.assert.strictEqual(FormData.prototype[Symbol.toStringTag], 'FormData') }) -test('formData.constructor.name', () => { +test('formData.constructor.name', (t) => { const form = new FormData() - assert.strictEqual(form.constructor.name, 'FormData') + t.assert.strictEqual(form.constructor.name, 'FormData') }) test('formData should be an instance of FormData', async (t) => { - await t.test('Invalid class FormData', () => { + await t.test('Invalid class FormData', (t) => { class FormData { constructor () { this.data = [] @@ -287,10 +284,10 @@ test('formData should be an instance of FormData', async (t) => { } const form = new FormData() - assert.strictEqual(isFormDataLike(form), false) + t.assert.strictEqual(isFormDataLike(form), false) }) - await t.test('Invalid function FormData', () => { + await t.test('Invalid function FormData', (t) => { function FormData () { const data = [] return { @@ -304,17 +301,17 @@ test('formData should be an instance of FormData', async (t) => { } const form = new FormData() - assert.strictEqual(isFormDataLike(form), false) + t.assert.strictEqual(isFormDataLike(form), false) }) - await t.test('Valid FormData', () => { + await t.test('Valid FormData', (t) => { const form = new FormData() - assert.strictEqual(isFormDataLike(form), true) + t.assert.strictEqual(isFormDataLike(form), true) }) }) test('FormData should be compatible with third-party libraries', (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) class FormData { constructor () { @@ -338,21 +335,21 @@ test('FormData should be compatible with third-party libraries', (t) => { } const form = new FormData() - strictEqual(isFormDataLike(form), true) + t.assert.strictEqual(isFormDataLike(form), true) }) -test('arguments', () => { - assert.strictEqual(FormData.constructor.length, 1) - assert.strictEqual(FormData.prototype.append.length, 2) - assert.strictEqual(FormData.prototype.delete.length, 1) - assert.strictEqual(FormData.prototype.get.length, 1) - assert.strictEqual(FormData.prototype.getAll.length, 1) - assert.strictEqual(FormData.prototype.has.length, 1) - assert.strictEqual(FormData.prototype.set.length, 2) +test('arguments', (t) => { + t.assert.strictEqual(FormData.length, 0) + t.assert.strictEqual(FormData.prototype.append.length, 2) + t.assert.strictEqual(FormData.prototype.delete.length, 1) + t.assert.strictEqual(FormData.prototype.get.length, 1) + t.assert.strictEqual(FormData.prototype.getAll.length, 1) + t.assert.strictEqual(FormData.prototype.has.length, 1) + t.assert.strictEqual(FormData.prototype.set.length, 2) }) // https://github.com/nodejs/undici/pull/1814 -test('FormData returned from bodyMixin.formData is not a clone', async () => { +test('FormData returned from bodyMixin.formData is not a clone', async (t) => { const fd = new FormData() fd.set('foo', 'bar') @@ -361,13 +358,13 @@ test('FormData returned from bodyMixin.formData is not a clone', async () => { const fd2 = await res.formData() - assert.strictEqual(fd2.get('foo'), 'bar') - assert.strictEqual(fd.get('foo'), 'foo') + t.assert.strictEqual(fd2.get('foo'), 'bar') + t.assert.strictEqual(fd.get('foo'), 'foo') fd2.set('foo', 'baz') - assert.strictEqual(fd2.get('foo'), 'baz') - assert.strictEqual(fd.get('foo'), 'foo') + t.assert.strictEqual(fd2.get('foo'), 'baz') + t.assert.strictEqual(fd.get('foo'), 'foo') }) test('.formData() with multipart/form-data body that ends with --\r\n', async (t) => { @@ -386,3 +383,26 @@ test('.formData() with multipart/form-data body that ends with --\r\n', async (t await request.formData() }) + +test('.formData() rejects malformed multipart header line ending with bare CR', async (t) => { + const boundary = '----formdata-undici-bare-cr-0000000000' + const body = Buffer.concat([ + Buffer.from('--' + boundary + '\r\n'), + Buffer.from('Content-Disposition: form-data; name="x"'), + Buffer.from([0x0d]), // bare CR (no LF) + Buffer.from('Content-Type: text/plain\r\n'), + Buffer.from('\r\n'), + Buffer.from('hello\r\n'), + Buffer.from('--' + boundary + '--\r\n') + ]) + + const request = new Request('http://localhost', { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data; boundary=' + boundary + }, + body + }) + + await t.assert.rejects(request.formData(), TypeError) +}) diff --git a/test/fetch/general.js b/test/fetch/general.js index a23554b931b..9e65f4ab039 100644 --- a/test/fetch/general.js +++ b/test/fetch/general.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { FormData, Headers, @@ -9,7 +8,7 @@ const { Response } = require('../../index') -test('Symbol.toStringTag descriptor', () => { +test('Symbol.toStringTag descriptor', (t) => { for (const cls of [ FormData, Headers, @@ -17,7 +16,7 @@ test('Symbol.toStringTag descriptor', () => { Response ]) { const desc = Object.getOwnPropertyDescriptor(cls.prototype, Symbol.toStringTag) - assert.deepStrictEqual(desc, { + t.assert.deepStrictEqual(desc, { value: cls.name, writable: false, enumerable: false, diff --git a/test/fetch/headers-case.js b/test/fetch/headers-case.js index e0314565bf7..1deb30b593c 100644 --- a/test/fetch/headers-case.js +++ b/test/fetch/headers-case.js @@ -4,14 +4,13 @@ const { fetch, Headers, Request } = require('../..') const { createServer } = require('node:http') const { once } = require('node:events') const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') const { closeServerAsPromise } = require('../utils/node-http') test('Headers retain keys case-sensitive', async (t) => { - const assert = tspl(t, { plan: 4 }) + t.plan(4) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.ok(req.rawHeaders.includes('Content-Type')) + t.assert.ok(req.rawHeaders.includes('Content-Type')) res.end() }).listen(0) diff --git a/test/fetch/headers-inspect-custom.js b/test/fetch/headers-inspect-custom.js index 8145e9d7536..5c5b556a14f 100644 --- a/test/fetch/headers-inspect-custom.js +++ b/test/fetch/headers-inspect-custom.js @@ -2,10 +2,9 @@ const { Headers } = require('../../lib/web/fetch/headers') const { test } = require('node:test') -const assert = require('node:assert') const util = require('node:util') -test('Headers class custom inspection', () => { +test('Headers class custom inspection', (t) => { const headers = new Headers() headers.set('Content-Type', 'application/json') headers.set('Authorization', 'Bearer token') @@ -13,5 +12,5 @@ test('Headers class custom inspection', () => { const inspectedOutput = util.inspect(headers, { depth: 1 }) const expectedOutput = "Headers { 'Content-Type': 'application/json', Authorization: 'Bearer token' }" - assert.strictEqual(inspectedOutput, expectedOutput) + t.assert.strictEqual(inspectedOutput, expectedOutput) }) diff --git a/test/fetch/headers.js b/test/fetch/headers.js index 1430abff1a9..a55994cace9 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -1,8 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') const { Headers, fill, setHeadersGuard } = require('../../lib/web/fetch/headers') const { once } = require('node:events') const { fetch } = require('../..') @@ -10,36 +8,36 @@ const { createServer } = require('node:http') const { closeServerAsPromise } = require('../utils/node-http') test('Headers initialization', async (t) => { - await t.test('allows undefined', () => { - const { doesNotThrow } = tspl(t, { plan: 1 }) + await t.test('allows undefined', (t) => { + t.plan(1) - doesNotThrow(() => new Headers()) + t.assert.doesNotThrow(() => new Headers()) }) await t.test('with array of header entries', async (t) => { await t.test('fails on invalid array-based init', (t) => { - const { throws } = tspl(t, { plan: 3 }) - throws( + t.plan(3) + t.assert.throws( () => new Headers([['undici', 'fetch'], ['fetch']]), TypeError('Headers constructor: expected name/value pair to be length 2, found 1.') ) - throws(() => new Headers(['undici', 'fetch', 'fetch']), TypeError) - throws( + t.assert.throws(() => new Headers(['undici', 'fetch', 'fetch']), TypeError) + t.assert.throws( () => new Headers([0, 1, 2]), TypeError('Headers constructor: init[0] (0) is not iterable.') ) }) await t.test('allows even length init', (t) => { - const { doesNotThrow } = tspl(t, { plan: 1 }) + t.plan(1) const init = [['undici', 'fetch'], ['fetch', 'undici']] - doesNotThrow(() => new Headers(init)) + t.assert.doesNotThrow(() => new Headers(init)) }) await t.test('fails for event flattened init', (t) => { - const { throws } = tspl(t, { plan: 1 }) + t.plan(1) const init = ['undici', 'fetch', 'fetch', 'undici'] - throws( + t.assert.throws( () => new Headers(init), TypeError('Headers constructor: init[0] ("undici") is not iterable.') ) @@ -47,69 +45,69 @@ test('Headers initialization', async (t) => { }) await t.test('with object of header entries', (t) => { - const { doesNotThrow } = tspl(t, { plan: 1 }) + t.plan(1) const init = { undici: 'fetch', fetch: 'undici' } - doesNotThrow(() => new Headers(init)) + t.assert.doesNotThrow(() => new Headers(init)) }) await t.test('fails silently if a boxed primitive object is passed', (t) => { - const { doesNotThrow } = tspl(t, { plan: 3 }) + t.plan(3) /* eslint-disable no-new-wrappers */ - doesNotThrow(() => new Headers(new Number())) - doesNotThrow(() => new Headers(new Boolean())) - doesNotThrow(() => new Headers(new String())) + t.assert.doesNotThrow(() => new Headers(new Number())) + t.assert.doesNotThrow(() => new Headers(new Boolean())) + t.assert.doesNotThrow(() => new Headers(new String())) /* eslint-enable no-new-wrappers */ }) await t.test('fails if primitive is passed', (t) => { - const { throws } = tspl(t, { plan: 2 }) + t.plan(2) const expectedTypeError = TypeError - throws(() => new Headers(1), expectedTypeError) - throws(() => new Headers('1'), expectedTypeError) + t.assert.throws(() => new Headers(1), expectedTypeError) + t.assert.throws(() => new Headers('1'), expectedTypeError) }) - await t.test('allows some weird stuff (because of webidl)', () => { - assert.doesNotThrow(() => { + await t.test('allows some weird stuff (because of webidl)', (t) => { + t.assert.doesNotThrow(() => { new Headers(function () {}) // eslint-disable-line no-new }) - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { new Headers(Function) // eslint-disable-line no-new }) }) await t.test('allows a myriad of header values to be passed', (t) => { - const { doesNotThrow, throws } = tspl(t, { plan: 4 }) + t.plan(4) // Headers constructor uses Headers.append - doesNotThrow(() => new Headers([ + t.assert.doesNotThrow(() => new Headers([ ['a', ['b', 'c']], ['d', ['e', 'f']] ]), 'allows any array values') - doesNotThrow(() => new Headers([ + t.assert.doesNotThrow(() => new Headers([ ['key', null] ]), 'allows null values') - throws(() => new Headers([ + t.assert.throws(() => new Headers([ ['key'] ]), 'throws when 2 arguments are not passed') - throws(() => new Headers([ + t.assert.throws(() => new Headers([ ['key', 'value', 'value2'] ]), 'throws when too many arguments are passed') }) await t.test('accepts headers as objects with array values', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const headers = new Headers({ c: '5', b: ['3', '4'], a: ['1', '2'] }) - deepStrictEqual([...headers.entries()], [ + t.assert.deepStrictEqual([...headers.entries()], [ ['a', '1,2'], ['b', '3,4'], ['c', '5'] @@ -119,17 +117,17 @@ test('Headers initialization', async (t) => { test('Headers append', async (t) => { await t.test('adds valid header entry to instance', (t) => { - const { doesNotThrow, strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers() const name = 'undici' const value = 'fetch' - doesNotThrow(() => headers.append(name, value)) - strictEqual(headers.get(name), value) + t.assert.doesNotThrow(() => headers.append(name, value)) + t.assert.strictEqual(headers.get(name), value) }) await t.test('adds valid header to existing entry', (t) => { - const { strictEqual, doesNotThrow } = tspl(t, { plan: 4 }) + t.plan(4) const headers = new Headers() const name = 'undici' @@ -137,200 +135,200 @@ test('Headers append', async (t) => { const value2 = 'fetch2' const value3 = 'fetch3' headers.append(name, value1) - strictEqual(headers.get(name), value1) - doesNotThrow(() => headers.append(name, value2)) - doesNotThrow(() => headers.append(name, value3)) - strictEqual(headers.get(name), [value1, value2, value3].join(', ')) + t.assert.strictEqual(headers.get(name), value1) + t.assert.doesNotThrow(() => headers.append(name, value2)) + t.assert.doesNotThrow(() => headers.append(name, value3)) + t.assert.strictEqual(headers.get(name), [value1, value2, value3].join(', ')) }) await t.test('throws on invalid entry', (t) => { - const { throws } = tspl(t, { plan: 3 }) + t.plan(3) const headers = new Headers() - throws(() => headers.append(), 'throws on missing name and value') - throws(() => headers.append('undici'), 'throws on missing value') - throws(() => headers.append('invalid @ header ? name', 'valid value'), 'throws on invalid name') + t.assert.throws(() => headers.append(), 'throws on missing name and value') + t.assert.throws(() => headers.append('undici'), 'throws on missing value') + t.assert.throws(() => headers.append('invalid @ header ? name', 'valid value'), 'throws on invalid name') }) }) test('Headers delete', async (t) => { await t.test('deletes valid header entry from instance', (t) => { - const { strictEqual, doesNotThrow } = tspl(t, { plan: 3 }) + t.plan(3) const headers = new Headers() const name = 'undici' const value = 'fetch' headers.append(name, value) - strictEqual(headers.get(name), value) - doesNotThrow(() => headers.delete(name)) - strictEqual(headers.get(name), null) + t.assert.strictEqual(headers.get(name), value) + t.assert.doesNotThrow(() => headers.delete(name)) + t.assert.strictEqual(headers.get(name), null) }) await t.test('does not mutate internal list when no match is found', (t) => { - const { strictEqual, doesNotThrow } = tspl(t, { plan: 3 }) + t.plan(3) const headers = new Headers() const name = 'undici' const value = 'fetch' headers.append(name, value) - strictEqual(headers.get(name), value) - doesNotThrow(() => headers.delete('not-undici')) - strictEqual(headers.get(name), value) + t.assert.strictEqual(headers.get(name), value) + t.assert.doesNotThrow(() => headers.delete('not-undici')) + t.assert.strictEqual(headers.get(name), value) }) await t.test('throws on invalid entry', (t) => { - const { throws } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers() - throws(() => headers.delete(), 'throws on missing namee') - throws(() => headers.delete('invalid @ header ? name'), 'throws on invalid name') + t.assert.throws(() => headers.delete(), 'throws on missing namee') + t.assert.throws(() => headers.delete('invalid @ header ? name'), 'throws on invalid name') }) // https://github.com/nodejs/undici/issues/2429 await t.test('`Headers#delete` returns undefined', (t) => { - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers({ test: 'test' }) - strictEqual(headers.delete('test'), undefined) - strictEqual(headers.delete('test2'), undefined) + t.assert.strictEqual(headers.delete('test'), undefined) + t.assert.strictEqual(headers.delete('test2'), undefined) }) }) test('Headers get', async (t) => { await t.test('returns null if not found in instance', (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const headers = new Headers() headers.append('undici', 'fetch') - strictEqual(headers.get('not-undici'), null) + t.assert.strictEqual(headers.get('not-undici'), null) }) await t.test('returns header values from valid header name', (t) => { - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers() const name = 'undici'; const value1 = 'fetch1'; const value2 = 'fetch2' headers.append(name, value1) - strictEqual(headers.get(name), value1) + t.assert.strictEqual(headers.get(name), value1) headers.append(name, value2) - strictEqual(headers.get(name), [value1, value2].join(', ')) + t.assert.strictEqual(headers.get(name), [value1, value2].join(', ')) }) await t.test('throws on invalid entry', (t) => { - const { throws } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers() - throws(() => headers.get(), 'throws on missing name') - throws(() => headers.get('invalid @ header ? name'), 'throws on invalid name') + t.assert.throws(() => headers.get(), 'throws on missing name') + t.assert.throws(() => headers.get('invalid @ header ? name'), 'throws on invalid name') }) }) test('Headers has', async (t) => { await t.test('returns boolean existence for a header name', (t) => { - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers() const name = 'undici' headers.append('not-undici', 'fetch') - strictEqual(headers.has(name), false) + t.assert.strictEqual(headers.has(name), false) headers.append(name, 'fetch') - strictEqual(headers.has(name), true) + t.assert.strictEqual(headers.has(name), true) }) await t.test('throws on invalid entry', (t) => { - const { throws } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers() - throws(() => headers.has(), 'throws on missing name') - throws(() => headers.has('invalid @ header ? name'), 'throws on invalid name') + t.assert.throws(() => headers.has(), 'throws on missing name') + t.assert.throws(() => headers.has('invalid @ header ? name'), 'throws on invalid name') }) }) test('Headers set', async (t) => { await t.test('sets valid header entry to instance', (t) => { - const { doesNotThrow, strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers() const name = 'undici' const value = 'fetch' headers.append('not-undici', 'fetch') - doesNotThrow(() => headers.set(name, value)) - strictEqual(headers.get(name), value) + t.assert.doesNotThrow(() => headers.set(name, value)) + t.assert.strictEqual(headers.get(name), value) }) await t.test('overwrites existing entry', (t) => { - const { doesNotThrow, strictEqual } = tspl(t, { plan: 4 }) + t.plan(4) const headers = new Headers() const name = 'undici' const value1 = 'fetch1' const value2 = 'fetch2' - doesNotThrow(() => headers.set(name, value1)) - strictEqual(headers.get(name), value1) - doesNotThrow(() => headers.set(name, value2)) - strictEqual(headers.get(name), value2) + t.assert.doesNotThrow(() => headers.set(name, value1)) + t.assert.strictEqual(headers.get(name), value1) + t.assert.doesNotThrow(() => headers.set(name, value2)) + t.assert.strictEqual(headers.get(name), value2) }) await t.test('allows setting a myriad of values', (t) => { - const { doesNotThrow, throws } = tspl(t, { plan: 4 }) + t.plan(4) const headers = new Headers() - doesNotThrow(() => headers.set('a', ['b', 'c']), 'sets array values properly') - doesNotThrow(() => headers.set('b', null), 'allows setting null values') - throws(() => headers.set('c'), 'throws when 2 arguments are not passed') - doesNotThrow(() => headers.set('c', 'd', 'e'), 'ignores extra arguments') + t.assert.doesNotThrow(() => headers.set('a', ['b', 'c']), 'sets array values properly') + t.assert.doesNotThrow(() => headers.set('b', null), 'allows setting null values') + t.assert.throws(() => headers.set('c'), 'throws when 2 arguments are not passed') + t.assert.doesNotThrow(() => headers.set('c', 'd', 'e'), 'ignores extra arguments') }) await t.test('throws on invalid entry', (t) => { - const { throws } = tspl(t, { plan: 3 }) + t.plan(3) const headers = new Headers() - throws(() => headers.set(), 'throws on missing name and value') - throws(() => headers.set('undici'), 'throws on missing value') - throws(() => headers.set('invalid @ header ? name', 'valid value'), 'throws on invalid name') + t.assert.throws(() => headers.set(), 'throws on missing name and value') + t.assert.throws(() => headers.set('undici'), 'throws on missing value') + t.assert.throws(() => headers.set('invalid @ header ? name', 'valid value'), 'throws on invalid name') }) // https://github.com/nodejs/undici/issues/2431 await t.test('`Headers#set` returns undefined', (t) => { - const { strictEqual, ok } = tspl(t, { plan: 2 }) + t.plan(2) const headers = new Headers() - strictEqual(headers.set('a', 'b'), undefined) + t.assert.strictEqual(headers.set('a', 'b'), undefined) - ok(!(headers.set('c', 'd') instanceof Map)) + t.assert.ok(!(headers.set('c', 'd') instanceof Map)) }) }) test('Headers forEach', async (t) => { const headers = new Headers([['a', 'b'], ['c', 'd']]) - await t.test('standard', () => { - assert.strictEqual(typeof headers.forEach, 'function') + await t.test('standard', (t) => { + t.assert.strictEqual(typeof headers.forEach, 'function') headers.forEach((value, key, headerInstance) => { - assert.ok(value === 'b' || value === 'd') - assert.ok(key === 'a' || key === 'c') - assert.strictEqual(headers, headerInstance) + t.assert.ok(value === 'b' || value === 'd') + t.assert.ok(key === 'a' || key === 'c') + t.assert.strictEqual(headers, headerInstance) }) }) - await t.test('when no thisArg is set, it is globalThis', () => { + await t.test('when no thisArg is set, it is globalThis', (t) => { headers.forEach(function () { - assert.strictEqual(this, globalThis) + t.assert.strictEqual(this, globalThis) }) }) - await t.test('with thisArg', () => { + await t.test('with thisArg', (t) => { const thisArg = { a: Math.random() } headers.forEach(function () { - assert.strictEqual(this, thisArg) + t.assert.strictEqual(this, thisArg) }, thisArg) }) }) test('Headers as Iterable', async (t) => { await t.test('should freeze values while iterating', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const init = [ ['foo', '123'], ['bar', '456'] @@ -344,11 +342,11 @@ test('Headers as Iterable', async (t) => { headers.delete(key) headers.set(`x-${key}`, val) } - deepStrictEqual([...headers], expected) + t.assert.deepStrictEqual([...headers], expected) }) await t.test('returns combined and sorted entries using .forEach()', (t) => { - const { deepStrictEqual, strictEqual } = tspl(t, { plan: 8 }) + t.plan(8) const init = [ ['a', '1'], ['b', '2'], @@ -366,13 +364,13 @@ test('Headers as Iterable', async (t) => { const that = {} let i = 0 headers.forEach(function (value, key, _headers) { - deepStrictEqual(expected[i++], [key, value]) - strictEqual(this, that) + t.assert.deepStrictEqual(expected[i++], [key, value]) + t.assert.strictEqual(this, that) }, that) }) await t.test('returns combined and sorted entries using .entries()', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 4 }) + t.plan(4) const init = [ ['a', '1'], ['b', '2'], @@ -389,12 +387,12 @@ test('Headers as Iterable', async (t) => { const headers = new Headers(init) let i = 0 for (const header of headers.entries()) { - deepStrictEqual(header, expected[i++]) + t.assert.deepStrictEqual(header, expected[i++]) } }) await t.test('returns combined and sorted keys using .keys()', (t) => { - const { strictEqual } = tspl(t, { plan: 4 }) + t.plan(4) const init = [ ['a', '1'], ['b', '2'], @@ -406,12 +404,12 @@ test('Headers as Iterable', async (t) => { const headers = new Headers(init) let i = 0 for (const key of headers.keys()) { - strictEqual(key, expected[i++]) + t.assert.strictEqual(key, expected[i++]) } }) await t.test('returns combined and sorted values using .values()', (t) => { - const { strictEqual } = tspl(t, { plan: 4 }) + t.plan(4) const init = [ ['a', '1'], ['b', '2'], @@ -423,12 +421,12 @@ test('Headers as Iterable', async (t) => { const headers = new Headers(init) let i = 0 for (const value of headers.values()) { - strictEqual(value, expected[i++]) + t.assert.strictEqual(value, expected[i++]) } }) await t.test('returns combined and sorted entries using for...of loop', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 5 }) + t.plan(5) const init = [ ['a', '1'], ['b', '2'], @@ -446,12 +444,12 @@ test('Headers as Iterable', async (t) => { ] let i = 0 for (const header of new Headers(init)) { - deepStrictEqual(header, expected[i++]) + t.assert.deepStrictEqual(header, expected[i++]) } }) await t.test('validate append ordering', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const headers = new Headers([['b', '2'], ['c', '3'], ['e', '5']]) headers.append('d', '4') headers.append('a', '1') @@ -469,7 +467,7 @@ test('Headers as Iterable', async (t) => { ['f', '6'] ])] - deepStrictEqual([...headers], expected) + t.assert.deepStrictEqual([...headers], expected) }) await t.test('always use the same prototype Iterator', (t) => { @@ -482,166 +480,166 @@ test('Headers as Iterable', async (t) => { const headers = new Headers(init) const iterator = headers[Symbol.iterator]() - assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: init[0], done: false }) - assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: init[1], done: false }) - assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: undefined, done: true }) + t.assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: init[0], done: false }) + t.assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: init[1], done: false }) + t.assert.deepStrictEqual(HeadersIteratorNext(iterator), { value: undefined, done: true }) }) }) -test('arg validation', () => { +test('arg validation', (t) => { // fill - assert.throws(() => { + t.assert.throws(() => { fill({}, 0) }, TypeError) const headers = new Headers() // constructor - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Headers(0) }, TypeError) // get [Symbol.toStringTag] - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { Object.prototype.toString.call(Headers.prototype) }) // toString - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { Headers.prototype.toString.call(null) }) // append - assert.throws(() => { + t.assert.throws(() => { Headers.prototype.append.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { headers.append() }, TypeError) // delete - assert.throws(() => { + t.assert.throws(() => { Headers.prototype.delete.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { headers.delete() }, TypeError) // get - assert.throws(() => { + t.assert.throws(() => { Headers.prototype.get.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { headers.get() }, TypeError) // has - assert.throws(() => { + t.assert.throws(() => { Headers.prototype.has.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { headers.has() }, TypeError) // set - assert.throws(() => { + t.assert.throws(() => { Headers.prototype.set.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { headers.set() }, TypeError) // forEach - assert.throws(() => { + t.assert.throws(() => { Headers.prototype.forEach.call(null) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { headers.forEach() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { headers.forEach(1) }, TypeError) // inspect - assert.throws(() => { + t.assert.throws(() => { Headers.prototype[Symbol.for('nodejs.util.inspect.custom')].call(null) }, TypeError) }) test('function signature verification', async (t) => { - await t.test('function length', () => { - assert.strictEqual(Headers.prototype.append.length, 2) - assert.strictEqual(Headers.prototype.constructor.length, 0) - assert.strictEqual(Headers.prototype.delete.length, 1) - assert.strictEqual(Headers.prototype.entries.length, 0) - assert.strictEqual(Headers.prototype.forEach.length, 1) - assert.strictEqual(Headers.prototype.get.length, 1) - assert.strictEqual(Headers.prototype.has.length, 1) - assert.strictEqual(Headers.prototype.keys.length, 0) - assert.strictEqual(Headers.prototype.set.length, 2) - assert.strictEqual(Headers.prototype.values.length, 0) - assert.strictEqual(Headers.prototype[Symbol.iterator].length, 0) - assert.strictEqual(Headers.prototype.toString.length, 0) - }) - - await t.test('function equality', () => { - assert.strictEqual(Headers.prototype.entries, Headers.prototype[Symbol.iterator]) - assert.strictEqual(Headers.prototype.toString, Object.prototype.toString) - }) - - await t.test('toString and Symbol.toStringTag', () => { - assert.strictEqual(Object.prototype.toString.call(Headers.prototype), '[object Headers]') - assert.strictEqual(Headers.prototype[Symbol.toStringTag], 'Headers') - assert.strictEqual(Headers.prototype.toString.call(null), '[object Null]') + await t.test('function length', (t) => { + t.assert.strictEqual(Headers.prototype.append.length, 2) + t.assert.strictEqual(Headers.prototype.constructor.length, 0) + t.assert.strictEqual(Headers.prototype.delete.length, 1) + t.assert.strictEqual(Headers.prototype.entries.length, 0) + t.assert.strictEqual(Headers.prototype.forEach.length, 1) + t.assert.strictEqual(Headers.prototype.get.length, 1) + t.assert.strictEqual(Headers.prototype.has.length, 1) + t.assert.strictEqual(Headers.prototype.keys.length, 0) + t.assert.strictEqual(Headers.prototype.set.length, 2) + t.assert.strictEqual(Headers.prototype.values.length, 0) + t.assert.strictEqual(Headers.prototype[Symbol.iterator].length, 0) + t.assert.strictEqual(Headers.prototype.toString.length, 0) + }) + + await t.test('function equality', (t) => { + t.assert.strictEqual(Headers.prototype.entries, Headers.prototype[Symbol.iterator]) + t.assert.strictEqual(Headers.prototype.toString, Object.prototype.toString) + }) + + await t.test('toString and Symbol.toStringTag', (t) => { + t.assert.strictEqual(Object.prototype.toString.call(Headers.prototype), '[object Headers]') + t.assert.strictEqual(Headers.prototype[Symbol.toStringTag], 'Headers') + t.assert.strictEqual(Headers.prototype.toString.call(null), '[object Null]') }) }) -test('various init paths of Headers', () => { +test('various init paths of Headers', (t) => { const h1 = new Headers() const h2 = new Headers({}) const h3 = new Headers(undefined) - assert.strictEqual([...h1.entries()].length, 0) - assert.strictEqual([...h2.entries()].length, 0) - assert.strictEqual([...h3.entries()].length, 0) + t.assert.strictEqual([...h1.entries()].length, 0) + t.assert.strictEqual([...h2.entries()].length, 0) + t.assert.strictEqual([...h3.entries()].length, 0) }) -test('immutable guard', () => { +test('immutable guard', (t) => { const headers = new Headers() headers.set('key', 'val') setHeadersGuard(headers, 'immutable') - assert.throws(() => { + t.assert.throws(() => { headers.set('asd', 'asd') }) - assert.throws(() => { + t.assert.throws(() => { headers.append('asd', 'asd') }) - assert.throws(() => { + t.assert.throws(() => { headers.delete('asd') }) - assert.strictEqual(headers.get('key'), 'val') - assert.strictEqual(headers.has('key'), true) + t.assert.strictEqual(headers.get('key'), 'val') + t.assert.strictEqual(headers.has('key'), true) }) -test('request-no-cors guard', () => { +test('request-no-cors guard', (t) => { const headers = new Headers() setHeadersGuard(headers, 'request-no-cors') - assert.doesNotThrow(() => { headers.set('key', 'val') }) - assert.doesNotThrow(() => { headers.append('key', 'val') }) + t.assert.doesNotThrow(() => { headers.set('key', 'val') }) + t.assert.doesNotThrow(() => { headers.append('key', 'val') }) }) -test('invalid headers', () => { - assert.doesNotThrow(() => new Headers({ "abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~": 'test' })) +test('invalid headers', (t) => { + t.assert.doesNotThrow(() => new Headers({ "abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~": 'test' })) const chars = '"(),/:;<=>?@[\\]{}'.split('') for (const char of chars) { - assert.throws(() => new Headers({ [char]: 'test' }), TypeError, `The string "${char}" should throw an error.`) + t.assert.throws(() => new Headers({ [char]: 'test' }), TypeError, `The string "${char}" should throw an error.`) } for (const byte of ['\r', '\n', '\t', ' ', String.fromCharCode(128), '']) { - assert.throws(() => { + t.assert.throws(() => { new Headers().set(byte, 'test') }, TypeError, 'invalid header name') } @@ -651,26 +649,26 @@ test('invalid headers', () => { '\r', '\n' ]) { - assert.throws(() => { + t.assert.throws(() => { new Headers().set('a', `a${byte}b`) }, TypeError, 'not allowed at all in header value') } - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { new Headers().set('a', '\r') }) - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { new Headers().set('a', '\n') }) - assert.throws(() => { + t.assert.throws(() => { new Headers().set('a', Symbol('symbol')) }, TypeError, 'symbols should throw') }) -test('headers that might cause a ReDoS', () => { - assert.doesNotThrow(() => { +test('headers that might cause a ReDoS', (t) => { + t.assert.doesNotThrow(() => { // This test will time out if the ReDoS attack is successful. const headers = new Headers() const attack = 'a' + '\t'.repeat(500_000) + '\ta' @@ -679,7 +677,7 @@ test('headers that might cause a ReDoS', () => { }) test('Headers.prototype.getSetCookie', async (t) => { - await t.test('Mutating the returned list does not affect the set-cookie list', () => { + await t.test('Mutating the returned list does not affect the set-cookie list', (t) => { const h = new Headers([ ['set-cookie', 'a=b'], ['set-cookie', 'c=d'] @@ -689,7 +687,7 @@ test('Headers.prototype.getSetCookie', async (t) => { h.getSetCookie().push('oh=no') const now = h.getSetCookie() - assert.deepStrictEqual(old, now) + t.assert.deepStrictEqual(old, now) }) // https://github.com/nodejs/undici/issues/1935 @@ -705,8 +703,8 @@ test('Headers.prototype.getSetCookie', async (t) => { const res = await fetch(`http://localhost:${server.address().port}`) const entries = Object.fromEntries(res.headers.entries()) - assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo']) - assert.ok('set-cookie' in entries) + t.assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo']) + t.assert.ok('set-cookie' in entries) }) await t.test('When Headers are cloned, so are the cookies (multiple entries)', async (t) => { @@ -721,32 +719,32 @@ test('Headers.prototype.getSetCookie', async (t) => { const res = await fetch(`http://localhost:${server.address().port}`) const entries = Object.fromEntries(res.headers.entries()) - assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo', 'test=onetwothree']) - assert.ok('set-cookie' in entries) + t.assert.deepStrictEqual(res.headers.getSetCookie(), ['test=onetwo', 'test=onetwothree']) + t.assert.ok('set-cookie' in entries) }) - await t.test('When Headers are cloned, so are the cookies (Headers constructor)', () => { + await t.test('When Headers are cloned, so are the cookies (Headers constructor)', (t) => { const headers = new Headers([['set-cookie', 'a'], ['set-cookie', 'b']]) - assert.deepStrictEqual([...headers], [...new Headers(headers)]) + t.assert.deepStrictEqual([...headers], [...new Headers(headers)]) }) }) test('When the value is updated, update the cache', (t) => { - const { deepStrictEqual } = tspl(t, { plan: 2 }) + t.plan(2) const expected = [['a', 'a'], ['b', 'b'], ['c', 'c']] const headers = new Headers(expected) - deepStrictEqual([...headers], expected) + t.assert.deepStrictEqual([...headers], expected) headers.append('d', 'd') - deepStrictEqual([...headers], [...expected, ['d', 'd']]) + t.assert.deepStrictEqual([...headers], [...expected, ['d', 'd']]) }) test('Symbol.iterator is only accessed once', (t) => { - const { ok } = tspl(t, { plan: 1 }) + t.plan(1) const dict = new Proxy({}, { get () { - ok(true) + t.assert.ok(true) return function * () {} } @@ -756,11 +754,11 @@ test('Symbol.iterator is only accessed once', (t) => { }) test('Invalid Symbol.iterators', (t) => { - const { throws } = tspl(t, { plan: 3 }) + t.plan(3) - throws(() => new Headers({ [Symbol.iterator]: null }), TypeError) - throws(() => new Headers({ [Symbol.iterator]: undefined }), TypeError) - throws(() => { + t.assert.throws(() => new Headers({ [Symbol.iterator]: null }), TypeError) + t.assert.throws(() => new Headers({ [Symbol.iterator]: undefined }), TypeError) + t.assert.throws(() => { const obj = { [Symbol.iterator]: null } Object.defineProperty(obj, Symbol.iterator, { enumerable: false }) @@ -770,12 +768,12 @@ test('Invalid Symbol.iterators', (t) => { // https://github.com/nodejs/undici/issues/3829 test('Invalid key/value records passed to constructor (issue #3829)', (t) => { - assert.throws( + t.assert.throws( () => new Headers({ [Symbol('x-fake-header')]: '??' }), new TypeError('Headers constructor: Key Symbol(x-fake-header) in init is a symbol, which cannot be converted to a ByteString.') ) - assert.throws( + t.assert.throws( () => new Headers({ 'x-fake-header': Symbol('why is this here?') }), new TypeError('Headers constructor: init["x-fake-header"] is a symbol, which cannot be converted to a ByteString.') ) diff --git a/test/fetch/headerslist-sortedarray.js b/test/fetch/headerslist-sortedarray.js index 9541901d2f2..ea7a330ee4e 100644 --- a/test/fetch/headerslist-sortedarray.js +++ b/test/fetch/headerslist-sortedarray.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { HeadersList, compareHeaderName } = require('../../lib/web/fetch/headers') const characters = 'abcdefghijklmnopqrstuvwxyz0123456789' @@ -17,22 +16,22 @@ function generateAsciiString (length) { const SORT_RUN = 4000 -test('toSortedArray (fast-path)', () => { +test('toSortedArray (fast-path)', (t) => { for (let i = 0; i < SORT_RUN; ++i) { const headersList = new HeadersList() for (let j = 0; j < 32; ++j) { headersList.append(generateAsciiString(4), generateAsciiString(4)) } - assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName)) + t.assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName)) } }) -test('toSortedArray (slow-path)', () => { +test('toSortedArray (slow-path)', (t) => { for (let i = 0; i < SORT_RUN; ++i) { const headersList = new HeadersList() for (let j = 0; j < 64; ++j) { headersList.append(generateAsciiString(4), generateAsciiString(4)) } - assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName)) + t.assert.deepStrictEqual(headersList.toSortedArray(), [...headersList].sort(compareHeaderName)) } }) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index f64756de788..6ee14aabd7a 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -1,14 +1,13 @@ 'use strict' const { createSecureServer } = require('node:http2') +const { createServer } = require('node:http') const { createReadStream, readFileSync } = require('node:fs') const { once } = require('node:events') -const { Blob } = require('node:buffer') const { Readable } = require('node:stream') const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { Client, fetch, Headers } = require('../..') @@ -17,7 +16,7 @@ const { closeClientAndServerAsPromise } = require('../utils/node-http') test('[Fetch] Issue#2311', async (t) => { const expectedBody = 'hello from client!' - const server = createSecureServer(pem, async (req, res) => { + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }), async (req, res) => { let body = '' req.setEncoding('utf8') @@ -34,7 +33,7 @@ test('[Fetch] Issue#2311', async (t) => { res.end(body) }) - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) server.listen() await once(server, 'listening') @@ -64,12 +63,12 @@ test('[Fetch] Issue#2311', async (t) => { t.after(closeClientAndServerAsPromise(client, server)) - strictEqual(responseBody, expectedBody) - strictEqual(response.headers.get('x-custom-h2'), 'foo') + t.assert.strictEqual(responseBody, expectedBody) + t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo') }) test('[Fetch] Simple GET with h2', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedRequestBody = 'hello h2!' server.on('stream', async (stream, headers) => { @@ -83,7 +82,7 @@ test('[Fetch] Simple GET with h2', async (t) => { stream.end(expectedRequestBody) }) - const { strictEqual, throws } = tspl(t, { plan: 5 }) + t.plan(5) server.listen() await once(server, 'listening') @@ -112,20 +111,20 @@ test('[Fetch] Simple GET with h2', async (t) => { t.after(closeClientAndServerAsPromise(client, server)) - strictEqual(responseBody, expectedRequestBody) - strictEqual(response.headers.get('x-method'), 'GET') - strictEqual(response.headers.get('x-custom-h2'), 'foo') + t.assert.strictEqual(responseBody, expectedRequestBody) + t.assert.strictEqual(response.headers.get('x-method'), 'GET') + t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo') // https://github.com/nodejs/undici/issues/2415 - throws(() => { + t.assert.throws(() => { response.headers.get(':status') }, TypeError) // See https://fetch.spec.whatwg.org/#concept-response-status-message - strictEqual(response.statusText, '') + t.assert.strictEqual(response.statusText, '') }) test('[Fetch] Should handle h2 request with body (string or buffer)', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello from client!' const expectedRequestBody = 'hello h2!' const requestBody = [] @@ -142,7 +141,7 @@ test('[Fetch] Should handle h2 request with body (string or buffer)', async (t) stream.end(expectedRequestBody) }) - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) server.listen() await once(server, 'listening') @@ -172,25 +171,25 @@ test('[Fetch] Should handle h2 request with body (string or buffer)', async (t) t.after(closeClientAndServerAsPromise(client, server)) - strictEqual(Buffer.concat(requestBody).toString('utf-8'), expectedBody) - strictEqual(responseBody, expectedRequestBody) + t.assert.strictEqual(Buffer.concat(requestBody).toString('utf-8'), expectedBody) + t.assert.strictEqual(responseBody, expectedRequestBody) }) // Skipping for now, there is something odd in the way the body is handled test( '[Fetch] Should handle h2 request with body (stream)', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = readFileSync(__filename, 'utf-8') const stream = createReadStream(__filename) const requestChunks = [] - const { strictEqual } = tspl(t, { plan: 8 }) + t.plan(8) server.on('stream', async (stream, headers) => { - strictEqual(headers[':method'], 'PUT') - strictEqual(headers[':path'], '/') - strictEqual(headers[':scheme'], 'https') + t.assert.strictEqual(headers[':method'], 'PUT') + t.assert.strictEqual(headers[':path'], '/') + t.assert.strictEqual(headers[':scheme'], 'https') stream.respond({ 'content-type': 'text/plain; charset=utf-8', @@ -234,27 +233,27 @@ test( const responseBody = await response.text() - strictEqual(response.status, 200) - strictEqual(response.headers.get('content-type'), 'text/plain; charset=utf-8') - strictEqual(response.headers.get('x-custom-h2'), 'foo') - strictEqual(responseBody, 'hello h2!') - strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.assert.strictEqual(response.status, 200) + t.assert.strictEqual(response.headers.get('content-type'), 'text/plain; charset=utf-8') + t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo') + t.assert.strictEqual(responseBody, 'hello h2!') + t.assert.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) } ) -test('Should handle h2 request with body (Blob)', { skip: !Blob }, async (t) => { - const server = createSecureServer(pem) +test('Should handle h2 request with body (Blob)', async (t) => { + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'asd' const requestChunks = [] const body = new Blob(['asd'], { type: 'text/plain' }) - const { strictEqual } = tspl(t, { plan: 8 }) + t.plan(8) server.on('stream', async (stream, headers) => { - strictEqual(headers[':method'], 'POST') - strictEqual(headers[':path'], '/') - strictEqual(headers[':scheme'], 'https') + t.assert.strictEqual(headers[':method'], 'POST') + t.assert.strictEqual(headers[':path'], '/') + t.assert.strictEqual(headers[':scheme'], 'https') stream.on('data', chunk => requestChunks.push(chunk)) @@ -295,18 +294,17 @@ test('Should handle h2 request with body (Blob)', { skip: !Blob }, async (t) => const responseBody = await response.arrayBuffer() - strictEqual(response.status, 200) - strictEqual(response.headers.get('content-type'), 'text/plain; charset=utf-8') - strictEqual(response.headers.get('x-custom-h2'), 'foo') - strictEqual(new TextDecoder().decode(responseBody).toString(), 'hello h2!') - strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.assert.strictEqual(response.status, 200) + t.assert.strictEqual(response.headers.get('content-type'), 'text/plain; charset=utf-8') + t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo') + t.assert.strictEqual(new TextDecoder().decode(responseBody).toString(), 'hello h2!') + t.assert.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) }) test( 'Should handle h2 request with body (Blob:ArrayBuffer)', - { skip: !Blob }, async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello' const requestChunks = [] const expectedResponseBody = { hello: 'h2' } @@ -315,12 +313,12 @@ test( buf.copy(new Uint8Array(body)) - const { strictEqual, deepStrictEqual } = tspl(t, { plan: 8 }) + t.plan(8) server.on('stream', async (stream, headers) => { - strictEqual(headers[':method'], 'PUT') - strictEqual(headers[':path'], '/') - strictEqual(headers[':scheme'], 'https') + t.assert.strictEqual(headers[':method'], 'PUT') + t.assert.strictEqual(headers[':path'], '/') + t.assert.strictEqual(headers[':scheme'], 'https') stream.on('data', chunk => requestChunks.push(chunk)) @@ -361,17 +359,17 @@ test( const responseBody = await response.json() - strictEqual(response.status, 200) - strictEqual(response.headers.get('content-type'), 'application/json') - strictEqual(response.headers.get('x-custom-h2'), 'foo') - deepStrictEqual(responseBody, expectedResponseBody) - strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.assert.strictEqual(response.status, 200) + t.assert.strictEqual(response.headers.get('content-type'), 'application/json') + t.assert.strictEqual(response.headers.get('x-custom-h2'), 'foo') + t.assert.deepStrictEqual(responseBody, expectedResponseBody) + t.assert.strictEqual(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) } ) test('Issue#2415', async (t) => { - const { doesNotThrow } = tspl(t, { plan: 1 }) - const server = createSecureServer(pem) + t.plan(1) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', async (stream, headers) => { stream.respond({ @@ -403,23 +401,23 @@ test('Issue#2415', async (t) => { t.after(closeClientAndServerAsPromise(client, server)) - doesNotThrow(() => new Headers(response.headers)) + t.assert.doesNotThrow(() => new Headers(response.headers)) }) test('Issue #2386', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const body = Buffer.from('hello') const requestChunks = [] const expectedResponseBody = { hello: 'h2' } const controller = new AbortController() const signal = controller.signal - const { strictEqual, ok } = tspl(t, { plan: 4 }) + t.plan(4) server.on('stream', async (stream, headers) => { - strictEqual(headers[':method'], 'PUT') - strictEqual(headers[':path'], '/') - strictEqual(headers[':scheme'], 'https') + t.assert.strictEqual(headers[':method'], 'PUT') + t.assert.strictEqual(headers[':path'], '/') + t.assert.strictEqual(headers[':scheme'], 'https') stream.on('data', chunk => requestChunks.push(chunk)) @@ -460,18 +458,18 @@ test('Issue #2386', async (t) => { ) controller.abort() - ok(true) + t.assert.ok(true) }) test('Issue #3046', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) - const { strictEqual, deepStrictEqual } = tspl(t, { plan: 6 }) + t.plan(6) server.on('stream', async (stream, headers) => { - strictEqual(headers[':method'], 'GET') - strictEqual(headers[':path'], '/') - strictEqual(headers[':scheme'], 'https') + t.assert.strictEqual(headers[':method'], 'GET') + t.assert.strictEqual(headers[':path'], '/') + t.assert.strictEqual(headers[':scheme'], 'https') stream.respond({ 'set-cookie': ['hello=world', 'foo=bar'], @@ -503,7 +501,79 @@ test('Issue #3046', async (t) => { } ) - strictEqual(response.status, 200) - strictEqual(response.headers.get('content-type'), 'text/html; charset=utf-8') - deepStrictEqual(response.headers.getSetCookie(), ['hello=world', 'foo=bar']) + t.assert.strictEqual(response.status, 200) + t.assert.strictEqual(response.headers.get('content-type'), 'text/html; charset=utf-8') + t.assert.deepStrictEqual(response.headers.getSetCookie(), ['hello=world', 'foo=bar']) +}) + +// The two following tests ensure that empty POST requests have a Content-Length of 0 +// specified, both with and without HTTP/2 enabled. +// The RFC 9110 (see https://httpwg.org/specs/rfc9110.html#field.content-length) +// states it SHOULD have one for methods like POST that define a meaning for enclosed content. +test('[Fetch] Empty POST without h2 has Content-Length', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.statusCode = 200 + res.end(`content-length:${req.headers['content-length']}`) + }).listen(0) + + const client = new Client(`http://localhost:${server.address().port}`) + + t.after(async () => { + server.close() + await client.close() + }) + + t.plan(1) + + await once(server, 'listening') + + const response = await fetch( + `http://localhost:${server.address().port}/`, { + method: 'POST', + dispatcher: client + } + ) + + const responseBody = await response.text() + t.assert.strictEqual(responseBody, `content-length:${0}`) +}) + +test('[Fetch] Empty POST with h2 has Content-Length', async (t) => { + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) + + server.on('stream', async (stream, headers) => { + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + ':status': 200 + }) + + stream.end(`content-length:${headers['content-length']}`) + }) + + t.plan(1) + + server.listen() + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.after(closeClientAndServerAsPromise(client, server)) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'POST', + dispatcher: client + } + ) + + const responseBody = await response.text() + + t.assert.strictEqual(responseBody, `content-length:${0}`) }) diff --git a/test/fetch/includes-credentials.js b/test/fetch/includes-credentials.js new file mode 100644 index 00000000000..3d8eb4c9278 --- /dev/null +++ b/test/fetch/includes-credentials.js @@ -0,0 +1,24 @@ +'use strict' + +const { test } = require('node:test') +const { includesCredentials } = require('../../lib/web/fetch/util') + +test('includesCredentials returns true for URL with both username and password', () => { + const url = new URL('http://user:pass@example.com') + require('node:assert').strictEqual(includesCredentials(url), true) +}) + +test('includesCredentials returns true for URL with only username', () => { + const url = new URL('http://user@example.com') + require('node:assert').strictEqual(includesCredentials(url), true) +}) + +test('includesCredentials returns true for URL with only password', () => { + const url = new URL('http://:pass@example.com') + require('node:assert').strictEqual(includesCredentials(url), true) +}) + +test('includesCredentials returns false for URL with no credentials', () => { + const url = new URL('http://example.com') + require('node:assert').strictEqual(includesCredentials(url), false) +}) diff --git a/test/fetch/integrity.js b/test/fetch/integrity.js index 5c34a87e20f..b01125b8ef6 100644 --- a/test/fetch/integrity.js +++ b/test/fetch/integrity.js @@ -1,25 +1,28 @@ 'use strict' const { test, after } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') -const assert = require('node:assert') const { createServer } = require('node:http') -const { createHash, getHashes } = require('node:crypto') const { gzipSync } = require('node:zlib') -const { fetch, setGlobalDispatcher, Agent } = require('../..') +const { fetch, setGlobalDispatcher, getGlobalDispatcher, Agent } = require('../..') const { once } = require('node:events') const { closeServerAsPromise } = require('../utils/node-http') +const { runtimeFeatures } = require('../../lib/util/runtime-features') -const supportedHashes = getHashes() - +const previousDispatcher = getGlobalDispatcher() setGlobalDispatcher(new Agent({ keepAliveTimeout: 1, keepAliveMaxTimeout: 1 })) -test('request with correct integrity checksum', (t, done) => { +after(() => { + setGlobalDispatcher(previousDispatcher) +}) + +const skip = runtimeFeatures.has('crypto') === false + +test('request with correct integrity checksum', { skip }, async (t) => { const body = 'Hello world!' - const hash = createHash('sha256').update(body).digest('base64') + const hash = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(body) @@ -27,16 +30,15 @@ test('request with correct integrity checksum', (t, done) => { t.after(closeServerAsPromise(server)) - server.listen(0, async () => { - const response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${hash}` - }) - assert.strictEqual(body, await response.text()) - done() + await once(server.listen(0), 'listening') + + const response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${hash}` }) + t.assert.strictEqual(body, await response.text()) }) -test('request with wrong integrity checksum', async (t) => { +test('request with wrong integrity checksum', { skip }, async (t) => { const body = 'Hello world!' const hash = 'c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51b' @@ -51,14 +53,14 @@ test('request with wrong integrity checksum', async (t) => { cause: new Error('integrity mismatch') }) - await assert.rejects(fetch(`http://localhost:${server.address().port}`, { + await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, { integrity: `sha256-${hash}` }), expectedError) }) -test('request with integrity checksum on encoded body', (t, done) => { +test('request with integrity checksum on encoded body', { skip }, async (t) => { const body = 'Hello world!' - const hash = createHash('sha256').update(body).digest('base64') + const hash = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.setHeader('content-encoding', 'gzip') @@ -67,16 +69,14 @@ test('request with integrity checksum on encoded body', (t, done) => { t.after(closeServerAsPromise(server)) - server.listen(0, async () => { - const response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${hash}` - }) - assert.strictEqual(body, await response.text()) - done() + await once(server.listen(0), 'listening') + const response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${hash}` }) + t.assert.strictEqual(body, await response.text()) }) -test('request with a totally incorrect integrity', async (t) => { +test('request with a totally incorrect integrity', { skip }, async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() }).listen(0) @@ -84,14 +84,14 @@ test('request with a totally incorrect integrity', async (t) => { t.after(closeServerAsPromise(server)) await once(server, 'listening') - await assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, { + await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, { integrity: 'what-integrityisthis' })) }) -test('request with mixed in/valid integrities', async (t) => { +test('request with mixed in/valid integrities', { skip }, async (t) => { const body = 'Hello world!' - const hash = createHash('sha256').update(body).digest('base64') + const hash = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(body) @@ -100,14 +100,14 @@ test('request with mixed in/valid integrities', async (t) => { t.after(closeServerAsPromise(server)) await once(server, 'listening') - await assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, { + await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, { integrity: `invalid-integrity sha256-${hash}` })) }) -test('request with sha384 hash', { skip: !supportedHashes.includes('sha384') }, async (t) => { +test('request with sha384 hash', { skip }, async (t) => { const body = 'Hello world!' - const hash = createHash('sha384').update(body).digest('base64') + const hash = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY/JV615lypu7hkHMz+' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(body) @@ -117,19 +117,19 @@ test('request with sha384 hash', { skip: !supportedHashes.includes('sha384') }, await once(server, 'listening') // request should succeed - await assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, { + await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, { integrity: `sha384-${hash}` })) // request should fail - await assert.rejects(fetch(`http://localhost:${server.address().port}`, { + await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, { integrity: 'sha384-ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=' })) }) -test('request with sha512 hash', { skip: !supportedHashes.includes('sha512') }, async (t) => { +test('request with sha512 hash', { skip }, async (t) => { const body = 'Hello world!' - const hash = createHash('sha512').update(body).digest('base64') + const hash = '9s3ioPgZMUzd5V/CJ9jX2uPSjMVWIioKitZtkcytSq1glPUXohgjYMmqz2o9wyMWLLb9jN/+2w/gOPVehf+1tg==' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(body) @@ -139,20 +139,37 @@ test('request with sha512 hash', { skip: !supportedHashes.includes('sha512') }, await once(server, 'listening') // request should succeed - await assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, { + await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}`, { integrity: `sha512-${hash}` })) // request should fail - await assert.rejects(fetch(`http://localhost:${server.address().port}`, { + await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, { integrity: 'sha512-ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=' })) }) -test('request with correct integrity checksum (base64url)', async (t) => { - t = tspl(t, { plan: 1 }) +test('request with sha512 hash', { skip }, async (t) => { + const body = 'Hello world!' + const hash384 = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY/JV615lypu7hkHMz+' + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.end(body) + }).listen(0) + + t.after(closeServerAsPromise(server)) + await once(server, 'listening') + + // request should fail + await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, { + integrity: `sha512-${hash384} sha384-${hash384}` + })) +}) + +test('request with correct integrity checksum (base64url)', { skip }, async (t) => { + t.plan(1) const body = 'Hello world!' - const hash = createHash('sha256').update(body).digest('base64url') + const hash = 'wFNeS-K3n_2TKRMFQ2v4iTFOSj-uwF7P_Lt98xrZ5Ro' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(body) @@ -160,21 +177,19 @@ test('request with correct integrity checksum (base64url)', async (t) => { after(closeServerAsPromise(server)) - server.listen(0, async () => { - const response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${hash}` - }) - t.strictEqual(body, await response.text()) + await once(server.listen(0), 'listening') + const response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${hash}` }) - - await t.completed + t.assert.strictEqual(body, await response.text()) }) -test('request with incorrect integrity checksum (base64url)', async (t) => { - t = tspl(t, { plan: 1 }) +test('request with incorrect integrity checksum (base64url)', { skip }, async (t) => { + t.plan(1) const body = 'Hello world!' - const hash = createHash('sha256').update('invalid').digest('base64url') + // base64url for 'invalid' sha256 + const hash = '8SNNdReNiSoTOkEDVaWpkM910vM-uiXVdZQ9TfYy86Q' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(body) @@ -182,17 +197,14 @@ test('request with incorrect integrity checksum (base64url)', async (t) => { after(closeServerAsPromise(server)) - server.listen(0, async () => { - await t.rejects(fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${hash}` - })) - }) - - await t.completed + await once(server.listen(0), 'listening') + await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${hash}` + })) }) -test('request with incorrect integrity checksum (only dash)', async (t) => { - t = tspl(t, { plan: 1 }) +test('request with incorrect integrity checksum (only dash)', { skip }, async (t) => { + t.plan(1) const body = 'Hello world!' @@ -202,17 +214,14 @@ test('request with incorrect integrity checksum (only dash)', async (t) => { after(closeServerAsPromise(server)) - server.listen(0, async () => { - await t.rejects(fetch(`http://localhost:${server.address().port}`, { - integrity: 'sha256--' - })) - }) - - await t.completed + await once(server.listen(0), 'listening') + await t.assert.rejects(fetch(`http://localhost:${server.address().port}`, { + integrity: 'sha256--' + })) }) -test('request with incorrect integrity checksum (non-ascii character)', async (t) => { - t = tspl(t, { plan: 1 }) +test('request with incorrect integrity checksum (non-ascii character)', { skip }, async (t) => { + t.plan(1) const body = 'Hello world!' @@ -222,20 +231,17 @@ test('request with incorrect integrity checksum (non-ascii character)', async (t after(closeServerAsPromise(server)) - server.listen(0, async () => { - await t.rejects(() => fetch(`http://localhost:${server.address().port}`, { - integrity: 'sha256-ä' - })) - }) - - await t.completed + await once(server.listen(0), 'listening') + await t.assert.rejects(() => fetch(`http://localhost:${server.address().port}`, { + integrity: 'sha256-ä' + })) }) -test('request with incorrect stronger integrity checksum (non-ascii character)', async (t) => { - t = tspl(t, { plan: 2 }) +test('request with incorrect stronger integrity checksum (non-ascii character)', { skip }, async (t) => { + t.plan(2) const body = 'Hello world!' - const sha256 = createHash('sha256').update(body).digest('base64') + const sha256 = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=' const sha384 = 'ä' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { @@ -244,24 +250,21 @@ test('request with incorrect stronger integrity checksum (non-ascii character)', after(closeServerAsPromise(server)) - server.listen(0, async () => { - await t.rejects(() => fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${sha256} sha384-${sha384}` - })) - await t.rejects(() => fetch(`http://localhost:${server.address().port}`, { - integrity: `sha384-${sha384} sha256-${sha256}` - })) - }) - - await t.completed + await once(server.listen(0), 'listening') + await t.assert.rejects(() => fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha384-${sha384}` + })) + await t.assert.rejects(() => fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha256-${sha256}` + })) }) -test('request with correct integrity checksum (base64). mixed', async (t) => { - t = tspl(t, { plan: 6 }) +test('request with correct integrity checksum (base64). mixed', { skip }, async (t) => { + t.plan(6) const body = 'Hello world!' - const sha256 = createHash('sha256').update(body).digest('base64') - const sha384 = createHash('sha384').update(body).digest('base64') - const sha512 = createHash('sha512').update(body).digest('base64') + const sha256 = 'wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=' + const sha384 = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY/JV615lypu7hkHMz+' + const sha512 = '9s3ioPgZMUzd5V/CJ9jX2uPSjMVWIioKitZtkcytSq1glPUXohgjYMmqz2o9wyMWLLb9jN/+2w/gOPVehf+1tg==' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(body) @@ -269,45 +272,42 @@ test('request with correct integrity checksum (base64). mixed', async (t) => { after(closeServerAsPromise(server)) - server.listen(0, async () => { - let response - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${sha256} sha512-${sha512}` - }) - t.strictEqual(body, await response.text()) - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha512-${sha512} sha256-${sha256}` - }) - - t.strictEqual(body, await response.text()) - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha384-${sha384} sha512-${sha512}` - }) - t.strictEqual(body, await response.text()) - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha384-${sha384} sha512-${sha512}` - }) - t.strictEqual(body, await response.text()) - - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${sha256} sha384-${sha384}` - }) - t.strictEqual(body, await response.text()) - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha384-${sha384} sha256-${sha256}` - }) - t.strictEqual(body, await response.text()) + await once(server.listen(0), 'listening') + let response + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha512-${sha512}` }) + t.assert.strictEqual(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha512-${sha512} sha256-${sha256}` + }) + + t.assert.strictEqual(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha512-${sha512}` + }) + t.assert.strictEqual(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha512-${sha512}` + }) + t.assert.strictEqual(body, await response.text()) - await t.completed + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha384-${sha384}` + }) + t.assert.strictEqual(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha256-${sha256}` + }) + t.assert.strictEqual(body, await response.text()) }) -test('request with correct integrity checksum (base64url). mixed', async (t) => { - t = tspl(t, { plan: 6 }) +test('request with correct integrity checksum (base64url). mixed', { skip }, async (t) => { + t.plan(6) const body = 'Hello world!' - const sha256 = createHash('sha256').update(body).digest('base64url') - const sha384 = createHash('sha384').update(body).digest('base64url') - const sha512 = createHash('sha512').update(body).digest('base64url') + const sha256 = 'wFNeS-K3n_2TKRMFQ2v4iTFOSj-uwF7P_Lt98xrZ5Ro' + const sha384 = 'hiVfosNuSzCWnq4X3DTHcsvr38WLWEA5AL6HYU6xo0uHgCY_JV615lypu7hkHMz-' + const sha512 = '9s3ioPgZMUzd5V_CJ9jX2uPSjMVWIioKitZtkcytSq1glPUXohgjYMmqz2o9wyMWLLb9jN_-2w_gOPVehf-1tg' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end(body) @@ -315,35 +315,32 @@ test('request with correct integrity checksum (base64url). mixed', async (t) => after(closeServerAsPromise(server)) - server.listen(0, async () => { - let response - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${sha256} sha512-${sha512}` - }) - t.strictEqual(body, await response.text()) - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha512-${sha512} sha256-${sha256}` - }) - - t.strictEqual(body, await response.text()) - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha384-${sha384} sha512-${sha512}` - }) - t.strictEqual(body, await response.text()) - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha384-${sha384} sha512-${sha512}` - }) - t.strictEqual(body, await response.text()) - - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha256-${sha256} sha384-${sha384}` - }) - t.strictEqual(body, await response.text()) - response = await fetch(`http://localhost:${server.address().port}`, { - integrity: `sha384-${sha384} sha256-${sha256}` - }) - t.strictEqual(body, await response.text()) + await once(server.listen(0), 'listening') + let response + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha512-${sha512}` }) + t.assert.strictEqual(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha512-${sha512} sha256-${sha256}` + }) + + t.assert.strictEqual(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha512-${sha512}` + }) + t.assert.strictEqual(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha512-${sha512}` + }) + t.assert.strictEqual(body, await response.text()) - await t.completed + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha384-${sha384}` + }) + t.assert.strictEqual(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha256-${sha256}` + }) + t.assert.strictEqual(body, await response.text()) }) diff --git a/test/fetch/issue-1447.js b/test/fetch/issue-1447.js index dc49113665b..55b704b720f 100644 --- a/test/fetch/issue-1447.js +++ b/test/fetch/issue-1447.js @@ -1,26 +1,29 @@ 'use strict' const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') const undici = require('../..') const { fetch: theoreticalGlobalFetch } = require('../../undici-fetch') test('Mocking works with both fetches', async (t) => { - const { strictEqual } = tspl(t, { plan: 3 }) + t.plan(3) const mockAgent = new undici.MockAgent() const body = JSON.stringify({ foo: 'bar' }) mockAgent.disableNetConnect() + const previousDispatcher = undici.getGlobalDispatcher() undici.setGlobalDispatcher(mockAgent) + t.after(() => { + undici.setGlobalDispatcher(previousDispatcher) + }) const pool = mockAgent.get('https://example.com') pool.intercept({ path: '/path', method: 'POST', body (bodyString) { - strictEqual(bodyString, body) + t.assert.strictEqual(bodyString, body) return true } }).reply(200, { ok: 1 }).times(2) diff --git a/test/fetch/issue-1711.js b/test/fetch/issue-1711.js index ca78897ae5f..5abe64c849d 100644 --- a/test/fetch/issue-1711.js +++ b/test/fetch/issue-1711.js @@ -1,6 +1,5 @@ 'use strict' -const assert = require('node:assert') const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') @@ -24,12 +23,12 @@ test('Redirecting a bunch does not cause a MaxListenersExceededWarning', async ( t.after(server.close.bind(server)) await once(server, 'listening') - process.emitWarning = assert.bind(null, false) + process.emitWarning = t.assert.fail.bind(t) const url = `http://localhost:${server.address().port}` const response = await fetch(url, { redirect: 'follow' }) - assert.deepStrictEqual(response.url, `${url}/${redirects - 1}`) + t.assert.deepStrictEqual(response.url, `${url}/${redirects - 1}`) }) test( diff --git a/test/fetch/issue-2009.js b/test/fetch/issue-2009.js index 14432e81cb2..4261b7134ae 100644 --- a/test/fetch/issue-2009.js +++ b/test/fetch/issue-2009.js @@ -1,14 +1,13 @@ 'use strict' const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') const { fetch } = require('../..') const { createServer } = require('node:http') const { once } = require('node:events') const { closeServerAsPromise } = require('../utils/node-http') test('issue 2009', async (t) => { - const { doesNotReject } = tspl(t, { plan: 10 }) + t.plan(10) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.setHeader('a', 'b') @@ -21,7 +20,7 @@ test('issue 2009', async (t) => { await once(server, 'listening') for (let i = 0; i < 10; i++) { - await doesNotReject( + await t.assert.doesNotReject( fetch(`http://localhost:${server.address().port}`).then( async (resp) => { await resp.body.cancel('Some message') diff --git a/test/fetch/issue-2021.js b/test/fetch/issue-2021.js index b88db725dbe..bd605219d3e 100644 --- a/test/fetch/issue-2021.js +++ b/test/fetch/issue-2021.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { once } = require('node:events') const { createServer } = require('node:http') const { fetch } = require('../..') @@ -24,7 +23,7 @@ test('content-length header is removed on redirect', async (t) => { const body = 'a+b+c' - await assert.doesNotReject(fetch(`http://localhost:${server.address().port}/redirect`, { + await t.assert.doesNotReject(fetch(`http://localhost:${server.address().port}/redirect`, { method: 'POST', body, headers: { diff --git a/test/fetch/issue-2171.js b/test/fetch/issue-2171.js index ba0c82674cf..a8d9fbbef8b 100644 --- a/test/fetch/issue-2171.js +++ b/test/fetch/issue-2171.js @@ -4,17 +4,16 @@ const { fetch } = require('../..') const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') -const assert = require('node:assert') const { closeServerAsPromise } = require('../utils/node-http') -test('error reason is forwarded - issue #2171', { skip: !AbortSignal.timeout }, async (t) => { +test('error reason is forwarded - issue #2171', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, () => {}).listen(0) t.after(closeServerAsPromise(server)) await once(server, 'listening') const timeout = AbortSignal.timeout(100) - await assert.rejects( + await t.assert.rejects( fetch(`http://localhost:${server.address().port}`, { signal: timeout }), diff --git a/test/fetch/issue-2242.js b/test/fetch/issue-2242.js index 1c0fe1f4303..ede276f72ba 100644 --- a/test/fetch/issue-2242.js +++ b/test/fetch/issue-2242.js @@ -1,7 +1,6 @@ 'use strict' const { beforeEach, describe, it } = require('node:test') -const assert = require('node:assert') const { fetch } = require('../..') const nodeFetch = require('../../index-fetch') @@ -15,17 +14,17 @@ describe('Issue #2242', () => { beforeEach(() => { signal = AbortSignal.abort(reason) }) - it('rejects with that reason ', async () => { - await assert.rejects(fetch('http://localhost', { signal }), (err) => { - assert.strictEqual(err, reason) + it('rejects with that reason ', async (t) => { + await t.assert.rejects(fetch('http://localhost', { signal }), (err) => { + t.assert.strictEqual(err, reason) return true }) }) - it('rejects with that reason (from index-fetch)', async () => { - await assert.rejects( + it('rejects with that reason (from index-fetch)', async (t) => { + await t.assert.rejects( nodeFetch.fetch('http://localhost', { signal }), (err) => { - assert.strictEqual(err, reason) + t.assert.strictEqual(err, reason) return true } ) @@ -38,14 +37,14 @@ describe('Issue #2242', () => { beforeEach(() => { signal = AbortSignal.abort(undefined) }) - it('rejects with an `AbortError`', async () => { - await assert.rejects( + it('rejects with an `AbortError`', async (t) => { + await t.assert.rejects( fetch('http://localhost', { signal }), new DOMException('This operation was aborted', 'AbortError') ) }) - it('rejects with an `AbortError` (from index-fetch)', async () => { - await assert.rejects( + it('rejects with an `AbortError` (from index-fetch)', async (t) => { + await t.assert.rejects( nodeFetch.fetch('http://localhost', { signal }), new DOMException('This operation was aborted', 'AbortError') ) diff --git a/test/fetch/issue-2294-patch-method.js b/test/fetch/issue-2294-patch-method.js index 6223989c07c..868737d83ce 100644 --- a/test/fetch/issue-2294-patch-method.js +++ b/test/fetch/issue-2294-patch-method.js @@ -1,11 +1,10 @@ 'use strict' -const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { Request } = require('../..') test('Using `patch` method emits a warning.', (t) => { - t = tspl(t, { plan: 1 }) + t.plan(1) const { emitWarning } = process @@ -14,7 +13,7 @@ test('Using `patch` method emits a warning.', (t) => { }) process.emitWarning = (warning, options) => { - t.strictEqual(options.code, 'UNDICI-FETCH-patch') + t.assert.strictEqual(options.code, 'UNDICI-FETCH-patch') } // eslint-disable-next-line no-new diff --git a/test/fetch/issue-2318.js b/test/fetch/issue-2318.js index fe27ac7ec35..8d5f17c7451 100644 --- a/test/fetch/issue-2318.js +++ b/test/fetch/issue-2318.js @@ -1,17 +1,16 @@ 'use strict' const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') const { once } = require('node:events') const { createServer } = require('node:http') const { fetch } = require('../..') const { closeServerAsPromise } = require('../utils/node-http') test('Undici overrides user-provided `Host` header', async (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - strictEqual(req.headers.host, `localhost:${server.address().port}`) + t.assert.strictEqual(req.headers.host, `localhost:${server.address().port}`) res.end() }).listen(0) diff --git a/test/fetch/issue-2828.js b/test/fetch/issue-2828.js index 98fd375b8e9..dc918cd7f97 100644 --- a/test/fetch/issue-2828.js +++ b/test/fetch/issue-2828.js @@ -4,10 +4,9 @@ const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') const { Agent, Request, fetch } = require('../..') -const { tspl } = require('@matteo.collina/tspl') test('issue #2828, dispatcher is allowed in RequestInit options', async (t) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) class CustomAgent extends Agent { dispatch (options, handler) { @@ -17,7 +16,7 @@ test('issue #2828, dispatcher is allowed in RequestInit options', async (t) => { } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - deepStrictEqual(req.headers['x-my-header'], 'hello') + t.assert.deepStrictEqual(req.headers['x-my-header'], 'hello') res.end() }).listen(0) diff --git a/test/fetch/issue-2898-comment.js b/test/fetch/issue-2898-comment.js index d1d326b2c35..d3532d336d4 100644 --- a/test/fetch/issue-2898-comment.js +++ b/test/fetch/issue-2898-comment.js @@ -4,10 +4,9 @@ const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') const { Agent, Request, fetch } = require('../..') -const { tspl } = require('@matteo.collina/tspl') test('issue #2828, RequestInit dispatcher options overrides Request input dispatcher', async (t) => { - const { strictEqual } = tspl(t, { plan: 2 }) + t.plan(2) class CustomAgentA extends Agent { dispatch (options, handler) { @@ -24,8 +23,8 @@ test('issue #2828, RequestInit dispatcher options overrides Request input dispat } const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - strictEqual(req.headers['x-my-header-a'], undefined) - strictEqual(req.headers['x-my-header-b'], 'world') + t.assert.strictEqual(req.headers['x-my-header-a'], undefined) + t.assert.strictEqual(req.headers['x-my-header-b'], 'world') res.end() }).listen(0) diff --git a/test/fetch/issue-2898.js b/test/fetch/issue-2898.js index d6188ab696a..a5f43c71d77 100644 --- a/test/fetch/issue-2898.js +++ b/test/fetch/issue-2898.js @@ -1,6 +1,5 @@ 'use strict' -const assert = require('node:assert') const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') @@ -27,7 +26,7 @@ test('421 requests with a body work as expected', async (t) => { body }) - assert.deepStrictEqual(response.status, 421) - assert.deepStrictEqual(await response.text(), expected) + t.assert.deepStrictEqual(response.status, 421) + t.assert.deepStrictEqual(await response.text(), expected) } }) diff --git a/test/fetch/issue-3267.js b/test/fetch/issue-3267.js index 8b7515e62af..94814ea8a66 100644 --- a/test/fetch/issue-3267.js +++ b/test/fetch/issue-3267.js @@ -2,7 +2,6 @@ const { Headers } = require('../..') const { test } = require('node:test') -const assert = require('node:assert') test('Spreading a Headers object yields 0 symbols', (t) => { const baseHeaders = { 'x-foo': 'bar' } @@ -13,6 +12,6 @@ test('Spreading a Headers object yields 0 symbols', (t) => { ...requestHeaders } - assert.deepStrictEqual(headers, { 'x-foo': 'bar' }) - assert.doesNotThrow(() => new Headers(headers)) + t.assert.deepStrictEqual(headers, { 'x-foo': 'bar' }) + t.assert.doesNotThrow(() => new Headers(headers)) }) diff --git a/test/fetch/issue-3334.js b/test/fetch/issue-3334.js index 1fcfd0e86c7..cc0e3a12783 100644 --- a/test/fetch/issue-3334.js +++ b/test/fetch/issue-3334.js @@ -1,17 +1,16 @@ 'use strict' const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') const { once } = require('node:events') const { createServer } = require('node:http') const { fetch } = require('../..') test('a non-empty origin is not appended (issue #3334)', async (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const origin = 'https://origin.example.com' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - strictEqual(req.headers.origin, origin) + t.assert.strictEqual(req.headers.origin, origin) res.end() }).listen(0) diff --git a/test/fetch/issue-3616.js b/test/fetch/issue-3616.js index 2de5c63672d..81e39532722 100644 --- a/test/fetch/issue-3616.js +++ b/test/fetch/issue-3616.js @@ -1,7 +1,6 @@ 'use strict' const { createServer } = require('node:http') -const { tspl } = require('@matteo.collina/tspl') const { describe, test, after } = require('node:test') const { fetch } = require('../..') const { once } = require('node:events') @@ -16,7 +15,7 @@ describe('https://github.com/nodejs/undici/issues/3616', () => { for (const encoding of cases) { test(encoding, async t => { - t = tspl(t, { plan: 2 }) + t.plan(2) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, { 'Content-Length': '0', @@ -35,14 +34,14 @@ describe('https://github.com/nodejs/undici/issues/3616', () => { await once(server, 'listening') const result = await fetch(`http://localhost:${server.address().port}/`) - t.ok(result.body.getReader()) + t.assert.ok(result.body.getReader()) process.on('uncaughtException', (reason) => { - t.fail('Uncaught Exception:', reason, encoding) + t.assert.fail('Uncaught Exception:', reason, encoding) }) await new Promise(resolve => setTimeout(resolve, 100)) - t.ok(true) + t.assert.ok(true) }) } }) diff --git a/test/fetch/issue-3624.js b/test/fetch/issue-3624.js index 487fed7b210..4b5a6e35311 100644 --- a/test/fetch/issue-3624.js +++ b/test/fetch/issue-3624.js @@ -1,7 +1,5 @@ 'use strict' -const assert = require('node:assert') -const { File } = require('node:buffer') const { test } = require('node:test') const { once } = require('node:events') const { createServer } = require('node:http') @@ -25,5 +23,5 @@ test('crlf is appended to formdata body (issue #3624)', async (t) => { method: 'POST' }) - assert((await response.text()).endsWith('\r\n')) + t.assert.ok((await response.text()).endsWith('\r\n')) }) diff --git a/test/fetch/issue-3630.js b/test/fetch/issue-3630.js index d40a5c64d99..029e14992a9 100644 --- a/test/fetch/issue-3630.js +++ b/test/fetch/issue-3630.js @@ -1,12 +1,11 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { Request, Agent } = require('../..') const { getRequestDispatcher } = require('../../lib/web/fetch/request') -test('Cloned request should inherit its dispatcher', () => { +test('Cloned request should inherit its dispatcher', (t) => { const agent = new Agent() const request = new Request('https://a', { dispatcher: agent }) - assert.strictEqual(getRequestDispatcher(request), agent) + t.assert.strictEqual(getRequestDispatcher(request), agent) }) diff --git a/test/fetch/issue-3767.js b/test/fetch/issue-3767.js index aa742aaffe6..b3223298f4d 100644 --- a/test/fetch/issue-3767.js +++ b/test/fetch/issue-3767.js @@ -4,16 +4,15 @@ const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') const { fetch } = require('../..') -const { tspl } = require('@matteo.collina/tspl') // https://github.com/nodejs/undici/issues/3767 test('referrerPolicy unsafe-url is respected', async (t) => { - const { completed, deepEqual } = tspl(t, { plan: 1 }) + t.plan(1) const referrer = 'https://google.com/hello/world' const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - deepEqual(req.headers.referer, referrer) + t.assert.deepEqual(req.headers.referer, referrer) res.end() }).listen(0) @@ -25,6 +24,4 @@ test('referrerPolicy unsafe-url is respected', async (t) => { referrer, referrerPolicy: 'unsafe-url' }) - - await completed }) diff --git a/test/fetch/issue-4105.js b/test/fetch/issue-4105.js index 8d4b480eca8..f9ac5001cd3 100644 --- a/test/fetch/issue-4105.js +++ b/test/fetch/issue-4105.js @@ -4,31 +4,34 @@ const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') const { fetch } = require('../..') -const { tspl } = require('@matteo.collina/tspl') const { PerformanceObserver } = require('node:perf_hooks') +const { createDeferredPromise } = require('../../lib/util/promise') const isAtLeastv22 = process.versions.node.split('.').map(Number)[0] >= 22 // https://github.com/nodejs/undici/issues/4105 test('markResourceTiming responseStatus is set', { skip: !isAtLeastv22 }, async (t) => { - const { completed, deepEqual } = tspl(t, { plan: 1 }) + t.plan(1) + + const promise = createDeferredPromise() const server = createServer((req, res) => { res.statusCode = 200 res.end('Hello World') - }).listen(3000) + }).listen(0) t.after(server.close.bind(server)) await once(server, 'listening') new PerformanceObserver(items => { items.getEntries().forEach(entry => { - deepEqual(entry.responseStatus, 200) + t.assert.strictEqual(entry.responseStatus, 200) + promise.resolve() }) }).observe({ type: 'resource', buffered: true }) - const response = await fetch('http://localhost:3000') + const response = await fetch(`http://localhost:${server.address().port}`) await response.text() - await completed + await promise.promise }) diff --git a/test/fetch/issue-4627.js b/test/fetch/issue-4627.js new file mode 100644 index 00000000000..7f3ac4ea1bf --- /dev/null +++ b/test/fetch/issue-4627.js @@ -0,0 +1,187 @@ +'use strict' + +// Regression test for https://github.com/nodejs/undici/issues/4627 +// Fetch abort may not take effect when fetch init.redirect = 'error' +// causing SSE connection leak + +const { test } = require('node:test') +const { fetch } = require('../..') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { closeServerAsPromise } = require('../utils/node-http') + +// This test requires --expose-gc flag +const hasGC = typeof global.gc === 'function' + +test('abort should work with redirect: error', { skip: !hasGC, timeout: 3000 }, async (t) => { + let connectionClosed = false + let messagesReceivedAfterAbort = 0 + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + }) + + // Send data every 20ms for faster test + const interval = setInterval(() => { + res.write(`data: ${Date.now()}\n\n`) + }, 20) + + res.on('close', () => { + connectionClosed = true + clearInterval(interval) + }) + }) + + t.after(closeServerAsPromise(server)) + await once(server.listen(0), 'listening') + const port = server.address().port + + const ac = new AbortController() + + const response = await fetch(`http://localhost:${port}/sse`, { + signal: ac.signal, + redirect: 'error' + }) + + let aborted = false + + // Start reading the stream in background + const readPromise = (async () => { + try { + const reader = response.body.getReader() + while (true) { + const { done } = await reader.read() + if (done) break + + if (aborted) { + messagesReceivedAfterAbort++ + if (messagesReceivedAfterAbort >= 3) { + // Bug confirmed - received multiple messages after abort + reader.cancel() + break + } + } + } + } catch (err) { + // AbortError is expected + if (err.name !== 'AbortError' && err.message !== 'aborted' && !err.message?.includes('cancel')) { + throw err + } + } + })() + + // Wait for some data to be received + await new Promise(resolve => setTimeout(resolve, 100)) + + // Trigger GC to potentially collect the AbortController + global.gc() + + // Abort the request + aborted = true + ac.abort() + + // Wait for the read to complete or timeout + const timeout = new Promise((_resolve, reject) => + setTimeout(() => reject(new Error('Read did not complete in time')), 1000) + ) + + try { + await Promise.race([readPromise, timeout]) + } catch (e) { + // If timed out, that's also a bug indication + if (e.message === 'Read did not complete in time') { + messagesReceivedAfterAbort = 999 // Force failure + } else { + throw e + } + } + + t.assert.strictEqual(messagesReceivedAfterAbort, 0, 'No data should be received after abort') + + // Give some time for the connection to close + await new Promise(resolve => setTimeout(resolve, 100)) + + t.assert.ok(connectionClosed, 'Server connection should be closed after abort') +}) + +test('abort should work without redirect: error (control test)', { skip: !hasGC, timeout: 3000 }, async (t) => { + let connectionClosed = false + let messagesReceivedAfterAbort = 0 + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + }) + + // Send data every 20ms + const interval = setInterval(() => { + res.write(`data: ${Date.now()}\n\n`) + }, 20) + + res.on('close', () => { + connectionClosed = true + clearInterval(interval) + }) + }) + + t.after(closeServerAsPromise(server)) + await once(server.listen(0), 'listening') + const port = server.address().port + + const ac = new AbortController() + + // Without redirect: 'error' - this should work correctly + const response = await fetch(`http://localhost:${port}/sse`, { + signal: ac.signal + }) + + let aborted = false + + // Start reading the stream in background + const readPromise = (async () => { + try { + const reader = response.body.getReader() + while (true) { + const { done } = await reader.read() + if (done) break + + if (aborted) { + messagesReceivedAfterAbort++ + if (messagesReceivedAfterAbort >= 3) { + reader.cancel() + break + } + } + } + } catch (err) { + // AbortError is expected + if (err.name !== 'AbortError' && err.message !== 'aborted' && !err.message?.includes('cancel')) { + throw err + } + } + })() + + // Wait for some data to be received + await new Promise(resolve => setTimeout(resolve, 100)) + + // Trigger GC + global.gc() + + // Abort the request + aborted = true + ac.abort() + + // Wait for the read to complete + await readPromise + + // Give some time for the connection to close + await new Promise(resolve => setTimeout(resolve, 100)) + + t.assert.strictEqual(messagesReceivedAfterAbort, 0, 'No data should be received after abort') + t.assert.ok(connectionClosed, 'Server connection should be closed after abort') +}) diff --git a/test/fetch/issue-4647.js b/test/fetch/issue-4647.js new file mode 100644 index 00000000000..91b3c9406ce --- /dev/null +++ b/test/fetch/issue-4647.js @@ -0,0 +1,23 @@ +'use strict' + +const { createServer } = require('node:http') +const { test } = require('node:test') +const { fetch } = require('../..') + +// https://github.com/nodejs/undici/issues/4647 +test('fetch with mode: no-cors does not hang', async (t) => { + const a = createServer((req, res) => { + res.writeHead(200).end() + }).listen(0) + + const b = createServer((req, res) => { + res.writeHead(301, { Location: `http://localhost:${a.address().port}${req.url}` }).end() + }).listen(0) + + t.after(() => { + a.close() + b.close() + }) + + await fetch(`http://localhost:${b.address().port}/abc`, { mode: 'no-cors' }) +}) diff --git a/test/fetch/issue-4789.js b/test/fetch/issue-4789.js new file mode 100644 index 00000000000..1ae6a6f6f47 --- /dev/null +++ b/test/fetch/issue-4789.js @@ -0,0 +1,56 @@ +'use strict' + +const { createServer } = require('node:http') +const { test } = require('node:test') +const { once } = require('node:events') +const { fetch } = require('../..') + +// https://github.com/nodejs/undici/issues/4789 +test('transferred buffers and extractBody works', { skip: !ArrayBuffer.prototype.transfer }, async (t) => { + const server = createServer((req, res) => { + if (req.url === '/') { + res.writeHead(307, undefined, { + location: '/test' + }) + res.end() + return + } + + req.pipe(res).on('end', res.end.bind(res)) + }).listen(0) + + t.after(server.close.bind(server)) + await once(server, 'listening') + + { + const response = await fetch(`http://localhost:${server.address().port}`, { + method: 'POST', + body: new TextEncoder().encode('test') + }) + + t.assert.strictEqual(await response.text(), 'test') + } + + { + const response = await fetch(`http://localhost:${server.address().port}`, { + method: 'POST', + body: Buffer.from('test') + }) + + t.assert.strictEqual(await response.text(), 'test') + } + + { + const buffer = new TextEncoder().encode('test') + buffer.buffer.transfer() + + const response = await fetch(`http://localhost:${server.address().port}`, { + method: 'POST', + body: buffer + }) + + // https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy + // "If IsDetachedBuffer(jsArrayBuffer) is true, then return the empty byte sequence." + t.assert.strictEqual(await response.text(), '') + } +}) diff --git a/test/fetch/issue-4799.js b/test/fetch/issue-4799.js new file mode 100644 index 00000000000..3e59df0030d --- /dev/null +++ b/test/fetch/issue-4799.js @@ -0,0 +1,109 @@ +'use strict' + +const { test } = require('node:test') +const { fetch } = require('../..') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { closeServerAsPromise } = require('../utils/node-http') + +test('response clone + abort should return AbortError, not TypeError', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ message: 'hello' })) + }) + + t.after(closeServerAsPromise(server)) + server.listen(0) + await once(server, 'listening') + + const controller = new AbortController() + const response = await fetch(`http://localhost:${server.address().port}`, { + signal: controller.signal + }) + + // Clone the response before aborting + const clonedResponse = response.clone() + + // Abort after cloning + controller.abort() + + // Both original and cloned response should reject with AbortError + await t.test('original response should reject with AbortError', async () => { + await t.assert.rejects( + response.text(), + { + name: 'AbortError', + code: DOMException.ABORT_ERR + } + ) + }) + + await t.test('cloned response should reject with AbortError', async () => { + await t.assert.rejects( + clonedResponse.text(), + { + name: 'AbortError', + code: DOMException.ABORT_ERR + } + ) + }) +}) + +test('response without clone + abort should still return AbortError', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ message: 'hello' })) + }) + + t.after(closeServerAsPromise(server)) + server.listen(0) + await once(server, 'listening') + + const controller = new AbortController() + const response = await fetch(`http://localhost:${server.address().port}`, { + signal: controller.signal + }) + + // Abort without cloning + controller.abort() + + await t.assert.rejects( + response.text(), + { + name: 'AbortError', + code: DOMException.ABORT_ERR + } + ) +}) + +test('response bodyUsed after clone and abort', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ message: 'hello' })) + }) + + t.after(closeServerAsPromise(server)) + server.listen(0) + await once(server, 'listening') + + const controller = new AbortController() + const response = await fetch(`http://localhost:${server.address().port}`, { + signal: controller.signal + }) + + t.assert.strictEqual(response.bodyUsed, false) + + const clonedResponse = response.clone() + + t.assert.strictEqual(response.bodyUsed, false) + t.assert.strictEqual(clonedResponse.bodyUsed, false) + + controller.abort() + + // Erroring a stream (see: https://fetch.spec.whatwg.org/#abort-fetch step 5) does not disturb it. + t.assert.strictEqual(response.bodyUsed, false) + t.assert.strictEqual(clonedResponse.bodyUsed, false) +}) diff --git a/test/fetch/issue-4836.js b/test/fetch/issue-4836.js new file mode 100644 index 00000000000..58142931fa6 --- /dev/null +++ b/test/fetch/issue-4836.js @@ -0,0 +1,37 @@ +'use strict' + +const { test } = require('node:test') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { fetch } = require('../..') +const { closeServerAsPromise } = require('../utils/node-http') + +// https://github.com/nodejs/undici/issues/4836 +test('fetch preserves trailing ? in request URL', async (t) => { + const server = createServer((req, res) => { + res.end(req.url) + }).listen(0) + + await once(server, 'listening') + t.after(closeServerAsPromise(server)) + + const base = `http://localhost:${server.address().port}` + + const cases = [ + ['/echo', '/echo'], + ['/echo?', '/echo?'], + ['/echo?a=b', '/echo?a=b'], + ['/echo?a=b&c=d', '/echo?a=b&c=d'], + ['/echo?#frag', '/echo?'], + ['/echo#?', '/echo'], + ['/echo#frag?bar', '/echo'] + ] + + for (const [path, expected] of cases) { + await t.test(`path: ${path} → ${expected}`, async (t) => { + const res = await fetch(`${base}${path}`) + const body = await res.text() + t.assert.strictEqual(body, expected) + }) + } +}) diff --git a/test/fetch/issue-4897.js b/test/fetch/issue-4897.js new file mode 100644 index 00000000000..aa307cc82d7 --- /dev/null +++ b/test/fetch/issue-4897.js @@ -0,0 +1,36 @@ +'use strict' + +const { test } = require('node:test') +const { fetch } = require('../..') + +function createAssertingDispatcher (t, expectedPath) { + return { + dispatch (opts, handler) { + t.assert.strictEqual(opts.path, expectedPath) + handler.onError(new Error('stop')) + return true + } + } +} + +async function assertPath (t, url, expectedPath) { + const dispatcher = createAssertingDispatcher(t, expectedPath) + + await t.assert.rejects(fetch(url, { dispatcher }), (err) => { + t.assert.strictEqual(err.cause?.message, 'stop') + return true + }) +} + +// https://github.com/nodejs/undici/issues/4897 +test('fetch path extraction does not match hostnames inside scheme', async (t) => { + const hosts = ['h', 't', 'p', 'ht', 'tp', 'tt'] + + for (const scheme of ['http', 'https']) { + for (const host of hosts) { + await t.test(`${scheme}://${host}/test?a=b#frag`, async (t) => { + await assertPath(t, `${scheme}://${host}/test?a=b#frag`, '/test?a=b') + }) + } + } +}) diff --git a/test/fetch/issue-node-56474.js b/test/fetch/issue-node-56474.js deleted file mode 100644 index 5c704a1bfcc..00000000000 --- a/test/fetch/issue-node-56474.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const { deepStrictEqual } = require('node:assert') -const { Response } = require('../..') - -// https://github.com/nodejs/node/issues/56474 -test('ReadableStream empty enqueue then other enqueued', async () => { - const iterable = { - async * [Symbol.asyncIterator] () { - yield '' - yield '3' - yield '4' - } - } - - const response = new Response(iterable) - deepStrictEqual(await response.text(), '34') -}) - -test('ReadableStream empty enqueue', async () => { - const iterable = { - async * [Symbol.asyncIterator] () { - yield '' - } - } - - const response = new Response(iterable) - deepStrictEqual(await response.text(), '') -}) diff --git a/test/fetch/issue-rsshub-15532.js b/test/fetch/issue-rsshub-15532.js index 988e12ceb5c..e8c20c482a5 100644 --- a/test/fetch/issue-rsshub-15532.js +++ b/test/fetch/issue-rsshub-15532.js @@ -4,14 +4,13 @@ const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') const { fetch } = require('../..') -const { tspl } = require('@matteo.collina/tspl') // https://github.com/DIYgod/RSSHub/issues/15532 test('An invalid Origin header is not set', async (t) => { - const { deepStrictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - deepStrictEqual(req.headers.origin, undefined) + t.assert.deepStrictEqual(req.headers.origin, undefined) res.end() }).listen(0) diff --git a/test/fetch/iterators.js b/test/fetch/iterators.js index dd8c420b486..7e5ba8bbaea 100644 --- a/test/fetch/iterators.js +++ b/test/fetch/iterators.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { Headers, FormData } = require('../..') test('Implements " Iterator" properly', async (t) => { @@ -14,18 +13,18 @@ test('Implements " Iterator" properly', async (t) => { const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) const iteratorProto = Object.getPrototypeOf(gen) - assert.ok(gen.constructor === IteratorPrototype.constructor) - assert.ok(gen.prototype === undefined) + t.assert.ok(gen.constructor === IteratorPrototype.constructor) + t.assert.ok(gen.prototype === undefined) // eslint-disable-next-line no-proto - assert.strictEqual(gen.__proto__[Symbol.toStringTag], 'Headers Iterator') + t.assert.strictEqual(gen.__proto__[Symbol.toStringTag], 'Headers Iterator') // https://github.com/node-fetch/node-fetch/issues/1119#issuecomment-100222049 - assert.ok(!(Headers.prototype[iterable] instanceof function * () {}.constructor)) + t.assert.ok(!(Headers.prototype[iterable] instanceof function * () {}.constructor)) // eslint-disable-next-line no-proto - assert.ok(gen.__proto__.next.__proto__ === Function.prototype) + t.assert.ok(gen.__proto__.next.__proto__ === Function.prototype) // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object // "The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%." - assert.strictEqual(gen[Symbol.iterator], IteratorPrototype[Symbol.iterator]) - assert.strictEqual(Object.getPrototypeOf(iteratorProto), IteratorPrototype) + t.assert.strictEqual(gen[Symbol.iterator], IteratorPrototype[Symbol.iterator]) + t.assert.strictEqual(Object.getPrototypeOf(iteratorProto), IteratorPrototype) } }) @@ -38,18 +37,18 @@ test('Implements " Iterator" properly', async (t) => { const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) const iteratorProto = Object.getPrototypeOf(gen) - assert.ok(gen.constructor === IteratorPrototype.constructor) - assert.ok(gen.prototype === undefined) + t.assert.ok(gen.constructor === IteratorPrototype.constructor) + t.assert.ok(gen.prototype === undefined) // eslint-disable-next-line no-proto - assert.strictEqual(gen.__proto__[Symbol.toStringTag], 'FormData Iterator') + t.assert.strictEqual(gen.__proto__[Symbol.toStringTag], 'FormData Iterator') // https://github.com/node-fetch/node-fetch/issues/1119#issuecomment-100222049 - assert.ok(!(Headers.prototype[iterable] instanceof function * () {}.constructor)) + t.assert.ok(!(Headers.prototype[iterable] instanceof function * () {}.constructor)) // eslint-disable-next-line no-proto - assert.ok(gen.__proto__.next.__proto__ === Function.prototype) + t.assert.ok(gen.__proto__.next.__proto__ === Function.prototype) // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object // "The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%." - assert.strictEqual(gen[Symbol.iterator], IteratorPrototype[Symbol.iterator]) - assert.strictEqual(Object.getPrototypeOf(iteratorProto), IteratorPrototype) + t.assert.strictEqual(gen[Symbol.iterator], IteratorPrototype[Symbol.iterator]) + t.assert.strictEqual(Object.getPrototypeOf(iteratorProto), IteratorPrototype) } }) @@ -58,16 +57,16 @@ test('Implements " Iterator" properly', async (t) => { const headers = new Headers([['a', 'b'], ['c', 'd']]) const gen = headers.entries() - assert.strictEqual(typeof gen[Symbol.toStringTag], 'string') - assert.strictEqual(typeof gen[Symbol.iterator], 'function') + t.assert.strictEqual(typeof gen[Symbol.toStringTag], 'string') + t.assert.strictEqual(typeof gen[Symbol.iterator], 'function') }) await t.test('FormData', () => { const fd = new FormData() const gen = fd.entries() - assert.strictEqual(typeof gen[Symbol.toStringTag], 'string') - assert.strictEqual(typeof gen[Symbol.iterator], 'function') + t.assert.strictEqual(typeof gen[Symbol.toStringTag], 'string') + t.assert.strictEqual(typeof gen[Symbol.iterator], 'function') }) }) @@ -76,43 +75,43 @@ test('Implements " Iterator" properly', async (t) => { const headers = new Headers([['a', 'b'], ['c', 'd']]) const gen = headers.entries() - assert.strictEqual(gen.return, undefined) - assert.strictEqual(gen.throw, undefined) - assert.strictEqual(typeof gen.next, 'function') + t.assert.strictEqual(gen.return, undefined) + t.assert.strictEqual(gen.throw, undefined) + t.assert.strictEqual(typeof gen.next, 'function') }) await t.test('FormData', () => { const fd = new FormData() const gen = fd.entries() - assert.strictEqual(gen.return, undefined) - assert.strictEqual(gen.throw, undefined) - assert.strictEqual(typeof gen.next, 'function') + t.assert.strictEqual(gen.return, undefined) + t.assert.strictEqual(gen.throw, undefined) + t.assert.strictEqual(typeof gen.next, 'function') }) }) await t.test('Symbol.iterator', () => { // Headers const headerValues = new Headers([['a', 'b']]).entries()[Symbol.iterator]() - assert.deepStrictEqual(Array.from(headerValues), [['a', 'b']]) + t.assert.deepStrictEqual(Array.from(headerValues), [['a', 'b']]) // FormData const formdata = new FormData() formdata.set('a', 'b') const formdataValues = formdata.entries()[Symbol.iterator]() - assert.deepStrictEqual(Array.from(formdataValues), [['a', 'b']]) + t.assert.deepStrictEqual(Array.from(formdataValues), [['a', 'b']]) }) await t.test('brand check', () => { // Headers - assert.throws(() => { + t.assert.throws(() => { const gen = new Headers().entries() // eslint-disable-next-line no-proto gen.__proto__.next() }, TypeError) // FormData - assert.throws(() => { + t.assert.throws(() => { const gen = new FormData().entries() // eslint-disable-next-line no-proto gen.__proto__.next() diff --git a/test/fetch/long-lived-abort-controller.js b/test/fetch/long-lived-abort-controller.js index befc19a9653..cf0a14af6e5 100644 --- a/test/fetch/long-lived-abort-controller.js +++ b/test/fetch/long-lived-abort-controller.js @@ -5,22 +5,20 @@ const { fetch } = require('../../') const { once, setMaxListeners } = require('node:events') const { test } = require('node:test') const { closeServerAsPromise } = require('../utils/node-http') -const { strictEqual } = require('node:assert') -test('long-lived-abort-controller', { skip: true }, async (t) => { +test('long-lived-abort-controller', async (t) => { const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }) - res.write('Hello World!') - res.end() - }).listen(0) + res.end('Hello World!') + }) - await once(server, 'listening') + await once(server.listen(0), 'listening') t.after(closeServerAsPromise(server)) - let warningEmitted = false - function onWarning () { - warningEmitted = true + let emittedWarning = '' + function onWarning (warning) { + emittedWarning = warning } process.on('warning', onWarning) t.after(() => { @@ -41,7 +39,10 @@ test('long-lived-abort-controller', { skip: true }, async (t) => { // drain body await res.text() + + // eslint-disable-next-line no-undef + gc() } - strictEqual(warningEmitted, false) + t.assert.strictEqual(emittedWarning, '') }) diff --git a/test/fetch/max-listeners.js b/test/fetch/max-listeners.js index 8537f477d41..2fa5fc7c346 100644 --- a/test/fetch/max-listeners.js +++ b/test/fetch/max-listeners.js @@ -2,7 +2,6 @@ const { setMaxListeners, getMaxListeners, defaultMaxListeners } = require('node:events') const { test } = require('node:test') -const assert = require('node:assert') const { Request } = require('../..') test('test max listeners', (t) => { @@ -12,5 +11,5 @@ test('test max listeners', (t) => { // eslint-disable-next-line no-new new Request('http://asd', { signal: controller.signal }) } - assert.strictEqual(getMaxListeners(controller.signal), Infinity) + t.assert.strictEqual(getMaxListeners(controller.signal), Infinity) }) diff --git a/test/fetch/pull-dont-push.js b/test/fetch/pull-dont-push.js index 7489b3fb0cd..7c32c16200c 100644 --- a/test/fetch/pull-dont-push.js +++ b/test/fetch/pull-dont-push.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { fetch } = require('../..') const { createServer } = require('node:http') const { once } = require('node:events') @@ -42,7 +41,7 @@ test('pull dont\'t push', async (t) => { await sleep(1000) socket.destroy() - assert.strictEqual(count < max, true) // the stream should be closed before the max + t.assert.strictEqual(count < max, true) // the stream should be closed before the max // consume the stream try { diff --git a/test/fetch/readable-stream-from.js b/test/fetch/readable-stream-from.js new file mode 100644 index 00000000000..7b047d31eec --- /dev/null +++ b/test/fetch/readable-stream-from.js @@ -0,0 +1,29 @@ +'use strict' + +const { test } = require('node:test') +const { Response } = require('../..') + +// https://github.com/nodejs/node/issues/56474 +test('ReadableStream empty enqueue then other enqueued', async (t) => { + const iterable = { + async * [Symbol.asyncIterator] () { + yield '' + yield '3' + yield '4' + } + } + + const response = new Response(iterable) + t.assert.deepStrictEqual(await response.text(), '34') +}) + +test('ReadableStream empty enqueue', async (t) => { + const iterable = { + async * [Symbol.asyncIterator] () { + yield '' + } + } + + const response = new Response(iterable) + t.assert.deepStrictEqual(await response.text(), '') +}) diff --git a/test/fetch/redirect-cross-origin-header.js b/test/fetch/redirect-cross-origin-header.js index 8f0d58587e7..3c0df8b6746 100644 --- a/test/fetch/redirect-cross-origin-header.js +++ b/test/fetch/redirect-cross-origin-header.js @@ -1,25 +1,24 @@ 'use strict' const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') const { once } = require('node:events') const { fetch } = require('../..') test('Cross-origin redirects clear forbidden headers', async (t) => { - const { strictEqual } = tspl(t, { plan: 6 }) + t.plan(6) const server1 = createServer({ joinDuplicateHeaders: true }, (req, res) => { - strictEqual(req.headers.cookie, undefined) - strictEqual(req.headers.authorization, undefined) - strictEqual(req.headers['proxy-authorization'], undefined) + t.assert.strictEqual(req.headers.cookie, undefined) + t.assert.strictEqual(req.headers.authorization, undefined) + t.assert.strictEqual(req.headers['proxy-authorization'], undefined) res.end('redirected') }).listen(0) const server2 = createServer({ joinDuplicateHeaders: true }, (req, res) => { - strictEqual(req.headers.authorization, 'test') - strictEqual(req.headers.cookie, 'ddd=dddd') + t.assert.strictEqual(req.headers.authorization, 'test') + t.assert.strictEqual(req.headers.cookie, 'ddd=dddd') res.writeHead(302, { ...req.headers, @@ -47,5 +46,5 @@ test('Cross-origin redirects clear forbidden headers', async (t) => { }) const text = await res.text() - strictEqual(text, 'redirected') + t.assert.strictEqual(text, 'redirected') }) diff --git a/test/fetch/redirect.js b/test/fetch/redirect.js index 6d41ff30849..913ec7972e9 100644 --- a/test/fetch/redirect.js +++ b/test/fetch/redirect.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { createServer } = require('node:http') const { once } = require('node:events') const { fetch } = require('../..') @@ -26,8 +25,8 @@ test('Redirecting with a body does not cancel the current request - #1776', asyn await once(server, 'listening') const resp = await fetch(`http://localhost:${server.address().port}/redirect`) - assert.strictEqual(await resp.text(), '/redirect/') - assert.ok(resp.redirected) + t.assert.strictEqual(await resp.text(), '/redirect/') + t.assert.ok(resp.redirected) }) test('Redirecting with an empty body does not throw an error - #2027', async (t) => { @@ -47,8 +46,8 @@ test('Redirecting with an empty body does not throw an error - #2027', async (t) await once(server, 'listening') const resp = await fetch(`http://localhost:${server.address().port}/redirect`, { method: 'PUT', body: '' }) - assert.strictEqual(await resp.text(), '/redirect/') - assert.ok(resp.redirected) + t.assert.strictEqual(await resp.text(), '/redirect/') + t.assert.ok(resp.redirected) }) test('Redirecting with a body does not fail to write body - #2543', async (t) => { @@ -60,7 +59,7 @@ test('Redirecting with a body does not fail to write body - #2543', async (t) => } else { let body = '' req.on('data', (chunk) => { body += chunk }) - req.on('end', () => assert.strictEqual(body, 'body')) + req.on('end', () => t.assert.strictEqual(body, 'body')) res.write('ok') res.end() } @@ -73,6 +72,6 @@ test('Redirecting with a body does not fail to write body - #2543', async (t) => method: 'POST', body: 'body' }) - assert.strictEqual(await resp.text(), 'ok') - assert.ok(resp.redirected) + t.assert.strictEqual(await resp.text(), 'ok') + t.assert.ok(resp.redirected) }) diff --git a/test/fetch/referrrer-policy.js b/test/fetch/referrrer-policy.js index 612d8e29d0d..849aab98b6c 100644 --- a/test/fetch/referrrer-policy.js +++ b/test/fetch/referrrer-policy.js @@ -4,7 +4,6 @@ const { once } = require('node:events') const { createServer } = require('node:http') const { describe, test } = require('node:test') const { fetch } = require('../..') -const tspl = require('@matteo.collina/tspl') describe('referrer-policy', () => { ;[ @@ -71,7 +70,7 @@ describe('referrer-policy', () => { ] ].forEach(([title, responseReferrerPolicy, expectedReferrerPolicy, referrer]) => { test(title, async (t) => { - t = tspl(t, { plan: 1 }) + t.plan(1) const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { switch (res.req.url) { @@ -85,16 +84,16 @@ describe('referrer-policy', () => { case '/target': switch (expectedReferrerPolicy) { case 'no-referrer': - t.strictEqual(req.headers['referer'], undefined) + t.assert.strictEqual(req.headers['referer'], undefined) break case 'origin': - t.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/`) + t.assert.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/`) break case 'strict-origin-when-cross-origin': - t.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/index.html?test=1`) + t.assert.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/index.html?test=1`) break case 'unsafe-url': - t.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/index.html?test=1`) + t.assert.strictEqual(req.headers['referer'], `http://127.0.0.1:${port}/index.html?test=1`) break } res.writeHead(200, 'dummy', { 'Content-Type': 'text/plain' }) @@ -111,8 +110,6 @@ describe('referrer-policy', () => { referrer: referrer || `http://127.0.0.1:${port}/index.html?test=1` }) - await t.completed - server.closeAllConnections() server.closeIdleConnections() server.close() diff --git a/test/fetch/relative-url.js b/test/fetch/relative-url.js index b6e28bda7ea..8d665c4e3c4 100644 --- a/test/fetch/relative-url.js +++ b/test/fetch/relative-url.js @@ -1,7 +1,6 @@ 'use strict' const { test, afterEach } = require('node:test') -const assert = require('node:assert') const { createServer } = require('node:http') const { once } = require('node:events') const { @@ -15,35 +14,35 @@ const { closeServerAsPromise } = require('../utils/node-http') afterEach(() => setGlobalOrigin(undefined)) -test('setGlobalOrigin & getGlobalOrigin', () => { - assert.strictEqual(getGlobalOrigin(), undefined) +test('setGlobalOrigin & getGlobalOrigin', (t) => { + t.assert.strictEqual(getGlobalOrigin(), undefined) setGlobalOrigin('http://localhost:3000') - assert.deepStrictEqual(getGlobalOrigin(), new URL('http://localhost:3000')) + t.assert.deepStrictEqual(getGlobalOrigin(), new URL('http://localhost:3000')) setGlobalOrigin(undefined) - assert.strictEqual(getGlobalOrigin(), undefined) + t.assert.strictEqual(getGlobalOrigin(), undefined) setGlobalOrigin(new URL('http://localhost:3000')) - assert.deepStrictEqual(getGlobalOrigin(), new URL('http://localhost:3000')) + t.assert.deepStrictEqual(getGlobalOrigin(), new URL('http://localhost:3000')) - assert.throws(() => { + t.assert.throws(() => { setGlobalOrigin('invalid.url') }, TypeError) - assert.throws(() => { + t.assert.throws(() => { setGlobalOrigin('wss://invalid.protocol') }, TypeError) - assert.throws(() => setGlobalOrigin(true)) + t.assert.throws(() => setGlobalOrigin(true)) }) -test('Response.redirect', () => { - assert.throws(() => { +test('Response.redirect', (t) => { + t.assert.throws(() => { Response.redirect('/relative/path', 302) }, TypeError('Failed to parse URL from /relative/path')) - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { setGlobalOrigin('http://localhost:3000') Response.redirect('/relative/path', 302) }) @@ -51,16 +50,16 @@ test('Response.redirect', () => { setGlobalOrigin('http://localhost:3000') const response = Response.redirect('/relative/path', 302) // See step #7 of https://fetch.spec.whatwg.org/#dom-response-redirect - assert.strictEqual(response.headers.get('location'), 'http://localhost:3000/relative/path') + t.assert.strictEqual(response.headers.get('location'), 'http://localhost:3000/relative/path') }) test('new Request', (t) => { - assert.throws( + t.assert.throws( () => new Request('/relative/path'), TypeError('Failed to parse URL from /relative/path') ) - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { setGlobalOrigin('http://localhost:3000') // eslint-disable-next-line no-new new Request('/relative/path') @@ -68,15 +67,15 @@ test('new Request', (t) => { setGlobalOrigin('http://localhost:3000') const request = new Request('/relative/path') - assert.strictEqual(request.url, 'http://localhost:3000/relative/path') + t.assert.strictEqual(request.url, 'http://localhost:3000/relative/path') }) test('fetch', async (t) => { - await assert.rejects(fetch('/relative/path'), TypeError('Failed to parse URL from /relative/path')) + await t.assert.rejects(fetch('/relative/path'), TypeError('Failed to parse URL from /relative/path')) await t.test('Basic fetch', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.url, '/relative/path') + t.assert.strictEqual(req.url, '/relative/path') res.end() }).listen(0) @@ -84,12 +83,12 @@ test('fetch', async (t) => { t.after(closeServerAsPromise(server)) await once(server, 'listening') - await assert.doesNotReject(fetch('/relative/path')) + await t.assert.doesNotReject(fetch('/relative/path')) }) await t.test('fetch return', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { - assert.strictEqual(req.url, '/relative/path') + t.assert.strictEqual(req.url, '/relative/path') res.end() }).listen(0) @@ -99,6 +98,6 @@ test('fetch', async (t) => { const response = await fetch('/relative/path') - assert.strictEqual(response.url, `http://localhost:${server.address().port}/relative/path`) + t.assert.strictEqual(response.url, `http://localhost:${server.address().port}/relative/path`) }) }) diff --git a/test/fetch/request-inspect-custom.js b/test/fetch/request-inspect-custom.js index e2e60bdab7b..9fdcb0f6901 100644 --- a/test/fetch/request-inspect-custom.js +++ b/test/fetch/request-inspect-custom.js @@ -1,12 +1,11 @@ 'use strict' const { describe, it } = require('node:test') -const assert = require('node:assert') const util = require('node:util') const { Request } = require('../../') describe('Request custom inspection', () => { - it('should return a custom inspect output', () => { + it('should return a custom inspect output', (t) => { const request = new Request('https://example.com/api', { method: 'POST', headers: { @@ -17,6 +16,6 @@ describe('Request custom inspection', () => { const inspectedOutput = util.inspect(request) const expectedOutput = "Request {\n method: 'POST',\n url: 'https://example.com/api',\n headers: Headers { 'Content-Type': 'application/json' },\n destination: '',\n referrer: 'about:client',\n referrerPolicy: '',\n mode: 'cors',\n credentials: 'same-origin',\n cache: 'default',\n redirect: 'follow',\n integrity: '',\n keepalive: false,\n isReloadNavigation: false,\n isHistoryNavigation: false,\n signal: AbortSignal { aborted: false }\n}" - assert.strictEqual(inspectedOutput, expectedOutput) + t.assert.strictEqual(inspectedOutput, expectedOutput) }) }) diff --git a/test/fetch/request.js b/test/fetch/request.js index 8ee4c8ed06d..51e92b5c061 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -3,166 +3,162 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') const { Request, Headers, fetch } = require('../../') -const hasSignalReason = 'reason' in AbortSignal.prototype - test('arg validation', async (t) => { // constructor - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', 0) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { const url = new URL('http://asd') url.password = 'asd' // eslint-disable-next-line new Request(url) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { const url = new URL('http://asd') url.username = 'asd' // eslint-disable-next-line new Request(url) }, TypeError) - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { // eslint-disable-next-line new Request('http://asd', undefined) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { window: {} }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { window: 1 }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { mode: 'navigate' }) }) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { referrerPolicy: 'agjhagna' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { mode: 'agjhagna' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { credentials: 'agjhagna' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { cache: 'agjhagna' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { method: 'agjhagnaöööö' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Request('http://asd', { method: 'TRACE' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.destination.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.referrer.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.referrerPolicy.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.mode.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.credentials.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.cache.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.redirect.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.integrity.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.keepalive.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.isReloadNavigation.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.isHistoryNavigation.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.signal.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line no-unused-expressions Request.prototype.body }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line no-unused-expressions Request.prototype.bodyUsed }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Request.prototype.clone.call(null) }, TypeError) - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { Request.prototype[Symbol.toStringTag].charAt(0) }) @@ -173,7 +169,7 @@ test('arg validation', async (t) => { 'blob', 'formData' ]) { - await assert.rejects(async () => { + await t.assert.rejects(async () => { await new Request('http://localhost')[method].call({ blob () { return { @@ -187,112 +183,104 @@ test('arg validation', async (t) => { } }) -test('undefined window', () => { - assert.doesNotThrow(() => new Request('http://asd', { window: undefined })) +test('undefined window', (t) => { + t.assert.doesNotThrow(() => new Request('http://asd', { window: undefined })) }) -test('undefined body', () => { +test('undefined body', (t) => { const req = new Request('http://asd', { body: undefined }) - assert.strictEqual(req.body, null) + t.assert.strictEqual(req.body, null) }) -test('undefined method', () => { +test('undefined method', (t) => { const req = new Request('http://asd', { method: undefined }) - assert.strictEqual(req.method, 'GET') + t.assert.strictEqual(req.method, 'GET') }) -test('undefined headers', () => { +test('undefined headers', (t) => { const req = new Request('http://asd', { headers: undefined }) - assert.strictEqual([...req.headers.entries()].length, 0) + t.assert.strictEqual([...req.headers.entries()].length, 0) }) -test('undefined referrer', () => { +test('undefined referrer', (t) => { const req = new Request('http://asd', { referrer: undefined }) - assert.strictEqual(req.referrer, 'about:client') + t.assert.strictEqual(req.referrer, 'about:client') }) -test('undefined referrerPolicy', () => { +test('undefined referrerPolicy', (t) => { const req = new Request('http://asd', { referrerPolicy: undefined }) - assert.strictEqual(req.referrerPolicy, '') + t.assert.strictEqual(req.referrerPolicy, '') }) -test('undefined mode', () => { +test('undefined mode', (t) => { const req = new Request('http://asd', { mode: undefined }) - assert.strictEqual(req.mode, 'cors') + t.assert.strictEqual(req.mode, 'cors') }) -test('undefined credentials', () => { +test('undefined credentials', (t) => { const req = new Request('http://asd', { credentials: undefined }) - assert.strictEqual(req.credentials, 'same-origin') + t.assert.strictEqual(req.credentials, 'same-origin') }) -test('undefined cache', () => { +test('undefined cache', (t) => { const req = new Request('http://asd', { cache: undefined }) - assert.strictEqual(req.cache, 'default') + t.assert.strictEqual(req.cache, 'default') }) -test('undefined redirect', () => { +test('undefined redirect', (t) => { const req = new Request('http://asd', { redirect: undefined }) - assert.strictEqual(req.redirect, 'follow') + t.assert.strictEqual(req.redirect, 'follow') }) -test('undefined keepalive', () => { +test('undefined keepalive', (t) => { const req = new Request('http://asd', { keepalive: undefined }) - assert.strictEqual(req.keepalive, false) + t.assert.strictEqual(req.keepalive, false) }) -test('undefined integrity', () => { +test('undefined integrity', (t) => { const req = new Request('http://asd', { integrity: undefined }) - assert.strictEqual(req.integrity, '') + t.assert.strictEqual(req.integrity, '') }) -test('null integrity', () => { +test('null integrity', (t) => { const req = new Request('http://asd', { integrity: null }) - assert.strictEqual(req.integrity, 'null') + t.assert.strictEqual(req.integrity, 'null') }) -test('undefined signal', () => { +test('undefined signal', (t) => { const req = new Request('http://asd', { signal: undefined }) - assert.strictEqual(req.signal.aborted, false) + t.assert.strictEqual(req.signal.aborted, false) }) -test('pre aborted signal', () => { +test('pre aborted signal', (t) => { const ac = new AbortController() ac.abort('gwak') const req = new Request('http://asd', { signal: ac.signal }) - assert.strictEqual(req.signal.aborted, true) - if (hasSignalReason) { - assert.strictEqual(req.signal.reason, 'gwak') - } + t.assert.strictEqual(req.signal.aborted, true) + t.assert.strictEqual(req.signal.reason, 'gwak') }) test('post aborted signal', (t) => { - const { strictEqual, ok } = tspl(t, { plan: 2 }) + t.plan(2) const ac = new AbortController() const req = new Request('http://asd', { signal: ac.signal }) - strictEqual(req.signal.aborted, false) + t.assert.strictEqual(req.signal.aborted, false) ac.signal.addEventListener('abort', () => { - if (hasSignalReason) { - strictEqual(req.signal.reason, 'gwak') - } else { - ok(true) - } + t.assert.strictEqual(req.signal.reason, 'gwak') }, { once: true }) ac.abort('gwak') }) -test('pre aborted signal cloned', () => { +test('pre aborted signal cloned', (t) => { const ac = new AbortController() ac.abort('gwak') const req = new Request('http://asd', { signal: ac.signal }).clone() - assert.strictEqual(req.signal.aborted, true) - if (hasSignalReason) { - assert.strictEqual(req.signal.reason, 'gwak') - } + t.assert.strictEqual(req.signal.aborted, true) + t.assert.strictEqual(req.signal.reason, 'gwak') }) -test('URLSearchParams body with Headers object - issue #1407', async () => { +test('URLSearchParams body with Headers object - issue #1407', async (t) => { const body = new URLSearchParams({ abc: 123 }) @@ -308,131 +296,127 @@ test('URLSearchParams body with Headers object - issue #1407', async () => { } ) - assert.strictEqual(request.headers.get('content-type'), 'application/x-www-form-urlencoded;charset=UTF-8') - assert.strictEqual(request.headers.get('authorization'), 'test') - assert.strictEqual(await request.text(), 'abc=123') + t.assert.strictEqual(request.headers.get('content-type'), 'application/x-www-form-urlencoded;charset=UTF-8') + t.assert.strictEqual(request.headers.get('authorization'), 'test') + t.assert.strictEqual(await request.text(), 'abc=123') }) test('post aborted signal cloned', (t) => { - const { strictEqual, ok } = tspl(t, { plan: 2 }) + t.plan(2) const ac = new AbortController() const req = new Request('http://asd', { signal: ac.signal }).clone() - strictEqual(req.signal.aborted, false) + t.assert.strictEqual(req.signal.aborted, false) ac.signal.addEventListener('abort', () => { - if (hasSignalReason) { - strictEqual(req.signal.reason, 'gwak') - } else { - ok(true) - } + t.assert.strictEqual(req.signal.reason, 'gwak') }, { once: true }) ac.abort('gwak') }) test('Passing headers in init', async (t) => { // https://github.com/nodejs/undici/issues/1400 - await t.test('Headers instance', () => { + await t.test('Headers instance', (t) => { const req = new Request('http://localhost', { headers: new Headers({ key: 'value' }) }) - assert.strictEqual(req.headers.get('key'), 'value') + t.assert.strictEqual(req.headers.get('key'), 'value') }) - await t.test('key:value object', () => { + await t.test('key:value object', (t) => { const req = new Request('http://localhost', { headers: { key: 'value' } }) - assert.strictEqual(req.headers.get('key'), 'value') + t.assert.strictEqual(req.headers.get('key'), 'value') }) - await t.test('[key, value][]', () => { + await t.test('[key, value][]', (t) => { const req = new Request('http://localhost', { headers: [['key', 'value']] }) - assert.strictEqual(req.headers.get('key'), 'value') + t.assert.strictEqual(req.headers.get('key'), 'value') }) }) -test('Symbol.toStringTag', () => { +test('Symbol.toStringTag', (t) => { const req = new Request('http://localhost') - assert.strictEqual(req[Symbol.toStringTag], 'Request') - assert.strictEqual(Request.prototype[Symbol.toStringTag], 'Request') + t.assert.strictEqual(req[Symbol.toStringTag], 'Request') + t.assert.strictEqual(Request.prototype[Symbol.toStringTag], 'Request') }) -test('invalid RequestInit values', () => { +test('invalid RequestInit values', (t) => { /* eslint-disable no-new */ - assert.throws(() => { + t.assert.throws(() => { new Request('http://l', { mode: 'CoRs' }) }, TypeError, 'not exact case = error') - assert.throws(() => { + t.assert.throws(() => { new Request('http://l', { mode: 'random' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { new Request('http://l', { credentials: 'OMIt' }) }, TypeError, 'not exact case = error') - assert.throws(() => { + t.assert.throws(() => { new Request('http://l', { credentials: 'random' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { new Request('http://l', { cache: 'DeFaULt' }) }, TypeError, 'not exact case = error') - assert.throws(() => { + t.assert.throws(() => { new Request('http://l', { cache: 'random' }) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { new Request('http://l', { redirect: 'FOllOW' }) }, TypeError, 'not exact case = error') - assert.throws(() => { + t.assert.throws(() => { new Request('http://l', { redirect: 'random' }) }, TypeError) /* eslint-enable no-new */ }) -test('RequestInit.signal option', async () => { - assert.throws(() => { +test('RequestInit.signal option', async (t) => { + t.assert.throws(() => { // eslint-disable-next-line no-new new Request('http://asd', { signal: true }) }, TypeError) - await assert.rejects(fetch('http://asd', { + await t.assert.rejects(fetch('http://asd', { signal: false }), TypeError) }) // https://github.com/nodejs/undici/issues/2050 -test('set-cookie headers get cleared when passing a Request as first param', () => { +test('set-cookie headers get cleared when passing a Request as first param', (t) => { const req1 = new Request('http://localhost', { headers: { 'set-cookie': 'a=1' } }) - assert.deepStrictEqual([...req1.headers], [['set-cookie', 'a=1']]) + t.assert.deepStrictEqual([...req1.headers], [['set-cookie', 'a=1']]) const req2 = new Request(req1, { headers: {} }) - assert.deepStrictEqual([...req1.headers], [['set-cookie', 'a=1']]) - assert.deepStrictEqual([...req2.headers], []) - assert.deepStrictEqual(req2.headers.getSetCookie(), []) + t.assert.deepStrictEqual([...req1.headers], [['set-cookie', 'a=1']]) + t.assert.deepStrictEqual([...req2.headers], []) + t.assert.deepStrictEqual(req2.headers.getSetCookie(), []) }) // https://github.com/nodejs/undici/issues/2124 -test('request.referrer', () => { +test('request.referrer', (t) => { for (const referrer of ['about://client', 'about://client:1234']) { const request = new Request('http://a', { referrer }) - assert.strictEqual(request.referrer, 'about:client') + t.assert.strictEqual(request.referrer, 'about:client') } }) @@ -440,22 +424,22 @@ test('request.referrer', () => { test('Clone the set-cookie header when Request is passed as the first parameter and no header is passed.', (t) => { const request = new Request('http://localhost', { headers: { 'set-cookie': 'A' } }) const request2 = new Request(request) - assert.deepStrictEqual([...request.headers], [['set-cookie', 'A']]) + t.assert.deepStrictEqual([...request.headers], [['set-cookie', 'A']]) request2.headers.append('set-cookie', 'B') - assert.deepStrictEqual([...request.headers], [['set-cookie', 'A']]) - assert.strictEqual(request.headers.getSetCookie().join(', '), request.headers.get('set-cookie')) - assert.strictEqual(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie')) + t.assert.deepStrictEqual([...request.headers], [['set-cookie', 'A']]) + t.assert.strictEqual(request.headers.getSetCookie().join(', '), request.headers.get('set-cookie')) + t.assert.strictEqual(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie')) }) // Tests for optimization introduced in https://github.com/nodejs/undici/pull/2456 test('keys to object prototypes method', (t) => { const request = new Request('http://localhost', { method: 'hasOwnProperty' }) - assert(typeof request.method === 'string') + t.assert.ok(typeof request.method === 'string') }) // https://github.com/nodejs/undici/issues/2465 test('Issue#2465', async (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const request = new Request('http://localhost', { body: new SharedArrayBuffer(0), method: 'POST' }) - strictEqual(await request.text(), '[object SharedArrayBuffer]') + t.assert.strictEqual(await request.text(), '[object SharedArrayBuffer]') }) diff --git a/test/fetch/resource-timing.js b/test/fetch/resource-timing.js index 4def7bf90e2..e6c8d0e830d 100644 --- a/test/fetch/resource-timing.js +++ b/test/fetch/resource-timing.js @@ -1,9 +1,8 @@ 'use strict' const { test } = require('node:test') -const { tspl } = require('@matteo.collina/tspl') const { createServer } = require('node:http') -const { fetch } = require('../..') +const { fetch, Agent } = require('../..') const { closeServerAsPromise } = require('../utils/node-http') const { @@ -12,23 +11,23 @@ const { } = require('node:perf_hooks') test('should create a PerformanceResourceTiming after each fetch request', (t, done) => { - const { strictEqual, ok, deepStrictEqual } = tspl(t, { plan: 8 }) + t.plan(8) const obs = new PerformanceObserver(list => { const expectedResourceEntryName = `http://localhost:${server.address().port}/` const entries = list.getEntries() - strictEqual(entries.length, 1) + t.assert.strictEqual(entries.length, 1) const [entry] = entries - strictEqual(entry.name, expectedResourceEntryName) - strictEqual(entry.entryType, 'resource') + t.assert.strictEqual(entry.name, expectedResourceEntryName) + t.assert.strictEqual(entry.entryType, 'resource') - ok(entry.duration >= 0) - ok(entry.startTime >= 0) + t.assert.ok(entry.duration >= 0) + t.assert.ok(entry.startTime >= 0) const entriesByName = list.getEntriesByName(expectedResourceEntryName) - strictEqual(entriesByName.length, 1) - deepStrictEqual(entriesByName[0], entry) + t.assert.strictEqual(entriesByName.length, 1) + t.assert.deepStrictEqual(entriesByName[0], entry) obs.disconnect() performance.clearResourceTimings() @@ -41,19 +40,19 @@ test('should create a PerformanceResourceTiming after each fetch request', (t, d res.end('ok') }).listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}`) - strictEqual('ok', await body.text()) + t.assert.strictEqual('ok', await body.text()) }) t.after(closeServerAsPromise(server)) }) test('should include encodedBodySize in performance entry', (t, done) => { - const { strictEqual } = tspl(t, { plan: 4 }) + t.plan(4) const obs = new PerformanceObserver(list => { const [entry] = list.getEntries() - strictEqual(entry.encodedBodySize, 2) - strictEqual(entry.decodedBodySize, 2) - strictEqual(entry.transferSize, 2 + 300) + t.assert.strictEqual(entry.encodedBodySize, 2) + t.assert.strictEqual(entry.decodedBodySize, 2) + t.assert.strictEqual(entry.transferSize, 2 + 300) obs.disconnect() performance.clearResourceTimings() @@ -66,30 +65,30 @@ test('should include encodedBodySize in performance entry', (t, done) => { res.end('ok') }).listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}`) - strictEqual('ok', await body.text()) + t.assert.strictEqual('ok', await body.text()) }) t.after(closeServerAsPromise(server)) }) test('timing entries should be in order', (t, done) => { - const { ok, strictEqual } = tspl(t, { plan: 13 }) + t.plan(13) const obs = new PerformanceObserver(list => { const [entry] = list.getEntries() - ok(entry.startTime > 0) - ok(entry.fetchStart >= entry.startTime) - ok(entry.domainLookupStart >= entry.fetchStart) - ok(entry.domainLookupEnd >= entry.domainLookupStart) - ok(entry.connectStart >= entry.domainLookupEnd) - ok(entry.connectEnd >= entry.connectStart) - ok(entry.requestStart >= entry.connectEnd) - ok(entry.responseStart >= entry.requestStart) - ok(entry.responseEnd >= entry.responseStart) - ok(entry.duration > 0) + t.assert.ok(entry.startTime > 0) + t.assert.ok(entry.fetchStart >= entry.startTime) + t.assert.ok(entry.domainLookupStart >= entry.fetchStart) + t.assert.ok(entry.domainLookupEnd >= entry.domainLookupStart) + t.assert.ok(entry.connectStart >= entry.domainLookupEnd) + t.assert.ok(entry.connectEnd >= entry.connectStart) + t.assert.ok(entry.requestStart >= entry.connectEnd) + t.assert.ok(entry.responseStart >= entry.requestStart) + t.assert.ok(entry.responseEnd >= entry.responseStart) + t.assert.ok(entry.duration > 0) - ok(entry.redirectStart === 0) - ok(entry.redirectEnd === 0) + t.assert.ok(entry.redirectStart === 0) + t.assert.ok(entry.redirectEnd === 0) obs.disconnect() performance.clearResourceTimings() @@ -102,20 +101,20 @@ test('timing entries should be in order', (t, done) => { res.end('ok') }).listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}/redirect`) - strictEqual('ok', await body.text()) + t.assert.strictEqual('ok', await body.text()) }) t.after(closeServerAsPromise(server)) }) test('redirect timing entries should be included when redirecting', (t, done) => { - const { ok, strictEqual } = tspl(t, { plan: 4 }) + t.plan(4) const obs = new PerformanceObserver(list => { const [entry] = list.getEntries() - ok(entry.redirectStart >= entry.startTime) - ok(entry.redirectEnd >= entry.redirectStart) - ok(entry.connectStart >= entry.redirectEnd) + t.assert.ok(entry.redirectStart >= entry.startTime) + t.assert.ok(entry.redirectEnd >= entry.redirectStart) + t.assert.ok(entry.connectStart >= entry.redirectEnd) obs.disconnect() performance.clearResourceTimings() @@ -134,7 +133,35 @@ test('redirect timing entries should be included when redirecting', (t, done) => res.end('ok') }).listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}/redirect`) - strictEqual('ok', await body.text()) + t.assert.strictEqual('ok', await body.text()) + }) + + t.after(closeServerAsPromise(server)) +}) + +test('responseStart should be greater than 0 with composed interceptor', (t, done) => { + t.plan(4) + const obs = new PerformanceObserver(list => { + const [entry] = list.getEntries() + + t.assert.ok(entry.requestStart > 0, `requestStart should be > 0, got ${entry.requestStart}`) + t.assert.ok(entry.responseStart > 0, `responseStart should be > 0, got ${entry.responseStart}`) + t.assert.ok(entry.responseStart >= entry.requestStart, 'responseStart should be >= requestStart') + + obs.disconnect() + performance.clearResourceTimings() + done() + }) + + obs.observe({ entryTypes: ['resource'] }) + + const dispatcher = new Agent().compose((dispatch) => (opts, handler) => dispatch(opts, handler)) + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.end('ok') + }).listen(0, async () => { + const body = await fetch(`http://localhost:${server.address().port}`, { dispatcher }) + t.assert.strictEqual('ok', await body.text()) }) t.after(closeServerAsPromise(server)) diff --git a/test/fetch/response-inspect-custom.js b/test/fetch/response-inspect-custom.js index ca8a5a0fc1a..014c3414414 100644 --- a/test/fetch/response-inspect-custom.js +++ b/test/fetch/response-inspect-custom.js @@ -1,12 +1,11 @@ 'use strict' const { describe, it } = require('node:test') -const assert = require('node:assert') const util = require('node:util') const { Response } = require('../../') describe('Response custom inspection', () => { - it('should return a custom inspect output', () => { + it('should return a custom inspect output', (t) => { const response = new Response(null) const inspectedOutput = util.inspect(response, { depth: null, @@ -25,6 +24,6 @@ describe('Response custom inspection', () => { url: '' }` - assert.strictEqual(inspectedOutput, expectedOutput) + t.assert.strictEqual(inspectedOutput, expectedOutput) }) }) diff --git a/test/fetch/response-json.js b/test/fetch/response-json.js index 7dc6ad20c37..46878cc0d82 100644 --- a/test/fetch/response-json.js +++ b/test/fetch/response-json.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const { Response } = require('../../') // https://github.com/web-platform-tests/wpt/pull/32825/ @@ -18,81 +17,81 @@ const INIT_TESTS = [ [{ headers: { 'x-foo': 'bar' } }, 200, '', APPLICATION_JSON, { 'x-foo': 'bar' }] ] -test('Check response returned by static json() with init', async () => { +test('Check response returned by static json() with init', async (t) => { for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) { const response = Response.json('hello world', init) - assert.strictEqual(response.type, 'default', "Response's type is default") - assert.strictEqual(response.status, expectedStatus, "Response's status is " + expectedStatus) - assert.strictEqual(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText)) - assert.strictEqual(response.headers.get('content-type'), expectedContentType, "Response's content-type is " + expectedContentType) + t.assert.strictEqual(response.type, 'default', "Response's type is default") + t.assert.strictEqual(response.status, expectedStatus, "Response's status is " + expectedStatus) + t.assert.strictEqual(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText)) + t.assert.strictEqual(response.headers.get('content-type'), expectedContentType, "Response's content-type is " + expectedContentType) for (const key in expectedHeaders) { - assert.strictEqual(response.headers.get(key), expectedHeaders[key], "Response's header " + key + ' is ' + JSON.stringify(expectedHeaders[key])) + t.assert.strictEqual(response.headers.get(key), expectedHeaders[key], "Response's header " + key + ' is ' + JSON.stringify(expectedHeaders[key])) } const data = await response.json() - assert.strictEqual(data, 'hello world', "Response's body is 'hello world'") + t.assert.strictEqual(data, 'hello world', "Response's body is 'hello world'") } }) -test('Throws TypeError when calling static json() with an invalid status', () => { +test('Throws TypeError when calling static json() with an invalid status', (t) => { const nullBodyStatus = [204, 205, 304] for (const status of nullBodyStatus) { - assert.throws(() => { + t.assert.throws(() => { Response.json('hello world', { status }) }, TypeError, `Throws TypeError when calling static json() with a status of ${status}`) } }) -test('Check static json() encodes JSON objects correctly', async () => { +test('Check static json() encodes JSON objects correctly', async (t) => { const response = Response.json({ foo: 'bar' }) const data = await response.json() - assert.strictEqual(typeof data, 'object', "Response's json body is an object") - assert.strictEqual(data.foo, 'bar', "Response's json body is { foo: 'bar' }") + t.assert.strictEqual(typeof data, 'object', "Response's json body is an object") + t.assert.strictEqual(data.foo, 'bar', "Response's json body is { foo: 'bar' }") }) -test('Check static json() throws when data is not encodable', () => { - assert.throws(() => { +test('Check static json() throws when data is not encodable', (t) => { + t.assert.throws(() => { Response.json(Symbol('foo')) }, TypeError) }) -test('Check static json() throws when data is circular', () => { +test('Check static json() throws when data is circular', (t) => { const a = { b: 1 } a.a = a - assert.throws(() => { + t.assert.throws(() => { Response.json(a) }, TypeError) }) -test('Check static json() propagates JSON serializer errors', () => { +test('Check static json() propagates JSON serializer errors', (t) => { class CustomError extends Error { name = 'CustomError' } - assert.throws(() => { + t.assert.throws(() => { Response.json({ get foo () { throw new CustomError('bar') } }) }, CustomError) }) // note: these tests are not part of any WPTs -test('unserializable values', () => { - assert.throws(() => { +test('unserializable values', (t) => { + t.assert.throws(() => { Response.json(Symbol('symbol')) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.json(undefined) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.json() }, TypeError) }) -test('invalid init', () => { - assert.throws(() => { +test('invalid init', (t) => { + t.assert.throws(() => { Response.json(null, 3) }, TypeError) }) diff --git a/test/fetch/response.js b/test/fetch/response.js index 230e70061d0..5c7ce840d87 100644 --- a/test/fetch/response.js +++ b/test/fetch/response.js @@ -1,40 +1,38 @@ 'use strict' -const { test } = require('node:test') -const assert = require('node:assert') +const { describe, test } = require('node:test') const { setImmediate } = require('node:timers/promises') const { AsyncLocalStorage } = require('node:async_hooks') -const { tspl } = require('@matteo.collina/tspl') const { Response, FormData } = require('../../') -test('arg validation', async () => { +test('arg validation', async (t) => { // constructor - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Response(null, 0) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Response(null, { status: 99 }) }, RangeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Response(null, { status: 600 }) }, RangeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Response(null, { status: '600' }) }, RangeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Response(null, { statusText: '\u0000' @@ -42,7 +40,7 @@ test('arg validation', async () => { }, TypeError) for (const nullStatus of [204, 205, 304]) { - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line new Response(new ArrayBuffer(16), { status: nullStatus @@ -50,53 +48,53 @@ test('arg validation', async () => { }, TypeError) } - assert.doesNotThrow(() => { + t.assert.doesNotThrow(() => { Response.prototype[Symbol.toStringTag].charAt(0) }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.prototype.type.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.prototype.url.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.prototype.redirected.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.prototype.status.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.prototype.ok.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.prototype.statusText.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.prototype.headers.toString() }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line no-unused-expressions Response.prototype.body }, TypeError) - assert.throws(() => { + t.assert.throws(() => { // eslint-disable-next-line no-unused-expressions Response.prototype.bodyUsed }, TypeError) - assert.throws(() => { + t.assert.throws(() => { Response.prototype.clone.call(null) }, TypeError) - await assert.rejects( + await t.assert.rejects( new Response('http://localhost').text.call({ blob () { return { @@ -108,24 +106,24 @@ test('arg validation', async () => { }), TypeError) }) -test('response clone', () => { +test('response clone', (t) => { // https://github.com/nodejs/undici/issues/1122 const response1 = new Response(null, { status: 201 }) const response2 = new Response(undefined, { status: 201 }) - assert.deepStrictEqual(response1.body, response1.clone().body) - assert.deepStrictEqual(response2.body, response2.clone().body) - assert.strictEqual(response2.body, null) + t.assert.deepStrictEqual(response1.body, response1.clone().body) + t.assert.deepStrictEqual(response2.body, response2.clone().body) + t.assert.strictEqual(response2.body, null) }) -test('Symbol.toStringTag', () => { +test('Symbol.toStringTag', (t) => { const resp = new Response() - assert.strictEqual(resp[Symbol.toStringTag], 'Response') - assert.strictEqual(Response.prototype[Symbol.toStringTag], 'Response') + t.assert.strictEqual(resp[Symbol.toStringTag], 'Response') + t.assert.strictEqual(Response.prototype[Symbol.toStringTag], 'Response') }) -test('async iterable body', async () => { +test('async iterable body', async (t) => { const asyncIterable = { async * [Symbol.asyncIterator] () { yield 'a' @@ -135,11 +133,11 @@ test('async iterable body', async () => { } const response = new Response(asyncIterable) - assert.strictEqual(await response.text(), 'abc') + t.assert.strictEqual(await response.text(), 'abc') }) // https://github.com/nodejs/node/pull/43752#issuecomment-1179678544 -test('Modifying headers using Headers.prototype.set', () => { +test('Modifying headers using Headers.prototype.set', (t) => { const response = new Response('body', { headers: { 'content-type': 'test/test', @@ -152,22 +150,22 @@ test('Modifying headers using Headers.prototype.set', () => { response.headers.set('content-type', 'application/wasm') response.headers.set('Content-Encoding', 'world/hello') - assert.strictEqual(response.headers.get('content-type'), 'application/wasm') - assert.strictEqual(response.headers.get('Content-Encoding'), 'world/hello') + t.assert.strictEqual(response.headers.get('content-type'), 'application/wasm') + t.assert.strictEqual(response.headers.get('Content-Encoding'), 'world/hello') response2.headers.delete('content-type') response2.headers.delete('Content-Encoding') - assert.strictEqual(response2.headers.get('content-type'), null) - assert.strictEqual(response2.headers.get('Content-Encoding'), null) + t.assert.strictEqual(response2.headers.get('content-type'), null) + t.assert.strictEqual(response2.headers.get('Content-Encoding'), null) }) // https://github.com/nodejs/node/issues/43838 -test('constructing a Response with a ReadableStream body', async (t) => { +describe('constructing a Response with a ReadableStream body', () => { const text = '{"foo":"bar"}' const uint8 = new TextEncoder().encode(text) - await t.test('Readable stream with Uint8Array chunks', async () => { + test('Readable stream with Uint8Array chunks', async (t) => { const readable = new ReadableStream({ start (controller) { controller.enqueue(uint8) @@ -179,12 +177,12 @@ test('constructing a Response with a ReadableStream body', async (t) => { const response2 = response1.clone() const response3 = response1.clone() - assert.strictEqual(await response1.text(), text) - assert.deepStrictEqual(await response2.arrayBuffer(), uint8.buffer) - assert.deepStrictEqual(await response3.json(), JSON.parse(text)) + t.assert.strictEqual(await response1.text(), text) + t.assert.deepStrictEqual(await response2.arrayBuffer(), uint8.buffer) + t.assert.deepStrictEqual(await response3.json(), JSON.parse(text)) }) - await t.test('.arrayBuffer() correctly clones multiple buffers', async () => { + test('.arrayBuffer() correctly clones multiple buffers', async (t) => { const buffer = Buffer.allocUnsafeSlow(2 * 1024 - 2) const readable = new ReadableStream({ start (controller) { @@ -196,10 +194,10 @@ test('constructing a Response with a ReadableStream body', async (t) => { }) const response = new Response(readable) - assert.deepStrictEqual(await response.arrayBuffer(), buffer.buffer) + t.assert.deepStrictEqual(await response.arrayBuffer(), buffer.buffer) }) - await t.test('Readable stream with non-Uint8Array chunks', async () => { + test('Readable stream with non-Uint8Array chunks', async (t) => { const readable = new ReadableStream({ start (controller) { controller.enqueue(text) // string @@ -209,10 +207,10 @@ test('constructing a Response with a ReadableStream body', async (t) => { const response = new Response(readable) - await assert.rejects(response.text(), TypeError) + await t.assert.rejects(response.text(), TypeError) }) - await t.test('Readable with ArrayBuffer chunk still throws', async () => { + test('Readable with ArrayBuffer chunk still throws', async (t) => { const readable = new ReadableStream({ start (controller) { controller.enqueue(uint8.buffer) @@ -225,51 +223,51 @@ test('constructing a Response with a ReadableStream body', async (t) => { const response3 = response1.clone() const response4 = response1.clone() - await assert.rejects(response1.arrayBuffer(), TypeError) - await assert.rejects(response2.text(), TypeError) - await assert.rejects(response3.json(), TypeError) - await assert.rejects(response4.blob(), TypeError) + await t.assert.rejects(response1.arrayBuffer(), TypeError) + await t.assert.rejects(response2.text(), TypeError) + await t.assert.rejects(response3.json(), TypeError) + await t.assert.rejects(response4.blob(), TypeError) }) }) // https://github.com/nodejs/undici/issues/2465 test('Issue#2465', async (t) => { - const { strictEqual } = tspl(t, { plan: 1 }) + t.plan(1) const response = new Response(new SharedArrayBuffer(0)) - strictEqual(await response.text(), '[object SharedArrayBuffer]') + t.assert.strictEqual(await response.text(), '[object SharedArrayBuffer]') }) -test('Check the Content-Type of invalid formData', async (t) => { - await t.test('_application/x-www-form-urlencoded', async (t) => { - const { rejects } = tspl(t, { plan: 1 }) +describe('Check the Content-Type of invalid formData', () => { + test('_application/x-www-form-urlencoded', async (t) => { + t.plan(1) const response = new Response('x=y', { headers: { 'content-type': '_application/x-www-form-urlencoded' } }) - await rejects(response.formData(), TypeError) + await t.assert.rejects(response.formData(), TypeError) }) - await t.test('_multipart/form-data', async (t) => { - const { rejects } = tspl(t, { plan: 1 }) + test('_multipart/form-data', async (t) => { + t.plan(1) const formData = new FormData() formData.append('x', 'y') const response = new Response(formData, { headers: { 'content-type': '_multipart/form-data' } }) - await rejects(response.formData(), TypeError) + await t.assert.rejects(response.formData(), TypeError) }) - await t.test('application/x-www-form-urlencoded_', async (t) => { - const { rejects } = tspl(t, { plan: 1 }) + test('application/x-www-form-urlencoded_', async (t) => { + t.plan(1) const response = new Response('x=y', { headers: { 'content-type': 'application/x-www-form-urlencoded_' } }) - await rejects(response.formData(), TypeError) + await t.assert.rejects(response.formData(), TypeError) }) - await t.test('multipart/form-data_', async (t) => { - const { rejects } = tspl(t, { plan: 1 }) + test('multipart/form-data_', async (t) => { + t.plan(1) const formData = new FormData() formData.append('x', 'y') const response = new Response(formData, { headers: { 'content-type': 'multipart/form-data_' } }) - await rejects(response.formData(), TypeError) + await t.assert.rejects(response.formData(), TypeError) }) }) -test('clone body garbage collection', async () => { +test('clone body garbage collection', async (t) => { if (typeof global.gc === 'undefined') { throw new Error('gc is not available. Run with \'--expose-gc\'.') } @@ -295,5 +293,5 @@ test('clone body garbage collection', async () => { global.gc() const cloneBody = ref.deref() - assert.equal(cloneBody, undefined, 'clone body was not garbage collected') + t.assert.strictEqual(cloneBody, undefined, 'clone body was not garbage collected') }) diff --git a/test/fetch/spread.js b/test/fetch/spread.js index ca05bea6e2a..12012614fc9 100644 --- a/test/fetch/spread.js +++ b/test/fetch/spread.js @@ -2,7 +2,6 @@ const undici = require('../..') const { test } = require('node:test') -const assert = require('node:assert') const { inspect } = require('node:util') test('spreading web classes yields empty objects', (t) => { @@ -11,7 +10,7 @@ test('spreading web classes yields empty objects', (t) => { new undici.Response(null), new undici.Request('http://a') ]) { - assert.deepStrictEqual({ ...object }, {}) + t.assert.deepStrictEqual({ ...object }, {}) } }) @@ -36,6 +35,6 @@ test('Objects only have an expected set of symbols on their prototypes', (t) => const symbols = Object.keys(Object.getOwnPropertyDescriptors(object.prototype)) .filter(v => typeof v === 'symbol') - assert(symbols.every(symbol => allowedSymbols.includes(symbol))) + t.assert.ok(symbols.every(symbol => allowedSymbols.includes(symbol))) } }) diff --git a/test/fetch/user-agent.js b/test/fetch/user-agent.js index 770f550725f..119bff175de 100644 --- a/test/fetch/user-agent.js +++ b/test/fetch/user-agent.js @@ -1,7 +1,6 @@ 'use strict' const { test } = require('node:test') -const assert = require('node:assert') const events = require('node:events') const http = require('node:http') const undici = require('../../') @@ -23,6 +22,26 @@ test('user-agent defaults correctly', async (t) => { undici.fetch(url).then((body) => body.json()) ]) - assert.strictEqual(nodeBuildJSON.userAgentHeader, 'node') - assert.strictEqual(undiciJSON.userAgentHeader, 'undici') + t.assert.strictEqual(nodeBuildJSON.userAgentHeader, 'node') + t.assert.strictEqual(undiciJSON.userAgentHeader, 'undici') +}) + +test('set user-agent for fetch', async (t) => { + const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.end(JSON.stringify({ userAgentHeader: req.headers['user-agent'] })) + }) + t.after(closeServerAsPromise(server)) + + server.listen(0) + await events.once(server, 'listening') + const url = `http://localhost:${server.address().port}` + const [nodeBuildJSON, undiciJSON] = await Promise.all([ + nodeBuild.fetch(url, { headers: { 'user-agent': 'AcmeCo Crawler - acme.co - node@acme.co' } }).then((body) => body.json()), + undici.fetch(url, { + headers: { 'user-agent': 'AcmeCo Crawler - acme.co - undici@acme.co' } + }).then((body) => body.json()) + ]) + + t.assert.strictEqual(nodeBuildJSON.userAgentHeader, 'AcmeCo Crawler - acme.co - node@acme.co') + t.assert.strictEqual(undiciJSON.userAgentHeader, 'AcmeCo Crawler - acme.co - undici@acme.co') }) diff --git a/test/fetch/util.js b/test/fetch/util.js index 0000d6afc0d..f5f23cb305b 100644 --- a/test/fetch/util.js +++ b/test/fetch/util.js @@ -1,28 +1,25 @@ 'use strict' const { describe, test } = require('node:test') -const assert = require('node:assert') -const { tspl } = require('@matteo.collina/tspl') const util = require('../../lib/web/fetch/util') const { HeadersList } = require('../../lib/web/fetch/headers') -const { createHash } = require('node:crypto') test('responseURL', (t) => { - const { ok } = tspl(t, { plan: 2 }) + t.plan(2) - ok(util.responseURL({ + t.assert.ok(util.responseURL({ urlList: [ new URL('http://asd'), new URL('http://fgh') ] })) - ok(!util.responseURL({ + t.assert.ok(!util.responseURL({ urlList: [] })) }) test('responseLocationURL', (t) => { - const { ok } = tspl(t, { plan: 3 }) + t.plan(3) const acceptHeaderList = new HeadersList() acceptHeaderList.append('Accept', '*/*') @@ -30,14 +27,14 @@ test('responseLocationURL', (t) => { const locationHeaderList = new HeadersList() locationHeaderList.append('Location', 'http://asd') - ok(!util.responseLocationURL({ + t.assert.ok(!util.responseLocationURL({ status: 200 })) - ok(!util.responseLocationURL({ + t.assert.ok(!util.responseLocationURL({ status: 301, headersList: acceptHeaderList })) - ok(util.responseLocationURL({ + t.assert.ok(util.responseLocationURL({ status: 301, headersList: locationHeaderList, urlList: [ @@ -48,15 +45,15 @@ test('responseLocationURL', (t) => { }) test('requestBadPort', (t) => { - const { strictEqual } = tspl(t, { plan: 3 }) + t.plan(3) - strictEqual('allowed', util.requestBadPort({ + t.assert.strictEqual('allowed', util.requestBadPort({ urlList: [new URL('https://asd')] })) - strictEqual('blocked', util.requestBadPort({ + t.assert.strictEqual('blocked', util.requestBadPort({ urlList: [new URL('http://asd:7')] })) - strictEqual('blocked', util.requestBadPort({ + t.assert.strictEqual('blocked', util.requestBadPort({ urlList: [new URL('https://asd:7')] })) }) @@ -64,7 +61,7 @@ test('requestBadPort', (t) => { // https://html.spec.whatwg.org/multipage/origin.html#same-origin // look at examples test('sameOrigin', async (t) => { - await t.test('first test', () => { + await t.test('first test', (t) => { const A = { protocol: 'https:', hostname: 'example.org', @@ -77,10 +74,10 @@ test('sameOrigin', async (t) => { port: '' } - assert.ok(util.sameOrigin(A, B)) + t.assert.ok(util.sameOrigin(A, B)) }) - await t.test('second test', () => { + await t.test('second test', (t) => { const A = { protocol: 'https:', hostname: 'example.org', @@ -93,28 +90,28 @@ test('sameOrigin', async (t) => { port: '420' } - assert.ok(!util.sameOrigin(A, B)) + t.assert.ok(!util.sameOrigin(A, B)) }) - await t.test('obviously shouldn\'t be equal', () => { - assert.ok(!util.sameOrigin( + await t.test('obviously shouldn\'t be equal', (t) => { + t.assert.ok(!util.sameOrigin( { protocol: 'http:', hostname: 'example.org' }, { protocol: 'https:', hostname: 'example.org' } )) - assert.ok(!util.sameOrigin( + t.assert.ok(!util.sameOrigin( { protocol: 'https:', hostname: 'example.org' }, { protocol: 'https:', hostname: 'example.com' } )) }) - await t.test('file:// urls', () => { + await t.test('file:// urls', (t) => { // urls with opaque origins should return true const a = new URL('file:///C:/undici') const b = new URL('file:///var/undici') - assert.ok(util.sameOrigin(a, b)) + t.assert.ok(util.sameOrigin(a, b)) }) }) @@ -145,18 +142,17 @@ test('isURLPotentiallyTrustworthy', (t) => { 'something:8080' ] - // t.plan(valid.length + invalid.length + 1) - const { ok } = tspl(t, { plan: valid.length + invalid.length + 1 }) - ok(!util.isURLPotentiallyTrustworthy('string')) + t.plan(valid.length + invalid.length + 1) + t.assert.ok(!util.isURLPotentiallyTrustworthy('string')) for (const url of valid) { const instance = new URL(url) - ok(util.isURLPotentiallyTrustworthy(instance), instance) + t.assert.ok(util.isURLPotentiallyTrustworthy(instance), instance) } for (const url of invalid) { const instance = new URL(url) - ok(!util.isURLPotentiallyTrustworthy(instance)) + t.assert.ok(!util.isURLPotentiallyTrustworthy(instance)) } }) @@ -204,6 +200,8 @@ describe('setRequestReferrerPolicyOnRedirect', () => { ] ].forEach(([title, responseReferrerPolicy, expected]) => { test(title, (t) => { + t.plan(1) + const request = { referrerPolicy: 'no-referrer, strict-origin-when-cross-origin' } @@ -212,8 +210,6 @@ describe('setRequestReferrerPolicyOnRedirect', () => { headersList: new HeadersList() } - const { strictEqual } = tspl(t, { plan: 1 }) - actualResponse.headersList.append('Connection', 'close') actualResponse.headersList.append('Location', 'https://some-location.com/redirect') if (responseReferrerPolicy) { @@ -221,134 +217,69 @@ describe('setRequestReferrerPolicyOnRedirect', () => { } util.setRequestReferrerPolicyOnRedirect(request, actualResponse) - strictEqual(request.referrerPolicy, expected) + t.assert.strictEqual(request.referrerPolicy, expected) }) }) }) -test('parseMetadata', async (t) => { - await t.test('should parse valid metadata with option', () => { - const body = 'Hello world!' - const hash256 = createHash('sha256').update(body).digest('base64') - const hash384 = createHash('sha384').update(body).digest('base64') - const hash512 = createHash('sha512').update(body).digest('base64') - - const validMetadata = `sha256-${hash256} !@ sha384-${hash384} !@ sha512-${hash512} !@` - const result = util.parseMetadata(validMetadata) - - assert.deepEqual(result, [ - { algo: 'sha256', hash: hash256.replace(/=/g, '') }, - { algo: 'sha384', hash: hash384.replace(/=/g, '') }, - { algo: 'sha512', hash: hash512.replace(/=/g, '') } - ]) - }) - - await t.test('should parse valid metadata with non ASCII chars option', () => { - const body = 'Hello world!' - const hash256 = createHash('sha256').update(body).digest('base64') - const hash384 = createHash('sha384').update(body).digest('base64') - const hash512 = createHash('sha512').update(body).digest('base64') - - const validMetadata = `sha256-${hash256} !© sha384-${hash384} !€ sha512-${hash512} !µ` - const result = util.parseMetadata(validMetadata) - - assert.deepEqual(result, [ - { algo: 'sha256', hash: hash256.replace(/=/g, '') }, - { algo: 'sha384', hash: hash384.replace(/=/g, '') }, - { algo: 'sha512', hash: hash512.replace(/=/g, '') } - ]) - }) - - await t.test('should parse valid metadata without option', () => { - const body = 'Hello world!' - const hash256 = createHash('sha256').update(body).digest('base64') - const hash384 = createHash('sha384').update(body).digest('base64') - const hash512 = createHash('sha512').update(body).digest('base64') - - const validMetadata = `sha256-${hash256} sha384-${hash384} sha512-${hash512}` - const result = util.parseMetadata(validMetadata) - - assert.deepEqual(result, [ - { algo: 'sha256', hash: hash256.replace(/=/g, '') }, - { algo: 'sha384', hash: hash384.replace(/=/g, '') }, - { algo: 'sha512', hash: hash512.replace(/=/g, '') } - ]) - }) - - await t.test('should set hash as undefined when invalid base64 chars are provided', () => { - const body = 'Hello world!' - const hash256 = createHash('sha256').update(body).digest('base64') - const invalidHash384 = 'zifp5hE1Xl5LQQqQz[]Bq/iaq9Wb6jVb//T7EfTmbXD2aEP5c2ZdJr9YTDfcTE1ZH+' - const hash512 = createHash('sha512').update(body).digest('base64') - - const validMetadata = `sha256-${hash256} sha384-${invalidHash384} sha512-${hash512}` - const result = util.parseMetadata(validMetadata) - - assert.deepEqual(result, [ - { algo: 'sha256', hash: hash256.replace(/=/g, '') }, - { algo: 'sha384', hash: undefined }, - { algo: 'sha512', hash: hash512.replace(/=/g, '') } - ]) - }) -}) - describe('urlHasHttpsScheme', () => { const { urlHasHttpsScheme } = util - test('should return false for http url', () => { - assert.strictEqual(urlHasHttpsScheme('http://example.com'), false) + test('should return false for http url', (t) => { + t.assert.strictEqual(urlHasHttpsScheme('http://example.com'), false) }) - test('should return true for https url', () => { - assert.strictEqual(urlHasHttpsScheme('https://example.com'), true) + test('should return true for https url', (t) => { + t.assert.strictEqual(urlHasHttpsScheme('https://example.com'), true) }) - test('should return false for http object', () => { - assert.strictEqual(urlHasHttpsScheme({ protocol: 'http:' }), false) + test('should return false for http object', (t) => { + t.assert.strictEqual(urlHasHttpsScheme({ protocol: 'http:' }), false) }) - test('should return true for https object', () => { - assert.strictEqual(urlHasHttpsScheme({ protocol: 'https:' }), true) + test('should return true for https object', (t) => { + t.assert.strictEqual(urlHasHttpsScheme({ protocol: 'https:' }), true) }) }) describe('isValidHeaderValue', () => { const { isValidHeaderValue } = util - test('should return true for valid string', () => { - assert.strictEqual(isValidHeaderValue('valid123'), true) - assert.strictEqual(isValidHeaderValue('va lid123'), true) - assert.strictEqual(isValidHeaderValue('va\tlid123'), true) + test('should return true for valid string', (t) => { + t.assert.strictEqual(isValidHeaderValue('valid123'), true) + t.assert.strictEqual(isValidHeaderValue('va lid123'), true) + t.assert.strictEqual(isValidHeaderValue('va\tlid123'), true) }) - test('should return false for string containing NUL', () => { - assert.strictEqual(isValidHeaderValue('invalid\0'), false) - assert.strictEqual(isValidHeaderValue('in\0valid'), false) - assert.strictEqual(isValidHeaderValue('\0invalid'), false) + test('should return false for string containing NUL', (t) => { + t.assert.strictEqual(isValidHeaderValue('invalid\0'), false) + t.assert.strictEqual(isValidHeaderValue('in\0valid'), false) + t.assert.strictEqual(isValidHeaderValue('\0invalid'), false) }) - test('should return false for string containing CR', () => { - assert.strictEqual(isValidHeaderValue('invalid\r'), false) - assert.strictEqual(isValidHeaderValue('in\rvalid'), false) - assert.strictEqual(isValidHeaderValue('\rinvalid'), false) + test('should return false for string containing CR', (t) => { + t.assert.strictEqual(isValidHeaderValue('invalid\r'), false) + t.assert.strictEqual(isValidHeaderValue('in\rvalid'), false) + t.assert.strictEqual(isValidHeaderValue('\rinvalid'), false) }) - test('should return false for string containing LF', () => { - assert.strictEqual(isValidHeaderValue('invalid\n'), false) - assert.strictEqual(isValidHeaderValue('in\nvalid'), false) - assert.strictEqual(isValidHeaderValue('\ninvalid'), false) + test('should return false for string containing LF', (t) => { + t.assert.strictEqual(isValidHeaderValue('invalid\n'), false) + t.assert.strictEqual(isValidHeaderValue('in\nvalid'), false) + t.assert.strictEqual(isValidHeaderValue('\ninvalid'), false) }) - test('should return false for string with leading TAB', () => { - assert.strictEqual(isValidHeaderValue('\tinvalid'), false) + test('should return false for string with leading TAB', (t) => { + t.assert.strictEqual(isValidHeaderValue('\tinvalid'), false) }) - test('should return false for string with trailing TAB', () => { - assert.strictEqual(isValidHeaderValue('invalid\t'), false) + test('should return false for string with trailing TAB', (t) => { + t.assert.strictEqual(isValidHeaderValue('invalid\t'), false) }) - test('should return false for string with leading SPACE', () => { - assert.strictEqual(isValidHeaderValue(' invalid'), false) + test('should return false for string with leading SPACE', (t) => { + t.assert.strictEqual(isValidHeaderValue(' invalid'), false) }) - test('should return false for string with trailing SPACE', () => { - assert.strictEqual(isValidHeaderValue('invalid '), false) + test('should return false for string with trailing SPACE', (t) => { + t.assert.strictEqual(isValidHeaderValue('invalid '), false) }) }) describe('isOriginIPPotentiallyTrustworthy()', () => { [ + ['', false], ['0000:0000:0000:0000:0000:0000:0000:0001', true], ['0001:0000:0000:0000:0000:0000:0000:0001', false], ['0000:0000:0000:0000:0000:0000::0001', true], @@ -359,19 +290,23 @@ describe('isOriginIPPotentiallyTrustworthy()', () => { ['0000:0000:0000::0001', true], ['0000:0000::0001', true], ['0000::0001', true], + ['::1001', false], ['::0001', true], + ['::0011', false], ['::1', true], ['[::1]', true], + ['[::1', false], + ['::1]', false], ['::2', false], ['::', false], + ['126.0.0.0', false], + ['127.0.0.0', true], ['127.0.0.1', true], ['127.255.255.255', true], - ['128.255.255.255', false], - ['127.0.0.1', true], - ['127.0.0.0', false] + ['128.255.255.255', false] ].forEach(([ip, expected]) => { - test(`${ip} is ${expected ? '' : 'not '}potentially trustworthy`, () => { - assert.strictEqual(util.isOriginIPPotentiallyTrustworthy(ip), expected) + test(`${ip} is ${expected ? '' : 'not '}potentially trustworthy`, (t) => { + t.assert.strictEqual(util.isOriginIPPotentiallyTrustworthy(ip), expected) }) }) }) diff --git a/test/fixtures/cache-tests b/test/fixtures/cache-tests new file mode 160000 index 00000000000..1000acb7071 --- /dev/null +++ b/test/fixtures/cache-tests @@ -0,0 +1 @@ +Subproject commit 1000acb7071ed401a654160f13d27c2a8a0139cc diff --git a/test/fixtures/cache-tests/LICENSE b/test/fixtures/cache-tests/LICENSE deleted file mode 100644 index f377bbc9118..00000000000 --- a/test/fixtures/cache-tests/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2018, Mark Nottingham -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/test/fixtures/cache-tests/results/apache.json b/test/fixtures/cache-tests/results/apache.json deleted file mode 100644 index 86c732f6e36..00000000000 --- a/test/fixtures/cache-tests/results/apache.json +++ /dev/null @@ -1,687 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": true, - "304-etag-update-response-Clear-Site-Data": true, - "304-etag-update-response-Content-Encoding": [ - "Setup", - "retry" - ], - "304-etag-update-response-Content-Foo": true, - "304-etag-update-response-Content-Length": [ - "Setup", - "retry" - ], - "304-etag-update-response-Content-Location": [ - "Setup", - "retry" - ], - "304-etag-update-response-Content-MD5": [ - "Setup", - "retry" - ], - "304-etag-update-response-Content-Range": [ - "Setup", - "retry" - ], - "304-etag-update-response-Content-Security-Policy": true, - "304-etag-update-response-Content-Type": [ - "Setup", - "retry" - ], - "304-etag-update-response-ETag": [ - "Setup", - "retry" - ], - "304-etag-update-response-Expires": true, - "304-etag-update-response-Public-Key-Pins": true, - "304-etag-update-response-Set-Cookie": true, - "304-etag-update-response-Set-Cookie2": true, - "304-etag-update-response-Test-Header": true, - "304-etag-update-response-X-Content-Foo": true, - "304-etag-update-response-X-Frame-Options": true, - "304-etag-update-response-X-Test-Header": true, - "304-etag-update-response-X-XSS-Protection": true, - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": true, - "age-parse-large": true, - "age-parse-large-minus-one": true, - "age-parse-larger": true, - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-suffix-twoline": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": true, - "cc-resp-no-cache-revalidate-fresh": true, - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-shared": true, - "ccreq-ma0": true, - "ccreq-ma1": true, - "ccreq-magreaterage": true, - "ccreq-max-stale": true, - "ccreq-max-stale-age": true, - "ccreq-min-fresh": true, - "ccreq-min-fresh-age": true, - "ccreq-no-cache": true, - "ccreq-no-cache-etag": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "ccreq-no-cache-lm": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "ccreq-no-store": true, - "ccreq-oic": true, - "cdn-cc-invalid-sh-type-unknown": true, - "cdn-cc-invalid-sh-type-wrong": true, - "cdn-date-update-exceed": true, - "cdn-expires-update-exceed": [ - "Assertion", - "Response 2 header Expires is \"null\", not \"Wed, 29 Jan 2025 03:37:47 GMT\"" - ], - "cdn-fresh-cc-nostore": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-0": true, - "cdn-max-age-0-expires": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-age": true, - "cdn-max-age-case-insensitive": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-cc-max-age-invalid-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-extension": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-long-cc-max-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-max": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-max-plus": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-short-cc-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-space-after-equals": true, - "cdn-max-age-space-before-equals": true, - "cdn-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-no-store-cc-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-private": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-remove-age-exceed": [ - "Assertion", - "Response 2 Age header not present." - ], - "cdn-remove-header": true, - "conditional-304-etag": true, - "conditional-etag-forward": true, - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-precedence": true, - "conditional-etag-quoted-respond-unquoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-strong-respond": true, - "conditional-etag-strong-respond-multiple-first": true, - "conditional-etag-strong-respond-multiple-last": true, - "conditional-etag-strong-respond-multiple-second": true, - "conditional-etag-strong-respond-obs-text": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-unquoted-respond-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-unquoted-respond-unquoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": true, - "conditional-etag-weak-generate-weak": true, - "conditional-etag-weak-respond": true, - "conditional-etag-weak-respond-backslash": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-weak-respond-lowercase": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-weak-respond-omit-slash": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-lm-fresh": true, - "conditional-lm-fresh-earlier": true, - "conditional-lm-fresh-no-lm": [ - "Setup", - "Response 2 does not come from cache" - ], - "conditional-lm-fresh-rfc850": true, - "conditional-lm-stale": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": true, - "freshness-expires-age-slow-date": true, - "freshness-expires-ansi-c": true, - "freshness-expires-far-future": true, - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": true, - "freshness-expires-invalid-2-digit-year": true, - "freshness-expires-invalid-aest": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": true, - "freshness-expires-invalid-multiple-lines": true, - "freshness-expires-invalid-multiple-spaces": true, - "freshness-expires-invalid-no-comma": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-time-periods": true, - "freshness-expires-invalid-utc": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-old-date": true, - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": true, - "freshness-expires-wrong-case-month": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-tz": true, - "freshness-expires-wrong-case-weekday": true, - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": true, - "freshness-max-age-decimal-five": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-decimal-zero": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-s-maxage-shared-longer": true, - "freshness-max-age-s-maxage-shared-longer-multiple": true, - "freshness-max-age-s-maxage-shared-longer-reversed": true, - "freshness-max-age-s-maxage-shared-shorter": true, - "freshness-max-age-s-maxage-shared-shorter-expires": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": true, - "freshness-max-age-space-before-equals": true, - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-fresh-stale-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sameline": true, - "freshness-max-age-two-stale-fresh-sepline": true, - "freshness-none": true, - "freshness-s-maxage-shared": true, - "head-200-freshness-update": [ - "Assertion", - "Response 3 does not come from cache" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-410-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-writethrough": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": true, - "headers-omit-headers-listed-in-Connection": true, - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": true, - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": true, - "headers-store-Set-Cookie2": true, - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": true, - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": true, - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": true, - "heuristic-204-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Setup", - "Response 2 status is 500, not 599" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": true, - "heuristic-delta-1800": true, - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": true, - "heuristic-delta-3600": true, - "heuristic-delta-43200": true, - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": true, - "heuristic-delta-600": true, - "heuristic-delta-86400": true, - "invalidate-DELETE": true, - "invalidate-DELETE-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-failed": true, - "invalidate-DELETE-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-failed": true, - "invalidate-M-SEARCH-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST": true, - "invalidate-POST-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-failed": true, - "invalidate-POST-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT": true, - "invalidate-PUT-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-failed": true, - "invalidate-PUT-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 age header not present." - ], - "other-age-gen": true, - "other-age-update-expires": true, - "other-age-update-max-age": true, - "other-authorization": true, - "other-authorization-must-revalidate": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-authorization-public": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-authorization-smaxage": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-cookie": true, - "other-date-update": [ - "Assertion", - "Response 2 header Date is \"Wed, 29 Jan 2025 03:37:41 GMT\", not \"Wed, 29 Jan 2025 03:37:38 GMT\"" - ], - "other-date-update-expires": [ - "Assertion", - "Response 2 header Date is \"Wed, 29 Jan 2025 03:37:41 GMT\", not \"Wed, 29 Jan 2025 03:37:38 GMT\"" - ], - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": true, - "other-set-cookie": true, - "partial-store-complete-reuse-partial": true, - "partial-store-complete-reuse-partial-no-last": true, - "partial-store-complete-reuse-partial-suffix": true, - "partial-store-partial-complete": [ - "Assertion", - "Request 2 header range is \"undefined\", not \"bytes=5-\"" - ], - "partial-store-partial-reuse-partial": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-use-headers": true, - "partial-use-stored-headers": true, - "pragma-request-extension": true, - "pragma-request-no-cache": [ - "Assertion", - "Response 2 does not come from cache" - ], - "pragma-response-extension": true, - "pragma-response-no-cache": [ - "Assertion", - "Response 2 does not come from cache" - ], - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": true, - "stale-close": [ - "TypeError", - "fetch failed" - ], - "stale-close-must-revalidate": [ - "TypeError", - "fetch failed" - ], - "stale-close-no-cache": [ - "TypeError", - "fetch failed" - ], - "stale-close-proxy-revalidate": [ - "TypeError", - "fetch failed" - ], - "stale-close-s-maxage=2": [ - "TypeError", - "fetch failed" - ], - "stale-sie-503": [ - "TypeError", - "fetch failed" - ], - "stale-sie-close": true, - "stale-warning-become": [ - "TypeError", - "fetch failed" - ], - "stale-warning-stored": [ - "TypeError", - "fetch failed" - ], - "stale-while-revalidate": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-while-revalidate-window": [ - "Setup", - "Response 2 does not come from cache" - ], - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-204-stale": true, - "status-299-fresh": [ - "Setup", - "Response 2 status is 500, not 299" - ], - "status-299-stale": true, - "status-301-fresh": true, - "status-301-stale": true, - "status-302-fresh": true, - "status-302-stale": true, - "status-303-fresh": true, - "status-303-stale": true, - "status-307-fresh": true, - "status-307-stale": true, - "status-308-fresh": true, - "status-308-stale": true, - "status-400-fresh": true, - "status-400-stale": true, - "status-404-fresh": true, - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": [ - "Setup", - "Response 2 status is 500, not 499" - ], - "status-499-stale": true, - "status-500-fresh": true, - "status-500-stale": true, - "status-502-fresh": true, - "status-502-stale": true, - "status-503-fresh": true, - "status-503-stale": true, - "status-504-fresh": true, - "status-504-stale": true, - "status-599-fresh": [ - "Setup", - "Response 2 status is 500, not 599" - ], - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": true, - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": true, - "vary-syntax-empty-star-lines": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-foo-star": true, - "vary-syntax-star": true, - "vary-syntax-star-foo": true, - "vary-syntax-star-star": true, - "vary-syntax-star-star-lines": true -} diff --git a/test/fixtures/cache-tests/results/caddy.json b/test/fixtures/cache-tests/results/caddy.json deleted file mode 100644 index d45ef15d4a9..00000000000 --- a/test/fixtures/cache-tests/results/caddy.json +++ /dev/null @@ -1,828 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Clear-Site-Data": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Content-Encoding": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Content-Foo": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Content-Length": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Content-Location": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Content-MD5": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Content-Range": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Content-Security-Policy": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Content-Type": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-ETag": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Expires": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Public-Key-Pins": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Set-Cookie": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Set-Cookie2": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Test-Header": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-X-Content-Foo": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-X-Frame-Options": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-X-Test-Header": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-X-XSS-Protection": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-lm-use-stored-Test-Header": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": true, - "age-parse-large": true, - "age-parse-large-minus-one": true, - "age-parse-larger": true, - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-suffix-twoline": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": [ - "Assertion", - "Request 3 should have been conditional, but it was not." - ], - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "cc-resp-no-cache-revalidate-fresh": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-shared": true, - "ccreq-ma0": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-ma1": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-magreaterage": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-max-stale": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-max-stale-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-min-fresh": true, - "ccreq-min-fresh-age": true, - "ccreq-no-cache": true, - "ccreq-no-cache-etag": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "ccreq-no-cache-lm": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "ccreq-no-store": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-oic": [ - "Assertion", - "Response 1 status is 200, not 504" - ], - "cdn-cc-invalid-sh-type-unknown": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-cc-invalid-sh-type-wrong": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-date-update-exceed": true, - "cdn-expires-update-exceed": [ - "Assertion", - "Response 2 header Expires is \"null\", not \"Tue, 21 Jan 2025 00:23:23 GMT\"" - ], - "cdn-fresh-cc-nostore": true, - "cdn-max-age": true, - "cdn-max-age-0": true, - "cdn-max-age-0-expires": true, - "cdn-max-age-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-case-insensitive": true, - "cdn-max-age-cc-max-age-invalid-expires": true, - "cdn-max-age-expires": true, - "cdn-max-age-extension": true, - "cdn-max-age-long-cc-max-age": true, - "cdn-max-age-max": true, - "cdn-max-age-max-plus": true, - "cdn-max-age-short-cc-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-space-after-equals": true, - "cdn-max-age-space-before-equals": true, - "cdn-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-no-store-cc-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-private": true, - "cdn-remove-age-exceed": [ - "Assertion", - "Response 2 Age header not present." - ], - "cdn-remove-header": true, - "conditional-304-etag": true, - "conditional-etag-forward": true, - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-precedence": true, - "conditional-etag-quoted-respond-unquoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-strong-generate": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "conditional-etag-strong-respond": true, - "conditional-etag-strong-respond-multiple-first": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-strong-respond-multiple-last": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-strong-respond-multiple-second": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-strong-respond-obs-text": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-unquoted-respond-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-etag-unquoted-respond-unquoted": true, - "conditional-etag-vary-headers": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "conditional-etag-vary-headers-mismatch": true, - "conditional-etag-weak-generate-weak": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "conditional-etag-weak-respond": true, - "conditional-etag-weak-respond-backslash": true, - "conditional-etag-weak-respond-lowercase": true, - "conditional-etag-weak-respond-omit-slash": true, - "conditional-lm-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-lm-fresh-earlier": [ - "Assertion", - "Response 2 does not come from cache" - ], - "conditional-lm-fresh-no-lm": [ - "Setup", - "Response 2 does not come from cache" - ], - "conditional-lm-fresh-rfc850": [ - "Setup", - "Response 2 does not come from cache" - ], - "conditional-lm-stale": true, - "freshness-expires-32bit": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-age-fast-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-age-slow-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-ansi-c": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-far-future": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": true, - "freshness-expires-invalid-2-digit-year": true, - "freshness-expires-invalid-aest": true, - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": true, - "freshness-expires-invalid-multiple-lines": true, - "freshness-expires-invalid-multiple-spaces": true, - "freshness-expires-invalid-no-comma": true, - "freshness-expires-invalid-time-periods": true, - "freshness-expires-invalid-utc": true, - "freshness-expires-old-date": true, - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-month": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-tz": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-weekday": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": true, - "freshness-max-age-decimal-five": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-decimal-zero": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": true, - "freshness-max-age-s-maxage-shared-longer": true, - "freshness-max-age-s-maxage-shared-longer-multiple": true, - "freshness-max-age-s-maxage-shared-longer-reversed": true, - "freshness-max-age-s-maxage-shared-shorter": true, - "freshness-max-age-s-maxage-shared-shorter-expires": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": true, - "freshness-max-age-space-before-equals": true, - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-fresh-stale-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sameline": true, - "freshness-max-age-two-stale-fresh-sepline": true, - "freshness-none": true, - "freshness-s-maxage-shared": true, - "head-200-freshness-update": [ - "Assertion", - "Response 3 does not come from cache" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-410-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-writethrough": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Connection": true, - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": true, - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": true, - "headers-store-Set-Cookie2": true, - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": [ - "Setup", - "Response 1 status is 502, not 200" - ], - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-204-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1800": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-3600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-43200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-86400": [ - "Assertion", - "Response 2 does not come from cache" - ], - "invalidate-DELETE": true, - "invalidate-DELETE-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-failed": true, - "invalidate-DELETE-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH": true, - "invalidate-M-SEARCH-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-failed": true, - "invalidate-M-SEARCH-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST": true, - "invalidate-POST-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-failed": true, - "invalidate-POST-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT": true, - "invalidate-PUT-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-failed": true, - "invalidate-PUT-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 age header not present." - ], - "other-age-gen": true, - "other-age-update-expires": true, - "other-age-update-max-age": true, - "other-authorization": true, - "other-authorization-must-revalidate": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-authorization-public": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-authorization-smaxage": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-cookie": true, - "other-date-update": true, - "other-date-update-expires": true, - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-set-cookie": true, - "partial-store-complete-reuse-partial": [ - "Assertion", - "Response 2 status is 200, not 206" - ], - "partial-store-complete-reuse-partial-no-last": [ - "Assertion", - "Response 2 status is 200, not 206" - ], - "partial-store-complete-reuse-partial-suffix": [ - "Assertion", - "Response 2 status is 200, not 206" - ], - "partial-store-partial-complete": [ - "Setup", - "Response 2 status is 206, not 200" - ], - "partial-store-partial-reuse-partial": true, - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response body is \"01234\", not \"234\"" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response body is \"01234\", not \"234\"" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response body is \"01234\", not \"4\"" - ], - "partial-use-headers": [ - "Setup", - "Response 2 status is 200, not 206" - ], - "partial-use-stored-headers": [ - "Setup", - "Response 2 status is 200, not 206" - ], - "pragma-request-extension": true, - "pragma-request-no-cache": true, - "pragma-response-extension": true, - "pragma-response-no-cache": true, - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close-must-revalidate": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-proxy-revalidate": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-s-maxage=2": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-sie-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-sie-close": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-warning-become": [ - "Setup", - "Response 2 does not come from cache" - ], - "stale-warning-stored": [ - "Setup", - "Response 2 does not come from cache" - ], - "stale-while-revalidate": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-while-revalidate-window": [ - "Setup", - "Response 2 does not come from cache" - ], - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": true, - "status-204-stale": true, - "status-299-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-299-stale": true, - "status-301-fresh": true, - "status-301-stale": true, - "status-302-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-302-stale": true, - "status-303-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-303-stale": true, - "status-307-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-307-stale": true, - "status-308-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-308-stale": true, - "status-400-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-400-stale": true, - "status-404-fresh": true, - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-499-stale": true, - "status-500-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-500-stale": true, - "status-502-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-502-stale": true, - "status-503-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-503-stale": true, - "status-504-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-504-stale": true, - "status-599-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": true, - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": true, - "vary-syntax-empty-star-lines": true, - "vary-syntax-foo-star": true, - "vary-syntax-star": true, - "vary-syntax-star-foo": true, - "vary-syntax-star-star": true, - "vary-syntax-star-star-lines": true -} diff --git a/test/fixtures/cache-tests/results/chrome.json b/test/fixtures/cache-tests/results/chrome.json deleted file mode 100644 index f243e299015..00000000000 --- a/test/fixtures/cache-tests/results/chrome.json +++ /dev/null @@ -1,599 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": true, - "304-etag-update-response-Clear-Site-Data": [ - "Assertion", - "Response 2 header Clear-Site-Data is \"null\", not \"cookies\"" - ], - "304-etag-update-response-Content-Encoding": [ - "Assertion", - "Response 2 header Content-Encoding is \"arizqhypgxofwne\", not \"askcumewogyqias\"" - ], - "304-etag-update-response-Content-Foo": true, - "304-etag-update-response-Content-Length": true, - "304-etag-update-response-Content-Location": [ - "Assertion", - "Response 2 header Content-Location is \"/foo\", not \"/bar\"" - ], - "304-etag-update-response-Content-MD5": [ - "Assertion", - "Response 2 header Content-MD5 is \"rL0Y20zC+Fzt72VPzMSk2A==\", not \"N7UdGUp1E+RbVvZSTy1R8g==\"" - ], - "304-etag-update-response-Content-Range": [ - "Assertion", - "Response 2 header Content-Range is \"apetixmbqfujync\", not \"aqgwmcsiyoeukaq\"" - ], - "304-etag-update-response-Content-Security-Policy": true, - "304-etag-update-response-Content-Type": [ - "Assertion", - "Response 2 header Content-Type is \"text/plain\", not \"text/plain;charset=utf-8\"" - ], - "304-etag-update-response-ETag": [ - "Assertion", - "Response 2 header ETag is \"\"abcdef\"\", not \"\"ghijkl\"\"" - ], - "304-etag-update-response-Expires": true, - "304-etag-update-response-Public-Key-Pins": true, - "304-etag-update-response-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "304-etag-update-response-Set-Cookie2": [ - "Assertion", - "Response 2 header Set-Cookie2 is \"null\", not \"a=c\"" - ], - "304-etag-update-response-Test-Header": true, - "304-etag-update-response-X-Content-Foo": [ - "Assertion", - "Response 2 header X-Content-Foo is \"azyxwvutsrqponm\", not \"aaaaaaaaaaaaaaa\"" - ], - "304-etag-update-response-X-Frame-Options": [ - "Assertion", - "Response 2 header X-Frame-Options is \"deny\", not \"sameorigin\"" - ], - "304-etag-update-response-X-Test-Header": true, - "304-etag-update-response-X-XSS-Protection": [ - "Assertion", - "Response 2 header X-XSS-Protection is \"1\", not \"1; mode=block\"" - ], - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": true, - "age-parse-large": true, - "age-parse-large-minus-one": true, - "age-parse-larger": true, - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": true, - "age-parse-suffix-twoline": true, - "cc-resp-immutable-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cc-resp-immutable-stale": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": true, - "cc-resp-no-cache-revalidate-fresh": true, - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-private": true, - "ccreq-ma0": true, - "ccreq-ma1": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-magreaterage": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-max-stale": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-max-stale-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-min-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-min-fresh-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache": true, - "ccreq-no-cache-etag": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "ccreq-no-cache-lm": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "ccreq-no-store": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-oic": [ - "Assertion", - "Response 1 status is 200, not 504" - ], - "conditional-etag-forward": true, - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": [ - "Assertion", - "Request 2 header If-None-Match is \"\"abcdef\"\"" - ], - "conditional-etag-weak-generate-weak": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": true, - "freshness-expires-age-slow-date": true, - "freshness-expires-ansi-c": true, - "freshness-expires-far-future": true, - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-2-digit-year": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-aest": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-lines": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-spaces": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-no-comma": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-time-periods": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-utc": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-old-date": true, - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": true, - "freshness-expires-wrong-case-month": true, - "freshness-expires-wrong-case-tz": true, - "freshness-expires-wrong-case-weekday": true, - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": true, - "freshness-max-age-decimal-five": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-decimal-zero": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-s-maxage-private": true, - "freshness-max-age-s-maxage-private-multiple": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-space-before-equals": true, - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": true, - "freshness-max-age-two-fresh-stale-sepline": true, - "freshness-max-age-two-stale-fresh-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-none": true, - "head-200-freshness-update": [ - "Assertion", - "Response 3 does not come from cache" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-410-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-writethrough": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": true, - "headers-omit-headers-listed-in-Connection": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": [ - "Assertion", - "Response 2 header Clear-Site-Data is \"null\", not \"cookies\"" - ], - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": true, - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "headers-store-Set-Cookie2": [ - "Assertion", - "Response 2 header Set-Cookie2 is \"null\", not \"a=c\"" - ], - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": true, - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": true, - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": true, - "heuristic-204-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": true, - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": true, - "heuristic-delta-1800": true, - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": true, - "heuristic-delta-3600": true, - "heuristic-delta-43200": true, - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": true, - "heuristic-delta-600": true, - "heuristic-delta-86400": true, - "invalidate-DELETE": true, - "invalidate-DELETE-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-failed": true, - "invalidate-DELETE-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-failed": true, - "invalidate-M-SEARCH-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST": true, - "invalidate-POST-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-failed": true, - "invalidate-POST-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT": true, - "invalidate-PUT-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-failed": true, - "invalidate-PUT-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 age header not present." - ], - "other-age-gen": [ - "Assertion", - "Response 2 Age header not present." - ], - "other-age-update-expires": [ - "Assertion", - "Response 2 header Age is 30, should be bigger than 32" - ], - "other-age-update-max-age": [ - "Assertion", - "Response 2 header Age is 30, should be bigger than 32" - ], - "other-cookie": true, - "other-date-update": true, - "other-date-update-expires": true, - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": true, - "other-set-cookie": true, - "partial-store-complete-reuse-partial": true, - "partial-store-complete-reuse-partial-no-last": true, - "partial-store-complete-reuse-partial-suffix": true, - "partial-store-partial-complete": [ - "Assertion", - "Request 2 header range is \"undefined\", not \"bytes=5-\"" - ], - "partial-store-partial-reuse-partial": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-use-headers": true, - "partial-use-stored-headers": true, - "pragma-request-extension": true, - "pragma-request-no-cache": [ - "Assertion", - "Response 2 does not come from cache" - ], - "pragma-response-extension": true, - "pragma-response-no-cache": [ - "Assertion", - "Response 2 does not come from cache" - ], - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close": [ - "TypeError", - "Failed to fetch" - ], - "stale-close-must-revalidate": [ - "TypeError", - "Failed to fetch" - ], - "stale-close-no-cache": [ - "TypeError", - "Failed to fetch" - ], - "stale-sie-503": [ - "TypeError", - "Failed to fetch" - ], - "stale-sie-close": [ - "TypeError", - "Failed to fetch" - ], - "stale-warning-become": [ - "TypeError", - "Failed to fetch" - ], - "stale-warning-stored": [ - "TypeError", - "Failed to fetch" - ], - "stale-while-revalidate": true, - "stale-while-revalidate-window": true, - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": true, - "status-204-stale": true, - "status-299-fresh": true, - "status-299-stale": true, - "status-400-fresh": true, - "status-400-stale": true, - "status-404-fresh": true, - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": true, - "status-499-stale": true, - "status-500-fresh": true, - "status-500-stale": true, - "status-502-fresh": true, - "status-502-stale": true, - "status-503-fresh": true, - "status-503-stale": true, - "status-504-fresh": true, - "status-504-stale": true, - "status-599-fresh": true, - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": [ - "Assertion", - "Response 3 does not come from cache" - ], - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": true, - "vary-syntax-empty-star-lines": true, - "vary-syntax-foo-star": true, - "vary-syntax-star": true, - "vary-syntax-star-foo": true, - "vary-syntax-star-star": true, - "vary-syntax-star-star-lines": true -} \ No newline at end of file diff --git a/test/fixtures/cache-tests/results/fastly.json b/test/fixtures/cache-tests/results/fastly.json deleted file mode 100644 index bbf11332be0..00000000000 --- a/test/fixtures/cache-tests/results/fastly.json +++ /dev/null @@ -1,879 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": [ - "Assertion", - "Response 2 header Cache-Control is \"max-age=1\", not \"max-age=3600\"" - ], - "304-etag-update-response-Clear-Site-Data": [ - "Assertion", - "Response 2 header Clear-Site-Data is \"cache\", not \"cookies\"" - ], - "304-etag-update-response-Content-Encoding": [ - "Assertion", - "Response 2 header Content-Encoding is \"arizqhypgxofwne\", not \"askcumewogyqias\"" - ], - "304-etag-update-response-Content-Foo": [ - "Assertion", - "Response 2 header Content-Foo is \"awsokgcyuqmieaw\", not \"axurolifczwtqnk\"" - ], - "304-etag-update-response-Content-Length": true, - "304-etag-update-response-Content-Location": [ - "Assertion", - "Response 2 header Content-Location is \"/foo\", not \"/bar\"" - ], - "304-etag-update-response-Content-MD5": [ - "Assertion", - "Response 2 header Content-MD5 is \"rL0Y20zC+Fzt72VPzMSk2A==\", not \"N7UdGUp1E+RbVvZSTy1R8g==\"" - ], - "304-etag-update-response-Content-Range": [ - "Assertion", - "Response 2 header Content-Range is \"null\", not \"aqgwmcsiyoeukaq\"" - ], - "304-etag-update-response-Content-Security-Policy": [ - "Assertion", - "Response 2 header Content-Security-Policy is \"default-src 'self'\", not \"default-src 'self' cdn.example.com\"" - ], - "304-etag-update-response-Content-Type": [ - "Assertion", - "Response 2 header Content-Type is \"text/plain\", not \"text/plain;charset=utf-8\"" - ], - "304-etag-update-response-ETag": [ - "Assertion", - "Response 2 header ETag is \"\"abcdef\"\", not \"\"ghijkl\"\"" - ], - "304-etag-update-response-Expires": [ - "Assertion", - "Response 2 header Expires is \"Fri, 01 Jan 2038 01:01:01 GMT\", not \"Mon, 11 Jan 2038 11:11:11 GMT\"" - ], - "304-etag-update-response-Public-Key-Pins": [ - "Assertion", - "Response 2 header Public-Key-Pins is \"auoicwqkeysmgau\", not \"avqlgbwrmhcxsni\"" - ], - "304-etag-update-response-Set-Cookie": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Set-Cookie2": [ - "Assertion", - "Response 2 header Set-Cookie2 is \"a=b\", not \"a=c\"" - ], - "304-etag-update-response-Test-Header": [ - "Assertion", - "Response 2 header Test-Header is \"aaaaaaaaaaaaaaa\", not \"abcdefghijklmno\"" - ], - "304-etag-update-response-X-Content-Foo": [ - "Assertion", - "Response 2 header X-Content-Foo is \"azyxwvutsrqponm\", not \"aaaaaaaaaaaaaaa\"" - ], - "304-etag-update-response-X-Frame-Options": [ - "Assertion", - "Response 2 header X-Frame-Options is \"deny\", not \"sameorigin\"" - ], - "304-etag-update-response-X-Test-Header": [ - "Assertion", - "Response 2 header X-Test-Header is \"adgjmpsvybehknq\", not \"aeimquycgkoswae\"" - ], - "304-etag-update-response-X-XSS-Protection": [ - "Assertion", - "Response 2 header X-XSS-Protection is \"1\", not \"1; mode=block\"" - ], - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": true, - "age-parse-large": true, - "age-parse-large-minus-one": true, - "age-parse-larger": true, - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": true, - "age-parse-parameter": true, - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": true, - "age-parse-suffix-twoline": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "cc-resp-no-cache-case-insensitive": [ - "Assertion", - "Response 2 comes from cache" - ], - "cc-resp-no-cache-revalidate": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "cc-resp-no-cache-revalidate-fresh": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-shared": true, - "ccreq-ma0": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-ma1": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-magreaterage": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-max-stale": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-max-stale-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-min-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-min-fresh-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache-etag": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "ccreq-no-cache-lm": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "ccreq-no-store": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-oic": [ - "Assertion", - "Response 1 status is 200, not 504" - ], - "cdn-cc-invalid-sh-type-unknown": true, - "cdn-cc-invalid-sh-type-wrong": true, - "cdn-date-update-exceed": true, - "cdn-expires-update-exceed": [ - "Assertion", - "Response 2 header Expires is \"null\", not \"Tue, 21 Jan 2025 00:41:28 GMT\"" - ], - "cdn-fresh-cc-nostore": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-0": true, - "cdn-max-age-0-expires": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-age": true, - "cdn-max-age-case-insensitive": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-cc-max-age-invalid-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-extension": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-long-cc-max-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-max": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-max-plus": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-short-cc-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-space-after-equals": true, - "cdn-max-age-space-before-equals": true, - "cdn-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-no-store-cc-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-private": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-remove-age-exceed": [ - "Assertion", - "Response 2 Age header not present." - ], - "cdn-remove-header": true, - "conditional-304-etag": true, - "conditional-etag-forward": [ - "Assertion", - "Request 1 header If-None-Match is \"undefined\", not \"\"abcdef\"\"" - ], - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"undefined\", not \"\"abcdef\"\"" - ], - "conditional-etag-precedence": true, - "conditional-etag-quoted-respond-unquoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-strong-respond": true, - "conditional-etag-strong-respond-multiple-first": true, - "conditional-etag-strong-respond-multiple-last": true, - "conditional-etag-strong-respond-multiple-second": true, - "conditional-etag-strong-respond-obs-text": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-quoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-unquoted": true, - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": true, - "conditional-etag-weak-generate-weak": true, - "conditional-etag-weak-respond": true, - "conditional-etag-weak-respond-backslash": true, - "conditional-etag-weak-respond-lowercase": true, - "conditional-etag-weak-respond-omit-slash": true, - "conditional-lm-fresh": true, - "conditional-lm-fresh-earlier": true, - "conditional-lm-fresh-no-lm": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-lm-fresh-rfc850": true, - "conditional-lm-stale": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-age-slow-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-ansi-c": true, - "freshness-expires-far-future": true, - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-2-digit-year": true, - "freshness-expires-invalid-aest": true, - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": true, - "freshness-expires-invalid-multiple-lines": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-spaces": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-no-comma": true, - "freshness-expires-invalid-time-periods": true, - "freshness-expires-invalid-utc": true, - "freshness-expires-old-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": true, - "freshness-expires-wrong-case-month": true, - "freshness-expires-wrong-case-tz": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-weekday": true, - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": true, - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-decimal-five": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-decimal-zero": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-s-maxage-shared-longer": true, - "freshness-max-age-s-maxage-shared-longer-multiple": true, - "freshness-max-age-s-maxage-shared-longer-reversed": true, - "freshness-max-age-s-maxage-shared-shorter": true, - "freshness-max-age-s-maxage-shared-shorter-expires": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-space-before-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": true, - "freshness-max-age-two-fresh-stale-sepline": true, - "freshness-max-age-two-stale-fresh-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-none": true, - "freshness-s-maxage-shared": true, - "head-200-freshness-update": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "head-410-update": [ - "Setup", - "Response 3 status is 410, not 200" - ], - "head-writethrough": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-omit-headers-listed-in-Connection": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": [ - "Assertion", - "Response 2 header Content-Range is \"null\", not \"ananananananana\"" - ], - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-store-Set-Cookie2": true, - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": [ - "Setup", - "Response 1 status is 503, not 200" - ], - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-204-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": [ - "Setup", - "retry" - ], - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1800": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-3600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-43200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-86400": [ - "Assertion", - "Response 2 does not come from cache" - ], - "invalidate-DELETE": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-failed": true, - "invalidate-DELETE-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-failed": true, - "invalidate-M-SEARCH-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-failed": true, - "invalidate-POST-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-failed": true, - "invalidate-PUT-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 header age is 0, should be bigger than 0" - ], - "other-age-gen": true, - "other-age-update-expires": [ - "Assertion", - "Response 2 header Age is 3, should be bigger than 32" - ], - "other-age-update-max-age": true, - "other-authorization": [ - "Assertion", - "Response 2 comes from cache" - ], - "other-authorization-must-revalidate": true, - "other-authorization-public": true, - "other-authorization-smaxage": true, - "other-cookie": true, - "other-date-update": [ - "Assertion", - "Response 2 header Date is \"Tue, 21 Jan 2025 00:41:23 GMT\", not \"Tue, 21 Jan 2025 00:41:20 GMT\"" - ], - "other-date-update-expires": [ - "Assertion", - "Response 2 header Date is \"Tue, 21 Jan 2025 00:41:23 GMT\", not \"Tue, 21 Jan 2025 00:41:20 GMT\"" - ], - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-set-cookie": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-complete-reuse-partial": true, - "partial-store-complete-reuse-partial-no-last": true, - "partial-store-complete-reuse-partial-suffix": true, - "partial-store-partial-complete": [ - "Assertion", - "Request 2 header range is \"undefined\", not \"bytes=5-\"" - ], - "partial-store-partial-reuse-partial": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-use-headers": true, - "partial-use-stored-headers": true, - "pragma-request-extension": true, - "pragma-request-no-cache": true, - "pragma-response-extension": true, - "pragma-response-no-cache": true, - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": [ - "Setup", - "retry" - ], - "stale-close": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close-must-revalidate": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-proxy-revalidate": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-s-maxage=2": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-sie-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-sie-close": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-warning-become": [ - "Setup", - "Response 2 does not come from cache" - ], - "stale-warning-stored": [ - "Setup", - "Response 2 does not come from cache" - ], - "stale-while-revalidate": true, - "stale-while-revalidate-window": true, - "status-200-fresh": true, - "status-200-must-understand": true, - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-204-stale": true, - "status-299-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-299-stale": true, - "status-301-fresh": true, - "status-301-stale": true, - "status-302-fresh": true, - "status-302-stale": true, - "status-303-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-303-stale": true, - "status-307-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-307-stale": true, - "status-308-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-308-stale": true, - "status-400-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-400-stale": true, - "status-404-fresh": true, - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-499-stale": true, - "status-500-fresh": [ - "Setup", - "retry" - ], - "status-500-stale": [ - "Setup", - "retry" - ], - "status-502-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-502-stale": true, - "status-503-fresh": [ - "Setup", - "retry" - ], - "status-503-stale": [ - "Setup", - "retry" - ], - "status-504-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-504-stale": true, - "status-599-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": true, - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-empty-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-empty-star-lines": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-foo-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star-foo": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star-star-lines": [ - "Assertion", - "Response 2 comes from cache" - ] -} diff --git a/test/fixtures/cache-tests/results/firefox.json b/test/fixtures/cache-tests/results/firefox.json deleted file mode 100644 index 2c3e5a87e67..00000000000 --- a/test/fixtures/cache-tests/results/firefox.json +++ /dev/null @@ -1,581 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": true, - "304-etag-update-response-Clear-Site-Data": true, - "304-etag-update-response-Content-Encoding": [ - "Assertion", - "Response 2 header Content-Encoding is \"arizqhypgxofwne\", not \"askcumewogyqias\"" - ], - "304-etag-update-response-Content-Foo": true, - "304-etag-update-response-Content-Length": true, - "304-etag-update-response-Content-Location": [ - "Assertion", - "Response 2 header Content-Location is \"/foo\", not \"/bar\"" - ], - "304-etag-update-response-Content-MD5": [ - "Assertion", - "Response 2 header Content-MD5 is \"rL0Y20zC+Fzt72VPzMSk2A==\", not \"N7UdGUp1E+RbVvZSTy1R8g==\"" - ], - "304-etag-update-response-Content-Range": [ - "Assertion", - "Response 2 header Content-Range is \"apetixmbqfujync\", not \"aqgwmcsiyoeukaq\"" - ], - "304-etag-update-response-Content-Security-Policy": true, - "304-etag-update-response-Content-Type": [ - "Assertion", - "Response 2 header Content-Type is \"text/plain\", not \"text/plain;charset=utf-8\"" - ], - "304-etag-update-response-ETag": [ - "Assertion", - "Response 2 header ETag is \"\"abcdef\"\", not \"\"ghijkl\"\"" - ], - "304-etag-update-response-Expires": true, - "304-etag-update-response-Public-Key-Pins": true, - "304-etag-update-response-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "304-etag-update-response-Set-Cookie2": [ - "Assertion", - "Response 2 header Set-Cookie2 is \"null\", not \"a=c\"" - ], - "304-etag-update-response-Test-Header": true, - "304-etag-update-response-X-Content-Foo": true, - "304-etag-update-response-X-Frame-Options": true, - "304-etag-update-response-X-Test-Header": true, - "304-etag-update-response-X-XSS-Protection": true, - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": [ - "Assertion", - "Response 2 does not come from cache" - ], - "age-parse-large": true, - "age-parse-large-minus-one": true, - "age-parse-larger": true, - "age-parse-negative": [ - "Assertion", - "Response 2 does not come from cache" - ], - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": true, - "age-parse-parameter": true, - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": true, - "age-parse-suffix-twoline": true, - "cc-resp-immutable-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cc-resp-immutable-stale": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": true, - "cc-resp-no-cache-revalidate-fresh": true, - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-private": true, - "ccreq-ma0": true, - "ccreq-ma1": true, - "ccreq-magreaterage": true, - "ccreq-max-stale": true, - "ccreq-max-stale-age": true, - "ccreq-min-fresh": true, - "ccreq-min-fresh-age": true, - "ccreq-no-cache": true, - "ccreq-no-cache-etag": true, - "ccreq-no-cache-lm": true, - "ccreq-no-store": true, - "ccreq-oic": [ - "Assertion", - "Response 1 status is 200, not 504" - ], - "conditional-etag-forward": true, - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": [ - "Assertion", - "Request 2 header If-None-Match is \"\"abcdef\"\"" - ], - "conditional-etag-weak-generate-weak": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": true, - "freshness-expires-age-slow-date": true, - "freshness-expires-ansi-c": true, - "freshness-expires-far-future": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-2-digit-year": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-aest": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-lines": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-spaces": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-no-comma": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-time-periods": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-utc": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-old-date": true, - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": true, - "freshness-expires-wrong-case-month": true, - "freshness-expires-wrong-case-tz": true, - "freshness-expires-wrong-case-weekday": true, - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": true, - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": true, - "freshness-max-age-decimal-five": true, - "freshness-max-age-decimal-zero": true, - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-s-maxage-private": true, - "freshness-max-age-s-maxage-private-multiple": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-space-before-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-fresh-stale-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sameline": true, - "freshness-max-age-two-stale-fresh-sepline": true, - "freshness-none": true, - "head-200-freshness-update": [ - "Assertion", - "Response 3 does not come from cache" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-410-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-writethrough": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Connection": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": true, - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "headers-store-Set-Cookie2": [ - "Assertion", - "Response 2 header Set-Cookie2 is \"null\", not \"a=c\"" - ], - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": true, - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": true, - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": true, - "heuristic-204-cached": true, - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": true, - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": true, - "heuristic-delta-1800": true, - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": true, - "heuristic-delta-3600": true, - "heuristic-delta-43200": true, - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": true, - "heuristic-delta-600": true, - "heuristic-delta-86400": true, - "invalidate-DELETE": true, - "invalidate-DELETE-cl": true, - "invalidate-DELETE-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-DELETE-location": true, - "invalidate-M-SEARCH": true, - "invalidate-M-SEARCH-cl": true, - "invalidate-M-SEARCH-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-M-SEARCH-location": true, - "invalidate-POST": true, - "invalidate-POST-cl": true, - "invalidate-POST-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-POST-location": true, - "invalidate-PUT": true, - "invalidate-PUT-cl": true, - "invalidate-PUT-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-PUT-location": true, - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 age header not present." - ], - "other-age-gen": [ - "Assertion", - "Response 2 Age header not present." - ], - "other-age-update-expires": [ - "Assertion", - "Response 2 header Age is 30, should be bigger than 32" - ], - "other-age-update-max-age": [ - "Assertion", - "Response 2 header Age is 30, should be bigger than 32" - ], - "other-cookie": true, - "other-date-update": true, - "other-date-update-expires": true, - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": true, - "other-set-cookie": true, - "partial-store-complete-reuse-partial": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-complete-reuse-partial-no-last": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-complete-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-complete": [ - "Assertion", - "Request 2 header range is \"undefined\", not \"bytes=5-\"" - ], - "partial-store-partial-reuse-partial": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-use-headers": [ - "Setup", - "Response 2 does not come from cache" - ], - "partial-use-stored-headers": [ - "Setup", - "Response 2 does not come from cache" - ], - "pragma-request-extension": true, - "pragma-request-no-cache": true, - "pragma-response-extension": true, - "pragma-response-no-cache": true, - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close": [ - "TypeError", - "NetworkError when attempting to fetch resource." - ], - "stale-close-must-revalidate": [ - "TypeError", - "NetworkError when attempting to fetch resource." - ], - "stale-close-no-cache": [ - "TypeError", - "NetworkError when attempting to fetch resource." - ], - "stale-sie-503": [ - "TypeError", - "NetworkError when attempting to fetch resource." - ], - "stale-sie-close": [ - "TypeError", - "NetworkError when attempting to fetch resource." - ], - "stale-warning-become": [ - "TypeError", - "NetworkError when attempting to fetch resource." - ], - "stale-warning-stored": [ - "TypeError", - "NetworkError when attempting to fetch resource." - ], - "stale-while-revalidate": true, - "stale-while-revalidate-window": true, - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": true, - "status-204-stale": true, - "status-299-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-299-stale": true, - "status-400-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-400-stale": true, - "status-404-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-499-stale": true, - "status-500-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-500-stale": true, - "status-502-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-502-stale": true, - "status-503-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-503-stale": true, - "status-504-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-504-stale": true, - "status-599-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": [ - "Assertion", - "Response 3 does not come from cache" - ], - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": true, - "vary-syntax-empty-star-lines": true, - "vary-syntax-foo-star": true, - "vary-syntax-star": true, - "vary-syntax-star-foo": true, - "vary-syntax-star-star": true, - "vary-syntax-star-star-lines": true -} \ No newline at end of file diff --git a/test/fixtures/cache-tests/results/index.mjs b/test/fixtures/cache-tests/results/index.mjs deleted file mode 100644 index 3d1afde079f..00000000000 --- a/test/fixtures/cache-tests/results/index.mjs +++ /dev/null @@ -1,71 +0,0 @@ - -export default [ - { - file: 'chrome.json', - name: 'Chrome', - type: 'browser', - version: '132.0.6834.84' - }, - { - file: 'firefox.json', - name: 'Firefox', - type: 'browser', - version: '134.0.1', - link: 'https://github.com/http-tests/cache-tests/wiki/Firefox' - }, - { - file: 'safari.json', - name: 'Safari', - type: 'browser', - version: '18.2 (20620.1.16.11.8)' - }, - { - file: 'nginx.json', - name: 'nginx', - type: 'rev-proxy', - version: '1.26.0-3ubuntu1', - link: 'https://github.com/http-tests/cache-tests/wiki/nginx' - }, - { - file: 'squid.json', - name: 'Squid', - type: 'rev-proxy', - version: '6.10-1ubuntu1', - link: 'https://github.com/http-tests/cache-tests/wiki/Squid' - }, - { - file: 'trafficserver.json', - name: 'ATS', - type: 'rev-proxy', - version: '9.2.5+ds-1', - link: 'https://github.com/http-tests/cache-tests/wiki/Traffic-Server' - }, - { - file: 'apache.json', - name: 'httpd', - type: 'rev-proxy', - version: '2.4.62-3ubuntu1', - link: 'https://github.com/http-tests/cache-tests/wiki/Apache-httpd' - }, - { - file: 'varnish.json', - name: 'Varnish', - type: 'rev-proxy', - version: '7.5.0-3', - link: 'https://github.com/http-tests/cache-tests/wiki/Varnish' - }, - { - file: 'caddy.json', - name: 'caddy', - type: 'rev-proxy', - version: '0.7.0', - link: 'https://github.com/http-tests/cache-tests/wiki/Caddy' - }, - { - file: 'fastly.json', - name: 'Fastly', - type: 'cdn', - version: '2025-01-21', - link: 'https://github.com/http-tests/cache-tests/wiki/Fastly' - } -] diff --git a/test/fixtures/cache-tests/results/nginx.json b/test/fixtures/cache-tests/results/nginx.json deleted file mode 100644 index 1f12ac9984e..00000000000 --- a/test/fixtures/cache-tests/results/nginx.json +++ /dev/null @@ -1,849 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": [ - "Assertion", - "Response 2 header Cache-Control is \"max-age=1\", not \"max-age=3600\"" - ], - "304-etag-update-response-Clear-Site-Data": [ - "Assertion", - "Response 2 header Clear-Site-Data is \"cache\", not \"cookies\"" - ], - "304-etag-update-response-Content-Encoding": [ - "Assertion", - "Response 2 header Content-Encoding is \"arizqhypgxofwne\", not \"askcumewogyqias\"" - ], - "304-etag-update-response-Content-Foo": [ - "Assertion", - "Response 2 header Content-Foo is \"awsokgcyuqmieaw\", not \"axurolifczwtqnk\"" - ], - "304-etag-update-response-Content-Length": true, - "304-etag-update-response-Content-Location": [ - "Assertion", - "Response 2 header Content-Location is \"/foo\", not \"/bar\"" - ], - "304-etag-update-response-Content-MD5": [ - "Assertion", - "Response 2 header Content-MD5 is \"rL0Y20zC+Fzt72VPzMSk2A==\", not \"N7UdGUp1E+RbVvZSTy1R8g==\"" - ], - "304-etag-update-response-Content-Range": [ - "Assertion", - "Response 2 header Content-Range is \"apetixmbqfujync\", not \"aqgwmcsiyoeukaq\"" - ], - "304-etag-update-response-Content-Security-Policy": [ - "Assertion", - "Response 2 header Content-Security-Policy is \"default-src 'self'\", not \"default-src 'self' cdn.example.com\"" - ], - "304-etag-update-response-Content-Type": [ - "Assertion", - "Response 2 header Content-Type is \"text/plain\", not \"text/plain;charset=utf-8\"" - ], - "304-etag-update-response-ETag": [ - "Assertion", - "Response 2 header ETag is \"\"abcdef\"\", not \"\"ghijkl\"\"" - ], - "304-etag-update-response-Expires": [ - "Assertion", - "Response 2 header Expires is \"Fri, 01 Jan 2038 01:01:01 GMT\", not \"Mon, 11 Jan 2038 11:11:11 GMT\"" - ], - "304-etag-update-response-Public-Key-Pins": [ - "Assertion", - "Response 2 header Public-Key-Pins is \"auoicwqkeysmgau\", not \"avqlgbwrmhcxsni\"" - ], - "304-etag-update-response-Set-Cookie": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Set-Cookie2": [ - "Assertion", - "Response 2 header Set-Cookie2 is \"a=b\", not \"a=c\"" - ], - "304-etag-update-response-Test-Header": [ - "Assertion", - "Response 2 header Test-Header is \"aaaaaaaaaaaaaaa\", not \"abcdefghijklmno\"" - ], - "304-etag-update-response-X-Content-Foo": [ - "Assertion", - "Response 2 header X-Content-Foo is \"azyxwvutsrqponm\", not \"aaaaaaaaaaaaaaa\"" - ], - "304-etag-update-response-X-Frame-Options": [ - "Assertion", - "Response 2 header X-Frame-Options is \"deny\", not \"sameorigin\"" - ], - "304-etag-update-response-X-Test-Header": [ - "Assertion", - "Response 2 header X-Test-Header is \"adgjmpsvybehknq\", not \"aeimquycgkoswae\"" - ], - "304-etag-update-response-X-XSS-Protection": [ - "Assertion", - "Response 2 header X-XSS-Protection is \"1\", not \"1; mode=block\"" - ], - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": true, - "age-parse-large": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-large-minus-one": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-larger": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-suffix-twoline": [ - "Assertion", - "Response 2 comes from cache" - ], - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "cc-resp-no-cache-revalidate-fresh": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-shared": true, - "ccreq-ma0": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-ma1": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-magreaterage": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-max-stale": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-max-stale-age": true, - "ccreq-min-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-min-fresh-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache-etag": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "ccreq-no-cache-lm": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "ccreq-no-store": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-oic": [ - "Assertion", - "Response 1 status is 200, not 504" - ], - "cdn-cc-invalid-sh-type-unknown": true, - "cdn-cc-invalid-sh-type-wrong": true, - "cdn-date-update-exceed": true, - "cdn-expires-update-exceed": [ - "Assertion", - "Response 2 header Expires is \"null\", not \"Wed, 29 Jan 2025 03:36:49 GMT\"" - ], - "cdn-fresh-cc-nostore": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-0": true, - "cdn-max-age-0-expires": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-age": true, - "cdn-max-age-case-insensitive": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-cc-max-age-invalid-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-extension": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-long-cc-max-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-max": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-max-plus": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-short-cc-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-space-after-equals": true, - "cdn-max-age-space-before-equals": true, - "cdn-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-no-store-cc-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-private": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-remove-age-exceed": [ - "Assertion", - "Response 2 Age header not present." - ], - "cdn-remove-header": true, - "conditional-304-etag": [ - "AbortError", - "This operation was aborted" - ], - "conditional-etag-forward": [ - "Assertion", - "Request 1 header If-None-Match is \"undefined\", not \"\"abcdef\"\"" - ], - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"undefined\", not \"\"abcdef\"\"" - ], - "conditional-etag-precedence": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-quoted-respond-unquoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-strong-respond": true, - "conditional-etag-strong-respond-multiple-first": true, - "conditional-etag-strong-respond-multiple-last": true, - "conditional-etag-strong-respond-multiple-second": true, - "conditional-etag-strong-respond-obs-text": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-quoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-unquoted": true, - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": true, - "conditional-etag-weak-generate-weak": true, - "conditional-etag-weak-respond": true, - "conditional-etag-weak-respond-backslash": true, - "conditional-etag-weak-respond-lowercase": true, - "conditional-etag-weak-respond-omit-slash": true, - "conditional-lm-fresh": true, - "conditional-lm-fresh-earlier": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-lm-fresh-no-lm": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-lm-fresh-rfc850": true, - "conditional-lm-stale": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-age-slow-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-ansi-c": true, - "freshness-expires-far-future": true, - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": true, - "freshness-expires-invalid-2-digit-year": true, - "freshness-expires-invalid-aest": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": true, - "freshness-expires-invalid-multiple-lines": true, - "freshness-expires-invalid-multiple-spaces": true, - "freshness-expires-invalid-no-comma": true, - "freshness-expires-invalid-time-periods": true, - "freshness-expires-invalid-utc": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-old-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-past": true, - "freshness-expires-present": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-rfc850": true, - "freshness-expires-wrong-case-month": true, - "freshness-expires-wrong-case-tz": true, - "freshness-expires-wrong-case-weekday": true, - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-decimal-five": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-decimal-zero": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-s-maxage-shared-longer": true, - "freshness-max-age-s-maxage-shared-longer-multiple": true, - "freshness-max-age-s-maxage-shared-longer-reversed": true, - "freshness-max-age-s-maxage-shared-shorter": true, - "freshness-max-age-s-maxage-shared-shorter-expires": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": true, - "freshness-max-age-space-before-equals": true, - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": true, - "freshness-max-age-two-fresh-stale-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sepline": true, - "freshness-none": true, - "freshness-s-maxage-shared": true, - "head-200-freshness-update": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "head-410-update": [ - "Setup", - "Response 3 status is 410, not 200" - ], - "head-writethrough": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Connection": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": true, - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-store-Set-Cookie2": true, - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": [ - "Setup", - "Response 1 status is 502, not 200" - ], - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-204-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1800": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-3600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-43200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-86400": [ - "Assertion", - "Response 2 does not come from cache" - ], - "invalidate-DELETE": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-failed": true, - "invalidate-DELETE-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-failed": true, - "invalidate-M-SEARCH-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-failed": true, - "invalidate-POST-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-failed": true, - "invalidate-PUT-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 age header not present." - ], - "other-age-gen": [ - "Assertion", - "Response 2 Age header not present." - ], - "other-age-update-expires": [ - "Assertion", - "Response 2 header Age is 30, should be bigger than 32" - ], - "other-age-update-max-age": [ - "Assertion", - "Response 2 header Age is 30, should be bigger than 32" - ], - "other-authorization": [ - "Assertion", - "Response 2 comes from cache" - ], - "other-authorization-must-revalidate": true, - "other-authorization-public": true, - "other-authorization-smaxage": true, - "other-cookie": true, - "other-date-update": [ - "Assertion", - "Response 2 header Date is \"Wed, 29 Jan 2025 03:36:43 GMT\", not \"Wed, 29 Jan 2025 03:36:40 GMT\"" - ], - "other-date-update-expires": [ - "Assertion", - "Response 2 header Date is \"Wed, 29 Jan 2025 03:36:43 GMT\", not \"Wed, 29 Jan 2025 03:36:40 GMT\"" - ], - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-set-cookie": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-complete-reuse-partial": [ - "Assertion", - "Response 2 status is 200, not 206" - ], - "partial-store-complete-reuse-partial-no-last": [ - "Assertion", - "Response 2 status is 200, not 206" - ], - "partial-store-complete-reuse-partial-suffix": [ - "Assertion", - "Response 2 status is 200, not 206" - ], - "partial-store-partial-complete": [ - "Setup", - "Response 2 status is 206, not 200" - ], - "partial-store-partial-reuse-partial": [ - "Setup", - "Request 1 header Range is \"undefined\", not \"bytes=-5\"" - ], - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response body is \"01234\", not \"234\"" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response body is \"01234\", not \"234\"" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response body is \"01234\", not \"4\"" - ], - "partial-use-headers": [ - "Setup", - "Response 2 status is 200, not 206" - ], - "partial-use-stored-headers": [ - "Setup", - "Response 2 status is 200, not 206" - ], - "pragma-request-extension": true, - "pragma-request-no-cache": true, - "pragma-response-extension": true, - "pragma-response-no-cache": true, - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close-must-revalidate": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-proxy-revalidate": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-s-maxage=2": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-sie-503": true, - "stale-sie-close": true, - "stale-warning-become": [ - "Setup", - "Response 2 does not come from cache" - ], - "stale-warning-stored": [ - "Setup", - "Response 2 does not come from cache" - ], - "stale-while-revalidate": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-while-revalidate-window": [ - "Setup", - "Response 2 does not come from cache" - ], - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": true, - "status-204-stale": true, - "status-299-fresh": true, - "status-299-stale": true, - "status-301-fresh": true, - "status-301-stale": true, - "status-302-fresh": true, - "status-302-stale": true, - "status-303-fresh": true, - "status-303-stale": true, - "status-307-fresh": true, - "status-307-stale": true, - "status-308-fresh": true, - "status-308-stale": true, - "status-400-fresh": true, - "status-400-stale": true, - "status-404-fresh": true, - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": true, - "status-499-stale": true, - "status-500-fresh": true, - "status-500-stale": true, - "status-502-fresh": true, - "status-502-stale": true, - "status-503-fresh": true, - "status-503-stale": true, - "status-504-fresh": true, - "status-504-stale": true, - "status-599-fresh": true, - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": true, - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "AbortError", - "This operation was aborted" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": true, - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-empty-star-lines": true, - "vary-syntax-foo-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star": true, - "vary-syntax-star-foo": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star-star-lines": true -} diff --git a/test/fixtures/cache-tests/results/safari.json b/test/fixtures/cache-tests/results/safari.json deleted file mode 100644 index dfbedaa4cf9..00000000000 --- a/test/fixtures/cache-tests/results/safari.json +++ /dev/null @@ -1,611 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": true, - "304-etag-update-response-Clear-Site-Data": true, - "304-etag-update-response-Content-Encoding": [ - "Assertion", - "Response 2 header Content-Encoding is \"arizqhypgxofwne\", not \"askcumewogyqias\"" - ], - "304-etag-update-response-Content-Foo": [ - "Assertion", - "Response 2 header Content-Foo is \"awsokgcyuqmieaw\", not \"axurolifczwtqnk\"" - ], - "304-etag-update-response-Content-Length": true, - "304-etag-update-response-Content-Location": [ - "Assertion", - "Response 2 header Content-Location is \"/foo\", not \"/bar\"" - ], - "304-etag-update-response-Content-MD5": [ - "Assertion", - "Response 2 header Content-MD5 is \"rL0Y20zC+Fzt72VPzMSk2A==\", not \"N7UdGUp1E+RbVvZSTy1R8g==\"" - ], - "304-etag-update-response-Content-Range": [ - "Assertion", - "Response 2 header Content-Range is \"apetixmbqfujync\", not \"aqgwmcsiyoeukaq\"" - ], - "304-etag-update-response-Content-Security-Policy": true, - "304-etag-update-response-Content-Type": [ - "Assertion", - "Response 2 header Content-Type is \"text/plain\", not \"text/plain;charset=utf-8\"" - ], - "304-etag-update-response-ETag": [ - "Assertion", - "Response 2 header ETag is \"\"abcdef\"\", not \"\"ghijkl\"\"" - ], - "304-etag-update-response-Expires": true, - "304-etag-update-response-Public-Key-Pins": true, - "304-etag-update-response-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "304-etag-update-response-Set-Cookie2": [ - "Assertion", - "Response 2 header Set-Cookie2 is \"null\", not \"a=c\"" - ], - "304-etag-update-response-Test-Header": true, - "304-etag-update-response-X-Content-Foo": [ - "Assertion", - "Response 2 header X-Content-Foo is \"azyxwvutsrqponm\", not \"aaaaaaaaaaaaaaa\"" - ], - "304-etag-update-response-X-Frame-Options": [ - "Assertion", - "Response 2 header X-Frame-Options is \"deny\", not \"sameorigin\"" - ], - "304-etag-update-response-X-Test-Header": true, - "304-etag-update-response-X-XSS-Protection": [ - "Assertion", - "Response 2 header X-XSS-Protection is \"1\", not \"1; mode=block\"" - ], - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": [ - "Assertion", - "Response 2 does not come from cache" - ], - "age-parse-large": true, - "age-parse-large-minus-one": true, - "age-parse-larger": true, - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-suffix-twoline": [ - "Assertion", - "Response 2 comes from cache" - ], - "cc-resp-immutable-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cc-resp-immutable-stale": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": true, - "cc-resp-no-cache-revalidate-fresh": true, - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-private": true, - "ccreq-ma0": true, - "ccreq-ma1": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-magreaterage": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-max-stale": true, - "ccreq-max-stale-age": true, - "ccreq-min-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-min-fresh-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache": true, - "ccreq-no-cache-etag": true, - "ccreq-no-cache-lm": true, - "ccreq-no-store": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-oic": [ - "TypeError", - "Load failed" - ], - "conditional-etag-forward": true, - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": true, - "conditional-etag-weak-generate-weak": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": true, - "freshness-expires-age-slow-date": true, - "freshness-expires-ansi-c": true, - "freshness-expires-far-future": true, - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-2-digit-year": true, - "freshness-expires-invalid-aest": true, - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-lines": true, - "freshness-expires-invalid-multiple-spaces": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-no-comma": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-time-periods": true, - "freshness-expires-invalid-utc": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-old-date": true, - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-month": true, - "freshness-expires-wrong-case-tz": true, - "freshness-expires-wrong-case-weekday": true, - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": true, - "freshness-max-age-decimal-five": true, - "freshness-max-age-decimal-zero": true, - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": true, - "freshness-max-age-s-maxage-private": true, - "freshness-max-age-s-maxage-private-multiple": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-space-before-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": true, - "freshness-max-age-two-fresh-stale-sepline": true, - "freshness-max-age-two-stale-fresh-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-none": true, - "head-200-freshness-update": [ - "Assertion", - "Response 3 does not come from cache" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-410-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-writethrough": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-omit-headers-listed-in-Connection": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": true, - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "headers-store-Set-Cookie2": [ - "Assertion", - "Response 2 header Set-Cookie2 is \"null\", not \"a=c\"" - ], - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": true, - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": true, - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": true, - "heuristic-204-cached": true, - "heuristic-403-not_cached": true, - "heuristic-404-cached": true, - "heuristic-405-cached": true, - "heuristic-410-cached": true, - "heuristic-414-cached": true, - "heuristic-501-cached": true, - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": true, - "heuristic-delta-1800": true, - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": true, - "heuristic-delta-3600": true, - "heuristic-delta-43200": true, - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": true, - "heuristic-delta-600": true, - "heuristic-delta-86400": true, - "invalidate-DELETE": true, - "invalidate-DELETE-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-DELETE-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH": true, - "invalidate-M-SEARCH-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-M-SEARCH-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST": true, - "invalidate-POST-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-POST-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT": true, - "invalidate-PUT-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-PUT-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 age header not present." - ], - "other-age-gen": [ - "Assertion", - "Response 2 Age header not present." - ], - "other-age-update-expires": [ - "Assertion", - "Response 2 header Age is 30, should be bigger than 32" - ], - "other-age-update-max-age": [ - "Assertion", - "Response 2 header Age is 30, should be bigger than 32" - ], - "other-cookie": true, - "other-date-update": true, - "other-date-update-expires": true, - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": true, - "other-set-cookie": true, - "partial-store-complete-reuse-partial": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-complete-reuse-partial-no-last": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-complete-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-complete": [ - "Assertion", - "Request 2 header range is \"undefined\", not \"bytes=5-\"" - ], - "partial-store-partial-reuse-partial": true, - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-use-headers": [ - "Setup", - "Response 2 does not come from cache" - ], - "partial-use-stored-headers": [ - "Setup", - "Response 2 does not come from cache" - ], - "pragma-request-extension": true, - "pragma-request-no-cache": [ - "Assertion", - "Response 2 does not come from cache" - ], - "pragma-response-extension": true, - "pragma-response-no-cache": [ - "Assertion", - "Response 2 does not come from cache" - ], - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close": [ - "TypeError", - "Load failed" - ], - "stale-close-must-revalidate": [ - "TypeError", - "Load failed" - ], - "stale-close-no-cache": [ - "TypeError", - "Load failed" - ], - "stale-sie-503": [ - "TypeError", - "Load failed" - ], - "stale-sie-close": [ - "TypeError", - "Load failed" - ], - "stale-warning-become": [ - "TypeError", - "Load failed" - ], - "stale-warning-stored": [ - "TypeError", - "Load failed" - ], - "stale-while-revalidate": true, - "stale-while-revalidate-window": true, - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": true, - "status-204-stale": true, - "status-299-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-299-stale": true, - "status-400-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-400-stale": true, - "status-404-fresh": true, - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-499-stale": true, - "status-500-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-500-stale": true, - "status-502-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-502-stale": true, - "status-503-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-503-stale": true, - "status-504-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-504-stale": true, - "status-599-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": [ - "Assertion", - "Response 3 does not come from cache" - ], - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": true, - "vary-syntax-empty-star-lines": true, - "vary-syntax-foo-star": true, - "vary-syntax-star": true, - "vary-syntax-star-foo": true, - "vary-syntax-star-star": true, - "vary-syntax-star-star-lines": true -} \ No newline at end of file diff --git a/test/fixtures/cache-tests/results/squid.json b/test/fixtures/cache-tests/results/squid.json deleted file mode 100644 index 906f12a9013..00000000000 --- a/test/fixtures/cache-tests/results/squid.json +++ /dev/null @@ -1,675 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": true, - "304-etag-update-response-Clear-Site-Data": true, - "304-etag-update-response-Content-Encoding": true, - "304-etag-update-response-Content-Foo": true, - "304-etag-update-response-Content-Length": [ - "Assertion", - "Response 2 header Content-Length is \"10\", not \"36\"" - ], - "304-etag-update-response-Content-Location": true, - "304-etag-update-response-Content-MD5": true, - "304-etag-update-response-Content-Range": true, - "304-etag-update-response-Content-Security-Policy": true, - "304-etag-update-response-Content-Type": true, - "304-etag-update-response-ETag": true, - "304-etag-update-response-Expires": true, - "304-etag-update-response-Public-Key-Pins": true, - "304-etag-update-response-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "304-etag-update-response-Set-Cookie2": true, - "304-etag-update-response-Test-Header": true, - "304-etag-update-response-X-Content-Foo": true, - "304-etag-update-response-X-Frame-Options": true, - "304-etag-update-response-X-Test-Header": true, - "304-etag-update-response-X-XSS-Protection": true, - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": [ - "Assertion", - "Response 2 does not come from cache" - ], - "age-parse-large": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-large-minus-one": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-larger": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": true, - "age-parse-parameter": true, - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": true, - "age-parse-suffix-twoline": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": true, - "cc-resp-no-cache-revalidate-fresh": true, - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-shared": true, - "ccreq-ma0": true, - "ccreq-ma1": true, - "ccreq-magreaterage": true, - "ccreq-max-stale": true, - "ccreq-max-stale-age": [ - "Setup", - "Response 1 status is 409, not 200" - ], - "ccreq-min-fresh": true, - "ccreq-min-fresh-age": true, - "ccreq-no-cache": true, - "ccreq-no-cache-etag": [ - "Setup", - "Response 2 status is 502, not 200" - ], - "ccreq-no-cache-lm": [ - "Setup", - "Response 2 status is 502, not 200" - ], - "ccreq-no-store": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-oic": true, - "cdn-cc-invalid-sh-type-unknown": true, - "cdn-cc-invalid-sh-type-wrong": true, - "cdn-date-update-exceed": true, - "cdn-expires-update-exceed": [ - "Assertion", - "Response 2 header Expires is \"null\", not \"Mon, 03 Feb 2025 05:33:42 GMT\"" - ], - "cdn-fresh-cc-nostore": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-0": true, - "cdn-max-age-0-expires": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-age": true, - "cdn-max-age-case-insensitive": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-cc-max-age-invalid-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-extension": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-long-cc-max-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-max": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-max-plus": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-short-cc-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-space-after-equals": true, - "cdn-max-age-space-before-equals": true, - "cdn-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-no-store-cc-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-private": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-remove-age-exceed": [ - "Assertion", - "Response 2 Age header not present." - ], - "cdn-remove-header": true, - "conditional-304-etag": [ - "Assertion", - "Response 2 header ETag is \"null\", not \"\"abcdef\"\"" - ], - "conditional-etag-forward": true, - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-precedence": true, - "conditional-etag-quoted-respond-unquoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "request 2 doesn't have if-none-match header" - ], - "conditional-etag-strong-respond": true, - "conditional-etag-strong-respond-multiple-first": true, - "conditional-etag-strong-respond-multiple-last": true, - "conditional-etag-strong-respond-multiple-second": true, - "conditional-etag-strong-respond-obs-text": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-quoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-unquoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": true, - "conditional-etag-weak-generate-weak": [ - "Assertion", - "request 2 doesn't have if-none-match header" - ], - "conditional-etag-weak-respond": true, - "conditional-etag-weak-respond-backslash": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-weak-respond-lowercase": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-weak-respond-omit-slash": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-lm-fresh": true, - "conditional-lm-fresh-earlier": true, - "conditional-lm-fresh-no-lm": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-lm-fresh-rfc850": true, - "conditional-lm-stale": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-age-slow-date": true, - "freshness-expires-ansi-c": true, - "freshness-expires-far-future": true, - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": true, - "freshness-expires-invalid-2-digit-year": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-aest": true, - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-lines": true, - "freshness-expires-invalid-multiple-spaces": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-no-comma": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-time-periods": true, - "freshness-expires-invalid-utc": true, - "freshness-expires-old-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": true, - "freshness-expires-wrong-case-month": true, - "freshness-expires-wrong-case-tz": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-weekday": true, - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": true, - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": true, - "freshness-max-age-decimal-five": true, - "freshness-max-age-decimal-zero": true, - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-negative": true, - "freshness-max-age-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-s-maxage-shared-longer": true, - "freshness-max-age-s-maxage-shared-longer-multiple": true, - "freshness-max-age-s-maxage-shared-longer-reversed": true, - "freshness-max-age-s-maxage-shared-shorter": true, - "freshness-max-age-s-maxage-shared-shorter-expires": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-space-before-equals": true, - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": true, - "freshness-max-age-two-fresh-stale-sepline": true, - "freshness-max-age-two-stale-fresh-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-none": true, - "freshness-s-maxage-shared": true, - "head-200-freshness-update": [ - "Assertion", - "Response 3 does not come from cache" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-410-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-writethrough": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Connection": true, - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": true, - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "headers-store-Set-Cookie2": true, - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": [ - "Setup", - "Response 1 status is 502, not 200" - ], - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": true, - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": true, - "heuristic-204-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": true, - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": true, - "heuristic-delta-1800": true, - "heuristic-delta-30": true, - "heuristic-delta-300": true, - "heuristic-delta-3600": true, - "heuristic-delta-43200": true, - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": true, - "heuristic-delta-600": true, - "heuristic-delta-86400": true, - "invalidate-DELETE": true, - "invalidate-DELETE-cl": true, - "invalidate-DELETE-failed": true, - "invalidate-DELETE-location": true, - "invalidate-M-SEARCH": true, - "invalidate-M-SEARCH-cl": [ - "TypeError", - "fetch failed" - ], - "invalidate-M-SEARCH-failed": [ - "Assertion", - "Response 3 does not come from cache" - ], - "invalidate-M-SEARCH-location": true, - "invalidate-POST": true, - "invalidate-POST-cl": true, - "invalidate-POST-failed": true, - "invalidate-POST-location": true, - "invalidate-PUT": true, - "invalidate-PUT-cl": true, - "invalidate-PUT-failed": true, - "invalidate-PUT-location": true, - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 age header not present." - ], - "other-age-gen": true, - "other-age-update-expires": true, - "other-age-update-max-age": true, - "other-authorization": [ - "Setup", - "Request 1 header Authorization is \"undefined\", not \"FOO\"" - ], - "other-authorization-must-revalidate": [ - "Setup", - "Request 1 header Authorization is \"undefined\", not \"FOO\"" - ], - "other-authorization-public": [ - "Setup", - "Request 1 header Authorization is \"undefined\", not \"FOO\"" - ], - "other-authorization-smaxage": [ - "Setup", - "Request 1 header Authorization is \"undefined\", not \"FOO\"" - ], - "other-cookie": true, - "other-date-update": true, - "other-date-update-expires": true, - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": true, - "other-set-cookie": true, - "partial-store-complete-reuse-partial": true, - "partial-store-complete-reuse-partial-no-last": true, - "partial-store-complete-reuse-partial-suffix": true, - "partial-store-partial-complete": [ - "Assertion", - "Request 2 header range is \"undefined\", not \"bytes=5-\"" - ], - "partial-store-partial-reuse-partial": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-use-headers": true, - "partial-use-stored-headers": true, - "pragma-request-extension": true, - "pragma-request-no-cache": [ - "Assertion", - "Response 2 does not come from cache" - ], - "pragma-response-extension": true, - "pragma-response-no-cache": true, - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": true, - "stale-close": true, - "stale-close-must-revalidate": true, - "stale-close-no-cache": true, - "stale-close-proxy-revalidate": true, - "stale-close-s-maxage=2": true, - "stale-sie-503": true, - "stale-sie-close": true, - "stale-warning-become": [ - "Assertion", - "Response 2 warning header not present." - ], - "stale-warning-stored": [ - "Assertion", - "Response 2 warning header not present." - ], - "stale-while-revalidate": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-while-revalidate-window": [ - "Setup", - "Response 2 does not come from cache" - ], - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-204-stale": true, - "status-299-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-299-stale": true, - "status-301-fresh": true, - "status-301-stale": true, - "status-302-fresh": true, - "status-302-stale": true, - "status-303-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-303-stale": true, - "status-307-fresh": true, - "status-307-stale": true, - "status-308-fresh": true, - "status-308-stale": true, - "status-400-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-400-stale": true, - "status-404-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-499-stale": true, - "status-500-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-500-stale": true, - "status-502-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-502-stale": true, - "status-503-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-503-stale": true, - "status-504-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-504-stale": true, - "status-599-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": true, - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": true, - "vary-syntax-empty-star-lines": true, - "vary-syntax-foo-star": true, - "vary-syntax-star": true, - "vary-syntax-star-foo": true, - "vary-syntax-star-star": true, - "vary-syntax-star-star-lines": true -} diff --git a/test/fixtures/cache-tests/results/trafficserver.json b/test/fixtures/cache-tests/results/trafficserver.json deleted file mode 100644 index c2112d1ad18..00000000000 --- a/test/fixtures/cache-tests/results/trafficserver.json +++ /dev/null @@ -1,666 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": true, - "304-etag-update-response-Clear-Site-Data": true, - "304-etag-update-response-Content-Encoding": true, - "304-etag-update-response-Content-Foo": true, - "304-etag-update-response-Content-Length": true, - "304-etag-update-response-Content-Location": true, - "304-etag-update-response-Content-MD5": true, - "304-etag-update-response-Content-Range": true, - "304-etag-update-response-Content-Security-Policy": true, - "304-etag-update-response-Content-Type": [ - "Assertion", - "Response 2 header Content-Type is \"text/plain\", not \"text/plain;charset=utf-8\"" - ], - "304-etag-update-response-ETag": true, - "304-etag-update-response-Expires": true, - "304-etag-update-response-Public-Key-Pins": true, - "304-etag-update-response-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "304-etag-update-response-Set-Cookie2": true, - "304-etag-update-response-Test-Header": true, - "304-etag-update-response-X-Content-Foo": true, - "304-etag-update-response-X-Frame-Options": true, - "304-etag-update-response-X-Test-Header": true, - "304-etag-update-response-X-XSS-Protection": true, - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": [ - "Assertion", - "Response 2 does not come from cache" - ], - "age-parse-large": true, - "age-parse-large-minus-one": true, - "age-parse-larger": true, - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": true, - "age-parse-parameter": true, - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": true, - "age-parse-suffix-twoline": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "cc-resp-no-cache-revalidate-fresh": true, - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-shared": true, - "ccreq-ma0": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-ma1": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-magreaterage": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-max-stale": true, - "ccreq-max-stale-age": true, - "ccreq-min-fresh": true, - "ccreq-min-fresh-age": true, - "ccreq-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache-etag": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "ccreq-no-cache-lm": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "ccreq-no-store": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-oic": true, - "cdn-cc-invalid-sh-type-unknown": true, - "cdn-cc-invalid-sh-type-wrong": true, - "cdn-date-update-exceed": true, - "cdn-expires-update-exceed": [ - "Assertion", - "Response 2 header Expires is \"null\", not \"Mon, 03 Feb 2025 05:35:20 GMT\"" - ], - "cdn-fresh-cc-nostore": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-0": true, - "cdn-max-age-0-expires": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-age": true, - "cdn-max-age-case-insensitive": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-cc-max-age-invalid-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-extension": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-long-cc-max-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-max": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-max-plus": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-short-cc-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-space-after-equals": true, - "cdn-max-age-space-before-equals": true, - "cdn-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-no-store-cc-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-private": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-remove-age-exceed": true, - "cdn-remove-header": true, - "conditional-304-etag": true, - "conditional-etag-forward": [ - "Assertion", - "Request 1 header If-None-Match is \"undefined\", not \"\"abcdef\"\"" - ], - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"undefined\", not \"\"abcdef\"\"" - ], - "conditional-etag-precedence": true, - "conditional-etag-quoted-respond-unquoted": true, - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-strong-respond": true, - "conditional-etag-strong-respond-multiple-first": true, - "conditional-etag-strong-respond-multiple-last": true, - "conditional-etag-strong-respond-multiple-second": true, - "conditional-etag-strong-respond-obs-text": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-quoted": true, - "conditional-etag-unquoted-respond-unquoted": true, - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": true, - "conditional-etag-weak-generate-weak": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "conditional-etag-weak-respond": true, - "conditional-etag-weak-respond-backslash": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-weak-respond-lowercase": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-weak-respond-omit-slash": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-lm-fresh": true, - "conditional-lm-fresh-earlier": true, - "conditional-lm-fresh-no-lm": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-lm-fresh-rfc850": true, - "conditional-lm-stale": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": true, - "freshness-expires-age-slow-date": true, - "freshness-expires-ansi-c": true, - "freshness-expires-far-future": true, - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-2-digit-year": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-aest": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-lines": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-multiple-spaces": true, - "freshness-expires-invalid-no-comma": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-invalid-time-periods": true, - "freshness-expires-invalid-utc": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-old-date": true, - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": true, - "freshness-expires-wrong-case-month": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-tz": true, - "freshness-expires-wrong-case-weekday": true, - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": true, - "freshness-max-age-a100": true, - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": true, - "freshness-max-age-decimal-five": true, - "freshness-max-age-decimal-zero": true, - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": true, - "freshness-max-age-s-maxage-shared-longer": true, - "freshness-max-age-s-maxage-shared-longer-multiple": true, - "freshness-max-age-s-maxage-shared-longer-reversed": true, - "freshness-max-age-s-maxage-shared-shorter": true, - "freshness-max-age-s-maxage-shared-shorter-expires": true, - "freshness-max-age-single-quoted": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-space-after-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-space-before-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-fresh-stale-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sameline": true, - "freshness-max-age-two-stale-fresh-sepline": true, - "freshness-none": true, - "freshness-s-maxage-shared": true, - "head-200-freshness-update": [ - "Assertion", - "Response 3 does not come from cache" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-410-update": [ - "Setup", - "Response 3 does not come from cache" - ], - "head-writethrough": true, - "headers-omit-headers-listed-in-Cache-Control-no-cache": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Connection": [ - "Assertion", - "Response 2 includes unexpected header a: \"1\"" - ], - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": true, - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": [ - "Assertion", - "Response 2 header Set-Cookie is \"null\", not \"a=c\"" - ], - "headers-store-Set-Cookie2": true, - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": [ - "Setup", - "Response 1 status is 400, not 200" - ], - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": true, - "heuristic-204-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": true, - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": true, - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1800": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-3600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-43200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-86400": [ - "Assertion", - "Response 2 does not come from cache" - ], - "invalidate-DELETE": [ - "Setup", - "Response 2 status is 403, not 200" - ], - "invalidate-DELETE-cl": [ - "Setup", - "Response 2 status is 403, not 200" - ], - "invalidate-DELETE-failed": [ - "Setup", - "Response 2 status is 403, not 500" - ], - "invalidate-DELETE-location": [ - "Setup", - "Response 2 status is 403, not 200" - ], - "invalidate-M-SEARCH": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-failed": true, - "invalidate-M-SEARCH-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST": true, - "invalidate-POST-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-failed": true, - "invalidate-POST-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT": true, - "invalidate-PUT-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-failed": true, - "invalidate-PUT-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": true, - "other-age-gen": true, - "other-age-update-expires": true, - "other-age-update-max-age": true, - "other-authorization": [ - "Assertion", - "Response 2 comes from cache" - ], - "other-authorization-must-revalidate": true, - "other-authorization-public": true, - "other-authorization-smaxage": true, - "other-cookie": true, - "other-date-update": true, - "other-date-update-expires": true, - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-set-cookie": true, - "partial-store-complete-reuse-partial": true, - "partial-store-complete-reuse-partial-no-last": true, - "partial-store-complete-reuse-partial-suffix": true, - "partial-store-partial-complete": [ - "Assertion", - "Request 2 header range is \"undefined\", not \"bytes=5-\"" - ], - "partial-store-partial-reuse-partial": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-absent": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-use-headers": true, - "partial-use-stored-headers": true, - "pragma-request-extension": true, - "pragma-request-no-cache": true, - "pragma-response-extension": true, - "pragma-response-no-cache": [ - "Assertion", - "Response 2 does not come from cache" - ], - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": true, - "stale-close": true, - "stale-close-must-revalidate": true, - "stale-close-no-cache": true, - "stale-close-proxy-revalidate": true, - "stale-close-s-maxage=2": true, - "stale-sie-503": true, - "stale-sie-close": true, - "stale-warning-become": true, - "stale-warning-stored": true, - "stale-while-revalidate": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-while-revalidate-window": [ - "Setup", - "Response 2 does not come from cache" - ], - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": true, - "status-204-stale": true, - "status-299-fresh": true, - "status-299-stale": true, - "status-301-fresh": true, - "status-301-stale": true, - "status-302-fresh": true, - "status-302-stale": true, - "status-303-fresh": true, - "status-303-stale": true, - "status-307-fresh": true, - "status-307-stale": true, - "status-308-fresh": true, - "status-308-stale": true, - "status-400-fresh": true, - "status-400-stale": true, - "status-404-fresh": true, - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": true, - "status-499-stale": true, - "status-500-fresh": true, - "status-500-stale": true, - "status-502-fresh": true, - "status-502-stale": true, - "status-503-fresh": true, - "status-503-stale": true, - "status-504-fresh": true, - "status-504-stale": true, - "status-599-fresh": true, - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": true, - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": true, - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": true, - "vary-normalise-space": true, - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": true, - "vary-syntax-empty-star-lines": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-foo-star": true, - "vary-syntax-star": true, - "vary-syntax-star-foo": true, - "vary-syntax-star-star": true, - "vary-syntax-star-star-lines": true -} diff --git a/test/fixtures/cache-tests/results/varnish.json b/test/fixtures/cache-tests/results/varnish.json deleted file mode 100644 index 067c395fc15..00000000000 --- a/test/fixtures/cache-tests/results/varnish.json +++ /dev/null @@ -1,801 +0,0 @@ -{ - "304-etag-update-response-Cache-Control": true, - "304-etag-update-response-Clear-Site-Data": true, - "304-etag-update-response-Content-Encoding": [ - "Assertion", - "Response 2 header Content-Encoding is \"arizqhypgxofwne\", not \"askcumewogyqias\"" - ], - "304-etag-update-response-Content-Foo": true, - "304-etag-update-response-Content-Length": true, - "304-etag-update-response-Content-Location": true, - "304-etag-update-response-Content-MD5": true, - "304-etag-update-response-Content-Range": [ - "Setup", - "Response 1 status is 503, not 200" - ], - "304-etag-update-response-Content-Security-Policy": true, - "304-etag-update-response-Content-Type": true, - "304-etag-update-response-ETag": true, - "304-etag-update-response-Expires": true, - "304-etag-update-response-Public-Key-Pins": true, - "304-etag-update-response-Set-Cookie": [ - "Setup", - "Request 2 should have been conditional, but it was not." - ], - "304-etag-update-response-Set-Cookie2": true, - "304-etag-update-response-Test-Header": true, - "304-etag-update-response-X-Content-Foo": true, - "304-etag-update-response-X-Frame-Options": true, - "304-etag-update-response-X-Test-Header": true, - "304-etag-update-response-X-XSS-Protection": true, - "304-lm-use-stored-Test-Header": true, - "age-parse-dup-0": true, - "age-parse-dup-0-twoline": true, - "age-parse-dup-old": true, - "age-parse-float": [ - "Assertion", - "Response 2 does not come from cache" - ], - "age-parse-large": true, - "age-parse-large-minus-one": true, - "age-parse-larger": true, - "age-parse-negative": true, - "age-parse-nonnumeric": true, - "age-parse-numeric-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-parameter": [ - "Assertion", - "Response 2 comes from cache" - ], - "age-parse-prefix": true, - "age-parse-prefix-twoline": true, - "age-parse-suffix": true, - "age-parse-suffix-twoline": true, - "cc-resp-must-revalidate-fresh": true, - "cc-resp-must-revalidate-stale": true, - "cc-resp-no-cache": true, - "cc-resp-no-cache-case-insensitive": true, - "cc-resp-no-cache-revalidate": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "cc-resp-no-cache-revalidate-fresh": [ - "Assertion", - "Request 2 should have been conditional, but it was not." - ], - "cc-resp-no-store": true, - "cc-resp-no-store-case-insensitive": true, - "cc-resp-no-store-fresh": true, - "cc-resp-no-store-old-max-age": true, - "cc-resp-no-store-old-new": true, - "cc-resp-private-shared": true, - "ccreq-ma0": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-ma1": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-magreaterage": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-max-stale": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-max-stale-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "ccreq-min-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-min-fresh-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-no-cache-etag": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "ccreq-no-cache-lm": [ - "Assertion", - "request 2 wasn't sent to server" - ], - "ccreq-no-store": [ - "Assertion", - "Response 2 comes from cache" - ], - "ccreq-oic": [ - "Assertion", - "Response 1 status is 200, not 504" - ], - "cdn-cc-invalid-sh-type-unknown": true, - "cdn-cc-invalid-sh-type-wrong": true, - "cdn-date-update-exceed": true, - "cdn-expires-update-exceed": [ - "Assertion", - "Response 2 header Expires is \"null\", not \"Tue, 21 Jan 2025 00:22:31 GMT\"" - ], - "cdn-fresh-cc-nostore": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-0": true, - "cdn-max-age-0-expires": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-age": true, - "cdn-max-age-case-insensitive": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-cc-max-age-invalid-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-expires": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-extension": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-long-cc-max-age": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-max-age-max": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-max-plus": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-short-cc-max-age": [ - "Assertion", - "Response 2 does not come from cache" - ], - "cdn-max-age-space-after-equals": true, - "cdn-max-age-space-before-equals": true, - "cdn-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-no-store-cc-fresh": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-private": [ - "Assertion", - "Response 2 comes from cache" - ], - "cdn-remove-age-exceed": true, - "cdn-remove-header": true, - "conditional-304-etag": true, - "conditional-etag-forward": [ - "Assertion", - "Request 1 header If-None-Match is \"undefined\", not \"\"abcdef\"\"" - ], - "conditional-etag-forward-unquoted": [ - "Assertion", - "Request 1 header If-None-Match is \"undefined\", not \"\"abcdef\"\"" - ], - "conditional-etag-precedence": true, - "conditional-etag-quoted-respond-unquoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-strong-generate": true, - "conditional-etag-strong-generate-unquoted": [ - "Assertion", - "Request 2 header If-None-Match is \"abcdef\", not \"\"abcdef\"\"" - ], - "conditional-etag-strong-respond": true, - "conditional-etag-strong-respond-multiple-first": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-strong-respond-multiple-last": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-strong-respond-multiple-second": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-strong-respond-obs-text": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-quoted": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-etag-unquoted-respond-unquoted": true, - "conditional-etag-vary-headers": true, - "conditional-etag-vary-headers-mismatch": true, - "conditional-etag-weak-generate-weak": true, - "conditional-etag-weak-respond": true, - "conditional-etag-weak-respond-backslash": true, - "conditional-etag-weak-respond-lowercase": true, - "conditional-etag-weak-respond-omit-slash": true, - "conditional-lm-fresh": true, - "conditional-lm-fresh-earlier": true, - "conditional-lm-fresh-no-lm": [ - "Assertion", - "Response 2 status is 200, not 304" - ], - "conditional-lm-fresh-rfc850": true, - "conditional-lm-stale": true, - "freshness-expires-32bit": true, - "freshness-expires-age-fast-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-expires-age-slow-date": true, - "freshness-expires-ansi-c": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-far-future": true, - "freshness-expires-future": true, - "freshness-expires-invalid": true, - "freshness-expires-invalid-1-digit-hour": true, - "freshness-expires-invalid-2-digit-year": true, - "freshness-expires-invalid-aest": true, - "freshness-expires-invalid-date": true, - "freshness-expires-invalid-date-dashes": true, - "freshness-expires-invalid-multiple-lines": true, - "freshness-expires-invalid-multiple-spaces": true, - "freshness-expires-invalid-no-comma": true, - "freshness-expires-invalid-time-periods": true, - "freshness-expires-invalid-utc": true, - "freshness-expires-old-date": true, - "freshness-expires-past": true, - "freshness-expires-present": true, - "freshness-expires-rfc850": true, - "freshness-expires-wrong-case-month": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-tz": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-expires-wrong-case-weekday": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age": true, - "freshness-max-age-0": true, - "freshness-max-age-0-expires": true, - "freshness-max-age-100a": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-a100": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-age": true, - "freshness-max-age-case-insenstive": true, - "freshness-max-age-date": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-decimal-five": true, - "freshness-max-age-decimal-zero": true, - "freshness-max-age-expires": true, - "freshness-max-age-expires-invalid": true, - "freshness-max-age-extension": true, - "freshness-max-age-ignore-quoted": true, - "freshness-max-age-ignore-quoted-rev": true, - "freshness-max-age-leading-zero": true, - "freshness-max-age-max": true, - "freshness-max-age-max-minus-1": true, - "freshness-max-age-max-plus": true, - "freshness-max-age-max-plus-1": true, - "freshness-max-age-negative": true, - "freshness-max-age-quoted": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-s-maxage-shared-longer": true, - "freshness-max-age-s-maxage-shared-longer-multiple": true, - "freshness-max-age-s-maxage-shared-longer-reversed": true, - "freshness-max-age-s-maxage-shared-shorter": true, - "freshness-max-age-s-maxage-shared-shorter-expires": true, - "freshness-max-age-single-quoted": true, - "freshness-max-age-space-after-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-space-before-equals": [ - "Assertion", - "Response 2 comes from cache" - ], - "freshness-max-age-stale": true, - "freshness-max-age-two-fresh-stale-sameline": true, - "freshness-max-age-two-fresh-stale-sepline": true, - "freshness-max-age-two-stale-fresh-sameline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-max-age-two-stale-fresh-sepline": [ - "Assertion", - "Response 2 does not come from cache" - ], - "freshness-none": true, - "freshness-s-maxage-shared": true, - "head-200-freshness-update": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "head-200-retain": [ - "Assertion", - "Response 2 header Template-A is \"null\", not \"1\"" - ], - "head-200-update": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "head-410-update": [ - "Setup", - "Response 3 status is 410, not 200" - ], - "head-writethrough": [ - "Assertion", - "Request 2 had method GET, not HEAD" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Cache-Control-no-cache-single": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-omit-headers-listed-in-Connection": true, - "headers-store-Cache-Control": true, - "headers-store-Clear-Site-Data": true, - "headers-store-Connection": true, - "headers-store-Content-Encoding": true, - "headers-store-Content-Foo": true, - "headers-store-Content-Length": true, - "headers-store-Content-Location": true, - "headers-store-Content-MD5": true, - "headers-store-Content-Range": [ - "Setup", - "Response 1 status is 503, not 200" - ], - "headers-store-Content-Security-Policy": true, - "headers-store-Content-Type": true, - "headers-store-ETag": true, - "headers-store-Expires": true, - "headers-store-Keep-Alive": true, - "headers-store-Proxy-Authenticate": true, - "headers-store-Proxy-Authentication-Info": true, - "headers-store-Proxy-Authorization": true, - "headers-store-Proxy-Connection": true, - "headers-store-Public-Key-Pins": true, - "headers-store-Set-Cookie": [ - "Setup", - "Response 2 does not come from cache" - ], - "headers-store-Set-Cookie2": true, - "headers-store-TE": true, - "headers-store-Test-Header": true, - "headers-store-Transfer-Encoding": [ - "Setup", - "Response 1 status is 503, not 200" - ], - "headers-store-Upgrade": true, - "headers-store-X-Content-Foo": true, - "headers-store-X-Frame-Options": true, - "headers-store-X-Test-Header": true, - "headers-store-X-XSS-Protection": true, - "heuristic-200-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-201-not_cached": true, - "heuristic-202-not_cached": true, - "heuristic-203-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-204-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-403-not_cached": true, - "heuristic-404-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-405-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-410-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-414-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-501-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-502-not_cached": true, - "heuristic-503-not_cached": true, - "heuristic-504-not_cached": true, - "heuristic-599-cached": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-599-not_cached": true, - "heuristic-delta-10": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-1800": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-30": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-300": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-3600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-43200": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-5": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-60": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-600": [ - "Assertion", - "Response 2 does not come from cache" - ], - "heuristic-delta-86400": [ - "Assertion", - "Response 2 does not come from cache" - ], - "invalidate-DELETE": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-DELETE-failed": true, - "invalidate-DELETE-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-M-SEARCH-failed": true, - "invalidate-M-SEARCH-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-POST-failed": true, - "invalidate-POST-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-cl": [ - "Assertion", - "Response 3 comes from cache" - ], - "invalidate-PUT-failed": true, - "invalidate-PUT-location": [ - "Assertion", - "Response 3 comes from cache" - ], - "method-POST": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-age-delay": [ - "Assertion", - "Response 1 header age is 0, should be bigger than 0" - ], - "other-age-gen": true, - "other-age-update-expires": true, - "other-age-update-max-age": true, - "other-authorization": true, - "other-authorization-must-revalidate": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-authorization-public": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-authorization-smaxage": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-cookie": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-date-update": true, - "other-date-update-expires": true, - "other-date-update-expires-update": true, - "other-fresh-content-disposition-attachment": true, - "other-heuristic-content-disposition-attachment": [ - "Assertion", - "Response 2 does not come from cache" - ], - "other-set-cookie": [ - "Assertion", - "Response 2 does not come from cache" - ], - "partial-store-complete-reuse-partial": true, - "partial-store-complete-reuse-partial-no-last": true, - "partial-store-complete-reuse-partial-suffix": true, - "partial-store-partial-complete": [ - "Setup", - "Response 1 status is 503, not 206" - ], - "partial-store-partial-reuse-partial": [ - "Setup", - "Response 1 status is 503, not 206" - ], - "partial-store-partial-reuse-partial-absent": [ - "Setup", - "Response 1 status is 503, not 206" - ], - "partial-store-partial-reuse-partial-byterange": [ - "Setup", - "Response 1 status is 503, not 206" - ], - "partial-store-partial-reuse-partial-suffix": [ - "Setup", - "Response 1 status is 503, not 206" - ], - "partial-use-headers": true, - "partial-use-stored-headers": true, - "pragma-request-extension": true, - "pragma-request-no-cache": true, - "pragma-response-extension": true, - "pragma-response-no-cache": true, - "pragma-response-no-cache-heuristic": [ - "Assertion", - "Response 2 does not come from cache" - ], - "query-args-different": true, - "query-args-same": true, - "stale-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-close-must-revalidate": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-no-cache": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-proxy-revalidate": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-close-s-maxage=2": [ - "Assertion", - "Response 2 comes from cache" - ], - "stale-sie-503": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-sie-close": [ - "Assertion", - "Response 2 does not come from cache" - ], - "stale-warning-become": [ - "Setup", - "Response 2 does not come from cache" - ], - "stale-warning-stored": [ - "Setup", - "Response 2 does not come from cache" - ], - "stale-while-revalidate": true, - "stale-while-revalidate-window": true, - "status-200-fresh": true, - "status-200-must-understand": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-200-stale": true, - "status-203-fresh": true, - "status-203-stale": true, - "status-204-fresh": true, - "status-204-stale": true, - "status-299-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-299-stale": true, - "status-301-fresh": true, - "status-301-stale": true, - "status-302-fresh": true, - "status-302-stale": true, - "status-303-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-303-stale": true, - "status-307-fresh": true, - "status-307-stale": true, - "status-308-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-308-stale": true, - "status-400-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-400-stale": true, - "status-404-fresh": true, - "status-404-stale": true, - "status-410-fresh": true, - "status-410-stale": true, - "status-499-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-499-stale": true, - "status-500-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-500-stale": true, - "status-502-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-502-stale": true, - "status-503-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-503-stale": true, - "status-504-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-504-stale": true, - "status-599-fresh": [ - "Assertion", - "Response 2 does not come from cache" - ], - "status-599-must-understand": true, - "status-599-stale": true, - "vary-2-match": true, - "vary-2-match-omit": true, - "vary-2-no-match": true, - "vary-3-match": true, - "vary-3-no-match": true, - "vary-3-omit": true, - "vary-3-order": true, - "vary-cache-key": true, - "vary-invalidate": true, - "vary-match": true, - "vary-no-match": true, - "vary-normalise-combine": true, - "vary-normalise-lang-case": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-order": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-select": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-lang-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-normalise-space": [ - "Assertion", - "Response 2 does not come from cache" - ], - "vary-omit": true, - "vary-omit-stored": true, - "vary-star": true, - "vary-syntax-empty-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-empty-star-lines": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-foo-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star": true, - "vary-syntax-star-foo": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star-star": [ - "Assertion", - "Response 2 comes from cache" - ], - "vary-syntax-star-star-lines": [ - "Assertion", - "Response 2 comes from cache" - ] -} diff --git a/test/fixtures/cache-tests/test-engine/cli.mjs b/test/fixtures/cache-tests/test-engine/cli.mjs deleted file mode 100644 index 2493a51a9c5..00000000000 --- a/test/fixtures/cache-tests/test-engine/cli.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { runTests, getResults } from './client/runner.mjs' -import { determineTestResult } from './lib/results.mjs' -import { GREEN, NC } from './lib/defines.mjs' -import tests from '../tests/index.mjs' - -const baseUrl = process.env.npm_config_base || process.env.npm_package_config_base -const testId = process.env.npm_config_id || process.env.npm_package_config_id - -let testsToRun -if (testId !== '') { - console.log(`Running ${testId}`) - tests.forEach(suite => { - suite.tests.forEach(test => { - if (test.id === testId) { - test.dump = true - testsToRun = [{ - name: suite.name, - id: suite.id, - description: suite.description, - tests: [test] - }] - } - }) - }) -} else { - testsToRun = tests -} - -await runTests(testsToRun, false, baseUrl).catch(err => { - console.error(err) - process.exit(1) -}) - -const results = getResults() - -if (testId !== '') { - console.log(`${GREEN}==== Results${NC}`) - const resultSymbol = determineTestResult(tests, testId, results, false) - const resultDetails = results[testId][1] || '' - console.log(`${resultSymbol[2]} - ${resultDetails}`) -} else { - console.log(JSON.stringify(results, null, 2)) -} diff --git a/test/fixtures/cache-tests/test-engine/client/config.mjs b/test/fixtures/cache-tests/test-engine/client/config.mjs deleted file mode 100644 index 11aa7910992..00000000000 --- a/test/fixtures/cache-tests/test-engine/client/config.mjs +++ /dev/null @@ -1,11 +0,0 @@ -export let useBrowserCache = false -export let baseUrl = '' -export const requestTimeout = 10 // seconds - -export function setUseBrowserCache (bool) { - if (bool !== undefined) useBrowserCache = bool -} - -export function setBaseUrl (url) { - if (url !== undefined) baseUrl = url -} diff --git a/test/fixtures/cache-tests/test-engine/client/fetching.mjs b/test/fixtures/cache-tests/test-engine/client/fetching.mjs deleted file mode 100644 index e1260da1e7f..00000000000 --- a/test/fixtures/cache-tests/test-engine/client/fetching.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import * as config from './config.mjs' -import { fixupHeader } from '../lib/header-fixup.mjs' - -export function init (idx, reqConfig, prevResp) { - const init = { - headers: [] - } - if (!config.useBrowserCache) { - init.cache = 'no-store' - init.headers.push(['Pragma', 'foo']) // dirty hack for Fetch - init.headers.push(['Cache-Control', 'nothing-to-see-here']) // ditto - } - if ('request_method' in reqConfig) init.method = reqConfig.request_method - if ('request_headers' in reqConfig) init.headers = init.headers.concat(reqConfig.request_headers) - if ('magic_ims' in reqConfig && reqConfig.magic_ims === true) { - for (let i = 0; i < init.headers.length; i++) { - const header = init.headers[i] - if (header[0].toLowerCase() === 'if-modified-since') { - init.headers[i] = fixupHeader(header, prevResp, reqConfig) - } - } - } - if ('name' in reqConfig) init.headers.push(['Test-Name', reqConfig.name]) - if ('request_body' in reqConfig) init.body = reqConfig.request_body - if ('mode' in reqConfig) init.mode = reqConfig.mode - if ('credentials' in reqConfig) init.mode = reqConfig.credentials - if ('cache' in reqConfig) init.cache = reqConfig.cache - if ('redirect' in reqConfig) init.redirect = reqConfig.redirect - init.headers.push(['Test-ID', reqConfig.id]) - init.headers.push(['Req-Num', (idx + 1).toString()]) - return init -} - -export function inflateRequests (test) { - const rawRequests = test.requests - const requests = [] - for (let i = 0; i < rawRequests.length; i++) { - const reqConfig = rawRequests[i] - reqConfig.name = test.name - reqConfig.id = test.id - reqConfig.dump = test.dump - requests.push(reqConfig) - } - return requests -} diff --git a/test/fixtures/cache-tests/test-engine/client/runner.mjs b/test/fixtures/cache-tests/test-engine/client/runner.mjs deleted file mode 100644 index 9c7426e658c..00000000000 --- a/test/fixtures/cache-tests/test-engine/client/runner.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import * as config from './config.mjs' -import { makeTest, testResults } from './test.mjs' - -export async function runTests (tests, browserCache, base, chunkSize = 25) { - config.setBaseUrl(base) - config.setUseBrowserCache(browserCache) - - const testArray = [] - tests.forEach(testSet => { - testSet.tests.forEach(test => { - if (test.id === undefined) throw new Error('Missing test id') - if (test.browser_only === true && !config.useBrowserCache === true) return - if (test.cdn_only === true && config.useBrowserCache === true) return - // note: still runs cdn tests on rev-proxy - if (test.browser_skip === true && config.useBrowserCache === true) return - testArray.push(test) - }) - }) - return runSome(testArray, chunkSize) -} - -export function getResults () { - const ordered = {} - Object.keys(testResults).sort().forEach(key => { - ordered[key] = testResults[key] - }) - return ordered -} - -async function runSome (tests, chunkSize) { - let index = 0 - function next () { - if (index < tests.length) { - const these = tests.slice(index, index + chunkSize).map(makeTest) - index += chunkSize - return Promise.all(these).then(next) - } - } - return next() -} diff --git a/test/fixtures/cache-tests/test-engine/client/test.mjs b/test/fixtures/cache-tests/test-engine/client/test.mjs deleted file mode 100644 index 56487a1135f..00000000000 --- a/test/fixtures/cache-tests/test-engine/client/test.mjs +++ /dev/null @@ -1,301 +0,0 @@ -import * as defines from '../lib/defines.mjs' -import { fixupHeader } from '../lib/header-fixup.mjs' -import * as utils from '../lib/utils.mjs' -import * as config from './config.mjs' -import * as clientUtils from './utils.mjs' -import * as fetching from './fetching.mjs' -const assert = utils.assert -const setupCheck = clientUtils.setupCheck - -export const testUUIDs = {} -export const testResults = {} - -export async function makeTest (test) { - const uuid = utils.token() - testUUIDs[test.id] = uuid - const requests = fetching.inflateRequests(test) - const responses = [] - const fetchFunctions = [] - for (let i = 0; i < requests.length; ++i) { - fetchFunctions.push({ - code: idx => { - const reqConfig = requests[idx] - const reqNum = idx + 1 - const url = clientUtils.makeTestUrl(uuid, reqConfig) - let prevRes - if (i > 0) { - prevRes = Object.fromEntries(responses[i - 1].headers) - } - const init = fetching.init(idx, reqConfig, prevRes) - const controller = new AbortController() - const timeout = setTimeout(() => { - controller.abort() - }, config.requestTimeout * 1000) - init.signal = controller.signal - if (test.dump === true) clientUtils.logRequest(url, init, reqNum) - return fetch(url, init) - .then(response => { - responses.push(response) - return checkResponse(test, requests, idx, response) - }) - .finally(() => { - clearTimeout(timeout) - }) - }, - pauseAfter: 'pause_after' in requests[i] - }) - } - - let idx = 0 - function runNextStep () { - if (fetchFunctions.length) { - const nextFetchFunction = fetchFunctions.shift() - if (nextFetchFunction.pauseAfter === true) { - return nextFetchFunction.code(idx++) - .then(clientUtils.pause) - .then(runNextStep) - } else { - return nextFetchFunction.code(idx++) - .then(runNextStep) - } - } - } - - function handleError (err) { - console.error(`ERROR: ${uuid} ${err.name} ${err.message}`) - } - - return clientUtils.putTestConfig(uuid, requests) - .catch(handleError) - .then(runNextStep) - .then(() => { - return clientUtils.getServerState(uuid) - }) - .then(serverState => { - checkServerRequests(requests, responses, serverState) - }) - .then(() => { // pass - if (test.id in testResults) throw new Error(`Duplicate test ${test.id}`) - testResults[test.id] = true - }) - .catch(err => { // fail - if (test.id in testResults) throw new Error(`Duplicate test ${test.id}`) - testResults[test.id] = [(err.name || 'unknown'), err.message] - }) -} - -function checkResponse (test, requests, idx, response) { - const reqNum = idx + 1 - const reqConfig = requests[idx] - const resNum = parseInt(response.headers.get('Server-Request-Count')) - if (test.dump === true) clientUtils.logResponse(response, reqNum) - - // catch retries - if (response.headers.has('Request-Numbers')) { - const serverRequests = response.headers.get('Request-Numbers').split(' ').map(item => parseInt(item)) - if (serverRequests.length !== new Set(serverRequests).size) { - assert(true, false, 'retry') - } - } - - // check response type - if ('expected_type' in reqConfig) { - const typeSetup = setupCheck(reqConfig, 'expected_type') - if (reqConfig.expected_type === 'cached') { - if (response.status === 304 && isNaN(resNum)) { // some caches will not include the hdr - // pass - } else { - assert(typeSetup, resNum < reqNum, `Response ${reqNum} does not come from cache`) - } - } - if (reqConfig.expected_type === 'not_cached') { - assert(typeSetup, resNum === reqNum, `Response ${reqNum} comes from cache`) - } - } - - // check response status - if ('expected_status' in reqConfig) { - if (reqConfig.expected_status !== null) { - assert(setupCheck(reqConfig, 'expected_status'), - response.status === reqConfig.expected_status, - `Response ${reqNum} status is ${response.status}, not ${reqConfig.expected_status}`) - } - } else if ('response_status' in reqConfig) { - assert(true, // response status is always setup - response.status === reqConfig.response_status[0], - `Response ${reqNum} status is ${response.status}, not ${reqConfig.response_status[0]}`) - } else if (response.status === 999) { - // special condition; the server thought it should have received a conditional request. - assert(setupCheck(reqConfig, 'expected_type'), false, - `Request ${reqNum} should have been conditional, but it was not.`) - } else { - assert(true, // default status is always setup - response.status === 200, - `Response ${reqNum} status is ${response.status}, not 200`) - } - - // check response headers - if ('expected_response_headers' in reqConfig) { - const respPresentSetup = setupCheck(reqConfig, 'expected_response_headers') - reqConfig.expected_response_headers.forEach(header => { - if (typeof header === 'string') { - assert(respPresentSetup, response.headers.has(header), - `Response ${reqNum} ${header} header not present.`) - } else if (header.length > 2) { - assert(respPresentSetup, response.headers.has(header[0]), - `Response ${reqNum} ${header[0]} header not present.`) - - const value = response.headers.get(header[0]) - let msg, condition - if (header[1] === '=') { - const expected = response.headers.get(header[2]) - condition = value === expected - msg = `match ${header[2]} (${expected})` - } else if (header[1] === '>') { - const expected = header[2] - condition = parseInt(value) > expected - msg = `be bigger than ${expected}` - } else { - throw new Error(`Unknown expected-header operator '${header[1]}'`) - } - - assert(respPresentSetup, condition, - `Response ${reqNum} header ${header[0]} is ${value}, should ${msg}`) - } else { - const expectedValue = fixupHeader( - header, Object.fromEntries(response.headers), reqConfig)[1] - assert(respPresentSetup, response.headers.get(header[0]) === expectedValue, - `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${expectedValue}"`) - } - }) - } - if ('expected_response_headers_missing' in reqConfig) { - const respMissingSetup = setupCheck(reqConfig, 'expected_response_headers_missing') - reqConfig.expected_response_headers_missing.forEach(header => { - if (typeof header === 'string') { - assert(respMissingSetup, !response.headers.has(header), - `Response ${reqNum} includes unexpected header ${header}: "${response.headers.get(header)}"`) - } else if (header.length === 2) { - if (response.headers.has(header[0]) && response.headers[header[0]]) { - const hdrValue = response.headers[header[0]] - assert(respMissingSetup, hdrValue.indexOf(header[1]) === -1, `Response ${reqNum} header ${header[0]} still has value "${hdrValue}"`) - } - } else { - throw new Error(`Unknown unexpected-header form '${header}'`) - } - }) - } - return response.text().then(makeCheckResponseBody(test, reqConfig, response.status)) -} - -function makeCheckResponseBody (test, reqConfig, statusCode) { - return function checkResponseBody (resBody) { - if ('check_body' in reqConfig && reqConfig.check_body === false) { - return true - } else if ('expected_response_text' in reqConfig) { - if (reqConfig.expected_response_text !== null) { - assert(setupCheck(reqConfig, 'expected_response_text'), - resBody === reqConfig.expected_response_text, - `Response body is "${resBody}", not "${reqConfig.expected_response_text}"`) - } - } else if ('response_body' in reqConfig && reqConfig.response_body !== null) { - assert(true, // response_body is always setup - resBody === reqConfig.response_body, - `Response body is "${resBody}", not "${reqConfig.response_body}"`) - } else if (!defines.noBodyStatus.has(statusCode) && reqConfig.request_method !== 'HEAD') { - const uuid = testUUIDs[test.id] - assert(true, // no_body is always setup - resBody === uuid, - `Response body is "${resBody}", not "${uuid}"`) - } - } -} - -function checkServerRequests (requests, responses, serverState) { - // compare a test's requests array against the server-side serverState - let testIdx = 0 - for (let i = 0; i < requests.length; ++i) { - const expectedValidatingHeaders = [] - const reqConfig = requests[i] - const response = responses[i] - const serverRequest = serverState[testIdx] - const reqNum = i + 1 - const typeSetup = setupCheck(reqConfig, 'expected_type') - if ('expected_type' in reqConfig) { - if (reqConfig.expected_type === 'cached') continue // the server will not see the request - if (reqConfig.expected_type === 'not_cached') { - assert(typeSetup, serverRequest.request_num === reqNum, `Response ${reqNum} comes from cache (${serverRequest.request_num} on server)`) - } - if (reqConfig.expected_type === 'etag_validated') { - expectedValidatingHeaders.push('if-none-match') - } - if (reqConfig.expected_type === 'lm_validated') { - expectedValidatingHeaders.push('if-modified-since') - } - } - testIdx++ // only increment for requests the server sees - expectedValidatingHeaders.forEach(vhdr => { - assert(typeSetup, typeof (serverRequest) !== 'undefined', `request ${reqNum} wasn't sent to server`) - assert(typeSetup, Object.prototype.hasOwnProperty.call(serverRequest.request_headers, vhdr), - `request ${reqNum} doesn't have ${vhdr} header`) - }) - if ('expected_request_headers' in reqConfig) { - const reqPresentSetup = setupCheck(reqConfig, 'expected_request_headers') - reqConfig.expected_request_headers.forEach(header => { - if (typeof header === 'string') { - const headerName = header.toLowerCase() - assert(reqPresentSetup, Object.prototype.hasOwnProperty.call(serverRequest.request_headers, headerName), - `Request ${reqNum} ${header} header not present.`) - } else { - const reqValue = serverRequest.request_headers[header[0].toLowerCase()] - assert(reqPresentSetup, reqValue === header[1], - `Request ${reqNum} header ${header[0]} is "${reqValue}", not "${header[1]}"`) - } - }) - } - if ('expected_request_headers_missing' in reqConfig) { - const reqmPresentSetup = setupCheck(reqConfig, 'expected_request_headers_missing') - reqConfig.expected_request_headers_missing.forEach(header => { - if (typeof header === 'string') { - const headerName = header.toLowerCase() - assert(reqmPresentSetup, !Object.prototype.hasOwnProperty.call(serverRequest.request_headers, headerName), - `Request ${reqNum} ${header} header present.`) - } else { - const reqValue = serverRequest.request_headers[header[0].toLowerCase()] - assert(reqmPresentSetup, reqValue !== header[1], - `Request ${reqNum} header ${header[0]} is "${reqValue}"`) - } - }) - } - if (typeof serverRequest !== 'undefined' && 'response_headers' in serverRequest) { - serverRequest.response_headers.forEach(header => { - if (config.useBrowserCache && defines.forbiddenResponseHeaders.has(header[0].toLowerCase())) { - // browsers prevent reading these headers through the Fetch API so we can't verify them - return - } - if (defines.skipResponseHeaders.has(header[0].toLowerCase())) { - // these just cause spurious failures - return - } - let received = response.headers.get(header[0]) - // XXX: assumes that if a proxy joins headers, it'll separate them with a comma and exactly one space - if (Array.isArray(received)) { - received = received.join(', ') - } - if (Array.isArray(header[1])) { - header[1] = header[1].join(', ') - } - assert(true, // default headers is always setup - received === header[1], - `Response ${reqNum} header ${header[0]} is "${received}", not "${header[1]}"`) - }) - } - if ('expected_method' in reqConfig) { - assert( - setupCheck(reqConfig, 'expected_method'), - serverRequest.request_method === reqConfig.expected_method, - `Request ${reqNum} had method ${serverRequest.request_method}, not ${reqConfig.expected_method}` - ) - } - } -} diff --git a/test/fixtures/cache-tests/test-engine/client/utils.mjs b/test/fixtures/cache-tests/test-engine/client/utils.mjs deleted file mode 100644 index 21d918d1e98..00000000000 --- a/test/fixtures/cache-tests/test-engine/client/utils.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import * as config from './config.mjs' -import * as utils from '../lib/utils.mjs' -import * as defines from '../lib/defines.mjs' - -export function pause () { - return new Promise(function (resolve, reject) { - setTimeout(() => { - return resolve() - }, 3000) - }) -} - -export function makeTestUrl (uuid, reqConfig) { - let extra = '' - if ('filename' in reqConfig) { - extra += `/${reqConfig.filename}` - } - if ('query_arg' in reqConfig) { - extra += `?${reqConfig.query_arg}` - } - return `${config.baseUrl}/test/${uuid}${extra}` -} - -const uninterestingHeaders = new Set(['date', 'expires', 'last-modified', 'content-length', 'content-type', 'connection', 'content-language', 'vary', 'mime-version']) - -export async function putTestConfig (uuid, requests) { - const init = { - method: 'PUT', - headers: [['content-type', 'application/json']], - body: JSON.stringify(requests) - } - return fetch(`${config.baseUrl}/config/${uuid}`, init) - .then(response => { - if (response.status !== 201) { - let headers = '' - response.headers.forEach((hvalue, hname) => { // for some reason, node-fetch reverses these - if (!uninterestingHeaders.has(hname.toLowerCase())) { - headers += `${hname}: ${hvalue} ` - } - }) - throw new utils.SetupError({ message: `PUT config resulted in ${response.status} ${response.statusText} - ${headers}` }) - } - }) -} - -export async function getServerState (uuid) { - return fetch(`${config.baseUrl}/state/${uuid}`) - .then(response => { - if (response.status === 200) { - return response.text() - } - }).then(text => { - if (text === undefined) return [] - return JSON.parse(text) - }) -} - -export function setupCheck (reqConfig, memberName) { - return reqConfig.setup === true || ('setup_tests' in reqConfig && reqConfig.setup_tests.indexOf(memberName) > -1) -} - -export function logRequest (url, init, reqNum) { - console.log(`${defines.GREEN}=== Client request ${reqNum}${defines.NC}`) - if ('method' in init) { - console.log(` ${init.method} ${url}`) - } else { - console.log(` GET ${url}`) - } - init.headers.forEach(header => { - console.log(` ${header[0]}: ${header[1]}`) - }) - console.log('') -} - -export function logResponse (response, reqNum) { - console.log(`${defines.GREEN}=== Client response ${reqNum}${defines.NC}`) - console.log(` HTTP ${response.status} ${response.statusText}`) - response.headers.forEach((hvalue, hname) => { // for some reason, node-fetch reverses these - console.log(` ${hname}: ${hvalue}`) - }) - console.log('') -} diff --git a/test/fixtures/cache-tests/test-engine/export.mjs b/test/fixtures/cache-tests/test-engine/export.mjs deleted file mode 100644 index 27eec3ae05b..00000000000 --- a/test/fixtures/cache-tests/test-engine/export.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import fs from 'fs' - -import Ajv from 'ajv' - -import tests from '../tests/index.mjs' - -if (process.argv[2] === 'validate') { - const ajv = new Ajv() - const schema = JSON.parse(fs.readFileSync('test-engine/lib/testsuite-schema.json', 'utf8')) - const validate = ajv.compile(schema) - const valid = validate(tests) - if (!valid) { - console.log(validate.errors) - process.exit(1) - } -} else { - console.log(JSON.stringify(tests, null, 2)) -} diff --git a/test/fixtures/cache-tests/test-engine/lib/defines.mjs b/test/fixtures/cache-tests/test-engine/lib/defines.mjs deleted file mode 100644 index fae246b8388..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/defines.mjs +++ /dev/null @@ -1,28 +0,0 @@ -export const noBodyStatus = new Set([204, 304]) - -export const dateHeaders = new Set(['date', 'expires', 'last-modified', 'if-modified-since', 'if-unmodified-since']) - -export const locationHeaders = new Set(['location', 'content-location']) - -// https://fetch.spec.whatwg.org/#forbidden-response-header-name -export const forbiddenResponseHeaders = new Set(['set-cookie', 'set-cookie2']) - -// headers to skip when checking response_headers (not expected) -export const skipResponseHeaders = new Set(['date']) - -// colours for console -export const RED = '\x1b[31m' -export const GREEN = '\x1b[32m' -export const BLUE = '\x1b[34m' -export const NC = '\x1b[0m' - -// mime types for server -export const mimeTypes = { - html: 'text/html', - jpeg: 'image/jpeg', - jpg: 'image/jpeg', - png: 'image/png', - js: 'application/javascript', - mjs: 'application/javascript', - css: 'text/css' -} diff --git a/test/fixtures/cache-tests/test-engine/lib/display.mjs b/test/fixtures/cache-tests/test-engine/lib/display.mjs deleted file mode 100644 index 75f14dd949a..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/display.mjs +++ /dev/null @@ -1,153 +0,0 @@ -/* global Blob marked */ - -import '../../asset/marked.min.js' -import { Liquid } from '../../asset/liquid.browser.esm.mjs' -import { modalOpen } from './modal.mjs' - -import { determineTestResult, resultTypes } from './results.mjs' - -const templateEngine = new Liquid({ root: 'test-engine/lib/tpl', extname: '.liquid', cache: true }) -templateEngine.registerFilter('typeof', v => typeof (v)) -templateEngine.registerFilter('toLocaleString', v => v.toLocaleString()) -templateEngine.registerFilter('skipHeaders', v => { - if (v) { - return v.filter(hdr => hdr.length < 3 || hdr[2] !== false) - } else { - return [] - } -}) - -export function downloadTestResults (target, fileName, data, auto) { - const dataBlob = new Blob([JSON.stringify(data, null, 2)], { type: 'text/json' }) - target.setAttribute('href', window.URL.createObjectURL(dataBlob)) - target.setAttribute('download', fileName) - target.style.display = 'inherit' - if (auto) { - target.click() - } -} - -export function renderTestResults (testSuites, testResults, testUUIDs, target, useBrowserCache) { - let totalTests = 0 - let totalPassed = 0 - testSuites.forEach(testSuite => { - const headerElement = document.createElement('h3') - target.appendChild(headerElement) - const headerText = document.createTextNode(testSuite.name) - headerElement.appendChild(headerText) - const listElement = document.createElement('ul') - const resultList = target.appendChild(listElement) - let tests = 0 - let passed = 0 - testSuite.tests.forEach(test => { - if (test.browser_only === true && !useBrowserCache === true) return - if (test.cdn_only === true && useBrowserCache === true) return - if (test.browser_skip === true && useBrowserCache === true) return - test.suiteName = testSuite.name - const testElement = resultList.appendChild(document.createElement('li')) - testElement.appendChild(showTestResult(testSuites, test.id, testResults)) - testElement.appendChild(showTestName(test, testUUIDs[test.id])) - tests++ - if (testResults[test.id] === true) { - passed++ - } - }) - const summaryElement = document.createElement('p') - const suiteSummary = target.appendChild(summaryElement) - suiteSummary.appendChild(document.createTextNode(tests + ' tests, ' + passed + ' passed.')) - totalTests += tests - totalPassed += passed - }) - const totalElement = document.createElement('p') - const totalSummary = target.appendChild(totalElement) - const totalText = document.createTextNode('Total ' + totalTests + ' tests, ' + totalPassed + ' passed.') - totalSummary.appendChild(totalText) -} - -export function showTestName (test, uuid) { - const wrapper = document.createElement('span') - const span = document.createElement('span') - span.setAttribute('class', 'clickhint') - span.innerHTML = marked.parse(test.name).slice(3, -5) - span.addEventListener('click', event => { - copyTextToClipboard(test.id) - showTestDetails(test) - }) - wrapper.appendChild(span) - - if (uuid) { - const uuidLinkElement = document.createElement('a') - uuidLinkElement.appendChild(document.createTextNode('⚙︎')) - uuidLinkElement.setAttribute('class', 'uuid') - uuidLinkElement.addEventListener('click', event => { - copyTextToClipboard(uuid) - }) - uuidLinkElement.title = 'Test UUID (click to copy)' - wrapper.appendChild(uuidLinkElement) - } - return wrapper -} - -export function showKey (element) { - const spans = element.getElementsByClassName('fa') - for (const span of spans) { - const kind = span.getAttribute('data-kind') - const styling = resultTypes[kind] - const contentNode = document.createTextNode(styling[0]) - span.style.color = styling[1] - span.appendChild(contentNode) - } -} - -export function showTestResult (testSuites, testId, testResults) { - const result = testResults[testId] - const resultValue = determineTestResult(testSuites, testId, testResults) - const resultNode = document.createTextNode(` ${resultValue[0]} `) - const span = document.createElement('span') - span.className = 'fa' - span.style.color = resultValue[1] - span.appendChild(resultNode) - if (result && typeof (result[1]) === 'string') { - span.title = result[1] - } - return span -} - -export function showTestDetails (test) { - templateEngine - .renderFile('explain-test', { test }) - .then(result => { - console.log(result) - const html = marked.parse(result) - modalOpen(html) - }) - .catch(err => { - console.log(`Template error: ${err}`) - }) -} - -function copyTextToClipboard (text) { - const textArea = document.createElement('textarea') - textArea.style.position = 'fixed' - textArea.style.top = 0 - textArea.style.left = 0 - textArea.style.width = '2em' - textArea.style.height = '2em' - textArea.style.padding = 0 - textArea.style.border = 'none' - textArea.style.outline = 'none' - textArea.style.boxShadow = 'none' - textArea.style.background = 'transparent' - textArea.value = text - document.body.appendChild(textArea) - textArea.focus() - textArea.select() - try { - const successful = document.execCommand('copy') - const msg = successful ? 'successful' : 'unsuccessful' - console.log(`Copying text "${text}" was ${msg}`) - } catch (err) { - console.log('Unable to copy') - } - document.body.removeChild(textArea) -} diff --git a/test/fixtures/cache-tests/test-engine/lib/header-fixup.mjs b/test/fixtures/cache-tests/test-engine/lib/header-fixup.mjs deleted file mode 100644 index 570e0410578..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/header-fixup.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { locationHeaders, dateHeaders } from './defines.mjs' -import { httpDate } from './utils.mjs' - -export function fixupHeader (header, respHeaders, reqConfig) { - const headerName = header[0].toLowerCase() - - // Date headers - const serverNow = parseInt(respHeaders['server-now']) - if (dateHeaders.has(headerName) && Number.isInteger(header[1])) { - let format - if ('rfc850date' in reqConfig && reqConfig.rfc850date.includes(headerName)) { - format = 'rfc850' - } - header[1] = httpDate(serverNow, header[1], format) - } - - // Location headers - const baseUrl = respHeaders['server-base-url'] - if (locationHeaders.has(headerName) && reqConfig.magic_locations) { - if (header[1]) { - header[1] = `${baseUrl}/${header[1]}` - } else { - header[1] = `${baseUrl}` - } - } - - return header -} diff --git a/test/fixtures/cache-tests/test-engine/lib/modal.mjs b/test/fixtures/cache-tests/test-engine/lib/modal.mjs deleted file mode 100644 index 76cb1da0366..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/modal.mjs +++ /dev/null @@ -1,27 +0,0 @@ -export function modalOpen (content) { - let modal = document.getElementById('modal') - if (!modal) { - modal = document.createElement('div') - modal.classList.add('modal') - modal.id = 'modal' - } - modal.classList.add('modal-open') - modal.innerHTML = content - const closeButton = document.createElement('button') - const closeText = document.createTextNode('❎') - closeButton.appendChild(closeText) - closeButton.classList.add('modal-exit') - closeButton.addEventListener('click', function (event) { - event.preventDefault() - modal.classList.remove('modal-open') - }) - modal.appendChild(closeButton) - document.body.appendChild(modal) - document.onkeydown = function (evt) { - evt = evt || window.event - if (evt.key === 'Escape' || evt.key === 'Esc') { - modal.classList.remove('modal-open') - document.onkeydown = function () {} - } - } -} diff --git a/test/fixtures/cache-tests/test-engine/lib/results.mjs b/test/fixtures/cache-tests/test-engine/lib/results.mjs deleted file mode 100644 index 236912fba38..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/results.mjs +++ /dev/null @@ -1,73 +0,0 @@ -export const resultTypes = { - untested: ['-', '', '-'], - pass: ['\uf058', '#1aa123', '✅'], - fail: ['\uf057', '#c33131', '⛔️'], - optional_fail: ['\uf05a', '#bbbd15', '⚠️'], - yes: ['\uf055', '#999696', 'Y'], - no: ['\uf056', '#999696', 'N'], - setup_fail: ['\uf059', '#4c61ae', '🔹'], - harness_fail: ['\uf06a', '#4c61ae', '⁉️'], - dependency_fail: ['\uf192', '#b4b2b2', '⚪️'], - retry: ['\uf01e', '#4c61ae', '↻'] -} -const passTypes = [resultTypes.pass, resultTypes.yes] - -export function determineTestResult (testSuites, testId, testResults, honorDependencies = true) { - const test = testLookup(testSuites, testId) - const result = testResults[testId] - if (result === undefined) { - return resultTypes.untested - } - if (honorDependencies && test.depends_on !== undefined) { - for (const dependencyId of test.depends_on) { - if (!passTypes.includes(determineTestResult(testSuites, dependencyId, testResults))) { - return resultTypes.dependency_fail - } - } - } - if (result[0] === 'Setup') { - if (result[1] === 'retry') { - return resultTypes.retry - } else { - return resultTypes.setup_fail - } - } - if (result === false && result[0] !== 'Assertion') { - return resultTypes.harness_fail - } - if (result[0] === 'AbortError') { - return resultTypes.harness_fail - } - if (test.kind === 'required' || test.kind === undefined) { - if (result === true) { - return resultTypes.pass - } else { - return resultTypes.fail - } - } else if (test.kind === 'optimal') { - if (result === true) { - return resultTypes.pass - } else { - return resultTypes.optional_fail - } - } else if (test.kind === 'check') { - if (result === true) { - return resultTypes.yes - } else { - return resultTypes.no - } - } else { - throw new Error(`Unrecognised test kind ${test.kind}`) - } -} - -export function testLookup (testSuites, testId) { - for (const testSuite of testSuites) { - for (const test of testSuite.tests) { - if (test.id === testId) { - return test - } - } - } - throw new Error(`Cannot find test ${testId}`) -} diff --git a/test/fixtures/cache-tests/test-engine/lib/summary.mjs b/test/fixtures/cache-tests/test-engine/lib/summary.mjs deleted file mode 100644 index f9cd2f64d53..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/summary.mjs +++ /dev/null @@ -1,178 +0,0 @@ -/* global fetch marked */ - -import '../../asset/marked.min.js' -import * as display from './display.mjs' -import { testLookup } from './results.mjs' - -export function loadResults (index) { - return Promise.all(index.map(item => - fetch(`results/${item.file}`) - .then(response => { - return response.json() - }) - .then(results => { - item.results = results - return item - } - )) - ) -} - -export function showResults (target, testSuites, results, testIds, suiteIds) { - const isDefault = testIds.length === 0 && suiteIds.length === 0 - testSuites.forEach(testSuite => { - const selectedTests = [] - const suiteTestIds = [] - testSuite.tests.forEach(test => { - if (isDefault || suiteIds.includes(testSuite.id)) { - selectedTests.push(test) - suiteTestIds.push(test.id) - } - if (isDefault === 0 || testIds.includes(test.id)) { - if (!suiteTestIds.includes(test.id)) { - selectedTests.push(test) - } - } - }) - if (selectedTests.length) { - showHeader(testSuite, results).forEach(row => { - target.appendChild(row) - }) - selectedTests.forEach(test => { - const result = showTest(testSuites, test.id, results) - if (target.childElementCount % 2) { - result.setAttribute('class', 'shade') - } - target.appendChild(result) - }) - } - }) -} - -export function showToC (target, testSuites) { - testSuites.forEach(testSuite => { - const suiteLink = document.createElement('a') - suiteLink.href = '#' + testSuite.id - suiteLink.appendChild(document.createTextNode(testSuite.name)) - const suiteLi = document.createElement('li') - suiteLi.appendChild(suiteLink) - target.appendChild(suiteLi) - }) -} - -function showHeader (testSuite, results) { - const rows = [] - const numCols = results.length + 2 - const blankRow = tableRow() - blankRow.appendChild(emptyCell(numCols)) - rows.push(blankRow) - const headerRow = tableRow() - headerRow.appendChild(tableCell('th', '\xa0', 'name category')) - const headerLink = document.createElement('a') - headerLink.href = '#' + testSuite.id - headerLink.appendChild(document.createTextNode(testSuite.name)) - const firstHeader = tableCell('th', headerLink, 'name category') - firstHeader.id = testSuite.id - headerRow.appendChild(firstHeader) - results.forEach(implementation => { - headerRow.appendChild(tableCell('th', implementation.name, 'category', implementation.version, implementation.link)) - }) - rows.push(headerRow) - if (testSuite.description !== undefined) { - const descriptionRow = tableRow() - const drCells = emptyCell(numCols) - drCells.innerHTML = marked.parse(testSuite.description).slice(3, -5) - descriptionRow.appendChild(drCells) - rows.push(descriptionRow) - } - return rows -} - -function showTest (testSuites, testId, results) { - const test = testLookup(testSuites, testId) - const testRow = tableRow() - testRow.appendChild(tableCell('td', testSelector(test.id))) - testRow.appendChild(tableCell('th', display.showTestName(test), 'name')) - results.forEach(implementation => { - testRow.appendChild( - tableCell('th', display.showTestResult(testSuites, test.id, implementation.results))) - }) - return testRow -} - -function tableRow (CssClass) { - const rowElement = document.createElement('tr') - if (CssClass) { - rowElement.setAttribute('class', CssClass) - } - return rowElement -} - -function tableCell (cellType, content, CssClass, hint, link, colspan) { - const cellElement = document.createElement(cellType) - if (CssClass) { - cellElement.setAttribute('class', CssClass) - } - if (colspan) { - cellElement.colSpan = colspan - } - let contentNode - if (typeof (content) === 'string') { - contentNode = document.createTextNode(content) - } else { - contentNode = content - } - if (link) { - const linkElement = document.createElement('a') - linkElement.setAttribute('href', link) - linkElement.appendChild(contentNode) - cellElement.appendChild(linkElement) - } else { - cellElement.appendChild(contentNode) - } - if (hint) { - cellElement.title = hint - } - return cellElement -} - -function testSelector (testId) { - const checkbox = document.createElement('input') - checkbox.type = 'checkbox' - checkbox.name = 'id' - checkbox.value = testId - checkbox.style.display = 'none' - checkbox.setAttribute('class', 'select') - return checkbox -} - -export function selectClickListen () { - const select = document.getElementById('select') - select.addEventListener('click', selectClick, { - once: true - }) -} - -function selectClick () { - const selectBoxes = document.getElementsByClassName('select') - for (const selectBox of selectBoxes) { - selectBox.style.display = 'inherit' - } - const submit = document.createElement('input') - submit.type = 'submit' - submit.value = 'Show only selected tests' - const select = document.getElementById('select') - select.replaceWith(submit) -} - -export function selectClearShow () { - const clear = document.createElement('a') - clear.href = '?' - clear.appendChild(document.createTextNode('Clear selections')) - const select = document.getElementById('select') - select.replaceWith(clear) -} - -function emptyCell (numCols = 1) { - return tableCell('td', '\xa0', undefined, undefined, undefined, numCols) -} diff --git a/test/fixtures/cache-tests/test-engine/lib/testsuite-schema.json b/test/fixtures/cache-tests/test-engine/lib/testsuite-schema.json deleted file mode 100644 index 95294ff81a6..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/testsuite-schema.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://cache-tests.fyi/testsuite-schema.json", - "title": "Cache Tests", - "description": "A list of test suites", - "type": "array", - "items": { - "description": "A test suite", - "type": "object", - "required": [ "name", "id", "tests"], - "additionalProperties": false, - "properties": { - "name": { - "description": "The name of the suite", - "type": "string" - }, - "id": { - "description": "A unique identifier for the suite", - "type": "string" - }, - "description": { - "description": "A Markdown description of the suite", - "type": "string" - }, - "spec_anchors": { - "description": "Anchors in the HTTP caching spec that this suite applies to", - "type": "array", - "items": { - "type": "string" - } - }, - "tests": { - "description": "An array of tests in the suite", - "type": "array", - "items": { - "description": "A test", - "type": "object", - "additionalProperties": false, - "required": ["name", "id", "requests"], - "properties": { - "name": { - "description": "The test name; can contain Markdown.", - "type": "string" - }, - "id": { - "$ref": "#/definitions/test-id" - }, - "description": { - "description": "A longer, Markdown description of the test", - "type": "string" - }, - "kind": { - "description": "The kind of test", - "type": "string", - "enum": ["required", "optimal", "check"] - }, - "spec_anchors": { - "description": "Anchors in the HTTP caching spec that this test applies to", - "type": "array", - "items": { - "type": "string" - } - }, - "requests": { - "description": "An array of requests", - "type": "array", - "items": { - "description": "A request to send in the test", - "type": "object", - "additionalProperties": false, - "properties": { - "request_method": { - "description": "the HTTP method to be used", - "type": "string" - }, - "request_headers": { - "description": "headers to emit in the request", - "type": "array", - "items": { - "type": "array", - "additionalItems": false, - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "$ref": "#/definitions/magic-field-value" - } - ] - } - }, - "request_body": { - "description": "the HTTP request body to be used", - "type": "string" - }, - "query_arg": { - "description": "query arguments to add to the URL", - "type": "string" - }, - "filename": { - "description": "filename to add to the URL", - "type": "string" - }, - "mode": { - "description": "the mode value to pass to fetch()", - "type": "string", - "enum": ["same-origin", "no-cors", "navigate", "websocket"] - }, - "credentials": { - "description": "the credentials value to pass to fetch()", - "type": "string", - "enum": ["omit", "same-origin", "include"] - }, - "cache": { - "description": "the cache value to pass to fetch()", - "type": "string", - "enum": ["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"] - }, - "redirect": { - "description": "the redirect value to pass to fetch()", - "type": "string", - "enum": ["follow", "error", "manual"] - }, - "pause_after": { - "description": "Whether to pause for three seconds after the request completes", - "type": "boolean" - }, - "disconnect": { - "description": "Whether to disconnect the client when receiving thsi request", - "type": "boolean" - }, - "magic_locations": { - "description": "Whether to rewrite Location and Content-Location to full URLs", - "type": "boolean" - }, - "magic_ims": { - "description": "Whether to rewrite If-Modified-Since to a delta from the previous Last-Modified", - "type": "boolean" - }, - "rfc850date": { - "description": "Header names to use RFC850 format on when converting dates", - "type": "array", - "items": [ - { - "$ref": "#/definitions/date-headers" - } - ] - }, - "response_status": { - "description": "HTTP status code and phrase to return from origin", - "type": "array", - "items": [ - { - "$ref": "#/definitions/status-code" - }, - { - "description": "status phrase", - "type": "string" - } - ] - }, - "response_headers": { - "description": "Response header fields to be returned from origin", - "type": "array", - "items": { - "anyOf": [ - { - "description": "name and value", - "type": "array", - "additionalItems": false, - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "oneOf": [ - { "type": "string" }, - { "type": "integer" } - ] - } - ] - }, - { - "description": "name and value with control over checking", - "type": "array", - "additionalItems": false, - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "$ref": "#/definitions/magic-field-value" - }, - { - "type": "boolean" - } - ] - } - ] - } - }, - "response_body": { - "description": "Response body to be returned from origin; defaults to the test identifier", - "$ref": "#/definitions/response-or-null" - }, - "check_body": { - "description": "Whether to check the response body on the client", - "type": "boolean" - }, - "expected_type": { - "description": "What the test result is expected to be", - "type": "string", - "enum": ["cached", "not_cached", "lm_validated", "etag_validated"] - }, - "expected_method": { - "description": "Expected request method received by the server", - "type": "string" - }, - "expected_status": { - "description": "Expected response status received by the client", - "anyOf": [ - { - "$ref": "#/definitions/status-code" - }, - { - "type": "null" - } - ] - }, - "expected_request_headers": { - "description": "Request headers to check for on the server", - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/field-name" - }, - { - "description": "name and value", - "type": "array", - "additionalItems": false, - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "description": "field value", - "type": "string" - } - ] - } - ] - } - }, - "response_pause": { - "description": "Pause the response body by the server", - "type": "integer" - }, - "expected_request_headers_missing": { - "description": "Request headers to check for absence on the server", - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/field-name" - }, - { - "description": "name and value", - "type": "array", - "additionalItems": false, - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "description": "field value", - "type": "string" - } - ] - } - ] - } - }, - "expected_response_headers": { - "description": "Response headers to check for on the client", - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/definitions/field-name" - }, - { - "description": "name and value", - "type": "array", - "additionalItems": false, - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "$ref": "#/definitions/magic-field-value" - } - ] - }, - { - "description": "check two headers have the same value", - "type": "array", - "additionalItems": false, - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "const": "=" - }, - { - "description": "field name to check against", - "type": "string" - } - ] - }, - { - "description": "header value is greater than an integer", - "type": "array", - "additionalItems": false, - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "const": ">" - }, - { - "description": "integer to check against", - "type": "integer" - } - ] - } - ] - } - }, - "expected_response_headers_missing": { - "description": "Response headers to check are missing on the client", - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/field-name" - }, - { - "description": "name and value", - "type": "array", - "items": [ - { - "$ref": "#/definitions/field-name" - }, - { - "description": "field value substring", - "type": "string" - } - ] - } - ] - } - }, - "expected_response_text": { - "description": "Expected response body received by the client", - "$ref": "#/definitions/response-or-null" - }, - "setup": { - "description": "Whether this is a setup request; failures don't mean the actual test failed", - "type": "boolean" - }, - "setup_tests": { - "description": "List of checks that are considered setup", - "type": "array", - "items": { - "type": "string", - "enum": ["expected_type", "expected_method", "expected_status", "expected_response_headers", "expected_response_text", "expected_request_headers"] - } - } - } - } - }, - "browser_only": { - "description": "Whether the test will only run on browsers", - "type": "boolean" - }, - "cdn_only": { - "description": "Whether the test will only run on CDN caches", - "type": "boolean" - }, - "browser_skip": { - "description": "Whether the test will skip browsers", - "type": "boolean" - }, - "depends_on": { - "description": "List of Test IDs that this test depends on", - "type": "array", - "items": { - "$ref": "#/definitions/test-id" - } - } - } - } - } - } - }, - "definitions": { - "field-name": { - "description": "HTTP header field name", - "type": "string", - "pattern": "^[a-zA-Z0-9-_]+$" - }, - "magic-field-value": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "test-id": { - "description": "A short identifer for a test", - "type": "string" - }, - "response-or-null": { - "oneOf": [ - { - "description": "The text of the response", - "type": "string" - }, - { - "description": "Do not check the response", - "type": "null" - } - ] - }, - "status-code": { - "description": "HTTP response status code", - "type": "integer", - "minimum": 100, - "maximum": 599 - }, - "date-headers": { - "type": "string", - "enum": ["date", "if-modified-since", "last-modified", "expires"] - } - } -} diff --git a/test/fixtures/cache-tests/test-engine/lib/tpl/checks.liquid b/test/fixtures/cache-tests/test-engine/lib/tpl/checks.liquid deleted file mode 100644 index 730290f5d81..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/tpl/checks.liquid +++ /dev/null @@ -1,52 +0,0 @@ - -{%- if request.setup -%} - {%- assign setup_prop = '' -%} -{%- else -%} - {%- assign setup_prop = ' _(Failure will be considered a test setup issue)_' -%} -{%- endif %} - -### The following checks will be performed: - -{%- if request.expected_type %} -- The client will check that this response {% case request.expected_type %}{% when "cached" %}is cached{% when "not_cached" %}is not cached{% when "lm_validated" %}is validated using `Last-Modified`{% when "etag_validated" %}is validated using `ETag`{% endcase %} {% if test.setup_tests contains "expected_type" %}{{ setup_prop }}{% endif %}{% endif -%} - -{%- if request.expected_method %} -- The server will check that the request method is `{{ request.expected_method }}`.{% endif -%} - -{%- if request.expected_request_headers.size > 0 %} -- The server will check that the following request headers (and values, when specified) are present{% if test.setup_tests contains "expected_request_headers" %}{{ setup_prop }}{% endif %}: -{%- render 'header-list' with request.expected_request_headers as headers %}{% endif -%} - -{%- if request.expected_request_headers_missing.size > 0 %} -- The server will check that the following request headers (and values, when specified) are absent{% if test.setup_tests contains "expected_request_headers_missing" %}{{ setup_prop }}{% endif %}: -{%- render 'header-list' with request.expected_request_headers_missing as headers %}{% endif -%} - -{%- if request.expected_status %} -- The client will check that the status code is `{{ request.expected_status }}` {% if test.setup_tests contains "expected_status" %}{{ setup_prop }}{% endif %} -{%- elsif request.response_status %} -- The client will check that the status code is `{{ request.response_status | join: " " }}`{{ setup_prop }} -{%- else %} -- The client will check that the status code is `200 OK`{{ setup_prop }} -{%- endif -%} - -{%- assign response_headers = request.response_headers | skipHeaders -%} -{%- if request.expected_response_headers.size > 0 or response_headers.size > 0 %} -- The client will check that the following response headers (and values, when specified) are present{% if test.setup_tests contains "expected_response_headers" %}{{ setup_prop }}{% endif -%}: -{%- render 'header-list' with request.expected_response_headers as headers -%} -{%- render 'header-list' with response_headers as headers %}{% endif -%} - -{%- if request.expected_response_headers_missing.size > 0 %} -- The client will check that the following response headers (and values, when specified) are missing: -{%- render 'header-list' with request.expected_response_headers_missing as headers %}{% endif -%} - -{%- if request.check_body != false %} -- The client will check the body -{%- if request.expected_response text -%} -, expecting: `{{ request.expected_response_text }}` {% if test.setup_tests contains "expected_response_text" %}{{ setup_prop }}{% endif %} -{%- else -%} -, expecting the generated response body. -{%- endif -%} -{%- endif -%} - - - diff --git a/test/fixtures/cache-tests/test-engine/lib/tpl/explain-test.liquid b/test/fixtures/cache-tests/test-engine/lib/tpl/explain-test.liquid deleted file mode 100644 index 975a6ceedfa..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/tpl/explain-test.liquid +++ /dev/null @@ -1,90 +0,0 @@ -# {{ test.name }} - -Test ID: `{{ test.id }}` - -{% case test.kind %} -{%- when "optimal" -%}This is an optional test for cache efficiency -{%- when "check" -%}This is an informational check -{%- else -%}This is a conformance test -{%- endcase -%} -{%- if test.browser_only %} run on browsers only{% endif -%} -{%- if test.cdn_only %} run on CDN caches only{% endif -%} -{%- if test.browser_skip %} not run on browsers{% endif -%} -. - -{%- if test.depends_on %} It depends on the following test IDs: -{%- for dependant in test.depends_on %} -- `{{ dependant }}` -{%- endfor %}{% endif %} - -{% for request in test.requests -%} -## Request {{ forloop.index }} - -{%- if request.setup == true %} - -_This is a setup request; if it fails, we can't perform the test._ -{%- endif -%} - -{%- if request.disconnect == true %} - -The server will disconnect the client when receiving this request. -{%- endif -%} - - -{%- if request.mode or request.credentials or request.cache or request.redirect %} - -### Fetch [init](https://fetch.spec.whatwg.org/#requestinit): -{%- if request.mode %} -- Mode: {{ request.mode }} -{%- endif -%} -{%- if request.credentials %} -- Credentials: {{ request.credentials }} -{%- endif -%} -{%- if request.cache %} -- Cache: {{ request.cache }} -{%- endif -%} -{%- if request.redirect %} -- Redirect: {{ request.redirect }} -{%- endif -%} -{%- endif %} - -### The client sends a request containing: -~~~ -{{ request.request_method | default: 'GET' }} [generated test URL]{% if request.filename %}/{% endif %}{{ request.filename }}{% if request.query_arg %}?{% endif %}{{ request.query_arg }} {{ magic_locations }} -{% for header in request.request_headers %}{% render 'header-magic' with header as header %} -{% endfor %} -{{ request.request_body }} -~~~ - -{%- if request.response_pause %} - -The server will pause for {{ request.response_pause }} seconds before responding.{% endif -%} - -{%- if request.response_status or request.response_headers or request.response_body %} - -### The server sends a response containing: -~~~ -{% if request.expected_type == "lm_validated" or request.expected_type = "etag_validated" -%} -HTTP/1.1 304 Not Modified -{%- else -%} -HTTP/1.1 {{ request.response_status[0] | default: 200 }} {{ request.response_status[1] | default: "OK" }} -{%- endif %} -{% for header in request.response_headers %}{% render 'header-magic' with header as header %} -{% endfor %} -{{ request.response_body | default: '[generated response body]' }} -~~~{% endif -%} - -{%- if request.rfc850date %} -All instances of the following headers will be send and checked for being in RFC850 date format: -{% for header in request.rfc850date %} - - `{{ header }}` -{% endfor %} -{% endif -%} - -{% render 'checks' with request as request %} - -{%- if request.pause_after == true %} - -The client will pause for three seconds after this request.{% endif %} - -{% endfor %} diff --git a/test/fixtures/cache-tests/test-engine/lib/tpl/header-list.liquid b/test/fixtures/cache-tests/test-engine/lib/tpl/header-list.liquid deleted file mode 100644 index 03acc22b86b..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/tpl/header-list.liquid +++ /dev/null @@ -1,16 +0,0 @@ -{%- for header in headers %} - - {% if header.first -%} - {%- if header.size == 3 -%} - {%- case header[1] -%} - {%- when ">" -%} - `{{ header[0] }}` is greater than `{{ header[2] }}` - {%- when "=" -%} - `{{ header[0] }}` has the same value as `{{ header[2] }}` - {%- endcase -%} - {%- else -%} - `{% render 'header-magic' with header as header %}` - {%- endif -%} - {%- else -%} - `{{ header }}` - {%- endif -%} -{% endfor -%} diff --git a/test/fixtures/cache-tests/test-engine/lib/tpl/header-magic.liquid b/test/fixtures/cache-tests/test-engine/lib/tpl/header-magic.liquid deleted file mode 100644 index 843c3a43b70..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/tpl/header-magic.liquid +++ /dev/null @@ -1,6 +0,0 @@ -{%- assign mytype = header[1] | typeof -%} -{{ header[0] }}: {% if mytype == 'number' -%} - [ {{ header[1] | toLocaleString }} seconds delta ] -{%- else -%} - {{ header[1] }} -{%- endif -%} diff --git a/test/fixtures/cache-tests/test-engine/lib/utils.mjs b/test/fixtures/cache-tests/test-engine/lib/utils.mjs deleted file mode 100644 index cc071adca26..00000000000 --- a/test/fixtures/cache-tests/test-engine/lib/utils.mjs +++ /dev/null @@ -1,89 +0,0 @@ -export function AssertionError (options) { - this.name = 'Assertion' - this.message = options.message -} - -export function SetupError (options) { - this.name = 'Setup' - this.message = options.message -} - -export function assert (isSetup, expr, message) { - if (expr) return - if (isSetup) { - throw new SetupError({ message }) - } else { - throw new AssertionError({ message }) - } -} - -export function token () { - return [toHex(randInt(32), 8), - toHex(randInt(16), 4), - toHex(0x4000 | randInt(12), 4), - toHex(0x8000 | randInt(14), 4), - toHex(randInt(48), 12)].join('-') -} - -function randInt (bits) { - if (bits < 1 || bits > 53) { - throw new TypeError() - } else { - if (bits >= 1 && bits <= 30) { - return 0 | ((1 << bits) * Math.random()) - } else { - const high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30) - const low = 0 | ((1 << 30) * Math.random()) - return high + low - } - } -} - -function toHex (x, length) { - let rv = x.toString(16) - while (rv.length < length) { - rv = '0' + rv - } - return rv -} - -const rfc850day = { - 0: 'Sunday', - 1: 'Monday', - 2: 'Tuesday', - 3: 'Wednesday', - 4: 'Thursday', - 5: 'Friday', - 6: 'Saturday' -} - -const rfc850month = { - 0: 'Jan', - 1: 'Feb', - 2: 'Mar', - 3: 'Apr', - 4: 'May', - 5: 'Jun', - 6: 'Jul', - 7: 'Aug', - 8: 'Sep', - 9: 'Oct', - 10: 'Nov', - 11: 'Dec' -} - -export function httpDate (now, deltaSecs, format) { - const instant = new Date(now + (deltaSecs * 1000)) - if (format && format === 'rfc850') { - const day = rfc850day[instant.getUTCDay()] - const date = instant.getUTCDate().toString().padStart(2, '0') - const month = rfc850month[instant.getUTCMonth()] - const year = instant.getUTCFullYear().toString().slice(2) - const hours = instant.getUTCHours().toString().padStart(2, '0') - const mins = instant.getUTCMinutes().toString().padStart(2, '0') - const secs = instant.getUTCSeconds().toString().padStart(2, '0') - // Sunday, 06-Nov-94 08:49:37 GMT - return `${day}, ${date}-${month}-${year} ${hours}:${mins}:${secs} GMT` - } - return instant.toGMTString() -} diff --git a/test/fixtures/cache-tests/test-engine/server/handle-config.mjs b/test/fixtures/cache-tests/test-engine/server/handle-config.mjs deleted file mode 100644 index 80c7492d13d..00000000000 --- a/test/fixtures/cache-tests/test-engine/server/handle-config.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { sendResponse, configs, setConfig } from './utils.mjs' - -export default function handleConfig (pathSegs, request, response) { - const uuid = pathSegs[0] - if (request.method !== 'PUT') { - sendResponse(response, 405, `${request.method} request to config for ${uuid}`) - return - } - if (configs.has(uuid)) { - sendResponse(response, 409, `Config already exists for ${uuid}`) - return - } - let body = '' - request.on('data', chunk => { - body += chunk - }) - request.on('end', () => { - setConfig(uuid, JSON.parse(body)) - response.statusCode = 201 - response.end('OK') - }) -} diff --git a/test/fixtures/cache-tests/test-engine/server/handle-file.mjs b/test/fixtures/cache-tests/test-engine/server/handle-file.mjs deleted file mode 100644 index a92a9dd94cc..00000000000 --- a/test/fixtures/cache-tests/test-engine/server/handle-file.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import fs from 'fs' -import path from 'path' -import process from 'process' - -import { sendResponse } from './utils.mjs' -import { mimeTypes } from '../lib/defines.mjs' - -export default function handleFile (url, request, response) { - let urlPath = path.normalize(url.pathname) - if (urlPath === '/') urlPath = '/index.html' - const filename = path.join(process.cwd(), urlPath) - let stat - try { - stat = fs.statSync(filename) - } catch {} - if (!stat || !stat.isFile()) { - sendResponse(response, 404, `${urlPath} Not Found`) - return - } - const mimeType = mimeTypes[path.extname(filename).split('.')[1]] || 'application/octet-stream' - const fileStream = fs.createReadStream(filename) - response.writeHead(200, { 'Content-Type': mimeType }) - fileStream.pipe(response) -} diff --git a/test/fixtures/cache-tests/test-engine/server/handle-state.mjs b/test/fixtures/cache-tests/test-engine/server/handle-state.mjs deleted file mode 100644 index 43b22ce88fe..00000000000 --- a/test/fixtures/cache-tests/test-engine/server/handle-state.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { sendResponse, stash } from './utils.mjs' - -export default function handleState (pathSegs, request, response) { - const uuid = pathSegs[0] - const state = stash.get(uuid) - if (state === undefined) { - sendResponse(response, 404, `State not found for ${uuid}`) - return - } - response.setHeader('Content-Type', 'text/plain') - response.end(JSON.stringify(state)) -} diff --git a/test/fixtures/cache-tests/test-engine/server/handle-test.mjs b/test/fixtures/cache-tests/test-engine/server/handle-test.mjs deleted file mode 100644 index 13aae63bee6..00000000000 --- a/test/fixtures/cache-tests/test-engine/server/handle-test.mjs +++ /dev/null @@ -1,118 +0,0 @@ -import { noBodyStatus } from '../lib/defines.mjs' -import { fixupHeader } from '../lib/header-fixup.mjs' -import { sendResponse, getHeader, configs, stash, setStash, logRequest, logResponse } from './utils.mjs' - -export default function handleTest (pathSegs, request, response) { - // identify the desired configuration for this request - const uuid = pathSegs[0] - if (!uuid) { - sendResponse(response, 404, `Config Not Found for ${uuid}`) - return - } - const requests = configs.get(uuid) - if (!requests) { - sendResponse(response, 409, `Requests not found for ${uuid}`) - return - } - - const serverState = stash.get(uuid) || [] - const srvReqNum = serverState.length + 1 - const cliReqNum = parseInt(request.headers['req-num']) - const reqNum = cliReqNum || srvReqNum - const reqConfig = requests[reqNum - 1] - - if (!reqConfig) { - sendResponse(response, 409, `${requests[0].id} config not found for request ${srvReqNum} (anticipating ${requests.length})`) - return - } - if (reqConfig.dump) logRequest(request, srvReqNum) - - // response_pause - if ('response_pause' in reqConfig) { - setTimeout(continueHandleTest, reqConfig.response_pause * 1000, uuid, request, response, requests, serverState) - } else { - continueHandleTest(uuid, request, response, requests, serverState) - } -} - -function continueHandleTest (uuid, request, response, requests, serverState) { - const srvReqNum = serverState.length + 1 - const cliReqNum = parseInt(request.headers['req-num']) - const reqNum = cliReqNum || srvReqNum - const reqConfig = requests[reqNum - 1] - const previousConfig = requests[reqNum - 2] - const now = Date.now() - - // Determine what the response status should be - let httpStatus = reqConfig.response_status || [200, 'OK'] - if ('expected_type' in reqConfig && reqConfig.expected_type.endsWith('validated')) { - const previousLm = getHeader(previousConfig.response_headers, 'Last-Modified') - if (previousLm && request.headers['if-modified-since'] === previousLm) { - httpStatus = [304, 'Not Modified'] - } - const previousEtag = getHeader(previousConfig.response_headers, 'ETag') - if (previousEtag && request.headers['if-none-match'] === previousEtag) { - httpStatus = [304, 'Not Modified'] - } - if (httpStatus[0] !== 304) { - httpStatus = [999, '304 Not Generated'] - } - } - response.statusCode = httpStatus[0] - response.statusPhrase = httpStatus[1] - - // header manipulation - const responseHeaders = reqConfig.response_headers || [] - const savedHeaders = new Map() - response.setHeader('Server-Base-Url', request.url) - response.setHeader('Server-Request-Count', srvReqNum) - response.setHeader('Client-Request-Count', cliReqNum) - response.setHeader('Server-Now', now, 0) - responseHeaders.forEach(header => { - header = fixupHeader(header, response.getHeaders(), reqConfig) - if (response.hasHeader(header[0])) { - const currentVal = response.getHeader(header[0]) - if (typeof currentVal === 'string') { - response.setHeader(header[0], [currentVal, header[1]]) - } else if (Array.isArray(currentVal)) { - response.setHeader(header[0], currentVal.concat(header[1])) - } else { - console.log(`ERROR: Unanticipated header type of ${typeof currentVal} for ${header[0]}`) - } - } else { - response.setHeader(header[0], header[1]) - } - if (header.length < 3 || header[2] === true) { - savedHeaders.set(header[0], response.getHeader(header[0])) - } - }) - - if (!response.hasHeader('content-type')) { - response.setHeader('Content-Type', 'text/plain') - } - - // stash information about this request for the client - serverState.push({ - request_num: cliReqNum, - request_method: request.method, - request_headers: request.headers, - response_headers: Array.from(savedHeaders.entries()) - }) - response.setHeader('Request-Numbers', serverState.map(item => item.request_num).join(' ')) - setStash(uuid, serverState) - - // Response body generation - if ('disconnect' in reqConfig && reqConfig.disconnect) { - // disconnect now because we want the state - response.socket.destroy() - response = 'disconnect' - } else if (noBodyStatus.has(response.statusCode)) { - response.end() - } else { - const content = reqConfig.response_body || uuid - response.end(content) - } - - // logging - if (reqConfig.dump) logResponse(response, srvReqNum) -} diff --git a/test/fixtures/cache-tests/test-engine/server/server.mjs b/test/fixtures/cache-tests/test-engine/server/server.mjs deleted file mode 100644 index 54f644019c7..00000000000 --- a/test/fixtures/cache-tests/test-engine/server/server.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/* global URL */ - -import fs from 'fs' -import http from 'http' -import https from 'https' -import process from 'process' - -import handleConfig from './handle-config.mjs' -import handleFile from './handle-file.mjs' -import handleState from './handle-state.mjs' -import handleTest from './handle-test.mjs' - -function handleMain (request, response) { - const url = new URL(request.url, baseUrl) - const pathSegs = url.pathname.split('/') - pathSegs.shift() - const dispatch = pathSegs.shift() - if (dispatch === 'config') { - handleConfig(pathSegs, request, response) - } else if (dispatch === 'test') { - handleTest(pathSegs, request, response) - } else if (dispatch === 'state') { - handleState(pathSegs, request, response) - } else { - handleFile(url, request, response) - } -} - -const protocol = process.env.npm_config_protocol || process.env.npm_package_config_protocol -const port = process.env.npm_config_port || process.env.npm_package_config_port -const baseUrl = `${protocol}://localhost:${port}/` -const pidfile = process.env.npm_config_pidfile || process.env.npm_package_config_pidfile - -fs.writeFile(pidfile, process.pid.toString(), 'ascii', function (err) { - if (err) { console.log(`PID file write error: ${err.message}`) } -}) - -let server -if (protocol.toLowerCase() === 'https') { - const options = { - key: fs.readFileSync(process.env.npm_config_keyfile), - cert: fs.readFileSync(process.env.npm_config_certfile), - joinDuplicateHeaders: true - } - server = https.createServer(options, handleMain) -} else { - server = http.createServer({ joinDuplicateHeaders: true }, handleMain) -} -server.on('listening', () => { - const host = (server.address().family === 'IPv6') - ? `[${server.address().address}]` - : server.address().address - console.log(`Listening on ${protocol.toLowerCase()}://${host}:${server.address().port}/`) -}) -server.listen(port) diff --git a/test/fixtures/cache-tests/test-engine/server/utils.mjs b/test/fixtures/cache-tests/test-engine/server/utils.mjs deleted file mode 100644 index 7f37a665313..00000000000 --- a/test/fixtures/cache-tests/test-engine/server/utils.mjs +++ /dev/null @@ -1,54 +0,0 @@ -import { BLUE, NC } from '../lib/defines.mjs' - -export function sendResponse (response, statusCode, message) { - console.log(`SERVER WARNING: ${message}`) - response.writeHead(statusCode, { 'Content-Type': 'text/plain' }) - response.write(`${message}\n`) - response.end() -} - -export function getHeader (headers, headerName) { - let result - headers.forEach(header => { - if (header[0].toLowerCase() === headerName.toLowerCase()) { - result = header[1] - } - }) - return result -} - -// stash for server state -export const stash = new Map() - -export function setStash (key, value) { - stash.set(key, value) -} - -// configurations -export const configs = new Map() - -export function setConfig (key, value) { - configs.set(key, value) -} - -export function logRequest (request, reqNum) { - console.log(`${BLUE}=== Server request ${reqNum}${NC}`) - console.log(` ${request.method} ${request.url}`) - for (const [key, value] of Object.entries(request.headers)) { - console.log(` ${key}: ${value}`) - } - console.log('') -} - -export function logResponse (response, resNum) { - console.log(`${BLUE}=== Server response ${resNum}${NC}`) - if (response === 'disconnect') { - console.log(' [ server disconnect ]') - } else { - console.log(` HTTP ${response.statusCode} ${response.statusPhrase}`) - for (const [key, value] of Object.entries(response.getHeaders())) { - console.log(` ${key}: ${value}`) - } - } - console.log('') -} diff --git a/test/fixtures/cache-tests/tests/age-parse.mjs b/test/fixtures/cache-tests/tests/age-parse.mjs deleted file mode 100644 index f304b3268a9..00000000000 --- a/test/fixtures/cache-tests/tests/age-parse.mjs +++ /dev/null @@ -1,301 +0,0 @@ -export default - -{ - name: 'Age Parsing', - id: 'age-parse', - description: 'These tests check how caches parse the `Age` response header.', - spec_anchors: ['field.age', 'expiration.model'], - tests: [ - { - name: 'HTTP cache should ignore an `Age` header with a non-numeric value', - id: 'age-parse-nonnumeric', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', 'abc', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache should ignore an `Age` header with a negative value', - id: 'age-parse-negative', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '-7200', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache should ignore an `Age` header with a float value', - id: 'age-parse-float', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '7200.0', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache should consider a response with a `Age` value of 2147483647 to be stale', - id: 'age-parse-large-minus-one', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '2147483647', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache should consider a response with a `Age` value of 2147483648 to be stale', - id: 'age-parse-large', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '2147483648', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache should consider a response with a `Age` value of 2147483649 to be stale', - id: 'age-parse-larger', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '2147483649', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache should consider a response with a single `Age` header line `old, 0` to be stale', - id: 'age-parse-suffix', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '7200, 0', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache should consider a response with a single `Age` header line `0, old` to be fresh', - id: 'age-parse-prefix', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '0, 7200', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache should use the first line in a response with multiple `Age` header lines: `old`, `0`', - id: 'age-parse-suffix-twoline', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '7200', false], - ['Age', '0', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache should use the first line in a response with multiple `Age` header lines: `0`, `old`', - id: 'age-parse-prefix-twoline', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '0', false], - ['Age', '7200', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache should consider a response with a single line `Age: 0, 0` to be fresh', - id: 'age-parse-dup-0', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '0, 0', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache should consider a response with two `Age: 0` header lines to be fresh', - id: 'age-parse-dup-0-twoline', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '0', false], - ['Age', '0', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache should consider a response with two `Age: not_old` header lines to be fresh', - id: 'age-parse-dup-old', - depends_on: ['freshness-max-age-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=10000'], - ['Age', '3600', false], - ['Age', '3600', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache consider an alphabetic parameter on `Age` header to be valid?', - id: 'age-parse-parameter', - depends_on: ['freshness-max-age-age'], - kind: 'check', - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '7200;foo=bar', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache should consider a numeric parameter on `Age` header to be valid?', - id: 'age-parse-numeric-parameter', - depends_on: ['freshness-max-age-age'], - kind: 'check', - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '7200;foo=111', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/authorization.mjs b/test/fixtures/cache-tests/tests/authorization.mjs deleted file mode 100644 index ba35a39fda4..00000000000 --- a/test/fixtures/cache-tests/tests/authorization.mjs +++ /dev/null @@ -1,110 +0,0 @@ -import * as templates from './lib/templates.mjs' - -export default - -{ - name: 'Storing Respones to Authenticated Requests', - id: 'auth', - description: 'These tests check for behaviours regarding authenticated HTTP responses.', - spec_anchors: ['caching.authenticated.responses'], - tests: [ - { - name: 'HTTP shared cache must not reuse a response to a request that contained `Authorization`, even with explicit freshness', - id: 'other-authorization', - depends_on: ['freshness-max-age'], - browser_skip: true, - requests: [ - templates.fresh({ - request_headers: [ - ['Authorization', 'FOO'] - ], - expected_request_headers: [ - ['Authorization', 'FOO'] - ] - }), - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP shared cache reuses a response to a request that contained `Authorization`, if it has `Cache-Control: public`', - id: 'other-authorization-public', - kind: 'optimal', - browser_skip: true, - depends_on: ['other-authorization'], - spec_anchors: ['cache-response-directive.public'], - requests: [ - { - request_headers: [ - ['Authorization', 'FOO'] - ], - expected_request_headers: [ - ['Authorization', 'FOO'] - ], - response_headers: [ - ['Cache-Control', 'max-age=3600, public'], - ['Date', 0] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP shared cache reuses a response to a request that contained `Authorization`, if it has `Cache-Control: must-revalidate`', - id: 'other-authorization-must-revalidate', - kind: 'optimal', - browser_skip: true, - depends_on: ['other-authorization'], - requests: [ - { - request_headers: [ - ['Authorization', 'FOO'] - ], - expected_request_headers: [ - ['Authorization', 'FOO'] - ], - response_headers: [ - ['Cache-Control', 'max-age=3600, must-revalidate'], - ['Date', 0] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP shared cache reuses a response to a request that contained `Authorization`, if it has `Cache-Control: s-maxage`', - id: 'other-authorization-smaxage', - kind: 'optimal', - browser_skip: true, - depends_on: ['other-authorization'], - requests: [ - { - request_headers: [ - ['Authorization', 'FOO'] - ], - expected_request_headers: [ - ['Authorization', 'FOO'] - ], - response_headers: [ - ['Cache-Control', 's-maxage=3600'], - ['Date', 0] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/cc-freshness.mjs b/test/fixtures/cache-tests/tests/cc-freshness.mjs deleted file mode 100644 index 64e57642a26..00000000000 --- a/test/fixtures/cache-tests/tests/cc-freshness.mjs +++ /dev/null @@ -1,468 +0,0 @@ -import * as templates from './lib/templates.mjs' - -export default - -{ - name: 'Cache-Control Freshness', - id: 'cc-freshness', - description: 'These tests check how caches calculate freshness using `Cache-Control`.', - spec_anchors: ['expiration.model', 'cache-response-directive'], - tests: [ - { - name: 'Does HTTP cache avoid reusing a response without explict freshness information or a validator (reuse is allowed, but not common, and many tests rely upon a cache _not_ doing it)?', - id: 'freshness-none', - kind: 'check', - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with positive `Cache-Control: max-age`', - id: 'freshness-max-age', - kind: 'optimal', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with `Cache-Control: max-age` after it becomes stale', - id: 'freshness-max-age-stale', - kind: 'optimal', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - templates.becomeStale({}), - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with `Cache-Control: max-age=0`', - id: 'freshness-max-age-0', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=0'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with `Cache-Control: max-age: 2147483647`', - id: 'freshness-max-age-max-minus-1', - kind: 'optimal', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=2147483647'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with `Cache-Control: max-age: 2147483648`', - id: 'freshness-max-age-max', - kind: 'optimal', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=2147483648'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with `Cache-Control: max-age: 2147483649`', - id: 'freshness-max-age-max-plus-1', - kind: 'optimal', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=2147483649'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with `Cache-Control: max-age: 99999999999`', - id: 'freshness-max-age-max-plus', - kind: 'optimal', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=99999999999'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response when the `Age` header is greater than its `Cache-Control: max-age` freshness lifetime', - id: 'freshness-max-age-age', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-response-directive.max-age', 'field.age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Cache-Control', 'max-age=3600'], - ['Age', '7200'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache consider `Date` when applying `Cache-Control: max-age` (i.e., is `apparent_age` used)?', - id: 'freshness-max-age-date', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-response-directive.max-age'], - kind: 'check', - requests: [ - { - response_headers: [ - ['Date', -7200], - ['Cache-Control', 'max-age=3600'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with positive `Cache-Control: max-age` and a past `Expires`', - id: 'freshness-max-age-expires', - depends_on: ['freshness-max-age'], - kind: 'optimal', - spec_anchors: ['cache-response-directive.max-age', 'field.expires'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Expires', -7200], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with positive `Cache-Control: max-age` and an invalid `Expires`', - id: 'freshness-max-age-expires-invalid', - depends_on: ['freshness-max-age'], - kind: 'optimal', - spec_anchors: ['cache-response-directive.max-age', 'field.expires'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Expires', '0', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with `Cache-Control: max-age=0` and a future `Expires`', - id: 'freshness-max-age-0-expires', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.max-age', 'field.expires'], - requests: [ - { - response_headers: [ - ['Expires', 3600], - ['Cache-Control', 'max-age=0'], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with positive `Cache-Control: max-age` and a CC extension present', - id: 'freshness-max-age-extension', - kind: 'optimal', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache.control.extensions'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'foobar, max-age=3600'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with positive `Cache-Control: MaX-AgE`', - id: 'freshness-max-age-case-insenstive', - kind: 'optimal', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'MaX-aGe=3600'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with negative `Cache-Control: max-age`', - id: 'freshness-max-age-negative', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=-3600'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Private HTTP cache must not prefer `Cache-Control: s-maxage` over shorter `Cache-Control: max-age`', - id: 'freshness-max-age-s-maxage-private', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-response-directive.max-age', 'cache-response-directive.s-maxage'], - requests: [ - { - response_headers: [ - ['Cache-Control', 's-maxage=3600, max-age=1'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'not_cached' - } - ], - browser_only: true - }, - { - name: 'Private HTTP cache must not prefer `Cache-Control: s-maxage` over shorter `Cache-Control: max-age` (multiple headers)', - id: 'freshness-max-age-s-maxage-private-multiple', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-response-directive.max-age', 'cache-response-directive.s-maxage'], - requests: [ - { - response_headers: [ - ['Cache-Control', 's-maxage=3600'], - ['Cache-Control', 'max-age=1'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'not_cached' - } - ], - browser_only: true - }, - { - name: 'An optimal shared HTTP cache reuses a response with positive `Cache-Control: s-maxage`', - id: 'freshness-s-maxage-shared', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.s-maxage'], - requests: [ - { - response_headers: [ - ['Cache-Control', 's-maxage=3600'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached' - } - ], - browser_skip: true - }, - { - name: 'Shared HTTP cache must prefer short `Cache-Control: s-maxage` over a longer `Cache-Control: max-age`', - id: 'freshness-max-age-s-maxage-shared-longer', - depends_on: ['freshness-s-maxage-shared'], - spec_anchors: ['cache-response-directive.max-age', 'cache-response-directive.s-maxage'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600, s-maxage=1'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'not_cached' - } - ], - browser_skip: true - }, - { - name: 'Shared HTTP cache must prefer short `Cache-Control: s-maxage` over a longer `Cache-Control: max-age` (reversed)', - id: 'freshness-max-age-s-maxage-shared-longer-reversed', - depends_on: ['freshness-s-maxage-shared'], - spec_anchors: ['cache-response-directive.max-age', 'cache-response-directive.s-maxage'], - requests: [ - { - response_headers: [ - ['Cache-Control', 's-maxage=1, max-age=3600'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'not_cached' - } - ], - browser_skip: true - }, - { - name: 'Shared HTTP cache must prefer short `Cache-Control: s-maxage` over a longer `Cache-Control: max-age` (multiple headers)', - id: 'freshness-max-age-s-maxage-shared-longer-multiple', - depends_on: ['freshness-s-maxage-shared'], - spec_anchors: ['cache-response-directive.max-age', 'cache-response-directive.s-maxage'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Cache-Control', 's-maxage=1'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'not_cached' - } - ], - browser_skip: true - }, - { - name: 'An optimal shared HTTP cache prefers long `Cache-Control: s-maxage` over a shorter `Cache-Control: max-age`', - id: 'freshness-max-age-s-maxage-shared-shorter', - depends_on: ['freshness-s-maxage-shared'], - kind: 'optimal', - spec_anchors: ['cache-response-directive.max-age', 'cache-response-directive.s-maxage'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1, s-maxage=3600'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached' - } - ], - browser_skip: true - }, - { - name: 'An optimal shared HTTP cache prefers long `Cache-Control: s-maxage` over `Cache-Control: max-age=0`, even with a past `Expires`', - id: 'freshness-max-age-s-maxage-shared-shorter-expires', - depends_on: ['freshness-s-maxage-shared'], - kind: 'optimal', - spec_anchors: ['cache-response-directive.max-age', 'cache-response-directive.s-maxage'], - requests: [ - { - response_headers: [ - ['Expires', -10], - ['Cache-Control', 'max-age=0, s-maxage=3600'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached' - } - ], - browser_skip: true - } - ] -} diff --git a/test/fixtures/cache-tests/tests/cc-parse.mjs b/test/fixtures/cache-tests/tests/cc-parse.mjs deleted file mode 100644 index 58b9a6319a1..00000000000 --- a/test/fixtures/cache-tests/tests/cc-parse.mjs +++ /dev/null @@ -1,278 +0,0 @@ -export default - -{ - name: 'Cache-Control Parsing', - id: 'cc-parse', - description: 'These tests check how caches parse the `Cache-Control` response header.', - spec_anchors: ['field.cache-control'], - tests: [ - { - name: 'Does HTTP cache reuse a response when first `Cache-Control: max-age` is fresh, but second is stale (same line)?', - id: 'freshness-max-age-two-fresh-stale-sameline', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1800, max-age=1', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a response when first `Cache-Control: max-age` is fresh, but second is stale (separate lines)?', - id: 'freshness-max-age-two-fresh-stale-sepline', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1800', false], - ['Cache-Control', 'max-age=1', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a response when first `Cache-Control: max-age` is stale, but second is fresh (same line)?', - id: 'freshness-max-age-two-stale-fresh-sameline', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1, max-age=1800', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a response when first `Cache-Control: max-age` is stale, but second is fresh (separate lines)?', - id: 'freshness-max-age-two-stale-fresh-sepline', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1', false], - ['Cache-Control', 'max-age=1800', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a response with a quoted `Cache-Control: max-age`?', - id: 'freshness-max-age-quoted', - kind: 'check', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age="3600"', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with `max-age` in a quoted string (before the "real" `max-age`)', - id: 'freshness-max-age-ignore-quoted', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'extension="max-age=3600", max-age=1', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache mut not reuse a response with `max-age` in a quoted string (after the "real" `max-age`)', - id: 'freshness-max-age-ignore-quoted-rev', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1, extension="max-age=3600"', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache ignore max-age with space before the `=`?', - id: 'freshness-max-age-space-before-equals', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age =3600', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache ignore max-age with space after the `=`?', - id: 'freshness-max-age-space-after-equals', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age= 3600', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses max-age with the value `003600`', - id: 'freshness-max-age-leading-zero', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=003600', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with a single-quoted `Cache-Control: max-age`', - id: 'freshness-max-age-single-quoted', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=\'3600\'', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache reuse max-age with `3600.0` value?', - id: 'freshness-max-age-decimal-zero', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600.0', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse max-age with `3600.5` value?', - id: 'freshness-max-age-decimal-five', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600.5', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a response with an invalid `Cache-Control: max-age` (leading alpha)?', - id: 'freshness-max-age-a100', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=a3600', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a response with an invalid `Cache-Control: max-age` (trailing alpha)?', - id: 'freshness-max-age-100a', - kind: 'check', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600a', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/cc-request.mjs b/test/fixtures/cache-tests/tests/cc-request.mjs deleted file mode 100644 index 4638b28ffdf..00000000000 --- a/test/fixtures/cache-tests/tests/cc-request.mjs +++ /dev/null @@ -1,241 +0,0 @@ -import * as templates from './lib/templates.mjs' -import * as utils from './lib/utils.mjs' - -export default { - name: 'Cache-Control Request Directives', - id: 'cc-request', - description: 'These tests check to see if caches respect `Cache-Control` request directives. Note that HTTP does not require them to be supported.', - spec_anchors: ['cache-request-directive'], - tests: [ - { - name: 'Does HTTP cache honor request `Cache-Control: max-age=0` when it holds a fresh response?', - id: 'ccreq-ma0', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.max-age'], - requests: [ - templates.fresh({}), - { - request_headers: [ - ['Cache-Control', 'max-age=0'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: max-age=1` when it holds a fresh response?', - id: 'ccreq-ma1', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.max-age'], - requests: [ - templates.fresh({}), - { - request_headers: [ - ['Cache-Control', 'max-age=1'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: max-age` when it holds a fresh but `Age`d response that is not fresh enough?', - id: 'ccreq-magreaterage', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.max-age'], - requests: [ - templates.fresh({ - response_headers: [ - ['Age', '1800'] - ] - }), - { - request_headers: [ - ['Cache-Control', 'max-age=600'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a stale response when request `Cache-Control: max-stale` allows it?', - id: 'ccreq-max-stale', - kind: 'check', - depends_on: ['freshness-max-age-stale'], - spec_anchors: ['cache-request-directive.max-stale'], - requests: [ - templates.becomeStale({}), - { - request_headers: [ - ['Cache-Control', 'max-stale=1000'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a stale `Age`d response when request `Cache-Control: max-stale` allows it?', - id: 'ccreq-max-stale-age', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.max-stale'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1500'], - ['Age', '2000'] - ], - setup: true - }, - { - request_headers: [ - ['Cache-Control', 'max-stale=1000'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: min-fresh` when the response it holds is not fresh enough?', - id: 'ccreq-min-fresh', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.min-fresh'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1500'] - ], - setup: true - }, - { - request_headers: [ - ['Cache-Control', 'min-fresh=2000'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: min-fresh` when the `Age`d response it holds is not fresh enough?', - id: 'ccreq-min-fresh-age', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.min-fresh'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1500'], - ['Age', '1000'] - ], - setup: true - }, - { - request_headers: [ - ['Cache-Control', 'min-fresh=1000'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: no-cache` when it holds a fresh response?', - id: 'ccreq-no-cache', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'] - ], - setup: true - }, - { - request_headers: [ - ['Cache-Control', 'no-cache'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: no-cache` by validating a response with `Last-Modified`?', - id: 'ccreq-no-cache-lm', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Last-Modified', -10000], - ['Date', 0] - ], - setup: true - }, - { - request_headers: [ - ['Cache-Control', 'no-cache'] - ], - expected_type: 'lm_validated' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: no-cache` by validating a response with an `ETag`?', - id: 'ccreq-no-cache-etag', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['ETag', `"${utils.httpContent('abc')}"`] - ], - setup: true - }, - { - request_headers: [ - ['Cache-Control', 'no-cache'] - ], - expected_type: 'etag_validated' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: no-store` when it holds a fresh response?', - id: 'ccreq-no-store', - kind: 'check', - depends_on: ['freshness-max-age'], - spec_anchors: ['cache-request-directive.no-store'], - requests: [ - templates.fresh({}), - { - request_headers: [ - ['Cache-Control', 'no-store'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache honour request `Cache-Control: only-if-cached` by generating a `504` response when it does not have a stored response?', - id: 'ccreq-oic', - kind: 'check', - spec_anchors: ['cache-request-directive.only-if-cached'], - requests: [ - { - request_headers: [ - ['Cache-Control', 'only-if-cached'] - ], - expected_status: 504, - expected_response_text: null - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/cc-response.mjs b/test/fixtures/cache-tests/tests/cc-response.mjs deleted file mode 100644 index 1c81bec70eb..00000000000 --- a/test/fixtures/cache-tests/tests/cc-response.mjs +++ /dev/null @@ -1,375 +0,0 @@ -export default - -{ - name: 'Cache-Control Response Directives', - id: 'cc-response', - description: 'These tests check how caches handle response `Cache-Control` directives other than those related to freshness, like `no-cache` and `no-store`.', - spec_anchors: ['cache-response-directive'], - tests: [ - { - name: 'Shared HTTP cache must not store a response with `Cache-Control: private`', - id: 'cc-resp-private-shared', - browser_skip: true, - spec_anchors: ['cache-response-directive.private'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'private, max-age=3600'] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal private HTTP cache reuses a fresh response with `Cache-Control: private`', - id: 'cc-resp-private-private', - browser_only: true, - kind: 'optimal', - spec_anchors: ['cache-response-directive.private'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'private, max-age=3600'] - ], - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not store a response with `Cache-Control: no-store`', - id: 'cc-resp-no-store', - spec_anchors: ['cache-response-directive.no-store'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'no-store'] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not store a response with `Cache-Control: nO-StOrE`', - id: 'cc-resp-no-store-case-insensitive', - depends_on: ['cc-resp-no-store'], - spec_anchors: ['cache-response-directive.no-store'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'No-StOrE'] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not store a response with `Cache-Control: no-store`, even with `max-age` and `Expires`', - id: 'cc-resp-no-store-fresh', - depends_on: ['cc-resp-no-store'], - spec_anchors: ['cache-response-directive.no-store'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000, no-store'], - ['Expires', 10000], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does HTTP cache use older stored response when newer one came with `Cache-Control: no-store`?', - id: 'cc-resp-no-store-old-new', - depends_on: ['cc-resp-no-store'], - spec_anchors: ['cache-response-directive.no-store'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000'], - ['Expires', 10000], - ['Date', 0], - ['A', '1'] - ], - setup: true, - pause_after: true - }, - { - response_headers: [ - ['Cache-Control', 'no-store'], - ['Date', 0], - ['A', '2'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached', - expected_response_headers: [['a', '1']] - } - ] - }, - { - name: 'Does HTTP cache use older stored response when newer one came with `Cache-Control: no-store, max-age=0`?', - id: 'cc-resp-no-store-old-max-age', - depends_on: ['cc-resp-no-store'], - spec_anchors: ['cache-response-directive.no-store'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000'], - ['Expires', 10000], - ['Date', 0], - ['A', '1'] - ], - setup: true, - pause_after: true - }, - { - response_headers: [ - ['Cache-Control', 'no-store, max-age=0'], - ['Date', 0], - ['A', '2'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached', - expected_response_headers: [['a', '1']] - } - ] - }, - { - name: 'HTTP cache must not use a cached response with `Cache-Control: no-cache`, even with `max-age` and `Expires`', - id: 'cc-resp-no-cache', - spec_anchors: ['cache-response-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000, no-cache'], - ['Expires', 10000], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not use a cached response with `Cache-Control: No-CaChE`, even with `max-age` and `Expires`', - id: 'cc-resp-no-cache-case-insensitive', - depends_on: ['cc-resp-no-cache'], - spec_anchors: ['cache-response-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000, No-CaChE'], - ['Expires', 10000], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache stores a response with `Cache-Control: no-cache`, but revalidates it upon use', - id: 'cc-resp-no-cache-revalidate', - kind: 'optimal', - depends_on: ['cc-resp-no-cache'], - spec_anchors: ['cache-response-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'no-cache'], - ['ETag', '"abcd"'] - ], - setup: true - }, - { - expected_type: 'etag_validated' - } - ] - }, - { - name: 'An optimal HTTP cache stores a response with `Cache-Control: no-cache`, but revalidates it upon use, even with `max-age` and `Expires`', - id: 'cc-resp-no-cache-revalidate-fresh', - kind: 'optimal', - depends_on: ['cc-resp-no-cache'], - spec_anchors: ['cache-response-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000, no-cache'], - ['Expires', 10000], - ['Date', 0], - ['ETag', '"abcd"'] - ], - setup: true - }, - { - expected_type: 'etag_validated' - } - ] - }, - { - name: 'Does `Cache-Control: no-cache` inhibit storing a listed header?', - id: 'headers-omit-headers-listed-in-Cache-Control-no-cache-single', - kind: 'check', - depends_on: ['cc-resp-no-cache-revalidate'], - spec_anchors: ['cache-response-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'no-cache="a"'], - ['a', '1'], - ['b', '2'], - ['Cache-Control', 'max-age=3600'], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached', - expected_response_headers: [['b', '2']], - expected_response_headers_missing: ['a'], - setup_tests: ['expected_type'] - } - ] - }, - { - name: 'Does `Cache-Control: no-cache` inhibit storing multiple listed headers?', - id: 'headers-omit-headers-listed-in-Cache-Control-no-cache', - kind: 'check', - depends_on: ['cc-resp-no-cache-revalidate'], - spec_anchors: ['cache-response-directive.no-cache'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'no-cache="a, b"'], - ['a', '1'], - ['b', '2'], - ['c', '3'], - ['Cache-Control', 'max-age=3600'], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached', - expected_response_headers: [['c', '3']], - expected_response_headers_missing: ['a', 'b'], - setup_tests: ['expected_type'] - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with positive `Cache-Control: max-age, must-revalidate`', - id: 'cc-resp-must-revalidate-fresh', - kind: 'optimal', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.must-revalidate'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000, must-revalidate'], - ['ETag', '"abcd"'] - ], - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must revalidate a stale response with positive `Cache-Control: max-age, must-revalidate`', - id: 'cc-resp-must-revalidate-stale', - depends_on: ['freshness-none'], - spec_anchors: ['cache-response-directive.must-revalidate'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=2, must-revalidate'], - ['ETag', '"abcd"'] - ], - setup: true - }, - { - expected_type: 'cached', - setup: true, - pause_after: true, - response_headers: [ - ['Cache-Control', 'max-age=2, must-revalidate'], - ['ETag', '"abcd"'] - ] - }, - { - expected_type: 'etag_validated' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a fresh response with `Cache-Control: immutable` without revalidation.', - id: 'cc-resp-immutable-fresh', - kind: 'optimal', - browser_only: true, - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000, immutable'], - ['ETag', '"abcd"'] - ], - setup: true, - pause_after: true - }, - { - cache: 'no-cache', - expected_type: 'cached' - } - ] - }, - { - name: 'A HTTP cache MUST revalidate a stale response with `Cache-Control: immutable`', - id: 'cc-resp-immutable-stale', - browser_only: true, - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=2, immutable'], - ['ETag', '"abcd"'] - ], - setup: true, - pause_after: true - }, - { - cache: 'no-cache', - expected_type: 'etag_validated', - expected_request_headers: [['cache-control', 'max-age=0']] - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/cdn-cache-control.mjs b/test/fixtures/cache-tests/tests/cdn-cache-control.mjs deleted file mode 100644 index e7f0d139c98..00000000000 --- a/test/fixtures/cache-tests/tests/cdn-cache-control.mjs +++ /dev/null @@ -1,491 +0,0 @@ -export default - -{ - name: 'CDN-Cache-Control', - id: 'cdn-cache-control', - description: 'These tests check non-browser caches for behaviours around the [`CDN-Cache-Control` response header](https://httpwg.org/specs/rfc9213.html).', - tests: [ - { - name: 'An optimal CDN reuses a response with positive `CDN-Cache-Control: max-age`', - id: 'cdn-max-age', - cdn_only: true, - depends_on: ['freshness-none'], - kind: 'optimal', - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age=3600', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal CDN reuses a response with `CDN-Cache-Control: max-age: 2147483648`', - id: 'cdn-max-age-max', - kind: 'optimal', - cdn_only: true, - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age=2147483648', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal CDN reuses a response with `CDN-Cache-Control: max-age: 99999999999`', - id: 'cdn-max-age-max-plus', - kind: 'optimal', - cdn_only: true, - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age=99999999999', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'CDN must not reuse a response when the `Age` header is greater than its `CDN-Cache-Control: max-age` freshness lifetime', - id: 'cdn-max-age-age', - cdn_only: true, - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['CDN-Cache-Control', 'max-age=3600', false], - ['Age', '7200'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does CDN ignore `CDN-Cache-Control: max-age` with space before the `=`?', - id: 'cdn-max-age-space-before-equals', - cdn_only: true, - kind: 'check', - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1'], - ['CDN-Cache-Control', 'max-age =100', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does CDN ignore `CDN-Cache-Control: max-age` with space after the `=`?', - id: 'cdn-max-age-space-after-equals', - cdn_only: true, - kind: 'check', - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1'], - ['CDN-Cache-Control', 'max-age= 100', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'CDN must not reuse a response with `CDN-Cache-Control: max-age=0`', - id: 'cdn-max-age-0', - cdn_only: true, - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age=0', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal CDN reuses a response with a positive `CDN-Cache-Control: max-age` and an extension cache directive', - id: 'cdn-max-age-extension', - cdn_only: true, - kind: 'optimal', - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'foobar, max-age=3600', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does CDN reuse a response with a positive `CDN-Cache-Control: MaX-aGe`?', - id: 'cdn-max-age-case-insensitive', - cdn_only: true, - kind: 'check', - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'MaX-aGe=3600', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - - { - name: 'An optimal CDN reuses a response with a positive `CDN-Cache-Control: max-age` and a past `Expires`', - id: 'cdn-max-age-expires', - cdn_only: true, - kind: 'optimal', - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age=3600', false], - ['Expires', -10000], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal CDN reuses a response with a positive `CDN-Cache-Control: max-age` and an invalid `Expires`', - id: 'cdn-max-age-cc-max-age-invalid-expires', - cdn_only: true, - kind: 'optimal', - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age=3600', false], - ['Expires', '0', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'CDN must not reuse a response with a `CDN-Cache-Control: max-age=0` and a future `Expires`', - id: 'cdn-max-age-0-expires', - cdn_only: true, - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age=0', false], - ['Expires', 10000], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal CDN prefers a long `CDN-Cache-Control: max-age` over a short `Cache-Control: max-age`', - id: 'cdn-max-age-short-cc-max-age', - cdn_only: true, - kind: 'optimal', - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1'], - ['CDN-Cache-Control', 'max-age=3600', false] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'CDN must prefer a short `CDN-Cache-Control: max-age` over a long `Cache-Control: max-age`', - id: 'cdn-max-age-long-cc-max-age', - cdn_only: true, - depends_on: ['cdn-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['CDN-Cache-Control', 'max-age=1', false] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'CDN must not reuse a cached response with `CDN-Cache-Control: private`, even with `Cache-Control: max-age` and `Expires`', - id: 'cdn-private', - cdn_only: true, - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'private'], - ['Cache-Control', 'max-age=10000'], - ['Expires', 10000], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'CDN must not reuse a cached response with `CDN-Cache-Control: no-cache`, even with `Cache-Control: max-age` and `Expires`', - id: 'cdn-no-cache', - cdn_only: true, - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'no-cache'], - ['Cache-Control', 'max-age=10000'], - ['Expires', 10000], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'CDN must not store a response with `CDN-Cache-Control: no-store`, even with `Cache-Control: max-age` and `Expires`', - id: 'cdn-no-store-cc-fresh', - cdn_only: true, - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=10000'], - ['CDN-Cache-Control', 'no-store', false], - ['Expires', 10000], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal CDN stores a response with a fresh `CDN-Cache-Control: max-age`, even with `Cache-Control: no-store`', - id: 'cdn-fresh-cc-nostore', - depends_on: ['freshness-none'], - cdn_only: true, - requests: [ - { - response_headers: [ - ['Cache-Control', 'no-store'], - ['CDN-Cache-Control', 'max-age=10000', false] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'CDN should ignore a `CDN-Cache-Control` that\'s an invalid Structured Field (unknown type)', - id: 'cdn-cc-invalid-sh-type-unknown', - depends_on: ['cdn-max-age'], - cdn_only: true, - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age=10000, &&&&&', false], - ['Cache-Control', 'no-store'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'CDN should ignore a `CDN-Cache-Control` that\'s an invalid Structured Field (wrong type)', - id: 'cdn-cc-invalid-sh-type-wrong', - depends_on: ['cdn-max-age'], - cdn_only: true, - requests: [ - { - response_headers: [ - ['CDN-Cache-Control', 'max-age="10000"', false], - ['Cache-Control', 'no-store'] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'Does the CDN forward the `CDN-Cache-Control` response header?', - id: 'cdn-remove-header', - cdn_only: true, - kind: 'check', - requests: [ - { - // only check for the header in expected_response_headers, so failing - // this is an assertion failure and not a setup error - response_headers: [ - ['Cache-Control', 'max-age=10000'], - ['CDN-Cache-Control', 'foo', false], - ['Expires', 10000], - ['Date', 0] - ], - expected_response_headers: [ - ['CDN-Cache-Control', 'foo'] - ] - } - ] - }, - { - name: 'Does the CDN send `Age` when `CDN-Cache-Control: max-age` exceeds `Cache-Control: max-age`?', - id: 'cdn-remove-age-exceed', - cdn_only: true, - depends_on: ['cdn-max-age'], - kind: 'check', - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1'], - ['CDN-Cache-Control', 'max-age=10000'], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_response_headers: [ - 'Age' - ] - } - ] - }, - { - name: 'Does the CDN preserve `Date` when `CDN-Cache-Control: max-age` exceeds `Cache-Control: max-age`?', - id: 'cdn-date-update-exceed', - cdn_only: true, - depends_on: ['cdn-max-age'], - kind: 'check', - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1'], - ['CDN-Cache-Control', 'max-age=10000'], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_response_headers: [ - ['Date', 0] - ] - } - ] - }, - { - name: 'Does the CDN preserve `Expires` when `CDN-Cache-Control: max-age` exceeds `Cache-Control: max-age`?', - id: 'cdn-expires-update-exceed', - cdn_only: true, - depends_on: ['cdn-max-age'], - kind: 'check', - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=1'], - ['Expires', 1], - ['CDN-Cache-Control', 'max-age=10000'], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_response_headers: [ - ['Expires', 1] - ] - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/conditional-etag.mjs b/test/fixtures/cache-tests/tests/conditional-etag.mjs deleted file mode 100644 index 98d9177ec63..00000000000 --- a/test/fixtures/cache-tests/tests/conditional-etag.mjs +++ /dev/null @@ -1,456 +0,0 @@ -import * as templates from './lib/templates.mjs' - -export default { - name: 'Conditional Requests: If-None-Match and ETag', - id: 'conditional-inm', - description: 'These tests check handling of conditional requests using `If-None-Match` and `ETag`.', - spec_anchors: ['validation.model'], - tests: [ - { - name: 'An optimal HTTP cache responds to `If-None-Match` with a `304` when holding a fresh response with a matching strong `ETag`', - id: 'conditional-etag-strong-respond', - kind: 'optimal', - depends_on: ['freshness-max-age'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', '"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', '"abcdef"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'HTTP cache must include `ETag` in a `304 Not Modified`', - id: 'conditional-304-etag', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', '"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', '"abcdef"'] - ], - expected_type: 'cached', - expected_status: 304, - expected_response_headers: [ - ['ETag', '"abcdef"'] - ] - } - ] - }, - { - name: 'HTTP cache must give precedence to `If-None-Match` over `If-Modified-Since`', - id: 'conditional-etag-precedence', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['Last-Modified', -5000], - ['ETag', '"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', '"abcdef"'], - ['If-Modified-Since', -1] - ], - magic_ims: true, - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'Does HTTP cache responds to `If-None-Match` with a `304` when holding a fresh response with a matching strong `ETag` containing obs-text?', - id: 'conditional-etag-strong-respond-obs-text', - kind: 'check', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', '"abcdefü"'] - ] - }), - { - request_headers: [ - ['If-None-Match', '"abcdefü"'] - ], - expected_type: 'cached', - expected_status: 304, - expected_response_headers: [ - ['ETag', '"abcdefü"'] - ] - } - ] - }, - { - name: 'HTTP cache responds to unquoted `If-None-Match` with a `304` when holding a fresh response with a matching strong `ETag` that is quoted', - id: 'conditional-etag-quoted-respond-unquoted', - kind: 'check', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', '"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', 'abcdef'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'HTTP cache responds to unquoted `If-None-Match` with a `304` when holding a fresh response with a matching strong `ETag` that is unquoted', - id: 'conditional-etag-unquoted-respond-unquoted', - kind: 'check', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', 'abcdef'] - ] - }), - { - request_headers: [ - ['If-None-Match', 'abcdef'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'HTTP cache responds to quoted `If-None-Match` with a `304` when holding a fresh response with a matching strong `ETag` that is unquoted', - id: 'conditional-etag-unquoted-respond-quoted', - kind: 'check', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', 'abcdef'] - ] - }), - { - request_headers: [ - ['If-None-Match', '"abcdef"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'An optimal HTTP cache responds to `If-None-Match` with a `304` when holding a fresh response with a matching weak `ETag`', - id: 'conditional-etag-weak-respond', - kind: 'optimal', - depends_on: ['freshness-max-age'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', 'W/"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', 'W/"abcdef"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'HTTP cache responds to `If-None-Match` with a `304` when holding a fresh response with a matching weak `ETag`, and the entity-tag weakness flag is lowercase', - id: 'conditional-etag-weak-respond-lowercase', - kind: 'check', - depends_on: ['conditional-etag-weak-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', 'w/"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', 'w/"abcdef"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'HTTP cache responds to `If-None-Match` with a `304` when holding a fresh response with a matching weak `ETag`, and the entity-tag weakness flag uses `\\` instead of `/`', - id: 'conditional-etag-weak-respond-backslash', - kind: 'check', - depends_on: ['conditional-etag-weak-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', 'W\\"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', 'W\\"abcdef"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'HTTP cache responds to `If-None-Match` with a `304` when holding a fresh response with a matching weak `ETag`, and the entity-tag weakness flag omits `/`', - id: 'conditional-etag-weak-respond-omit-slash', - depends_on: ['conditional-etag-weak-respond'], - kind: 'check', - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', 'W"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', 'W"abcdef"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'An optimal HTTP cache responds to `If-None-Match` with a `304` when it contains multiple entity-tags (first one)', - id: 'conditional-etag-strong-respond-multiple-first', - kind: 'optimal', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', '"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', '"abcdef", "1234", "5678"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'An optimal HTTP cache responds to `If-None-Match` with a `304` when it contains multiple entity-tags (middle one)', - id: 'conditional-etag-strong-respond-multiple-second', - kind: 'optimal', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', '"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', '"1234", "abcdef", "5678"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'An optimal HTTP cache responds to `If-None-Match` with a `304` when it contains multiple entity-tags (last one)', - id: 'conditional-etag-strong-respond-multiple-last', - kind: 'optimal', - depends_on: ['conditional-etag-strong-respond'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['ETag', '"abcdef"'] - ] - }), - { - request_headers: [ - ['If-None-Match', '"1234", "5678", "abcdef"'] - ], - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'HTTP cache must include stored response headers identified by `Vary` in a conditional request it generates', - id: 'conditional-etag-vary-headers', - requests: [ - { - request_headers: [ - ['Abc', '123'] - ], - response_headers: [ - ['Expires', 1], - ['ETag', '"abcdef"'], - ['Date', 0], - ['Vary', 'Abc'] - ], - setup: true, - pause_after: true - }, - { - request_headers: [ - ['Abc', '123'] - ], - expected_type: 'etag_validated', - expected_request_headers: [ - ['Abc', '123'] - ], - setup_tests: ['expected_type'] - } - ] - }, - { - name: 'HTTP cache must not use a stored `ETag` to validate when the presented `Vary`ing request header differs', - id: 'conditional-etag-vary-headers-mismatch', - depends_on: ['conditional-etag-vary-headers', 'vary-no-match'], - requests: [ - { - request_headers: [ - ['Abc', '123'] - ], - response_headers: [ - ['Expires', 10000], - ['ETag', '"abcdef"'], - ['Date', 0], - ['Vary', 'Abc'] - ], - setup: true, - pause_after: true - }, - { - request_headers: [ - ['Abc', '456'] - ], - expected_request_headers_missing: [ - ['If-None-Match', '"abcdef"'] - ] - } - ] - }, - { - name: 'An optimal HTTP cache generates a `If-None-Match` request when holding a stale response with a matching strong `ETag`', - id: 'conditional-etag-strong-generate', - kind: 'optimal', - depends_on: ['freshness-max-age-stale'], - requests: [ - templates.becomeStale({ - response_headers: [ - ['ETag', '"abcdef"'] - ] - }), - { - expected_request_headers: [ - ['If-None-Match', '"abcdef"'] - ], - expected_type: 'etag_validated' - } - ] - }, - { - name: 'An optimal HTTP cache generates a `If-None-Match` request when holding a stale response with a matching weak `ETag`', - id: 'conditional-etag-weak-generate-weak', - kind: 'optimal', - depends_on: ['freshness-max-age-stale'], - requests: [ - templates.becomeStale({ - response_headers: [ - ['ETag', 'W/"abcdef"'] - ] - }), - { - expected_request_headers: [ - ['If-None-Match', 'W/"abcdef"'] - ], - expected_type: 'etag_validated' - } - ] - }, - { - name: 'Does HTTP cache generate a quoted `If-None-Match` request when holding a stale response with a matching, unquoted strong `ETag`?', - id: 'conditional-etag-strong-generate-unquoted', - kind: 'check', - depends_on: ['conditional-etag-strong-generate'], - requests: [ - templates.becomeStale({ - response_headers: [ - ['ETag', 'abcdef'] - ] - }), - { - expected_request_headers: [ - ['If-None-Match', '"abcdef"'] - ], - expected_type: 'etag_validated' - } - ] - }, - { - name: 'Does HTTP cache forward `If-None-Match` request header when no stored response is available?', - id: 'conditional-etag-forward', - kind: 'check', - requests: [ - { - request_headers: [ - ['If-None-Match', '"abcdef"'] - ], - expected_request_headers: [ - ['If-None-Match', '"abcdef"'] - ] - } - ] - }, - { - name: 'Does HTTP cache add quotes to an unquoted `If-None-Match` request when forwarding it?', - id: 'conditional-etag-forward-unquoted', - depends_on: ['conditional-etag-forward'], - kind: 'check', - requests: [ - { - request_headers: [ - ['If-None-Match', 'abcdef'] - ], - expected_request_headers: [ - ['If-None-Match', '"abcdef"'] - ] - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/conditional-lm.mjs b/test/fixtures/cache-tests/tests/conditional-lm.mjs deleted file mode 100644 index eae5c2bed89..00000000000 --- a/test/fixtures/cache-tests/tests/conditional-lm.mjs +++ /dev/null @@ -1,119 +0,0 @@ -import * as templates from './lib/templates.mjs' - -export default { - name: 'Conditional Requests: If-Modified-Since and Last-Modified', - id: 'conditional-lm', - description: 'These tests check handling of conditional requests using `If-Modified-Since` and `Last-Modified`.', - spec_anchors: ['validation.model'], - tests: [ - { - name: 'An optimal HTTP cache responds to `If-Modified-Since` with a `304` when holding a fresh response with a matching `Last-Modified`', - id: 'conditional-lm-fresh', - kind: 'optimal', - depends_on: ['freshness-max-age'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['Last-Modified', -3000] - ] - }), - { - request_headers: [ - ['If-Modified-Since', -3000] - ], - magic_ims: true, - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'An optimal HTTP cache responds to `If-Modified-Since` with a `304` when holding a fresh response with an earlier `Last-Modified`', - id: 'conditional-lm-fresh-earlier', - kind: 'optimal', - depends_on: ['freshness-max-age'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['Last-Modified', -3000] - ] - }), - { - request_headers: [ - ['If-Modified-Since', -2000] - ], - magic_ims: true, - expected_type: 'cached', - expected_status: 304 - } - ] - }, - { - name: 'An optimal HTTP cache responds to `If-Modified-Since` with a `304` when holding a stale response with a matching `Last-Modified`, after validation', - id: 'conditional-lm-stale', - kind: 'optimal', - depends_on: ['freshness-max-age-stale'], - browser_skip: true, - requests: [ - templates.becomeStale({ - response_headers: [ - ['Last-Modified', -3000] - ] - }), - { - request_headers: [ - ['If-Modified-Since', -3000] - ], - magic_ims: true, - expected_type: 'lm_validated', - expected_status: 304 - } - ] - }, - { - name: 'An optimal HTTP cache responds to `If-Modified-Since` with a `304` when holding a newer fresh response with no `Last-Modified`', - id: 'conditional-lm-fresh-no-lm', - kind: 'optimal', - depends_on: ['freshness-max-age'], - browser_skip: true, - requests: [ - templates.fresh({}), - { - request_headers: [ - ['If-Modified-Since', -3000] - ], - magic_ims: true, - expected_type: 'cached', - expected_status: 304, - setup_tests: ['expected_type'] - } - ] - }, - { - name: 'An optimal HTTP cache responds to `If-Modified-Since` with a `304` when holding a newer fresh response when IMS uses an equivalent rfc850 date', - id: 'conditional-lm-fresh-rfc850', - kind: 'optimal', - depends_on: ['freshness-max-age'], - browser_skip: true, - requests: [ - templates.fresh({ - response_headers: [ - ['Last-Modified', -3000] - ] - }), - { - request_headers: [ - ['If-Modified-Since', -3000] - ], - magic_ims: true, - rfc850date: ['if-modified-since'], - expected_type: 'cached', - expected_status: 304, - setup_tests: ['expected_type'] - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/expires-freshness.mjs b/test/fixtures/cache-tests/tests/expires-freshness.mjs deleted file mode 100644 index c0f6f9f727b..00000000000 --- a/test/fixtures/cache-tests/tests/expires-freshness.mjs +++ /dev/null @@ -1,154 +0,0 @@ -export default - -{ - name: 'Expires Freshness', - id: 'expires', - description: 'These tests check how caches calculate freshness using `Expires`.', - spec_anchors: ['expiration.model', 'field.expires'], - tests: [ - { - name: 'An optimal HTTP cache reuses a response with a future `Expires`', - id: 'freshness-expires-future', - kind: 'optimal', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Expires', 30 * 24 * 60 * 60], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with a past `Expires`', - id: 'freshness-expires-past', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', -30 * 24 * 60 * 60], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with a present `Expires`', - id: 'freshness-expires-present', - depends_on: ['freshness-none'], - requests: [ - { - response_headers: [ - ['Expires', 0], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an `Expires` older than `Date`, both fast', - id: 'freshness-expires-old-date', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 300], - ['Date', 400] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (0)', - id: 'freshness-expires-invalid', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', '0', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with `Expires`, even if `Date` is invalid', - id: 'freshness-expires-invalid-date', - depends_on: ['freshness-expires-future'], - kind: 'optimal', - requests: [ - { - response_headers: [ - ['Date', 'foo', false], - ['Expires', 10] - ], - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response when the `Age` header is greater than its `Expires` minus `Date`, and `Date` is slow', - id: 'freshness-expires-age-slow-date', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Date', -10], - ['Expires', 10], - ['Age', '25'] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response when the `Age` header is greater than its `Expires` minus `Date`, and `Date` is fast', - id: 'freshness-expires-age-fast-date', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Date', 10], - ['Expires', 20], - ['Age', '15'] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/expires-parse.mjs b/test/fixtures/cache-tests/tests/expires-parse.mjs deleted file mode 100644 index 74c93b516e7..00000000000 --- a/test/fixtures/cache-tests/tests/expires-parse.mjs +++ /dev/null @@ -1,301 +0,0 @@ -export default - -{ - name: 'Expires Parsing', - id: 'expires-parse', - description: 'These tests check how caches parse the `Expires` response header.', - spec_anchors: ['field.expires'], - tests: [ - { - name: 'An optimal HTTP cache reuses a response with an `Expires` that is exactly 32 bits', - id: 'freshness-expires-32bit', - kind: 'optimal', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Tue, 19 Jan 2038 14:14:08 GMT', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with an `Expires` that is far in the future', - id: 'freshness-expires-far-future', - kind: 'optimal', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Sun, 21 Nov 2286 04:46:39 GMT', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with a future `Expires` in obsolete RFC 850 format', - id: 'freshness-expires-rfc850', - kind: 'optimal', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thursday, 18-Aug-50 02:01:18 GMT', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with a future `Expires` in ANSI C\'s asctime() format', - id: 'freshness-expires-ansi-c', - kind: 'optimal', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu Aug 8 02:01:18 2050', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with a future `Expires` using wrong case (weekday)', - id: 'freshness-expires-wrong-case-weekday', - kind: 'optimal', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'THU, 18 Aug 2050 02:01:18 GMT', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with a future `Expires` using wrong case (month)', - id: 'freshness-expires-wrong-case-month', - kind: 'optimal', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 AUG 2050 02:01:18 GMT', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a response with a future `Expires` using wrong case (tz)', - id: 'freshness-expires-wrong-case-tz', - kind: 'optimal', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 Aug 2050 02:01:18 gMT', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (UTC)', - id: 'freshness-expires-invalid-utc', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 Aug 2050 02:01:18 UTC', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (other tz)', - id: 'freshness-expires-invalid-aest', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 Aug 2050 02:01:18 AEST', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (two-digit year)', - id: 'freshness-expires-invalid-2-digit-year', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 Aug 50 02:01:18 GMT', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (missing comma)', - id: 'freshness-expires-invalid-no-comma', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu 18 Aug 2050 02:01:18 GMT', false], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (multiple spaces)', - id: 'freshness-expires-invalid-multiple-spaces', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 Aug 2050 02:01:18 GMT', false], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (date dashes)', - id: 'freshness-expires-invalid-date-dashes', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18-Aug-2050 02:01:18 GMT', false], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (time periods)', - id: 'freshness-expires-invalid-time-periods', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 Aug 2050 02.01.18 GMT', false], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (1-digit hour)', - id: 'freshness-expires-invalid-1-digit-hour', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 Aug 2050 2:01:18 GMT', false], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse a response with an invalid `Expires` (multiple lines)', - id: 'freshness-expires-invalid-multiple-lines', - depends_on: ['freshness-expires-future'], - requests: [ - { - response_headers: [ - ['Expires', 'Thu, 18 Aug 2050 2:01:18 GMT', false], - ['Expires', 'Thu, 18 Aug 2050 2:01:19 GMT', false], - ['Date', 0] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/headers.mjs b/test/fixtures/cache-tests/tests/headers.mjs deleted file mode 100644 index e27aee2a98f..00000000000 --- a/test/fixtures/cache-tests/tests/headers.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import * as templates from './lib/templates.mjs' -import * as utils from './lib/utils.mjs' -import headerList from './lib/header-list.mjs' - -const tests = [] - -tests.push({ - name: '`Connection` header must inhibit a HTTP cache from storing listed headers', - id: 'headers-omit-headers-listed-in-Connection', - kind: 'required', - depends_on: ['freshness-max-age'], - requests: [ - templates.fresh({ - response_headers: [ - ['Connection', 'a, b', false], - ['a', '1', false], - ['b', '2', false], - ['c', '3', false] - ] - }), - { - expected_type: 'cached', - expected_response_headers: [['c', '3']], - expected_response_headers_missing: ['a', 'b'], - setup_tests: ['expected_type', 'expected_response_headers'] - } - ] -}) - -function checkStoreHeader (config) { - const id = `store-${config.name}` - const value = 'valB' in config ? config.valB : utils.httpContent(`${config.name}-store-value`) - const storeHeader = 'noStore' in config ? !config.noStore : true - const requirement = storeHeader ? 'must' : 'must not' - const expectedHeaders = storeHeader ? [[config.name, value]] : [] - const unexpectedHeaders = storeHeader ? [] : [[config.name, value]] - - const respHeaders = [ - ['Date', 0], - [config.name, value, storeHeader] - ] - if (config.name !== 'Cache-Control') { - respHeaders.push(['Cache-Control', 'max-age=3600']) - } - - tests.push({ - name: `HTTP cache ${requirement} store \`${config.name}\` header field`, - id: `headers-${id}`, - kind: 'required', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: respHeaders, - setup: true, - pause_after: true, - check_body: 'checkBody' in config ? config.checkBody : true - }, - { - expected_type: 'cached', - expected_response_headers: expectedHeaders, - expected_response_headers_missing: unexpectedHeaders, - setup_tests: ['expected_type'], - check_body: 'checkBody' in config ? config.checkBody : true - } - ] - }) -} - -headerList.forEach(checkStoreHeader) - -export default { - name: 'Storing Header Fields', - id: 'headers', - description: 'These tests examine how caches store headers in responses.', - spec_anchors: ['storing.fields'], - tests -} diff --git a/test/fixtures/cache-tests/tests/heuristic-freshness.mjs b/test/fixtures/cache-tests/tests/heuristic-freshness.mjs deleted file mode 100644 index 9b0c3600069..00000000000 --- a/test/fixtures/cache-tests/tests/heuristic-freshness.mjs +++ /dev/null @@ -1,95 +0,0 @@ -import * as utils from './lib/utils.mjs' - -const tests = [] - -function checkStatus (status) { - const succeed = status[0] - const code = status[1] - const phrase = status[2] - let body = status[3] - if (body === undefined) { - body = utils.httpContent(code) - } - const extra = status[4] || '' - const extraHdr = status[5] - const specAnchors = status[6] || [] - let expectedType = 'not_cached' - let desired = 'HTTP cache must not reuse' - if (succeed === true) { - expectedType = 'cached' - desired = 'An optimal HTTP cache should reuse' - } - const responseHeaders = [ - ['Last-Modified', -24 * 60 * 60], - ['Date', 0] - ] - if (extraHdr) { - responseHeaders.push(extraHdr) - } - tests.push({ - name: `${desired} a \`${code} ${phrase}\` response with \`Last-Modified\` based upon heuristic freshness ${extra}`, - id: `heuristic-${code}-${expectedType}`, - kind: succeed ? 'optimal' : 'required', - spec_anchors: specAnchors, - requests: [{ - response_status: [code, phrase], - response_headers: responseHeaders, - response_body: body, - setup: true - }, { - expected_type: expectedType, - response_status: [code, phrase], - response_body: body - }] - }) -} - -[ - [true, 200, 'OK'], - [false, 201, 'Created'], - [false, 202, 'Accepted'], - [true, 203, 'Non-Authoritative Information'], - [true, 204, 'No Content', null], - [false, 403, 'Forbidden'], - [true, 404, 'Not Found'], - [true, 405, 'Method Not Allowed'], - [true, 410, 'Gone'], - [true, 414, 'URI Too Long'], - [true, 501, 'Not Implemented'], - [false, 502, 'Bad Gateway'], - [false, 503, 'Service Unavailable'], - [false, 504, 'Gateway Timeout'], - [false, 599, 'Unknown', undefined, 'when `Cache-Control: public` is not present', undefined, ['cache-response-directive.public']], - [true, 599, 'Unknown', undefined, 'when `Cache-Control: public` is present', ['Cache-Control', 'public'], ['cache-response-directive.public']] -].forEach(checkStatus) - -function checkHeuristic (delta) { - tests.push({ - name: `Does HTTP cache consider a \`Last-Modified\` ${delta} seconds ago heuristically fresh?`, - id: `heuristic-delta-${delta}`, - kind: 'check', - requests: [{ - response_headers: [ - ['Last-Modified', -delta], - ['Date', 0] - ], - setup: true, - pause_after: true - }, - { - expected_type: 'cached' - }] - }) -} - -[ - 5, 10, 30, 60, 300, 600, 1200, 1800, 3600, 3600 * 12, 3600 * 24 -].forEach(checkHeuristic) - -export default { - name: 'Heuristic Freshness', - id: 'heuristic', - description: 'These tests check how caches handle heuristic freshness.', - spec_anchors: ['heuristic.freshness'], - tests -} diff --git a/test/fixtures/cache-tests/tests/index.mjs b/test/fixtures/cache-tests/tests/index.mjs deleted file mode 100644 index 3b7378c1537..00000000000 --- a/test/fixtures/cache-tests/tests/index.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import ccParse from './cc-parse.mjs' -import ccRequest from './cc-request.mjs' -import ccResponse from './cc-response.mjs' -import ccFreshness from './cc-freshness.mjs' -import ageParse from './age-parse.mjs' -import pragma from './pragma.mjs' -import expiresParse from './expires-parse.mjs' -import expires from './expires-freshness.mjs' -import stale from './stale.mjs' -import heuristic from './heuristic-freshness.mjs' -import methods from './method.mjs' -import statuses from './status.mjs' -import vary from './vary.mjs' -import varyParse from './vary-parse.mjs' -import conditionalLm from './conditional-lm.mjs' -import conditionalEtag from './conditional-etag.mjs' -import headers from './headers.mjs' -import update304 from './update304.mjs' -import updateHead from './updateHead.mjs' -import invalidation from './invalidation.mjs' -import partial from './partial.mjs' -import auth from './authorization.mjs' -import other from './other.mjs' -import cdncc from './cdn-cache-control.mjs' - -export default [ccFreshness, ccParse, ageParse, expires, expiresParse, ccResponse, stale, heuristic, methods, statuses, ccRequest, pragma, vary, varyParse, conditionalLm, conditionalEtag, headers, update304, updateHead, invalidation, partial, auth, other, cdncc] diff --git a/test/fixtures/cache-tests/tests/invalidation.mjs b/test/fixtures/cache-tests/tests/invalidation.mjs deleted file mode 100644 index 8ed8c7e46ec..00000000000 --- a/test/fixtures/cache-tests/tests/invalidation.mjs +++ /dev/null @@ -1,121 +0,0 @@ -import { makeTemplate, fresh } from './lib/templates.mjs' - -const contentLocation = makeTemplate({ - filename: 'content_location_target', - response_headers: [ - ['Cache-Control', 'max-age=100000'], - ['Last-Modified', 0], - ['Date', 0] - ] -}) - -const location = makeTemplate({ - filename: 'location_target', - response_headers: [ - ['Cache-Control', 'max-age=100000'], - ['Last-Modified', 0], - ['Date', 0] - ] -}) - -const lclResponse = makeTemplate({ - response_headers: [ - ['Location', 'location_target'], - ['Content-Location', 'content_location_target'] - ], - magic_locations: true -}) - -const tests = [] - -function checkInvalidation (method) { - tests.push({ - name: `HTTP cache must invalidate the URL after a successful response to a \`${method}\` request`, - id: `invalidate-${method}`, - depends_on: ['freshness-max-age'], - requests: [ - fresh({}), { - request_method: method, - request_body: 'abc', - setup: true - }, { - expected_type: 'not_cached' - } - ] - }) - tests.push({ - name: `An optimal HTTP cache does not invalidate the URL after a failed response to a \`${method}\` request`, - id: `invalidate-${method}-failed`, - kind: 'optimal', - depends_on: [`invalidate-${method}`], - requests: [ - fresh({}), { - request_method: method, - request_body: 'abc', - response_status: [500, 'Internal Server Error'], - setup: true - }, { - expected_type: 'cached' - } - ] - }) -} - -function checkLocationInvalidation (method) { - tests.push({ - name: `Does HTTP cache invalidate \`Location\` URL after a successful response to a \`${method}\` request?`, - id: `invalidate-${method}-location`, - kind: 'check', - depends_on: [`invalidate-${method}`], - requests: [ - location({ - setup: true - }), lclResponse({ - request_method: method, - request_body: 'abc', - setup: true - }), location({ - expected_type: 'not_cached' - }) - ] - }) -} - -function checkClInvalidation (method) { - tests.push({ - name: `Does HTTP cache must invalidate \`Content-Location\` URL after a successful response to a \`${method}\` request?`, - id: `invalidate-${method}-cl`, - kind: 'check', - depends_on: [`invalidate-${method}`], - requests: [ - contentLocation({ - setup: true - }), lclResponse({ - request_method: method, - request_body: 'abc', - setup: true - }), contentLocation({ - expected_type: 'not_cached' - }) - ] - }) -} - -const methods = [ - 'POST', - 'PUT', - 'DELETE', - 'M-SEARCH' -] - -methods.forEach(checkInvalidation) -methods.forEach(checkLocationInvalidation) -methods.forEach(checkClInvalidation) - -export default { - name: 'Cache Invalidation', - id: 'invalidation', - description: 'These tests check how caches support invalidation, including when it is triggered by the `Location` and `Content-Location` response headers.', - spec_anchors: ['invalidation'], - tests -} diff --git a/test/fixtures/cache-tests/tests/lib/header-list.mjs b/test/fixtures/cache-tests/tests/lib/header-list.mjs deleted file mode 100644 index dcdf3a253e0..00000000000 --- a/test/fixtures/cache-tests/tests/lib/header-list.mjs +++ /dev/null @@ -1,135 +0,0 @@ -export default [ - { - name: 'Test-Header', - reqUpdate: true - }, - { - name: 'X-Test-Header', - reqUpdate: true - }, - { - name: 'Content-Foo', - reqUpdate: true - }, - { - name: 'X-Content-Foo', - reqUpdate: true - }, - { - name: 'Cache-Control', - valA: 'max-age=1', - valB: 'max-age=3600', - reqUpdate: true - }, - { - name: 'Connection', - noStore: true - }, - { - name: 'Content-Encoding' - }, - { - name: 'Content-Length', - valA: '36', - valB: '10', - noUpdate: true, - checkBody: false - }, - { - name: 'Content-Location', - valA: '/foo', - valB: '/bar' - }, - { - name: 'Content-MD5', - valA: 'rL0Y20zC+Fzt72VPzMSk2A==', - valB: 'N7UdGUp1E+RbVvZSTy1R8g==' - }, - { - name: 'Content-Range' - }, - { - name: 'Content-Security-Policy', - valA: 'default-src \'self\'', - valB: 'default-src \'self\' cdn.example.com' - }, - { - name: 'Content-Type', - valA: 'text/plain', - valB: 'text/plain;charset=utf-8' - }, - { - name: 'Clear-Site-Data', - valA: 'cache', - valB: 'cookies' - }, - { - name: 'ETag', - valA: '"abcdef"', - valB: '"ghijkl"' - }, - { - name: 'Expires', - valA: 'Fri, 01 Jan 2038 01:01:01 GMT', - valB: 'Mon, 11 Jan 2038 11:11:11 GMT' - }, - { - name: 'Keep-Alive', - noStore: true - }, - { - name: 'Proxy-Authenticate', - noStore: true - }, - { - name: 'Proxy-Authentication-Info', - noStore: true - }, - { - name: 'Proxy-Authorization', - noStore: true - }, - { - name: 'Proxy-Connection', - noStore: true - }, - { - name: 'Public-Key-Pins' - }, - { - name: 'Set-Cookie', - valA: 'a=b', - valB: 'a=c' - }, - { - name: 'Set-Cookie2', - valA: 'a=b', - valB: 'a=c' - }, - { - name: 'TE', - noStore: true - }, - // { - // name: 'Trailer', - // noStore: true - // }, - { - name: 'Transfer-Encoding', - noStore: true - }, - { - name: 'Upgrade', - noStore: true - }, - { - name: 'X-Frame-Options', - valA: 'deny', - valB: 'sameorigin' - }, - { - name: 'X-XSS-Protection', - valA: '1', - valB: '1; mode=block' - } -] diff --git a/test/fixtures/cache-tests/tests/lib/templates.mjs b/test/fixtures/cache-tests/tests/lib/templates.mjs deleted file mode 100644 index 914b97b45c0..00000000000 --- a/test/fixtures/cache-tests/tests/lib/templates.mjs +++ /dev/null @@ -1,74 +0,0 @@ -/* -makeTemplate(template) - -templates take an optional request object; the template -will be updated with the request object in the following manner: - -- Object members will be assigned from the request -- Array members will be concatenated from the request -- Other members will be updated from the request -*/ -export function makeTemplate (template) { - return function (request) { - return mergeDeep({}, template, request) - } -} - -function isObject (item) { - return (item && typeof item === 'object' && !Array.isArray(item)) -} - -function mergeDeep (target, ...sources) { - if (!sources.length) return target - const source = sources.shift() - - if (isObject(target) && isObject(source)) { - for (const key in source) { - if (isObject(source[key])) { - if (!target[key]) Object.assign(target, { [key]: {} }) - mergeDeep(target[key], source[key]) - } else if (Array.isArray(source[key])) { - if (!target[key]) Object.assign(target, { [key]: [] }) - Object.assign(target, { [key]: target[key].concat(source[key]) }) - } else { - Object.assign(target, { [key]: source[key] }) - } - } - } - - return mergeDeep(target, ...sources) -} - -/* - Templates below are shared between multiple suites; - suite-specific tests should go in that file. -*/ - -export const fresh = makeTemplate({ - response_headers: [ - ['Cache-Control', 'max-age=100000'], - ['Date', 0] - ], - setup: true, - pause_after: true -}) - -export const stale = makeTemplate({ - response_headers: [ - ['Expires', -5000], - ['Last-Modified', -100000], - ['Date', 0] - ], - setup: true, - pause_after: true -}) - -export const becomeStale = makeTemplate({ - response_headers: [ - ['Cache-Control', 'max-age=2'], - ['Date', 0], - ['Template-A', '1'] - ], - setup: true, - pause_after: true -}) diff --git a/test/fixtures/cache-tests/tests/lib/utils.mjs b/test/fixtures/cache-tests/tests/lib/utils.mjs deleted file mode 100644 index affe7a3614f..00000000000 --- a/test/fixtures/cache-tests/tests/lib/utils.mjs +++ /dev/null @@ -1,20 +0,0 @@ -const contentSeed = 1 -const contentStore = {} -export function httpContent (csKey, contentLength = 15) { - if (csKey in contentStore) { - return contentStore[csKey] - } else { - let keySeed = 0 - for (let i = 0; i < csKey.length; i++) { - keySeed += csKey.charCodeAt(i) - } - const contents = [] - for (let i = 0; i < contentLength; ++i) { - const idx = ((i * keySeed * contentSeed) % 26) + 97 - contents.push(String.fromCharCode(idx)) - } - const content = contents.join('') - contentStore[csKey] = content - return content - } -} diff --git a/test/fixtures/cache-tests/tests/method.mjs b/test/fixtures/cache-tests/tests/method.mjs deleted file mode 100644 index 31b24094eaa..00000000000 --- a/test/fixtures/cache-tests/tests/method.mjs +++ /dev/null @@ -1,35 +0,0 @@ -export default - -{ - name: 'Method-related Caching Requirements', - id: 'method', - description: 'These tests check how caches handle different HTTP methods.', - spec_anchors: ['response.cacheability'], - tests: [ - { - name: 'An optimal HTTP cache reuses a stored `POST` response (that has `Content-Location` with the same URL and explicit freshness) for subsequent `GET` requests', - id: 'method-POST', - kind: 'optimal', - requests: [ - { - request_method: 'POST', - request_body: '12345', - request_headers: [ - ['Content-Type', 'text/plain'] - ], - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Content-Location', ''], - ['Date', 0] - ], - magic_locations: true, - pause_after: true, - setup: true - }, - { - expected_type: 'cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/other.mjs b/test/fixtures/cache-tests/tests/other.mjs deleted file mode 100644 index 6e8464251a0..00000000000 --- a/test/fixtures/cache-tests/tests/other.mjs +++ /dev/null @@ -1,239 +0,0 @@ -import * as templates from './lib/templates.mjs' -import * as utils from './lib/utils.mjs' - -export default - -{ - name: 'Other Caching Requirements', - id: 'other', - description: 'These tests check miscellaneous HTTP cache behaviours. ', - tests: [ - { - name: 'HTTP cache must generate an `Age` header field when using a stored response.', - id: 'other-age-gen', - depends_on: ['freshness-max-age'], - spec_anchors: ['field.age', 'constructing.responses.from.caches'], - requests: [ - templates.fresh({}), - { - expected_type: 'cached', - expected_response_headers: [ - ['Age', '>', 2] - ] - } - ] - }, - { - name: 'Does HTTP cache insert an `Age` header field when there is delay generating the response?', - id: 'other-age-delay', - kind: 'check', - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Date', 0] - ], - response_pause: 5, - expected_response_headers: [['age', '>', 0]] - } - ] - }, - { - name: 'HTTP cache must update the `Age` header field when freshness is based upon `Expires`', - id: 'other-age-update-expires', - depends_on: ['freshness-expires-future'], - spec_anchors: ['constructing.responses.from.caches', 'field.age'], - requests: [ - { - response_headers: [ - ['Expires', 30 * 24 * 60 * 60], - ['Date', 0], - ['Age', '30'] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached', - expected_response_headers: [ - ['Age', '>', 32] - ] - } - ] - }, - { - name: 'HTTP cache must update the `Age` header field when freshness is based upon `CC: max-age`', - id: 'other-age-update-max-age', - depends_on: ['freshness-max-age'], - spec_anchors: ['constructing.responses.from.caches', 'field.age'], - requests: [ - templates.fresh({ - response_headers: [ - ['Age', '30'] - ] - }), - { - expected_type: 'cached', - expected_response_headers: [ - ['Age', '>', 32] - ] - } - ] - }, - { - name: 'HTTP cache must not update the `Date` header field', - id: 'other-date-update', - depends_on: ['freshness-max-age'], - spec_anchors: ['field.date'], - requests: [ - templates.fresh({}), - { - expected_type: 'cached', - expected_response_headers: [ - ['Date', 0] - ] - } - ] - }, - { - name: 'HTTP cache must not update the `Date` header field when `Expires` is present', - id: 'other-date-update-expires', - depends_on: ['freshness-expires-future'], - spec_anchors: ['field.date'], - requests: [ - { - response_headers: [ - ['Expires', 30 * 24 * 60 * 60], - ['Date', 0] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached', - expected_response_headers: [ - ['Date', 0] - ] - } - ] - }, - { - name: 'Does HTTP cache leave the `Expires` header field alone?', - id: 'other-date-update-expires-update', - kind: 'check', - depends_on: ['freshness-expires-future'], - spec_anchors: ['field.date'], - requests: [ - { - response_headers: [ - ['Expires', 30 * 24 * 60 * 60], - ['Date', 0] - ], - pause_after: true, - setup: true - }, - { - expected_type: 'cached', - expected_response_headers: [ - ['Expires', 30 * 24 * 60 * 60] - ] - } - ] - }, - { - name: 'Different query arguments must be different cache keys', - id: 'query-args-different', - depends_on: ['freshness-max-age'], - requests: [ - templates.fresh({ - query_arg: 'test=' + utils.httpContent('query-args-different-1') - }), - { - query_arg: 'test=' + utils.httpContent('query-args-different-2'), - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache should not be affected by the presence of a URL query', - id: 'query-args-same', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - templates.fresh({ - query_arg: 'test=' + utils.httpContent('query-args-same') - }), - { - query_arg: 'test=' + utils.httpContent('query-args-same'), - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP heuristically cache a response with a `Content-Disposition: attachment` header?', - id: 'other-heuristic-content-disposition-attachment', - kind: 'check', - depends_on: ['heuristic-200-cached'], - requests: [ - { - response_headers: [ - ['Last-Modified', -100000], - ['Date', 0], - ['Content-Disposition', 'attachment; filename=example.txt'] - ], - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP reuse a fresh response with a `Content-Disposition: attachment` header?', - id: 'other-fresh-content-disposition-attachment', - kind: 'check', - depends_on: ['freshness-max-age'], - requests: [ - templates.fresh({ - response_headers: [ - ['Content-Disposition', 'attachment; filename=example.txt'] - ] - }), - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a fresh response with a `Set-Cookie` header', - id: 'other-set-cookie', - depends_on: ['freshness-max-age'], - kind: 'optimal', - requests: [ - templates.fresh({ - response_headers: [ - ['Set-Cookie', 'a=b'] - ] - }), - { - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a fresh response when the request has a `Cookie` header', - id: 'other-cookie', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - templates.fresh({}), - { - request_headers: [ - ['Cookie', 'a=b'] - ], - expected_type: 'cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/partial.mjs b/test/fixtures/cache-tests/tests/partial.mjs deleted file mode 100644 index 5b3f4f5de3a..00000000000 --- a/test/fixtures/cache-tests/tests/partial.mjs +++ /dev/null @@ -1,271 +0,0 @@ -export default { - name: 'Combining Partial Content', - id: 'partial', - description: 'These tests check how caches handle partial content (also known as `Range` requests).', - spec_anchors: ['combining.responses'], - tests: [ - { - name: 'An optimal HTTP cache stores partial content and reuses it', - id: 'partial-store-partial-reuse-partial', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - { - request_headers: [ - ['Range', 'bytes=-5'] - ], - response_status: [206, 'Partial Content'], - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Content-Range', 'bytes 4-9/10'] - ], - response_body: '01234', - expected_request_headers: [ - ['Range', 'bytes=-5'] - ], - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=-5'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: '01234' - } - ] - }, - { - name: 'An optimal HTTP cache stores complete responses and serves smaller ranges from them (byte-range-spec)', - id: 'partial-store-complete-reuse-partial', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'] - ], - response_body: '01234567890', - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=0-1'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: '01' - } - ] - }, - { - name: 'An optimal HTTP cache stores complete responses and serves smaller ranges from them (absent last-byte-pos)', - id: 'partial-store-complete-reuse-partial-no-last', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'] - ], - response_body: '01234567890', - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=1-'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: '1234567890' - } - ] - }, - { - name: 'An optimal HTTP cache stores complete responses and serves smaller ranges from them (suffix-byte-range-spec)', - id: 'partial-store-complete-reuse-partial-suffix', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'] - ], - response_body: '0123456789A', - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=-1'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: 'A' - } - ] - }, - { - name: 'An optimal HTTP cache stores partial responses and serves smaller ranges from them (byte-range-spec)', - id: 'partial-store-partial-reuse-partial-byterange', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - { - request_headers: [ - ['Range', 'bytes=-5'] - ], - response_status: [206, 'Partial Content'], - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Content-Range', 'bytes 4-9/10'] - ], - response_body: '01234', - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=6-8'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: '234' - } - ] - }, - { - name: 'An optimal HTTP cache stores partial responses and serves smaller ranges from them (absent last-byte-pos)', - id: 'partial-store-partial-reuse-partial-absent', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - { - request_headers: [ - ['Range', 'bytes=-5'] - ], - response_status: [206, 'Partial Content'], - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Content-Range', 'bytes 4-9/10'] - ], - response_body: '01234', - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=6-'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: '234' - } - ] - }, - { - name: 'An optimal HTTP cache stores partial responses and serves smaller ranges from them (suffix-byte-range-spec)', - id: 'partial-store-partial-reuse-partial-suffix', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - { - request_headers: [ - ['Range', 'bytes=-5'] - ], - response_status: [206, 'Partial Content'], - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Content-Range', 'bytes 4-9/10'] - ], - response_body: '01234', - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=-1'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: '4' - } - ] - }, - { - name: 'An optimal HTTP cache stores partial content and completes it', - id: 'partial-store-partial-complete', - kind: 'optimal', - depends_on: ['freshness-max-age'], - requests: [ - { - request_headers: [ - ['Range', 'bytes=-5'] - ], - response_status: [206, 'Partial Content'], - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Content-Range', 'bytes 0-4/10'] - ], - response_body: '01234', - setup: true - }, - { - expected_request_headers: [ - ['range', 'bytes=5-'] - ] - } - ] - }, - { - name: 'HTTP cache must use header fields from the new response', - id: 'partial-use-headers', - depends_on: ['partial-store-complete-reuse-partial'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['A', '1'] - ], - response_body: '01234567890', - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=0-1'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: '01', - setup_tests: ['expected_type', 'expected_status', 'expected_response_text'], - response_headers: [ - ['A', '2'] - ] - } - ] - }, - { - name: 'HTTP cache must preserve unupdated header fields from the stored response', - id: 'partial-use-stored-headers', - depends_on: ['partial-store-complete-reuse-partial'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['A', '1'] - ], - response_body: '01234567890', - setup: true - }, - { - request_headers: [ - ['Range', 'bytes=0-1'] - ], - expected_type: 'cached', - expected_status: 206, - expected_response_text: '01', - setup_tests: ['expected_type', 'expected_status', 'expected_response_text'], - expected_response_headers: [ - ['A', '1'] - ] - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/pragma.mjs b/test/fixtures/cache-tests/tests/pragma.mjs deleted file mode 100644 index 0ced90bb203..00000000000 --- a/test/fixtures/cache-tests/tests/pragma.mjs +++ /dev/null @@ -1,97 +0,0 @@ -import * as templates from './lib/templates.mjs' - -export default - -{ - name: 'Pragma', - id: 'pragma', - description: 'These tests check how caches handle the deprecated `Pragma` header in reqeusts and responses. Note that This field is deprecated - it is not required to be supported.', - spec_anchors: ['field.pragma'], - tests: [ - { - name: 'Does HTTP cache use a stored fresh response when request contains `Pragma: no-cache`?', - id: 'pragma-request-no-cache', - kind: 'check', - depends_on: ['freshness-max-age'], - requests: [ - templates.fresh({}), - { - request_headers: [ - ['Pragma', 'no-cache'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a stored fresh response when request contains `Pragma: unrecognised-extension`?', - id: 'pragma-request-extension', - kind: 'check', - depends_on: ['freshness-max-age'], - requests: [ - templates.fresh({}), - { - request_headers: [ - ['Pragma', 'unrecognised-extension'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a stored and otherwise fresh response when it contains `Pragma: no-cache`?', - id: 'pragma-response-no-cache', - kind: 'check', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Pragma', 'no-cache'] - ], - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache reuse a stored and heuristically fresh response when it contains `Pragma: no-cache`?', - id: 'pragma-response-no-cache-heuristic', - kind: 'check', - depends_on: ['heuristic-200-cached'], - requests: [ - { - response_headers: [ - ['Date', 0], - ['Last-Modified', -10000], - ['Pragma', 'no-cache'] - ], - setup: true - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache use a stored and otherwise fresh response when it contains `Pragma: unrecognised-extension`?', - id: 'pragma-response-extension', - kind: 'check', - depends_on: ['freshness-max-age'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ['Pragma', 'unrecognised-extension'] - ], - setup: true - }, - { - expected_type: 'cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/stale.mjs b/test/fixtures/cache-tests/tests/stale.mjs deleted file mode 100644 index 0714d212a2d..00000000000 --- a/test/fixtures/cache-tests/tests/stale.mjs +++ /dev/null @@ -1,179 +0,0 @@ -import * as templates from './lib/templates.mjs' - -function makeStaleCheckCC (cc, sharedOnly, value) { - const shared = sharedOnly === true ? 'Shared ' : '' - return { - name: `${shared}HTTP cache must not serve stale stored response when prohibited by \`Cache-Control: ${cc}\``, - id: `stale-close-${cc}${value || ''}`, - browser_skip: sharedOnly, - depends_on: ['stale-close'], - spec_anchors: [`cache-response-directive.${cc}`], - requests: [ - { - response_headers: [ - ['Cache-Control', `max-age=2, ${cc}${value || ''}`] - ], - setup: true, - pause_after: true - }, - { - disconnect: true, - expected_status: null, - check_body: false, - expected_response_headers_missing: ['server-request-count'] - } - ] - } -} - -export default { - name: 'Serving Stale', - id: 'stale', - description: 'These tests check how caches serve stale content.', - spec_anchors: ['serving.stale.responses'], - tests: [ - { - name: 'Does HTTP cache serve stale stored response when server closes the connection?', - id: 'stale-close', - depends_on: ['freshness-max-age-stale'], - kind: 'check', - requests: [ - templates.becomeStale({}), - { - disconnect: true, - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache serve stale stored response when server sends a `503 Service Unavailable`?', - id: 'stale-503', - depends_on: ['freshness-max-age-stale'], - kind: 'check', - requests: [ - templates.becomeStale({}), - { - response_status: [503, 'Service Unavailable'], - expected_status: 200, - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal cache serves stale stored response with [`Cache-Control: stale-while-revalidate`](https://httpwg.org/specs/rfc5861.html)', - id: 'stale-while-revalidate', - depends_on: ['freshness-max-age-stale'], - kind: 'optimal', - requests: [ - { - setup: true, - pause_after: true, - response_headers: [ - ['Cache-Control', 'max-age=1, stale-while-revalidate=3600'], - ['ETag', '"abc"'] - ] - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not serve stale stored response after the [`stale-while-revalidate`](https://httpwg.org/specs/rfc5861.html) window', - id: 'stale-while-revalidate-window', - depends_on: ['stale-while-revalidate'], - requests: [ - { - setup: true, - pause_after: true, - response_headers: [ - ['Cache-Control', 'max-age=1, stale-while-revalidate=4'], - ['ETag', '"abc"'] - ] - }, - { - setup: true, - pause_after: true, - expected_type: 'cached' - }, - { - expected_response_headers: [ - ['client-request-count', '3'] - ] - } - ] - }, - { - name: 'Does HTTP cache serve stale stored response when server sends `Cache-Control: stale-if-error` and subsequently closes the connection?', - id: 'stale-sie-close', - depends_on: ['freshness-max-age-stale'], - kind: 'check', - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=2, stale-if-error=60'] - ], - setup: true, - pause_after: true - }, - { - disconnect: true, - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache serve stale stored response when server sends `Cache-Control: stale-if-error` and subsequently a `503 Service Unavailable`?', - id: 'stale-sie-503', - depends_on: ['freshness-max-age-stale'], - kind: 'check', - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=2, stale-if-error=60'] - ], - setup: true, - pause_after: true - }, - { - disconnect: true, - expected_type: 'cached' - } - ] - }, - makeStaleCheckCC('must-revalidate', false), - makeStaleCheckCC('proxy-revalidate', true), - makeStaleCheckCC('no-cache', false), - makeStaleCheckCC('s-maxage', true, '=2'), - { - name: 'Does HTTP cache generate a `Warning` header when using a response that was stored already stale?', - id: 'stale-warning-stored', - kind: 'check', - depends_on: ['stale-close'], - requests: [ - templates.stale({}), - { - disconnect: true, - expected_type: 'cached', - expected_response_headers: ['warning'], - setup_tests: ['expected_type'] - } - ] - }, - { - name: 'Does HTTP cache generate a `Warning` header when using a stored response that became stale?', - id: 'stale-warning-become', - kind: 'check', - depends_on: ['stale-close'], - requests: [ - templates.becomeStale({}), - { - disconnect: true, - expected_type: 'cached', - expected_response_headers: ['warning'], - setup_tests: ['expected_type'] - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/status.mjs b/test/fixtures/cache-tests/tests/status.mjs deleted file mode 100644 index e485056ea4b..00000000000 --- a/test/fixtures/cache-tests/tests/status.mjs +++ /dev/null @@ -1,118 +0,0 @@ -import * as templates from './lib/templates.mjs' -import * as utils from './lib/utils.mjs' - -const tests = [] - -function checkStatus (status) { - const code = status[0] - const phrase = status[1] - let body = status[2] - if (body === undefined) { - body = utils.httpContent(code) - } - const is3xx = code > 299 && code < 400 - tests.push({ - name: 'An optimal HTTP cache reuses a fresh `' + code + '` response with explict freshness', - id: `status-${code}-fresh`, - kind: 'optimal', - depends_on: ['freshness-max-age'], - browser_skip: is3xx, - requests: [ - templates.fresh({ - response_status: [code, phrase], - response_body: body, - redirect: 'manual' - }), { - expected_type: 'cached', - response_status: [code, phrase], - redirect: 'manual', - response_body: body - } - ] - }) - tests.push({ - name: 'HTTP cache must not reuse a stale `' + code + '` response with explicit freshness', - id: `status-${code}-stale`, - depends_on: [`status-${code}-fresh`], - browser_skip: is3xx, - requests: [ - templates.stale({ - response_status: [code, phrase], - response_body: body, - redirect: 'manual', - setup: true - }), { - expected_type: 'not_cached', - redirect: 'manual', - response_body: body - } - ] - }) -} -[ - [200, 'OK'], - [203, 'Non-Authoritative Information'], - [204, 'No Content', null], - [299, 'Whatever'], - [301, 'Moved Permanently'], - [302, 'Found'], - [303, 'See Other'], - [307, 'Temporary Redirect'], - [308, 'Permanent Redirect'], - [400, 'Bad Request'], - [404, 'Not Found'], - [410, 'Gone'], - [499, 'Whatever'], - [500, 'Internal Server Error'], - [502, 'Bad Gateway'], - [503, 'Service Unavailable'], - [504, 'Gateway Timeout'], - [599, 'Whatever'] -].forEach(checkStatus) - -tests.push({ - name: 'HTTP cache must not reuse a fresh response with an unrecognised status code and `Cache-Control: no-store, must-understand`', - id: 'status-599-must-understand', - depends_on: ['status-599-fresh'], - spec_anchors: ['cache-response-directive.must-understand'], - requests: [ - { - response_status: [599, 'Whatever'], - response_headers: [ - ['Cache-Control', 'max-age=3600, no-store, must-understand'] - ], - setup: true - }, - { - expected_type: 'not_cached' - } - ] -}) - -tests.push({ - name: 'An optimal HTTP cache reuses a fresh response with a recognised status code and `Cache-Control: no-store, must-understand`', - id: 'status-200-must-understand', - kind: 'optimal', - depends_on: ['status-200-fresh', 'cc-resp-no-store-fresh'], - spec_anchors: ['cache-response-directive.must-understand'], - requests: [ - { - response_status: [200, 'OK'], - response_headers: [ - ['Cache-Control', 'max-age=3600, no-store, must-understand'] - ], - setup: true - }, - { - expected_type: 'cached' - } - ] -}) - -export default { - name: 'Status Code Cacheability', - id: 'status', - description: 'These tests check to see if a cache will store and reuse various status codes when they have explicit freshness information associated with them.', - spec_anchors: ['response.cacheability'], - tests -} diff --git a/test/fixtures/cache-tests/tests/update304.mjs b/test/fixtures/cache-tests/tests/update304.mjs deleted file mode 100644 index b83f6d843f9..00000000000 --- a/test/fixtures/cache-tests/tests/update304.mjs +++ /dev/null @@ -1,124 +0,0 @@ -import * as utils from './lib/utils.mjs' -import headerList from './lib/header-list.mjs' - -const tests = [] - -// first, check to see that the cache actually returns a stored header -const storedHeader = 'Test-Header' -const valueA = utils.httpContent(`${storedHeader}-value-A`) -const lm1 = 'Wed, 01 Jan 2020 00:00:00 GMT' -tests.push({ - name: `HTTP cache must return stored \`${storedHeader}\` from a \`304\` that omits it`, - id: `304-lm-use-stored-${storedHeader}`, - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=2'], - ['Last-Modified', lm1], - ['Date', 0], - [storedHeader, valueA] - ], - setup: true, - pause_after: true - }, - { - response_headers: [ - ['Last-Modified', lm1], - ['Date', 0] - ], - expected_type: 'lm_validated', - expected_response_headers: [ - [storedHeader, valueA] - ], - setup_tests: ['expected_type'] - } - ] -}) - -// now check headers in the list -function check304 (config) { - if (config.noStore) return - config.valueA = config.valA || utils.httpContent(`${config.name}-value-A`) - config.valueB = config.valB || utils.httpContent(`${config.name}-value-B`) - if (config.noUpdate === true) { - config.expectedValue = config.valueA - config.requirement = 'HTTP cache must not' - config.punctuation = '' - config.kind = 'required' - } else if (config.reqUpdate === true) { - config.expectedValue = config.valueB - config.requirement = 'HTTP cache must' - config.punctuation = '' - config.kind = 'required' - } else { - config.expectedValue = config.valueB - config.requirement = 'Does HTTP cache' - config.punctuation = '?' - config.kind = 'check' - } - config.etagVal = utils.httpContent(`${config.name}-etag-1`) - config.etag = `"${config.etagVal}"` - config.lm = 'Wed, 01 Jan 2020 00:00:00 GMT' - - tests.push({ - name: `${config.requirement} update and return \`${config.name}\` from a \`304\`${config.punctuation}`, - id: `304-etag-update-response-${config.name}`, - kind: config.kind, - depends_on: [`304-lm-use-stored-${storedHeader}`], - requests: makeRequests(config, 'ETag', config.etag) - }) -} - -function makeRequests (config, validatorType, validatorValue) { - return [ - { - response_headers: makeResponse(config, config.valueA, validatorType, validatorValue), - setup: true, - pause_after: true, - check_body: 'checkBody' in config ? config.checkBody : true - }, - { - response_headers: makeResponse(config, config.valueB, validatorType, validatorValue), - expected_type: validatorType === 'ETag' ? 'etag_validated' : 'lm_validated', - setup_tests: ['expected_type'], - expected_response_headers: [ - [config.name, config.expectedValue] - ], - check_body: 'checkBody' in config ? config.checkBody : true - }, - { - response_headers: makeResponse(config, config.expectedValue), - expected_type: 'cached', - setup_tests: ['expected_type'], - expected_response_headers: [ - [config.name, config.expectedValue] - ], - check_body: 'checkBody' in config ? config.checkBody : true - } - ] -} - -function makeResponse (config, value, validatorType, validatorValue) { - const checkHeader = 'noUpdate' in config ? !config.noUpdate : true - const responseHeaders = [ - ['Date', 0], - [config.name, value, checkHeader] - ] - if (config.name !== 'Cache-Control') { - responseHeaders.push(['Cache-Control', 'max-age=2']) - } - if (validatorType && validatorType !== config.name) { - responseHeaders.push([validatorType, validatorValue]) - } - return responseHeaders -} - -headerList.forEach(check304) - -export default { - name: 'Update Headers Upon a 304', - id: 'update304', - description: 'These tests check cache behaviour upon receiving a `304 Not Modified` response.', - spec_anchors: ['freshening.responses'], - tests -} diff --git a/test/fixtures/cache-tests/tests/updateHead.mjs b/test/fixtures/cache-tests/tests/updateHead.mjs deleted file mode 100644 index 9b8998c8429..00000000000 --- a/test/fixtures/cache-tests/tests/updateHead.mjs +++ /dev/null @@ -1,109 +0,0 @@ -import * as templates from './lib/templates.mjs' - -export default - -{ - name: 'HEAD updates', - id: 'updateHEAD', - description: 'These tests check how a cache updates stored responses when receiving a `HEAD` response.', - spec_anchors: ['head.effects'], - tests: [ - { - name: 'Does HTTP cache write through a HEAD when stored response is stale?', - id: 'head-writethrough', - kind: 'check', - depends_on: ['freshness-max-age-stale'], - requests: [ - templates.becomeStale({}), - { - request_method: 'HEAD', - expected_method: 'HEAD' - } - ] - }, - { - name: 'Does HTTP cache preserve stored fields not received in a `200` response to a `HEAD`?', - id: 'head-200-retain', - kind: 'check', - depends_on: ['head-writethrough'], - requests: [ - templates.becomeStale({}), - { - request_method: 'HEAD', - expected_method: 'HEAD', - expected_response_headers: [ - ['Template-A', '1'] - ] - } - ] - }, - { - name: 'Does HTTP cache update freshness lifetime recieved in a `200` response to a `HEAD`?', - id: 'head-200-freshness-update', - kind: 'check', - depends_on: ['head-writethrough'], - requests: [ - templates.becomeStale({}), - { - request_method: 'HEAD', - expected_method: 'HEAD', - response_headers: [ - ['Cache-Control', 'max-age=1000'] - ] - }, - { - expected_type: 'cached' - } - ] - }, - { - name: 'Does HTTP cache update stored fields recieved in a `200` response to a `HEAD`?', - id: 'head-200-update', - kind: 'check', - depends_on: ['head-200-freshness-update'], - requests: [ - templates.becomeStale({}), - { - request_method: 'HEAD', - expected_method: 'HEAD', - response_headers: [ - ['Template-A', '2'], - ['Cache-Control', 'max-age=1000'] - ] - }, - { - expected_type: 'cached', - setup_tests: ['expected_type'], - expected_response_headers: [ - ['Template-A', '2'] - ] - } - ] - }, - { - name: 'Does HTTP cache update stored fields recieved in a `410` response to a `HEAD`?', - id: 'head-410-update', - kind: 'check', - depends_on: ['head-200-freshness-update'], - requests: [ - templates.becomeStale({}), - { - request_method: 'HEAD', - expected_method: 'HEAD', - response_status: [410, 'Gone'], - response_headers: [ - ['Template-A', '2'], - ['Cache-Control', 'max-age=1000'] - ] - }, - { - expected_type: 'cached', - setup_tests: ['expected_type'], - expected_response_headers: [ - ['Template-A', '2'] - ] - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/vary-parse.mjs b/test/fixtures/cache-tests/tests/vary-parse.mjs deleted file mode 100644 index 43124611831..00000000000 --- a/test/fixtures/cache-tests/tests/vary-parse.mjs +++ /dev/null @@ -1,157 +0,0 @@ -import { makeTemplate } from './lib/templates.mjs' - -const varyParseSetup = makeTemplate({ - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0] - ], - setup: true -}) - -export default { - name: 'Vary Parsing', - id: 'vary-parse', - description: 'These tests check how caches parse the `Vary` response header.', - spec_anchors: ['caching.negotiated.responses'], - tests: [ - { - name: 'HTTP cache must not reuse `Vary` response with a value of `*`', - id: 'vary-syntax-star', - requests: [ - varyParseSetup({ - response_headers: [ - ['Vary', '*', false] - ] - }), - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response with a value of `*, *`', - id: 'vary-syntax-star-star', - depends_on: ['freshness-max-age'], - requests: [ - varyParseSetup({ - response_headers: [ - ['Vary', '*, *', false] - ] - }), - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response with a value of `*, *` on different lines', - id: 'vary-syntax-star-star-lines', - depends_on: ['freshness-max-age'], - requests: [ - varyParseSetup({ - response_headers: [ - ['Vary', '*', false], - ['Vary', '*', false] - ] - }), - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response with a value of `, *`', - id: 'vary-syntax-empty-star', - depends_on: ['freshness-max-age'], - requests: [ - varyParseSetup({ - response_headers: [ - ['Vary', ', *', false] - ] - }), - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response with a value of `, *` on different lines', - id: 'vary-syntax-empty-star-lines', - depends_on: ['freshness-max-age'], - requests: [ - varyParseSetup({ - response_headers: [ - ['Vary', '', false], - ['Vary', '*', false] - ] - }), - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response with a value of `*, Foo`', - id: 'vary-syntax-star-foo', - depends_on: ['freshness-max-age'], - requests: [ - varyParseSetup({ - response_headers: [ - ['Vary', '*, Foo', false] - ] - }), - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response with a value of `Foo, *`', - id: 'vary-syntax-foo-star', - depends_on: ['freshness-max-age'], - requests: [ - varyParseSetup({ - response_headers: [ - ['Vary', 'Foo, *', false] - ] - }), - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - } - ] -} diff --git a/test/fixtures/cache-tests/tests/vary.mjs b/test/fixtures/cache-tests/tests/vary.mjs deleted file mode 100644 index 501b54800ee..00000000000 --- a/test/fixtures/cache-tests/tests/vary.mjs +++ /dev/null @@ -1,470 +0,0 @@ -import { makeTemplate } from './lib/templates.mjs' -import * as utils from './lib/utils.mjs' - -const varySetup = makeTemplate({ - request_headers: [ - ['Foo', '1'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Foo'] - ], - setup: true -}) - -const vary2Setup = makeTemplate({ - request_headers: [ - ['Foo', '1'], - ['Bar', 'abc'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Foo, Bar', false] - ], - setup: true -}) - -const vary3Setup = makeTemplate({ - request_headers: [ - ['Foo', '1'], - ['Bar', 'abc'], - ['Baz', '789'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Foo, Bar, Baz', false] - ], - setup: true -}) - -export default { - name: 'Vary and Cache Keys', - id: 'vary', - description: 'These tests check how caches calculate a cache key using `Vary`.', - spec_anchors: ['caching.negotiated.responses'], - tests: [ - { - name: 'An optimal HTTP cache reuses a `Vary` response when the request matches', - id: 'vary-match', - depends_on: ['freshness-max-age'], - kind: 'optimal', - requests: [ - varySetup({}), - { - request_headers: [ - ['Foo', '1'] - ], - expected_type: 'cached' - } - ] - }, - { - name: "HTTP cache must not reuse `Vary` response when request doesn't match", - id: 'vary-no-match', - depends_on: ['vary-match'], - requests: [ - varySetup({}), - { - request_headers: [ - ['Foo', '2'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response when stored request omits variant request header', - id: 'vary-omit-stored', - depends_on: ['vary-match'], - requests: [ - { - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Foo'] - ], - setup: true - }, - { - request_headers: [ - ['Foo', '1'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response when presented request omits variant request header', - id: 'vary-omit', - depends_on: ['vary-match'], - requests: [ - varySetup({}), - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache can store two different variants', - id: 'vary-invalidate', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - varySetup({ - response_body: utils.httpContent('foo_1') - }), - { - request_headers: [ - ['Foo', '2'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Foo'] - ], - expected_type: 'not_cached', - response_body: utils.httpContent('foo_2'), - setup: true - }, - { - request_headers: [ - ['Foo', '1'] - ], - response_body: utils.httpContent('foo_1'), - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache should not include headers not listed in `Vary` in the cache key', - id: 'vary-cache-key', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - varySetup({ - request_headers: [ - ['Other', '2'] - ] - }), - { - request_headers: [ - ['Foo', '1'], - ['Other', '3'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a two-way `Vary` response when request matches', - id: 'vary-2-match', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - vary2Setup({}), - { - request_headers: [ - ['Foo', '1'], - ['Bar', 'abc'] - ], - expected_type: 'cached' - } - ] - }, - { - name: "HTTP cache must not reuse two-way `Vary` response when request doesn't match", - id: 'vary-2-no-match', - depends_on: ['vary-2-match'], - requests: [ - vary2Setup({}), - { - request_headers: [ - ['Foo', '2'], - ['Bar', 'abc'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'HTTP cache must not reuse two-way `Vary` response when request omits variant request header', - id: 'vary-2-match-omit', - depends_on: ['vary-2-match'], - requests: [ - vary2Setup({}), - { - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a three-way `Vary` response when request matches', - id: 'vary-3-match', - depends_on: ['vary-2-match'], - kind: 'optimal', - requests: [ - vary3Setup({}), - { - request_headers: [ - ['Foo', '1'], - ['Bar', 'abc'], - ['Baz', '789'] - ], - expected_type: 'cached' - } - ] - }, - { - name: "HTTP cache must not reuse three-way `Vary` response when request doesn't match", - id: 'vary-3-no-match', - depends_on: ['vary-3-match'], - requests: [ - vary3Setup({}), - { - request_headers: [ - ['Foo', '2'], - ['Bar', 'abc'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: "HTTP cache must not reuse three-way `Vary` response when request doesn't match, regardless of header order", - id: 'vary-3-order', - depends_on: ['vary-3-match'], - requests: [ - vary3Setup({}), - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'], - ['Bar', 'abcde'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache reuses a three-way `Vary` response when both request and the original request omited a variant header', - id: 'vary-3-omit', - depends_on: ['vary-3-match'], - kind: 'optimal', - requests: [ - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Date', 0], - ['Last-Modified', -3000], - ['Vary', 'Foo, Bar, Baz', false] // FIXME: allow whitespace changes - ], - setup: true - }, - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'HTTP cache must not reuse `Vary` response with a value of `*`', - id: 'vary-star', - requests: [ - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', '*'] - ], - setup: true - }, - { - request_headers: [ - ['Foo', '1'], - ['Baz', '789'] - ], - expected_type: 'not_cached' - } - ] - }, - { - name: 'An optimal HTTP cache normalises unknown selecting headers by combining fields', - id: 'vary-normalise-combine', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - { - request_headers: [ - ['Foo', '1, 2'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Foo'] - ], - setup: true - }, - { - request_headers: [ - ['Foo', '1'], - ['Foo', '2'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache normalises `Accept-Language` by ignoring language order', - id: 'vary-normalise-lang-order', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - { - request_headers: [ - ['Accept-Language', 'en, de'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Accept-Language'] - ], - setup: true - }, - { - request_headers: [ - ['Accept-Language', 'de, en'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache normalises `Accept-Language` by ignoring language case', - id: 'vary-normalise-lang-case', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - { - request_headers: [ - ['Accept-Language', 'en, de'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Accept-Language'] - ], - setup: true - }, - { - request_headers: [ - ['Accept-Language', 'eN, De'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache normalises `Accept-Language` by ignoring whitespace', - id: 'vary-normalise-lang-space', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - { - request_headers: [ - ['Accept-Language', 'en, de'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Accept-Language'] - ], - setup: true - }, - { - request_headers: [ - ['Accept-Language', ' en , de'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache selects `Content-Language` by using the qvalue on `Accept-Language`', - id: 'vary-normalise-lang-select', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - { - request_headers: [ - ['Accept-Language', 'en, de'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Accept-Language'], - ['Content-Language', 'de'] - ], - setup: true - }, - { - request_headers: [ - ['Accept-Language', 'fr;q=0.5, de;q=1.0'] - ], - expected_type: 'cached' - } - ] - }, - { - name: 'An optimal HTTP cache normalises unknown selecting headers by removing whitespace', - id: 'vary-normalise-space', - depends_on: ['vary-match'], - kind: 'optimal', - requests: [ - { - request_headers: [ - ['Foo', '1,2'] - ], - response_headers: [ - ['Cache-Control', 'max-age=5000'], - ['Last-Modified', -3000], - ['Date', 0], - ['Vary', 'Foo'] - ], - setup: true - }, - { - request_headers: [ - ['Foo', ' 1, 2 '] - ], - expected_type: 'cached' - } - ] - } - ] -} diff --git a/test/fixtures/docker/dante/Dockerfile b/test/fixtures/docker/dante/Dockerfile new file mode 100644 index 00000000000..d3ec411db40 --- /dev/null +++ b/test/fixtures/docker/dante/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:latest + +# Install Dante SOCKS server +RUN apk add --no-cache dante-server + +# Create dante user +RUN adduser -D -s /bin/false dante + +# Copy configuration +COPY danted.conf /etc/danted.conf + +# Create log directory +RUN mkdir -p /var/log/dante && chown dante:dante /var/log/dante + +# Expose SOCKS port +EXPOSE 1080 + +# Run Dante +CMD ["danted", "-f", "/etc/danted.conf"] \ No newline at end of file diff --git a/test/fixtures/docker/dante/danted.conf b/test/fixtures/docker/dante/danted.conf new file mode 100644 index 00000000000..5da3a4d9066 --- /dev/null +++ b/test/fixtures/docker/dante/danted.conf @@ -0,0 +1,37 @@ +# Dante SOCKS5 server configuration for testing + +# Log settings +logoutput: /var/log/dante/danted.log +debug: 1 + +# Network interface configuration +internal: 0.0.0.0 port = 1080 +external: eth0 + +# Authentication methods +socksmethod: none +socksmethod: username + +# User for username/password auth +user.privileged: root +user.unprivileged: dante + +# Client access rules +client pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + log: error +} + +# SOCKS rules +socks pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + protocol: tcp udp + log: error +} + +# Route rules +route { + from: 0.0.0.0/0 to: 0.0.0.0/0 via: eth0 + protocol: tcp udp + proxyprotocol: socks_v5 +} \ No newline at end of file diff --git a/test/fixtures/docker/docker-compose.yml b/test/fixtures/docker/docker-compose.yml new file mode 100644 index 00000000000..1b2b8a1d80b --- /dev/null +++ b/test/fixtures/docker/docker-compose.yml @@ -0,0 +1,91 @@ +services: + # SOCKS5 proxy without authentication + socks5-no-auth: + image: serjs/go-socks5-proxy:latest + container_name: socks5-no-auth + environment: + - PROXY_PORT=1080 + - REQUIRE_AUTH=false + ports: + - "1080:1080" + networks: + - test-network + + # SOCKS5 proxy with username/password authentication + socks5-auth: + image: serjs/go-socks5-proxy:latest + container_name: socks5-auth + environment: + - PROXY_USER=testuser + - PROXY_PASSWORD=testpass + - PROXY_PORT=1081 + ports: + - "1081:1081" + networks: + - test-network + + # Alternative: Dante SOCKS5 server (more configurable) + dante-socks5: + build: + context: ./dante + dockerfile: Dockerfile + container_name: dante-socks5 + ports: + - "1082:1080" + networks: + - test-network + volumes: + - ./dante/danted.conf:/etc/danted.conf:ro + + # HTTP test server + http-server: + image: node:20-alpine + container_name: http-test-server + working_dir: /app + volumes: + - ../servers/http-server.js:/app/server.js:ro + command: node server.js + ports: + - "8080:8080" + networks: + - test-network + + # HTTPS test server + https-server: + image: node:20-alpine + container_name: https-test-server + working_dir: /app + volumes: + - ../servers/https-server.js:/app/server.js:ro + - ../certs:/app/certs:ro + command: node server.js + ports: + - "8443:8443" + networks: + - test-network + + # Echo server for testing + echo-server: + image: ealen/echo-server:latest + container_name: echo-server + environment: + - PORT=3000 + ports: + - "3000:3000" + networks: + - test-network + + # Blocked target (for testing connection failures) + blocked-server: + image: alpine:latest + container_name: blocked-server + command: sleep infinity + networks: + - isolated-network + +networks: + test-network: + driver: bridge + isolated-network: + driver: bridge + internal: true \ No newline at end of file diff --git a/test/fixtures/duplicate-debug.js b/test/fixtures/duplicate-debug.js new file mode 100644 index 00000000000..b8735cfd98b --- /dev/null +++ b/test/fixtures/duplicate-debug.js @@ -0,0 +1,24 @@ +'use strict' + +const { createServer } = require('node:http') +const { request } = require('../..') + +// Simulate the scenario where diagnostics module is loaded multiple times +// This mimics having both Node.js built-in undici and undici as dependency +delete require.cache[require.resolve('../../lib/core/diagnostics.js')] +require('../../lib/core/diagnostics.js') + +const server = createServer({ joinDuplicateHeaders: true }, (_req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.end('hello world') +}) + +server.listen(0, () => { + const { port, address, family } = server.address() + const hostname = family === 'IPv6' ? `[${address}]` : address + request(`http://${hostname}:${port}`) + .then(res => res.body.dump()) + .then(() => { + server.close() + }) +}) diff --git a/test/fixtures/socks5-test-server.js b/test/fixtures/socks5-test-server.js new file mode 100644 index 00000000000..5f7c846d4d0 --- /dev/null +++ b/test/fixtures/socks5-test-server.js @@ -0,0 +1,227 @@ +'use strict' + +const net = require('node:net') +const { AUTH_METHODS, REPLY_CODES } = require('../../lib/core/socks5-client') + +/** + * Test SOCKS5 server for unit tests + * Implements SOCKS5 protocol with optional authentication + */ +class TestSocks5Server { + constructor (options = {}) { + this.options = options + this.server = null + this.connections = new Set() + this.requireAuth = options.requireAuth || false + this.validCredentials = options.credentials || { username: 'test', password: 'pass' } + } + + async listen (port = 0) { + return new Promise((resolve, reject) => { + this.server = net.createServer((socket) => { + this.connections.add(socket) + this.handleConnection(socket) + + socket.on('close', () => { + this.connections.delete(socket) + }) + }) + + this.server.listen(port, (err) => { + if (err) { + reject(err) + } else { + resolve(this.server.address()) + } + }) + }) + } + + handleConnection (socket) { + let state = 'handshake' + let buffer = Buffer.alloc(0) + + socket.on('data', (data) => { + buffer = Buffer.concat([buffer, data]) + + if (state === 'handshake') { + this.handleHandshake(socket, buffer, (newBuffer, method) => { + buffer = newBuffer + if (method === AUTH_METHODS.NO_AUTH) { + state = 'connect' + } else if (method === AUTH_METHODS.USERNAME_PASSWORD) { + state = 'auth' + } + }) + } else if (state === 'auth') { + this.handleAuth(socket, buffer, (newBuffer, success) => { + buffer = newBuffer + if (success) { + state = 'connect' + } else { + socket.end() + } + }) + } else if (state === 'connect') { + this.handleConnect(socket, buffer, (newBuffer) => { + buffer = newBuffer + state = 'relay' + }) + } + }) + + socket.on('error', () => { + // Handle socket errors silently + }) + } + + handleHandshake (socket, buffer, callback) { + if (buffer.length >= 2) { + const version = buffer[0] + const nmethods = buffer[1] + + if (version === 0x05 && buffer.length >= 2 + nmethods) { + const methods = Array.from(buffer.subarray(2, 2 + nmethods)) + + // Select authentication method + let selectedMethod + if (this.requireAuth && methods.includes(AUTH_METHODS.USERNAME_PASSWORD)) { + selectedMethod = AUTH_METHODS.USERNAME_PASSWORD + } else if (!this.requireAuth && methods.includes(AUTH_METHODS.NO_AUTH)) { + selectedMethod = AUTH_METHODS.NO_AUTH + } else { + selectedMethod = AUTH_METHODS.NO_ACCEPTABLE + } + + socket.write(Buffer.from([0x05, selectedMethod])) + callback(buffer.subarray(2 + nmethods), selectedMethod) + } + } + } + + handleAuth (socket, buffer, callback) { + if (buffer.length >= 2) { + const version = buffer[0] + if (version !== 0x01) { + socket.write(Buffer.from([0x01, 0x01])) // Failure + callback(buffer, false) + return + } + + const usernameLen = buffer[1] + if (buffer.length >= 3 + usernameLen) { + const username = buffer.subarray(2, 2 + usernameLen).toString() + const passwordLen = buffer[2 + usernameLen] + + if (buffer.length >= 3 + usernameLen + passwordLen) { + const password = buffer.subarray(3 + usernameLen, 3 + usernameLen + passwordLen).toString() + + const success = username === this.validCredentials.username && + password === this.validCredentials.password + + socket.write(Buffer.from([0x01, success ? 0x00 : 0x01])) + callback(buffer.subarray(3 + usernameLen + passwordLen), success) + } + } + } + } + + handleConnect (socket, buffer, callback) { + if (buffer.length >= 4) { + const version = buffer[0] + const cmd = buffer[1] + const atyp = buffer[3] + + if (version === 0x05 && cmd === 0x01) { + let addressLength = 0 + if (atyp === 0x01) { + addressLength = 4 // IPv4 + } else if (atyp === 0x03) { + if (buffer.length >= 5) { + addressLength = 1 + buffer[4] // Domain length + domain + } else { + return // Not enough data + } + } else if (atyp === 0x04) { + addressLength = 16 // IPv6 + } + + if (buffer.length >= 4 + addressLength + 2) { + // Extract target address and port + let targetHost + let offset = 4 + + if (atyp === 0x01) { + targetHost = Array.from(buffer.subarray(offset, offset + 4)).join('.') + offset += 4 + } else if (atyp === 0x03) { + const domainLen = buffer[offset] + offset += 1 + targetHost = buffer.subarray(offset, offset + domainLen).toString() + offset += domainLen + } + + const targetPort = buffer.readUInt16BE(offset) + + // Simulate connection failure if requested + if (this.options.simulateFailure) { + const response = Buffer.concat([ + Buffer.from([0x05, REPLY_CODES.CONNECTION_REFUSED, 0x00, 0x01]), + Buffer.from([0, 0, 0, 0]), + Buffer.from([0, 0]) + ]) + socket.write(response) + socket.end() + return + } + + // Connect to target + const targetSocket = net.connect(targetPort, targetHost) + + targetSocket.on('connect', () => { + // Send success response + const response = Buffer.concat([ + Buffer.from([0x05, 0x00, 0x00, 0x01]), // VER, REP, RSV, ATYP + Buffer.from([127, 0, 0, 1]), // Bind address (localhost) + Buffer.allocUnsafe(2) // Bind port + ]) + response.writeUInt16BE(targetPort, response.length - 2) + socket.write(response) + + // Start relaying data + socket.pipe(targetSocket) + targetSocket.pipe(socket) + + callback(buffer.subarray(4 + addressLength + 2)) + }) + + targetSocket.on('error', () => { + // Send connection refused + const response = Buffer.concat([ + Buffer.from([0x05, REPLY_CODES.CONNECTION_REFUSED, 0x00, 0x01]), + Buffer.from([0, 0, 0, 0]), + Buffer.from([0, 0]) + ]) + socket.write(response) + socket.end() + }) + } + } + } + } + + async close () { + if (this.server) { + // Close all connections + for (const socket of this.connections) { + socket.destroy() + } + + return new Promise((resolve) => { + this.server.close(resolve) + }) + } + } +} + +module.exports = { TestSocks5Server } diff --git a/test/fixtures/wpt/LICENSE.md b/test/fixtures/wpt/LICENSE.md deleted file mode 100644 index 39c46d03ac2..00000000000 --- a/test/fixtures/wpt/LICENSE.md +++ /dev/null @@ -1,11 +0,0 @@ -# The 3-Clause BSD License - -Copyright © web-platform-tests contributors - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/test/fixtures/wpt/common/CustomCorsResponse.py b/test/fixtures/wpt/common/CustomCorsResponse.py deleted file mode 100644 index fc4d122f1b1..00000000000 --- a/test/fixtures/wpt/common/CustomCorsResponse.py +++ /dev/null @@ -1,30 +0,0 @@ -import json - -def main(request, response): - '''Handler for getting an HTTP response customised by the given query - parameters. - - The returned response will have - - HTTP headers defined by the 'headers' query parameter - - Must be a serialized JSON dictionary mapping header names to header - values - - HTTP status code defined by the 'status' query parameter - - Must be a positive serialized JSON integer like the string '200' - - Response content defined by the 'content' query parameter - - Must be a serialized JSON string representing the desired response body - ''' - def query_parameter_or_default(param, default): - return request.GET.first(param) if param in request.GET else default - - headers = json.loads(query_parameter_or_default(b'headers', b'"{}"')) - for k, v in headers.items(): - response.headers.set(k, v) - - # Note that, in order to have out-of-the-box support for tests that don't call - # setup({'allow_uncaught_exception': true}) - # we return a no-op JS payload. This approach will avoid syntax errors in - # script resources that would otherwise cause the test harness to fail. - response.content = json.loads(query_parameter_or_default(b'content', - b'"/* CustomCorsResponse.py content */"')) - response.status_code = json.loads(query_parameter_or_default(b'status', - b'200')) diff --git a/test/fixtures/wpt/common/META.yml b/test/fixtures/wpt/common/META.yml deleted file mode 100644 index 963dff9d1f1..00000000000 --- a/test/fixtures/wpt/common/META.yml +++ /dev/null @@ -1,2 +0,0 @@ -suggested_reviewers: - - deniak diff --git a/test/fixtures/wpt/common/PrefixedLocalStorage.js b/test/fixtures/wpt/common/PrefixedLocalStorage.js deleted file mode 100644 index 2f4e7b6a055..00000000000 --- a/test/fixtures/wpt/common/PrefixedLocalStorage.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Supports pseudo-"namespacing" localStorage for a given test - * by generating and using a unique prefix for keys. Why trounce on other - * tests' localStorage items when you can keep it "separated"? - * - * PrefixedLocalStorageTest: Instantiate in testharness.js tests to generate - * a new unique-ish prefix - * PrefixedLocalStorageResource: Instantiate in supporting test resource - * files to use/share a prefix generated by a test. - */ -var PrefixedLocalStorage = function () { - this.prefix = ''; // Prefix for localStorage keys - this.param = 'prefixedLocalStorage'; // Param to use in querystrings -}; - -PrefixedLocalStorage.prototype.clear = function () { - if (this.prefix === '') { return; } - Object.keys(localStorage).forEach(sKey => { - if (sKey.indexOf(this.prefix) === 0) { - localStorage.removeItem(sKey); - } - }); -}; - -/** - * Append/replace prefix parameter and value in URI querystring - * Use to generate URLs to resource files that will share the prefix. - */ -PrefixedLocalStorage.prototype.url = function (uri) { - function updateUrlParameter (uri, key, value) { - var i = uri.indexOf('#'); - var hash = (i === -1) ? '' : uri.substr(i); - uri = (i === -1) ? uri : uri.substr(0, i); - var re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i'); - var separator = uri.indexOf('?') !== -1 ? '&' : '?'; - uri = (uri.match(re)) ? uri.replace(re, `$1${key}=${value}$2`) : - `${uri}${separator}${key}=${value}`; - return uri + hash; - } - return updateUrlParameter(uri, this.param, this.prefix); -}; - -PrefixedLocalStorage.prototype.prefixedKey = function (baseKey) { - return `${this.prefix}${baseKey}`; -}; - -PrefixedLocalStorage.prototype.setItem = function (baseKey, value) { - localStorage.setItem(this.prefixedKey(baseKey), value); -}; - -/** - * Listen for `storage` events pertaining to a particular key, - * prefixed with this object's prefix. Ignore when value is being set to null - * (i.e. removeItem). - */ -PrefixedLocalStorage.prototype.onSet = function (baseKey, fn) { - window.addEventListener('storage', e => { - var match = this.prefixedKey(baseKey); - if (e.newValue !== null && e.key.indexOf(match) === 0) { - fn.call(this, e); - } - }); -}; - -/***************************************************************************** - * Use in a testharnessjs test to generate a new key prefix. - * async_test(t => { - * var prefixedStorage = new PrefixedLocalStorageTest(); - * t.add_cleanup(() => prefixedStorage.cleanup()); - * /... - * }); - */ -var PrefixedLocalStorageTest = function () { - PrefixedLocalStorage.call(this); - this.prefix = `${document.location.pathname}-${Math.random()}-${Date.now()}-`; -}; -PrefixedLocalStorageTest.prototype = Object.create(PrefixedLocalStorage.prototype); -PrefixedLocalStorageTest.prototype.constructor = PrefixedLocalStorageTest; - -/** - * Use in a cleanup function to clear out prefixed entries in localStorage - */ -PrefixedLocalStorageTest.prototype.cleanup = function () { - this.setItem('closeAll', 'true'); - this.clear(); -}; - -/***************************************************************************** - * Use in test resource files to share a prefix generated by a - * PrefixedLocalStorageTest. Will look in URL querystring for prefix. - * Setting `close_on_cleanup` opt truthy will make this script's window listen - * for storage `closeAll` event from controlling test and close itself. - * - * var PrefixedLocalStorageResource({ close_on_cleanup: true }); - */ -var PrefixedLocalStorageResource = function (options) { - PrefixedLocalStorage.call(this); - this.options = Object.assign({}, { - close_on_cleanup: false - }, options || {}); - // Check URL querystring for prefix to use - var regex = new RegExp(`[?&]${this.param}(=([^&#]*)|&|#|$)`), - results = regex.exec(document.location.href); - if (results && results[2]) { - this.prefix = results[2]; - } - // Optionally have this window close itself when the PrefixedLocalStorageTest - // sets a `closeAll` item. - if (this.options.close_on_cleanup) { - this.onSet('closeAll', () => { - window.close(); - }); - } -}; -PrefixedLocalStorageResource.prototype = Object.create(PrefixedLocalStorage.prototype); -PrefixedLocalStorageResource.prototype.constructor = PrefixedLocalStorageResource; diff --git a/test/fixtures/wpt/common/PrefixedLocalStorage.js.headers b/test/fixtures/wpt/common/PrefixedLocalStorage.js.headers deleted file mode 100644 index 6805c323df5..00000000000 --- a/test/fixtures/wpt/common/PrefixedLocalStorage.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/javascript; charset=utf-8 diff --git a/test/fixtures/wpt/common/PrefixedPostMessage.js b/test/fixtures/wpt/common/PrefixedPostMessage.js deleted file mode 100644 index 674b52877c0..00000000000 --- a/test/fixtures/wpt/common/PrefixedPostMessage.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Supports pseudo-"namespacing" for window-posted messages for a given test - * by generating and using a unique prefix that gets wrapped into message - * objects. This makes it more feasible to have multiple tests that use - * `window.postMessage` in a single test file. Basically, make it possible - * for the each test to listen for only the messages that are pertinent to it. - * - * 'Prefix' not an elegant term to use here but this models itself after - * PrefixedLocalStorage. - * - * PrefixedMessageTest: Instantiate in testharness.js tests to generate - * a new unique-ish prefix that can be used by other test support files - * PrefixedMessageResource: Instantiate in supporting test resource - * files to use/share a prefix generated by a test. - */ -var PrefixedMessage = function () { - this.prefix = ''; - this.param = 'prefixedMessage'; // Param to use in querystrings -}; - -/** - * Generate a URL that adds/replaces param with this object's prefix - * Use to link to test support files that make use of - * PrefixedMessageResource. - */ -PrefixedMessage.prototype.url = function (uri) { - function updateUrlParameter (uri, key, value) { - var i = uri.indexOf('#'); - var hash = (i === -1) ? '' : uri.substr(i); - uri = (i === -1) ? uri : uri.substr(0, i); - var re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i'); - var separator = uri.indexOf('?') !== -1 ? '&' : '?'; - uri = (uri.match(re)) ? uri.replace(re, `$1${key}=${value}$2`) : - `${uri}${separator}${key}=${value}`; - return uri + hash; - } - return updateUrlParameter(uri, this.param, this.prefix); -}; - -/** - * Add an eventListener on `message` but only invoke the given callback - * for messages whose object contains this object's prefix. Remove the - * event listener once the anticipated message has been received. - */ -PrefixedMessage.prototype.onMessage = function (fn) { - window.addEventListener('message', e => { - if (typeof e.data === 'object' && e.data.hasOwnProperty('prefix')) { - if (e.data.prefix === this.prefix) { - // Only invoke callback when `data` is an object containing - // a `prefix` key with this object's prefix value - // Note fn is invoked with "unwrapped" data first, then the event `e` - // (which contains the full, wrapped e.data should it be needed) - fn.call(this, e.data.data, e); - window.removeEventListener('message', fn); - } - } - }); -}; - -/** - * Instantiate in a test file (e.g. during `setup`) to create a unique-ish - * prefix that can be shared by support files - */ -var PrefixedMessageTest = function () { - PrefixedMessage.call(this); - this.prefix = `${document.location.pathname}-${Math.random()}-${Date.now()}-`; -}; -PrefixedMessageTest.prototype = Object.create(PrefixedMessage.prototype); -PrefixedMessageTest.prototype.constructor = PrefixedMessageTest; - -/** - * Instantiate in a test support script to use a "prefix" generated by a - * PrefixedMessageTest in a controlling test file. It will look for - * the prefix in a URL param (see also PrefixedMessage#url) - */ -var PrefixedMessageResource = function () { - PrefixedMessage.call(this); - // Check URL querystring for prefix to use - var regex = new RegExp(`[?&]${this.param}(=([^&#]*)|&|#|$)`), - results = regex.exec(document.location.href); - if (results && results[2]) { - this.prefix = results[2]; - } -}; -PrefixedMessageResource.prototype = Object.create(PrefixedMessage.prototype); -PrefixedMessageResource.prototype.constructor = PrefixedMessageResource; - -/** - * This is how a test resource document can "send info" to its - * opener context. It will whatever message is being sent (`data`) in - * an object that injects the prefix. - */ -PrefixedMessageResource.prototype.postToOpener = function (data) { - if (window.opener) { - window.opener.postMessage({ - prefix: this.prefix, - data: data - }, '*'); - } -}; diff --git a/test/fixtures/wpt/common/PrefixedPostMessage.js.headers b/test/fixtures/wpt/common/PrefixedPostMessage.js.headers deleted file mode 100644 index 6805c323df5..00000000000 --- a/test/fixtures/wpt/common/PrefixedPostMessage.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/javascript; charset=utf-8 diff --git a/test/fixtures/wpt/common/README.md b/test/fixtures/wpt/common/README.md deleted file mode 100644 index 9aef19cb73f..00000000000 --- a/test/fixtures/wpt/common/README.md +++ /dev/null @@ -1,10 +0,0 @@ -The files in this directory are non-infrastructure support files that can be used by tests. - -* `blank.html` - An empty HTML document. -* `domain-setter.sub.html` - An HTML document that sets `document.domain`. -* `dummy.xhtml` - An XHTML document. -* `dummy.xml` - An XML document. -* `text-plain.txt` - A text/plain document. -* `*.js` - Utility scripts. These are documented in the source. -* `*.py` - wptserve [Python Handlers](https://web-platform-tests.org/writing-tests/python-handlers/). These are documented in the source. -* `security-features` - Documented in `security-features/README.md`. diff --git a/test/fixtures/wpt/common/__init__.py b/test/fixtures/wpt/common/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/common/arrays.js b/test/fixtures/wpt/common/arrays.js deleted file mode 100644 index 2b31bb4179c..00000000000 --- a/test/fixtures/wpt/common/arrays.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Callback for checking equality of c and d. - * - * @callback equalityCallback - * @param {*} c - * @param {*} d - * @returns {boolean} - */ - -/** - * Returns true if the given arrays are equal. Optionally can pass an equality function. - * @param {Array} a - * @param {Array} b - * @param {equalityCallback} callbackFunction - defaults to `c === d` - * @returns {boolean} - */ -export function areArraysEqual(a, b, equalityFunction = (c, d) => { return c === d; }) { - try { - if (a.length !== b.length) - return false; - - for (let i = 0; i < a.length; i++) { - if (!equalityFunction(a[i], b[i])) - return false; - } - } catch (ex) { - return false; - } - - return true; -} diff --git a/test/fixtures/wpt/common/blank-with-cors.html b/test/fixtures/wpt/common/blank-with-cors.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/common/blank-with-cors.html.headers b/test/fixtures/wpt/common/blank-with-cors.html.headers deleted file mode 100644 index cb762eff806..00000000000 --- a/test/fixtures/wpt/common/blank-with-cors.html.headers +++ /dev/null @@ -1 +0,0 @@ -Access-Control-Allow-Origin: * diff --git a/test/fixtures/wpt/common/blank.html b/test/fixtures/wpt/common/blank.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/common/custom-cors-response.js b/test/fixtures/wpt/common/custom-cors-response.js deleted file mode 100644 index be9c7ce3bdc..00000000000 --- a/test/fixtures/wpt/common/custom-cors-response.js +++ /dev/null @@ -1,32 +0,0 @@ -const custom_cors_response = (payload, base_url) => { - base_url = base_url || new URL(location.href); - - // Clone the given `payload` so that, as we modify it, we won't be mutating - // the caller's value in unexpected ways. - payload = Object.assign({}, payload); - payload.headers = payload.headers || {}; - // Note that, in order to have out-of-the-box support for tests that don't - // call `setup({'allow_uncaught_exception': true})` we return a no-op JS - // payload. This approach will avoid hitting syntax errors if the resource is - // interpreted as script. Without this workaround, the SyntaxError would be - // caught by the test harness and trigger a test failure. - payload.content = payload.content || '/* custom-cors-response.js content */'; - payload.status_code = payload.status_code || 200; - - // Assume that we'll be doing a CORS-enabled fetch so we'll need to set ACAO. - const acao = "Access-Control-Allow-Origin"; - if (!(acao in payload.headers)) { - payload.headers[acao] = '*'; - } - - if (!("Content-Type" in payload.headers)) { - payload.headers["Content-Type"] = "text/javascript"; - } - - let ret = new URL("/common/CustomCorsResponse.py", base_url); - for (const key in payload) { - ret.searchParams.append(key, JSON.stringify(payload[key])); - } - - return ret; -}; diff --git a/test/fixtures/wpt/common/dispatcher/README.md b/test/fixtures/wpt/common/dispatcher/README.md deleted file mode 100644 index cfaafb6e5d6..00000000000 --- a/test/fixtures/wpt/common/dispatcher/README.md +++ /dev/null @@ -1,228 +0,0 @@ -# `RemoteContext`: API for script execution in another context - -`RemoteContext` in `/common/dispatcher/dispatcher.js` provides an interface to -execute JavaScript in another global object (page or worker, the "executor"), -based on: - -- [WPT RFC 88: context IDs from uuid searchParams in URL](https://github.com/web-platform-tests/rfcs/pull/88), -- [WPT RFC 89: execute_script](https://github.com/web-platform-tests/rfcs/pull/89) and -- [WPT RFC 91: RemoteContext](https://github.com/web-platform-tests/rfcs/pull/91). - -Tests can send arbitrary javascript to executors to evaluate in its global -object, like: - -``` -// injector.html -const argOnLocalContext = ...; - -async function execute() { - window.open('executor.html?uuid=' + uuid); - const ctx = new RemoteContext(uuid); - await ctx.execute_script( - (arg) => functionOnRemoteContext(arg), - [argOnLocalContext]); -}; -``` - -and on executor: - -``` -// executor.html -function functionOnRemoteContext(arg) { ... } - -const uuid = new URLSearchParams(window.location.search).get('uuid'); -const executor = new Executor(uuid); -``` - -For concrete examples, see -[events.html](../../html/browsers/browsing-the-web/back-forward-cache/events.html) -and -[executor.html](../../html/browsers/browsing-the-web/back-forward-cache/resources/executor.html) -in back-forward cache tests. - -Note that `executor*` files under `/common/dispatcher/` are NOT for -`RemoteContext.execute_script()`. Use `remote-executor.html` instead. - -This is universal and avoids introducing many specific `XXX-helper.html` -resources. -Moreover, tests are easier to read, because the whole logic of the test can be -defined in a single file. - -## `new RemoteContext(uuid)` - -- `uuid` is a UUID string that identifies the remote context and should match - with the `uuid` parameter of the URL of the remote context. -- Callers should create the remote context outside this constructor (e.g. - `window.open('executor.html?uuid=' + uuid)`). - -## `RemoteContext.execute_script(fn, args)` - -- `fn` is a JavaScript function to execute on the remote context, which is - converted to a string using `toString()` and sent to the remote context. -- `args` is null or an array of arguments to pass to the function on the - remote context. Arguments are passed as JSON. -- If the return value of `fn` when executed in the remote context is a promise, - the promise returned by `execute_script` resolves to the resolved value of - that promise. Otherwise the `execute_script` promise resolves to the return - value of `fn`. - -Note that `fn` is evaluated on the remote context (`executor.html` in the -example above), while `args` are evaluated on the caller context -(`injector.html`) and then passed to the remote context. - -## Return value of injected functions and `execute_script()` - -If the return value of the injected function when executed in the remote -context is a promise, the promise returned by `execute_script` resolves to the -resolved value of that promise. Otherwise the `execute_script` promise resolves -to the return value of the function. - -When the return value of an injected script is a Promise, it should be resolved -before any navigation starts on the remote context. For example, it shouldn't -be resolved after navigating out and navigating back to the page again. -It's fine to create a Promise to be resolved after navigations, if it's not the -return value of the injected function. - -## Calling timing of `execute_script()` - -When `RemoteContext.execute_script()` is called when the remote context is not -active (for example before it is created, before navigation to the page, or -during the page is in back-forward cache), the injected script is evaluated -after the remote context becomes active. - -Multiple calls to `RemoteContext.execute_script()` will result in multiple scripts -being executed in remote context and ordering will be maintained. - -## Errors from `execute_script()` - -Errors from `execute_script()` will result in promise rejections, so it is -important to await the result. This can be `await ctx.execute_script(...)` for -every call but if there are multiple scripts to executed, it may be preferable -to wait on them in parallel to avoid incurring full round-trip time for each, -e.g. - -```js -await Promise.all( - ctx1.execute_script(...), - ctx1.execute_script(...), - ctx2.execute_script(...), - ctx2.execute_script(...), - ... -) -``` - -## Evaluation timing of injected functions - -The script injected by `RemoteContext.execute_script()` can be evaluated any -time during the remote context is active. -For example, even before DOMContentLoaded events or even during navigation. -It's the responsibility of test-specific code/helpers to ensure evaluation -timing constraints (which can be also test-specific), if any needed. - -### Ensuring evaluation timing around page load - -For example, to ensure that injected functions (`mainFunction` below) are -evaluated after the first `pageshow` event, we can use pure JavaScript code -like below: - -``` -// executor.html -window.pageShowPromise = new Promise(resolve => - window.addEventListener('pageshow', resolve, {once: true})); - - -// injector.html -const waitForPageShow = async () => { - while (!window.pageShowPromise) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - await window.pageShowPromise; -}; - -await ctx.execute(waitForPageShow); -await ctx.execute(mainFunction); -``` - -### Ensuring evaluation timing around navigation out/unloading - -It can be important to ensure there are no injected functions nor code behind -`RemoteContext` (such as Fetch APIs accessing server-side stash) running after -navigation is initiated, for example in the case of back-forward cache testing. - -To ensure this, - -- Do not call the next `RemoteContext.execute()` for the remote context after - triggering the navigation, until we are sure that the remote context is not - active (e.g. after we confirm that the new page is loaded). -- Call `Executor.suspend(callback)` synchronously within the injected script. - This suspends executor-related code, and calls `callback` when it is ready - to start navigation. - -The code on the injector side would be like: - -``` -// injector.html -await ctx.execute_script(() => { - executor.suspend(() => { - location.href = 'new-url.html'; - }); -}); -``` - -## Future Work: Possible integration with `test_driver` - -Currently `RemoteContext` is implemented by JavaScript and WPT-server-side -stash, and not integrated with `test_driver` nor `testharness`. -There is a proposal of `test_driver`-integrated version (see the RFCs listed -above). - -The API semantics and guidelines in this document are designed to be applicable -to both the current stash-based `RemoteContext` and `test_driver`-based -version, and thus the tests using `RemoteContext` will be migrated with minimum -modifications (mostly in `/common/dispatcher/dispatcher.js` and executors), for -example in a -[draft CL](https://chromium-review.googlesource.com/c/chromium/src/+/3082215/). - - -# `send()`/`receive()` Message passing APIs - -`dispatcher.js` (and its server-side backend `dispatcher.py`) provides a -universal queue-based message passing API. -Each queue is identified by a UUID, and accessed via the following APIs: - -- `send(uuid, message)` pushes a string `message` to the queue `uuid`. -- `receive(uuid)` pops the first item from the queue `uuid`. -- `showRequestHeaders(origin, uuid)` and - `cacheableShowRequestHeaders(origin, uuid)` return URLs, that push request - headers to the queue `uuid` upon fetching. - -It works cross-origin, and even access different browser context groups. - -Messages are queued, this means one doesn't need to wait for the receiver to -listen, before sending the first message -(but still need to wait for the resolution of the promise returned by `send()` -to ensure the order between `send()`s). - -## Executors - -Similar to `RemoteContext.execute_script()`, `send()`/`receive()` can be used -for sending arbitrary javascript to be evaluated in another page or worker. - -- `executor.html` (as a Document), -- `executor-worker.js` (as a Web Worker), and -- `executor-service-worker.js` (as a Service Worker) - -are examples of executors. -Note that these executors are NOT compatible with -`RemoteContext.execute_script()`. - -## Future Work - -`send()`, `receive()` and the executors below are kept for COEP/COOP tests. - -For remote script execution, new tests should use -`RemoteContext.execute_script()` instead. - -For message passing, -[WPT RFC 90](https://github.com/web-platform-tests/rfcs/pull/90) is still under -discussion. diff --git a/test/fixtures/wpt/common/dispatcher/dispatcher.js b/test/fixtures/wpt/common/dispatcher/dispatcher.js deleted file mode 100644 index dab0100020b..00000000000 --- a/test/fixtures/wpt/common/dispatcher/dispatcher.js +++ /dev/null @@ -1,281 +0,0 @@ -// Define a universal message passing API. It works cross-origin and across -// browsing context groups. -const dispatcher_path = "/common/dispatcher/dispatcher.py"; - -// Finds the nearest ancestor window that has a non srcdoc location. This should -// give us a usable location for constructing further URLs. -function findLocationFromAncestors(w) { - if (w.location.href == 'about:srcdoc') { - return findLocationFromAncestors(w.parent); - } - return w.location; -} - -// Handles differences between workers vs frames (src vs srcdoc). -function findLocation() { - if (location.href == 'about:srcdoc') { - return findLocationFromAncestors(window.parent); - } - if (location.protocol == 'blob:' || location.protocol == 'data:') { - // Allows working around blob and data URLs. - if (self.document && self.document.baseURI) { - return self.document.baseURI; - } - } - return location; -} - -const dispatcherLocation = findLocation(); -const dispatcher_url = new URL(dispatcher_path, dispatcherLocation).href; - -// Return a promise, limiting the number of concurrent accesses to a shared -// resources to |max_concurrent_access|. -const concurrencyLimiter = (max_concurrency) => { - let pending = 0; - let waiting = []; - return async (task) => { - pending++; - if (pending > max_concurrency) - await new Promise(resolve => waiting.push(resolve)); - let result = await task(); - pending--; - waiting.shift()?.(); - return result; - }; -} - -// Wait for a random amount of time in the range [10ms,100ms]. -const randomDelay = () => { - return new Promise(resolve => setTimeout(resolve, 10 + 90*Math.random())); -} - -// Sending too many requests in parallel causes congestion. Limiting it improves -// throughput. -// -// Note: The following table has been determined on the test: -// ../cache-storage.tentative.https.html -// using Chrome with a 64 core CPU / 64GB ram, in release mode: -// ┌───────────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────┐ -// │concurrency│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 10│ 15│ 20│ 30│ 50│ 100│ -// ├───────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼────┤ -// │time (s) │ 54│ 38│ 31│ 29│ 26│ 24│ 22│ 22│ 22│ 22│ 34│ 36 │ -// └───────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴────┘ -const limiter = concurrencyLimiter(6); - -// While requests to different remote contexts can go in parallel, we need to -// ensure that requests to each remote context are done in order. This maps a -// uuid to a queue of requests to send. A queue is processed until it is empty -// and then is deleted from the map. -const sendQueues = new Map(); - -// Sends a single item (with rate-limiting) and calls the associated resolver -// when it is successfully sent. -const sendItem = async function (uuid, resolver, message) { - await limiter(async () => { - // Requests might be dropped. Retry until getting a confirmation it has been - // processed. - while(1) { - try { - let response = await fetch(dispatcher_url + `?uuid=${uuid}`, { - method: 'POST', - body: message - }) - if (await response.text() == "done") { - resolver(); - return; - } - } catch (fetch_error) {} - await randomDelay(); - }; - }); -} - -// While the queue is non-empty, send the next item. This is async and new items -// may be added to the queue while others are being sent. -const processQueue = async function (uuid, queue) { - while (queue.length) { - const [resolver, message] = queue.shift(); - await sendItem(uuid, resolver, message); - } - // The queue is empty, delete it. - sendQueues.delete(uuid); -} - -const send = async function (uuid, message) { - const itemSentPromise = new Promise((resolve) => { - const item = [resolve, message]; - if (sendQueues.has(uuid)) { - // There is already a queue for `uuid`, just add to it and it will be processed. - sendQueues.get(uuid).push(item); - } else { - // There is no queue for `uuid`, create it and start processing. - const queue = [item]; - sendQueues.set(uuid, queue); - processQueue(uuid, queue); - } - }); - // Wait until the item has been successfully sent. - await itemSentPromise; -} - -const receive = async function (uuid) { - while(1) { - let data = "not ready"; - try { - data = await limiter(async () => { - let response = await fetch(dispatcher_url + `?uuid=${uuid}`); - return await response.text(); - }); - } catch (fetch_error) {} - - if (data == "not ready") { - await randomDelay(); - continue; - } - - return data; - } -} - -// Returns an URL. When called, the server sends toward the `uuid` queue the -// request headers. Useful for determining if something was requested with -// Cookies. -const showRequestHeaders = function(origin, uuid) { - return origin + dispatcher_path + `?uuid=${uuid}&show-headers`; -} - -// Same as above, except for the response is cacheable. -const cacheableShowRequestHeaders = function(origin, uuid) { - return origin + dispatcher_path + `?uuid=${uuid}&cacheable&show-headers`; -} - -// This script requires -// - `/common/utils.js` for `token()`. - -// Returns the URL of a document that can be used as a `RemoteContext`. -// -// `uuid` should be a UUID uniquely identifying the given remote context. -// `options` has the following shape: -// -// { -// host: (optional) Sets the returned URL's `host` property. Useful for -// cross-origin executors. -// protocol: (optional) Sets the returned URL's `protocol` property. -// } -function remoteExecutorUrl(uuid, options) { - const url = new URL("/common/dispatcher/remote-executor.html", dispatcherLocation); - url.searchParams.set("uuid", uuid); - - if (options?.host) { - url.host = options.host; - } - - if (options?.protocol) { - url.protocol = options.protocol; - } - - return url; -} - -// Represents a remote executor. For more detailed explanation see `README.md`. -class RemoteContext { - // `uuid` is a UUID string that identifies the remote context and should - // match with the `uuid` parameter of the URL of the remote context. - constructor(uuid) { - this.context_id = uuid; - } - - // Evaluates the script `expr` on the executor. - // - If `expr` is evaluated to a Promise that is resolved with a value: - // `execute_script()` returns a Promise resolved with the value. - // - If `expr` is evaluated to a non-Promise value: - // `execute_script()` returns a Promise resolved with the value. - // - If `expr` throws an error or is evaluated to a Promise that is rejected: - // `execute_script()` returns a rejected Promise with the error's - // `message`. - // Note that currently the type of error (e.g. DOMException) is not - // preserved, except for `TypeError`. - // The values should be able to be serialized by JSON.stringify(). - async execute_script(fn, args) { - const receiver = token(); - await this.send({receiver: receiver, fn: fn.toString(), args: args}); - const response = JSON.parse(await receive(receiver)); - if (response.status === 'success') { - return response.value; - } - - // exception - if (response.name === 'TypeError') { - throw new TypeError(response.value); - } - throw new Error(response.value); - } - - async send(msg) { - return await send(this.context_id, JSON.stringify(msg)); - } -}; - -class Executor { - constructor(uuid) { - this.uuid = uuid; - - // If `suspend_callback` is not `null`, the executor should be suspended - // when there are no ongoing tasks. - this.suspend_callback = null; - - this.execute(); - } - - // Wait until there are no ongoing tasks nor fetch requests for polling - // tasks, and then suspend the executor and call `callback()`. - // Navigation from the executor page should be triggered inside `callback()`, - // to avoid conflict with in-flight fetch requests. - suspend(callback) { - this.suspend_callback = callback; - } - - resume() { - } - - async execute() { - while(true) { - if (this.suspend_callback !== null) { - this.suspend_callback(); - this.suspend_callback = null; - // Wait for `resume()` to be called. - await new Promise(resolve => this.resume = resolve); - - // Workaround for https://crbug.com/1244230. - // Without this workaround, the executor is resumed and the fetch - // request to poll the next task is initiated synchronously from - // pageshow event after the page restored from BFCache, and the fetch - // request promise is never resolved (and thus the test results in - // timeout) due to https://crbug.com/1244230. The root cause is not yet - // known, but setTimeout() with 0ms causes the resume triggered on - // another task and seems to resolve the issue. - await new Promise(resolve => setTimeout(resolve, 0)); - - continue; - } - - const task = JSON.parse(await receive(this.uuid)); - - let response; - try { - const value = await eval(task.fn).apply(null, task.args); - response = JSON.stringify({ - status: 'success', - value: value - }); - } catch(e) { - response = JSON.stringify({ - status: 'exception', - name: e.name, - value: e.message - }); - } - await send(task.receiver, response); - } - } -} diff --git a/test/fixtures/wpt/common/dispatcher/dispatcher.py b/test/fixtures/wpt/common/dispatcher/dispatcher.py deleted file mode 100644 index 9fe7a38ac87..00000000000 --- a/test/fixtures/wpt/common/dispatcher/dispatcher.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -from wptserve.utils import isomorphic_decode - -# A server used to store and retrieve arbitrary data. -# This is used by: ./dispatcher.js -def main(request, response): - # This server is configured so that is accept to receive any requests and - # any cookies the web browser is willing to send. - response.headers.set(b"Access-Control-Allow-Credentials", b"true") - response.headers.set(b'Access-Control-Allow-Methods', b'OPTIONS, GET, POST') - response.headers.set(b'Access-Control-Allow-Headers', b'Content-Type') - response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin") or '*') - - if b"cacheable" in request.GET: - response.headers.set(b"Cache-Control", b"max-age=31536000") - else: - response.headers.set(b'Cache-Control', b'no-cache, no-store, must-revalidate') - - # CORS preflight - if request.method == u'OPTIONS': - return b'' - - uuid = request.GET[b'uuid'] - stash = request.server.stash; - - # The stash is accessed concurrently by many clients. A lock is used to - # avoid unterleaved read/write from different clients. - with stash.lock: - queue = stash.take(uuid, '/common/dispatcher') or []; - - # Push into the |uuid| queue, the requested headers. - if b"show-headers" in request.GET: - headers = {}; - for key, value in request.headers.items(): - headers[isomorphic_decode(key)] = isomorphic_decode(request.headers[key]) - headers = json.dumps(headers); - queue.append(headers); - ret = b''; - - # Push into the |uuid| queue, the posted data. - elif request.method == u'POST': - queue.append(request.body) - ret = b'done' - - # Pull from the |uuid| queue, the posted data. - else: - if len(queue) == 0: - ret = b'not ready' - else: - ret = queue.pop(0) - - stash.put(uuid, queue, '/common/dispatcher') - return ret; diff --git a/test/fixtures/wpt/common/dispatcher/executor-service-worker.js b/test/fixtures/wpt/common/dispatcher/executor-service-worker.js deleted file mode 100644 index 0b47d66b65f..00000000000 --- a/test/fixtures/wpt/common/dispatcher/executor-service-worker.js +++ /dev/null @@ -1,24 +0,0 @@ -importScripts('./dispatcher.js'); - -const params = new URLSearchParams(location.search); -const uuid = params.get('uuid'); - -// The fetch handler must be registered before parsing the main script response. -// So do it here, for future use. -fetchHandler = () => {} -addEventListener('fetch', e => { - fetchHandler(e); -}); - -// Force ServiceWorker to immediately activate itself. -addEventListener('install', event => { - skipWaiting(); -}); - -let executeOrders = async function() { - while(true) { - let task = await receive(uuid); - eval(`(async () => {${task}})()`); - } -}; -executeOrders(); diff --git a/test/fixtures/wpt/common/dispatcher/executor-worker.js b/test/fixtures/wpt/common/dispatcher/executor-worker.js deleted file mode 100644 index ea065a6bf11..00000000000 --- a/test/fixtures/wpt/common/dispatcher/executor-worker.js +++ /dev/null @@ -1,12 +0,0 @@ -importScripts('./dispatcher.js'); - -const params = new URLSearchParams(location.search); -const uuid = params.get('uuid'); - -let executeOrders = async function() { - while(true) { - let task = await receive(uuid); - eval(`(async () => {${task}})()`); - } -}; -executeOrders(); diff --git a/test/fixtures/wpt/common/dispatcher/executor.html b/test/fixtures/wpt/common/dispatcher/executor.html deleted file mode 100644 index 5fe6a95efaf..00000000000 --- a/test/fixtures/wpt/common/dispatcher/executor.html +++ /dev/null @@ -1,15 +0,0 @@ - - diff --git a/test/fixtures/wpt/common/dispatcher/remote-executor-worker.js b/test/fixtures/wpt/common/dispatcher/remote-executor-worker.js deleted file mode 100644 index afba4bae24f..00000000000 --- a/test/fixtures/wpt/common/dispatcher/remote-executor-worker.js +++ /dev/null @@ -1,5 +0,0 @@ -importScripts('./dispatcher.js'); - -const params = new URLSearchParams(location.search); -const uuid = params.get('uuid'); -const executor = new Executor(uuid); // `execute()` is called in constructor. diff --git a/test/fixtures/wpt/common/dispatcher/remote-executor.html b/test/fixtures/wpt/common/dispatcher/remote-executor.html deleted file mode 100644 index f87f566be96..00000000000 --- a/test/fixtures/wpt/common/dispatcher/remote-executor.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/test/fixtures/wpt/common/domain-setter.sub.html b/test/fixtures/wpt/common/domain-setter.sub.html deleted file mode 100644 index ad3b9f8b808..00000000000 --- a/test/fixtures/wpt/common/domain-setter.sub.html +++ /dev/null @@ -1,8 +0,0 @@ - - -A page that will likely be same-origin-domain but not same-origin - - diff --git a/test/fixtures/wpt/common/dummy.json b/test/fixtures/wpt/common/dummy.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/test/fixtures/wpt/common/dummy.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/test/fixtures/wpt/common/dummy.xhtml b/test/fixtures/wpt/common/dummy.xhtml deleted file mode 100644 index dba6945f4ba..00000000000 --- a/test/fixtures/wpt/common/dummy.xhtml +++ /dev/null @@ -1,2 +0,0 @@ - -Dummy XHTML document diff --git a/test/fixtures/wpt/common/dummy.xml b/test/fixtures/wpt/common/dummy.xml deleted file mode 100644 index 4a60c3035fc..00000000000 --- a/test/fixtures/wpt/common/dummy.xml +++ /dev/null @@ -1 +0,0 @@ -Dummy XML document diff --git a/test/fixtures/wpt/common/echo.py b/test/fixtures/wpt/common/echo.py deleted file mode 100644 index 911b54a0b48..00000000000 --- a/test/fixtures/wpt/common/echo.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(request, response): - # Without X-XSS-Protection to disable non-standard XSS protection the functionality this - # resource offers is useless - response.headers.set(b"X-XSS-Protection", b"0") - response.headers.set(b"Content-Type", b"text/html") - response.content = request.GET.first(b"content") diff --git a/test/fixtures/wpt/common/gc.js b/test/fixtures/wpt/common/gc.js deleted file mode 100644 index ac43a4cfaf7..00000000000 --- a/test/fixtures/wpt/common/gc.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Does a best-effort attempt at invoking garbage collection. Attempts to use - * the standardized `TestUtils.gc()` function, but falls back to other - * environment-specific nonstandard functions, with a final result of just - * creating a lot of garbage (in which case you will get a console warning). - * - * This should generally only be used to attempt to trigger bugs and crashes - * inside tests, i.e. cases where if garbage collection happened, then this - * should not trigger some misbehavior. You cannot rely on garbage collection - * successfully trigger, or that any particular unreachable object will be - * collected. - * - * @returns {Promise} A promise you should await to ensure garbage - * collection has had a chance to complete. - */ -self.garbageCollect = async () => { - // https://testutils.spec.whatwg.org/#the-testutils-namespace - if (self.TestUtils?.gc) { - return TestUtils.gc(); - } - - // Use --expose_gc for V8 (and Node.js) - // to pass this flag at chrome launch use: --js-flags="--expose-gc" - // Exposed in SpiderMonkey shell as well - if (self.gc) { - return self.gc(); - } - - // Present in some WebKit development environments - if (self.GCController) { - return GCController.collect(); - } - - console.warn( - 'Tests are running without the ability to do manual garbage collection. ' + - 'They will still work, but coverage will be suboptimal.'); - - for (var i = 0; i < 1000; i++) { - gcRec(10); - } - - function gcRec(n) { - if (n < 1) { - return {}; - } - - let temp = { i: "ab" + i + i / 100000 }; - temp += "foo"; - - gcRec(n - 1); - } -}; diff --git a/test/fixtures/wpt/common/get-host-info.sub.js b/test/fixtures/wpt/common/get-host-info.sub.js deleted file mode 100644 index 9b8c2b5de63..00000000000 --- a/test/fixtures/wpt/common/get-host-info.sub.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Host information for cross-origin tests. - * @returns {Object} with properties for different host information. - */ -function get_host_info() { - - var HTTP_PORT = '{{ports[http][0]}}'; - var HTTP_PORT2 = '{{ports[http][1]}}'; - var HTTPS_PORT = '{{ports[https][0]}}'; - var HTTPS_PORT2 = '{{ports[https][1]}}'; - var PROTOCOL = self.location.protocol; - var IS_HTTPS = (PROTOCOL == "https:"); - var PORT = IS_HTTPS ? HTTPS_PORT : HTTP_PORT; - var PORT2 = IS_HTTPS ? HTTPS_PORT2 : HTTP_PORT2; - var HTTP_PORT_ELIDED = HTTP_PORT == "80" ? "" : (":" + HTTP_PORT); - var HTTP_PORT2_ELIDED = HTTP_PORT2 == "80" ? "" : (":" + HTTP_PORT2); - var HTTPS_PORT_ELIDED = HTTPS_PORT == "443" ? "" : (":" + HTTPS_PORT); - var PORT_ELIDED = IS_HTTPS ? HTTPS_PORT_ELIDED : HTTP_PORT_ELIDED; - var ORIGINAL_HOST = '{{host}}'; - var REMOTE_HOST = (ORIGINAL_HOST === 'localhost') ? '127.0.0.1' : ('www1.' + ORIGINAL_HOST); - var OTHER_HOST = '{{domains[www2]}}'; - var NOTSAMESITE_HOST = (ORIGINAL_HOST === 'localhost') ? '127.0.0.1' : ('{{hosts[alt][]}}'); - - return { - HTTP_PORT: HTTP_PORT, - HTTP_PORT2: HTTP_PORT2, - HTTPS_PORT: HTTPS_PORT, - HTTPS_PORT2: HTTPS_PORT2, - PORT: PORT, - PORT2: PORT2, - ORIGINAL_HOST: ORIGINAL_HOST, - REMOTE_HOST: REMOTE_HOST, - - ORIGIN: PROTOCOL + "//" + ORIGINAL_HOST + PORT_ELIDED, - HTTP_ORIGIN: 'http://' + ORIGINAL_HOST + HTTP_PORT_ELIDED, - HTTPS_ORIGIN: 'https://' + ORIGINAL_HOST + HTTPS_PORT_ELIDED, - HTTPS_ORIGIN_WITH_CREDS: 'https://foo:bar@' + ORIGINAL_HOST + HTTPS_PORT_ELIDED, - HTTP_ORIGIN_WITH_DIFFERENT_PORT: 'http://' + ORIGINAL_HOST + HTTP_PORT2_ELIDED, - REMOTE_ORIGIN: PROTOCOL + "//" + REMOTE_HOST + PORT_ELIDED, - OTHER_ORIGIN: PROTOCOL + "//" + OTHER_HOST + PORT_ELIDED, - HTTP_REMOTE_ORIGIN: 'http://' + REMOTE_HOST + HTTP_PORT_ELIDED, - HTTP_NOTSAMESITE_ORIGIN: 'http://' + NOTSAMESITE_HOST + HTTP_PORT_ELIDED, - HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT: 'http://' + REMOTE_HOST + HTTP_PORT2_ELIDED, - HTTPS_REMOTE_ORIGIN: 'https://' + REMOTE_HOST + HTTPS_PORT_ELIDED, - HTTPS_REMOTE_ORIGIN_WITH_CREDS: 'https://foo:bar@' + REMOTE_HOST + HTTPS_PORT_ELIDED, - HTTPS_NOTSAMESITE_ORIGIN: 'https://' + NOTSAMESITE_HOST + HTTPS_PORT_ELIDED, - UNAUTHENTICATED_ORIGIN: 'http://' + OTHER_HOST + HTTP_PORT_ELIDED, - AUTHENTICATED_ORIGIN: 'https://' + OTHER_HOST + HTTPS_PORT_ELIDED - }; -} - -/** - * When a default port is used, location.port returns the empty string. - * This function attempts to provide an exact port, assuming we are running under wptserve. - * @param {*} loc - can be Location///URL, but assumes http/https only. - * @returns {string} The port number. - */ -function get_port(loc) { - if (loc.port) { - return loc.port; - } - return loc.protocol === 'https:' ? '443' : '80'; -} diff --git a/test/fixtures/wpt/common/get-host-info.sub.js.headers b/test/fixtures/wpt/common/get-host-info.sub.js.headers deleted file mode 100644 index 6805c323df5..00000000000 --- a/test/fixtures/wpt/common/get-host-info.sub.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/javascript; charset=utf-8 diff --git a/test/fixtures/wpt/common/media.js b/test/fixtures/wpt/common/media.js deleted file mode 100644 index a5a8e957e9b..00000000000 --- a/test/fixtures/wpt/common/media.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Returns the URL of a supported video source based on the user agent - * @param {string} base - media URL without file extension - * @returns {string} - */ -function getVideoURI(base) -{ - var extension = '.mp4'; - - var videotag = document.createElement("video"); - - if ( videotag.canPlayType ) - { - if (videotag.canPlayType('video/webm; codecs="vp9, opus"') ) - { - extension = '.webm'; - } - } - - return base + extension; -} - -/** - * Returns the URL of a supported audio source based on the user agent - * @param {string} base - media URL without file extension - * @returns {string} - */ -function getAudioURI(base) -{ - var extension = '.mp3'; - - var audiotag = document.createElement("audio"); - - if ( audiotag.canPlayType && - audiotag.canPlayType('audio/ogg') ) - { - extension = '.oga'; - } - - return base + extension; -} - -/** - * Returns the MIME type for a media URL based on the file extension. - * @param {string} url - * @returns {string} - */ -function getMediaContentType(url) { - var extension = new URL(url, location).pathname.split(".").pop(); - var map = { - "mp4" : "video/mp4", - "webm": "video/webm", - "mp3" : "audio/mp3", - "oga" : "application/ogg", - }; - return map[extension]; -} diff --git a/test/fixtures/wpt/common/media.js.headers b/test/fixtures/wpt/common/media.js.headers deleted file mode 100644 index 6805c323df5..00000000000 --- a/test/fixtures/wpt/common/media.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/javascript; charset=utf-8 diff --git a/test/fixtures/wpt/common/object-association.js b/test/fixtures/wpt/common/object-association.js deleted file mode 100644 index 669c17c07b1..00000000000 --- a/test/fixtures/wpt/common/object-association.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; - -// This is for testing whether an object (e.g., a global property) is associated with Window, or -// with Document. Recall that Window and Document are 1:1 except when doing a same-origin navigation -// away from the initial about:blank. In that case the Window object gets reused for the new -// Document. -// -// So: -// - If something is per-Window, then it should maintain its identity across an about:blank -// navigation. -// - If something is per-Document, then it should be recreated across an about:blank navigation. - -window.testIsPerWindow = propertyName => { - runTests(propertyName, assert_equals, "must not"); -}; - -window.testIsPerDocument = propertyName => { - runTests(propertyName, assert_not_equals, "must"); -}; - -function runTests(propertyName, equalityOrInequalityAsserter, mustOrMustNotReplace) { - async_test(t => { - const iframe = document.createElement("iframe"); - document.body.appendChild(iframe); - const frame = iframe.contentWindow; - - const before = frame[propertyName]; - assert_implements(before, `window.${propertyName} must be implemented`); - - iframe.onload = t.step_func_done(() => { - const after = frame[propertyName]; - equalityOrInequalityAsserter(after, before); - }); - - iframe.src = "/common/blank.html"; - }, `Navigating from the initial about:blank ${mustOrMustNotReplace} replace window.${propertyName}`); - - // Per spec, discarding a browsing context should not change any of the global objects. - test(() => { - const iframe = document.createElement("iframe"); - document.body.appendChild(iframe); - const frame = iframe.contentWindow; - - const before = frame[propertyName]; - assert_implements(before, `window.${propertyName} must be implemented`); - - iframe.remove(); - - const after = frame[propertyName]; - assert_equals(after, before, `window.${propertyName} should not change after iframe.remove()`); - }, `Discarding the browsing context must not change window.${propertyName}`); - - // Per spec, document.open() should not change any of the global objects. In historical versions - // of the spec, it did, so we test here. - async_test(t => { - const iframe = document.createElement("iframe"); - - iframe.onload = t.step_func_done(() => { - const frame = iframe.contentWindow; - const before = frame[propertyName]; - assert_implements(before, `window.${propertyName} must be implemented`); - - frame.document.open(); - - const after = frame[propertyName]; - assert_equals(after, before); - - frame.document.close(); - }); - - iframe.src = "/common/blank.html"; - document.body.appendChild(iframe); - }, `document.open() must not replace window.${propertyName}`); -} diff --git a/test/fixtures/wpt/common/object-association.js.headers b/test/fixtures/wpt/common/object-association.js.headers deleted file mode 100644 index 6805c323df5..00000000000 --- a/test/fixtures/wpt/common/object-association.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/javascript; charset=utf-8 diff --git a/test/fixtures/wpt/common/performance-timeline-utils.js b/test/fixtures/wpt/common/performance-timeline-utils.js deleted file mode 100644 index b20241cc610..00000000000 --- a/test/fixtures/wpt/common/performance-timeline-utils.js +++ /dev/null @@ -1,56 +0,0 @@ -/* -author: W3C http://www.w3.org/ -help: http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute -*/ -var performanceNamespace = window.performance; -var namespace_check = false; -function wp_test(func, msg, properties) -{ - // only run the namespace check once - if (!namespace_check) - { - namespace_check = true; - - if (performanceNamespace === undefined || performanceNamespace == null) - { - // show a single error that window.performance is undefined - // The window.performance attribute provides a hosting area for performance related attributes. - test(function() { assert_true(performanceNamespace !== undefined && performanceNamespace != null, "window.performance is defined and not null"); }, "window.performance is defined and not null."); - } - } - - test(func, msg, properties); -} - -function test_true(value, msg, properties) -{ - wp_test(function () { assert_true(value, msg); }, msg, properties); -} - -function test_equals(value, equals, msg, properties) -{ - wp_test(function () { assert_equals(value, equals, msg); }, msg, properties); -} - -// assert for every entry in `expectedEntries`, there is a matching entry _somewhere_ in `actualEntries` -function test_entries(actualEntries, expectedEntries) { - test_equals(actualEntries.length, expectedEntries.length) - expectedEntries.forEach(function (expectedEntry) { - var foundEntry = actualEntries.find(function (actualEntry) { - return typeof Object.keys(expectedEntry).find(function (key) { - return actualEntry[key] !== expectedEntry[key] - }) === 'undefined' - }) - test_true(!!foundEntry, `Entry ${JSON.stringify(expectedEntry)} could not be found.`) - if (foundEntry) { - assert_object_equals(foundEntry.toJSON(), expectedEntry) - } - }) -} - -function delayedLoadListener(callback) { - window.addEventListener('load', function() { - // TODO(cvazac) Remove this setTimeout when spec enforces sync entries. - step_timeout(callback, 0) - }) -} diff --git a/test/fixtures/wpt/common/performance-timeline-utils.js.headers b/test/fixtures/wpt/common/performance-timeline-utils.js.headers deleted file mode 100644 index 6805c323df5..00000000000 --- a/test/fixtures/wpt/common/performance-timeline-utils.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/javascript; charset=utf-8 diff --git a/test/fixtures/wpt/common/proxy-all.sub.pac b/test/fixtures/wpt/common/proxy-all.sub.pac deleted file mode 100644 index de601e5d702..00000000000 --- a/test/fixtures/wpt/common/proxy-all.sub.pac +++ /dev/null @@ -1,3 +0,0 @@ -function FindProxyForURL(url, host) { - return "PROXY {{host}}:{{ports[http][0]}}" -} diff --git a/test/fixtures/wpt/common/redirect-opt-in.py b/test/fixtures/wpt/common/redirect-opt-in.py deleted file mode 100644 index b5e674a27fb..00000000000 --- a/test/fixtures/wpt/common/redirect-opt-in.py +++ /dev/null @@ -1,20 +0,0 @@ -def main(request, response): - """Simple handler that causes redirection. - - The request should typically have two query parameters: - status - The status to use for the redirection. Defaults to 302. - location - The resource to redirect to. - """ - status = 302 - if b"status" in request.GET: - try: - status = int(request.GET.first(b"status")) - except ValueError: - pass - - response.status = status - - location = request.GET.first(b"location") - - response.headers.set(b"Location", location) - response.headers.set(b"Timing-Allow-Origin", b"*") diff --git a/test/fixtures/wpt/common/redirect.py b/test/fixtures/wpt/common/redirect.py deleted file mode 100644 index 1ddde9a579b..00000000000 --- a/test/fixtures/wpt/common/redirect.py +++ /dev/null @@ -1,29 +0,0 @@ -def main(request, response): - """Simple handler that causes redirection. - - The request should typically have two query parameters: - status - The status to use for the redirection. Defaults to 302. - location - The resource to redirect to. - - This utility optionally supports CORS (iff the `enable-cors` query param is - present). - """ - status = 302 - if b"status" in request.GET: - try: - status = int(request.GET.first(b"status")) - except ValueError: - pass - - response.status = status - - location = request.GET.first(b"location") - - response.headers.set(b"Location", location) - - if request.GET.get(b"enable-cors") is not None: - origin = request.headers.get(b"Origin") - if origin: - response.headers.set(b"Content-Type", b"text/plain") - response.headers.set(b"Access-Control-Allow-Origin", origin) - response.headers.set(b"Access-Control-Allow-Credentials", 'true') diff --git a/test/fixtures/wpt/common/refresh.py b/test/fixtures/wpt/common/refresh.py deleted file mode 100644 index 0d309900d47..00000000000 --- a/test/fixtures/wpt/common/refresh.py +++ /dev/null @@ -1,11 +0,0 @@ -def main(request, response): - """ - Respond with a blank HTML document and a `Refresh` header which describes - an immediate redirect to the URL specified by the requests `location` query - string parameter - """ - headers = [ - (b'Content-Type', b'text/html'), - (b'Refresh', b'0; URL=' + request.GET.first(b'location')) - ] - return (200, headers, b'') diff --git a/test/fixtures/wpt/common/reftest-wait.js b/test/fixtures/wpt/common/reftest-wait.js deleted file mode 100644 index 64fe9bfd7f5..00000000000 --- a/test/fixtures/wpt/common/reftest-wait.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Remove the `reftest-wait` class on the document element. - * The reftest runner will wait with taking a screenshot while - * this class is present. - * - * See https://web-platform-tests.org/writing-tests/reftests.html#controlling-when-comparison-occurs - */ -function takeScreenshot() { - document.documentElement.classList.remove("reftest-wait"); -} - -/** - * Call `takeScreenshot()` after a delay of at least |timeout| milliseconds. - * @param {number} timeout - milliseconds - */ -function takeScreenshotDelayed(timeout) { - setTimeout(function() { - takeScreenshot(); - }, timeout); -} - -/** - * Ensure that a precondition is met before waiting for a screenshot. - * @param {bool} condition - Fail the test if this evaluates to false - * @param {string} msg - Error message to write to the screenshot - */ -function failIfNot(condition, msg) { - const fail = () => { - (document.body || document.documentElement).textContent = `Precondition Failed: ${msg}`; - takeScreenshot(); - }; - if (!condition) { - if (document.readyState == "interactive") { - fail(); - } else { - document.addEventListener("DOMContentLoaded", fail, false); - } - } -} diff --git a/test/fixtures/wpt/common/reftest-wait.js.headers b/test/fixtures/wpt/common/reftest-wait.js.headers deleted file mode 100644 index 6805c323df5..00000000000 --- a/test/fixtures/wpt/common/reftest-wait.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/javascript; charset=utf-8 diff --git a/test/fixtures/wpt/common/rendering-utils.js b/test/fixtures/wpt/common/rendering-utils.js deleted file mode 100644 index 46283bd5d07..00000000000 --- a/test/fixtures/wpt/common/rendering-utils.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; - -/** - * Waits until we have at least one frame rendered, regardless of the engine. - * - * @returns {Promise} - */ -function waitForAtLeastOneFrame() { - return new Promise(resolve => { - // Different web engines work slightly different on this area but waiting - // for two requestAnimationFrames() to happen, one after another, should be - // sufficient to ensure at least one frame has been generated anywhere. - window.requestAnimationFrame(() => { - window.requestAnimationFrame(() => { - resolve(); - }); - }); - }); -} diff --git a/test/fixtures/wpt/common/sab.js b/test/fixtures/wpt/common/sab.js deleted file mode 100644 index a3ea610e165..00000000000 --- a/test/fixtures/wpt/common/sab.js +++ /dev/null @@ -1,21 +0,0 @@ -const createBuffer = (() => { - // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` - let sabConstructor; - try { - sabConstructor = new WebAssembly.Memory({ shared:true, initial:0, maximum:0 }).buffer.constructor; - } catch(e) { - sabConstructor = null; - } - return (type, length, opts) => { - if (type === "ArrayBuffer") { - return new ArrayBuffer(length, opts); - } else if (type === "SharedArrayBuffer") { - if (sabConstructor && sabConstructor.name !== "SharedArrayBuffer") { - throw new Error("WebAssembly.Memory does not support shared:true"); - } - return new sabConstructor(length, opts); - } else { - throw new Error("type has to be ArrayBuffer or SharedArrayBuffer"); - } - } -})(); diff --git a/test/fixtures/wpt/common/security-features/README.md b/test/fixtures/wpt/common/security-features/README.md deleted file mode 100644 index f957541f75e..00000000000 --- a/test/fixtures/wpt/common/security-features/README.md +++ /dev/null @@ -1,460 +0,0 @@ -This directory contains the common infrastructure for the following tests (also referred below as projects). - -- referrer-policy/ -- mixed-content/ -- upgrade-insecure-requests/ - -Subdirectories: - -- `resources`: - Serves JavaScript test helpers. -- `subresource`: - Serves subresources, with support for redirects, stash, etc. - The subresource paths are managed by `subresourceMap` and - fetched in `requestVia*()` functions in `resources/common.js`. -- `scope`: - Serves nested contexts, such as iframe documents or workers. - Used from `invokeFrom*()` functions in `resources/common.js`. -- `tools`: - Scripts that generate test HTML files. Not used while running tests. -- `/referrer-policy/generic/subresource-test`: - Sanity checking tests for subresource invocation - (This is still placed outside common/) - -# Test generator - -The test generator ([common/security-features/tools/generate.py](tools/generate.py)) generates test HTML files from templates and a seed (`spec.src.json`) that defines all the test scenarios. - -The project (i.e. a WPT subdirectory, for example `referrer-policy/`) that uses the generator should define per-project data and invoke the common generator logic in `common/security-features/tools`. - -This is the overview of the project structure: - -``` -common/security-features/ -└── tools/ - the common test generator logic - ├── spec.src.json - └── template/ - the test files templates -project-directory/ (e.g. referrer-policy/) -├── spec.src.json -├── generic/ -│ ├── test-case.sub.js - Per-project test helper -│ ├── sanity-checker.js (Used by debug target only) -│ └── spec_json.js (Used by debug target only) -└── gen/ - generated tests -``` - -## Generating the tests - -Note: When the repository already contains generated tests, [remove all generated tests](#removing-all-generated-tests) first. - -```bash -# Install json5 module if needed. -pip install --user json5 - -# Generate the test files under gen/ (HTMLs and .headers files). -path/to/common/security-features/tools/generate.py --spec path/to/project-directory/ - -# Add all generated tests to the repo. -git add path/to/project-directory/gen/ && git commit -m "Add generated tests" -``` - -This will parse the spec JSON5 files and determine which tests to generate (or skip) while using templates. - -- The default spec JSON5: `common/security-features/tools/spec.src.json`. - - Describes common configurations, such as subresource types, source context types, etc. -- The per-project spec JSON5: `project-directory/spec.src.json`. - - Describes project-specific configurations, particularly those related to test generation patterns (`specification`), policy deliveries (e.g. `delivery_type`, `delivery_value`) and `expectation`. - -For how these two spec JSON5 files are merged, see [Sub projects](#sub-projects) section. - -Note: `spec.src.json` is transitioning to JSON5 [#21710](https://github.com/web-platform-tests/wpt/issues/21710). - -During the generation, the spec is validated by ```common/security-features/tools/spec_validator.py```. This is specially important when you're making changes to `spec.src.json`. Make sure it's a valid JSON (no comments or trailing commas). The validator reports specific errors (missing keys etc.), if any. - -### Removing all generated tests - -Simply remove all files under `project-directory/gen/`. - -```bash -rm -r path/to/project-directory/gen/ -``` - -### Options for generating tests - -Note: this section is currently obsolete. Only the release template is working. - -The generator script has two targets: ```release``` and ```debug```. - -* Using **release** for the target will produce tests using a template for optimizing size and performance. The release template is intended for the official web-platform-tests and possibly other test suites. No sanity checking is done in release mode. Use this option whenever you're checking into web-platform-tests. - -* When generating for ```debug```, the produced tests will contain more verbosity and sanity checks. Use this target to identify problems with the test suites when making changes locally. Make sure you don't check in tests generated with the debug target. - -Note that **release** is the default target when invoking ```generate.py```. - - -## Sub projects - -Projects can be nested, for example to reuse a single `spec.src.json` across similar but slightly different sets of generated tests. -The directory structure would look like: - -``` -project-directory/ (e.g. referrer-policy/) -├── spec.src.json - Parent project's spec JSON -├── generic/ -│ └── test-case.sub.js - Parent project's test helper -├── gen/ - parent project's generated tests -└── sub-project-directory/ (e.g. 4K) - ├── spec.src.json - Child project's spec JSON - ├── generic/ - │ └── test-case.sub.js - Child project's test helper - └── gen/ - child project's generated tests -``` - -`generate.py --spec project-directory/sub-project-directory` generates test files under `project-directory/sub-project-directory/gen`, based on `project-directory/spec.src.json` and `project-directory/sub-project-directory/spec.src.json`. - -- The child project's `spec.src.json` is merged into parent project's `spec.src.json`. - - Two spec JSON objects are merged recursively. - - If a same key exists in both objects, the child's value overwrites the parent's value. - - If both (child's and parent's) values are arrays, then the child's value is concatenated to the parent's value. - - For debugging, `generate.py` dumps the merged spec JSON object as `generic/debug-output.spec.src.json`. -- The child project's generated tests include both of the parent and child project's `test-case.sub.js`: - ```html - - - - ``` - - -## Updating the tests - -The main test logic lives in ```project-directory/generic/test-case.sub.js``` with helper functions defined in ```/common/security-features/resources/common.js``` so you should probably start there. - -For updating the test suites you will most likely do **a subset** of the following: - -* Add a new subresource type: - - * Add a new sub-resource python script to `/common/security-features/subresource/`. - * Add a sanity check test for a sub-resource to `referrer-policy/generic/subresource-test/`. - * Add a new entry to `subresourceMap` in `/common/security-features/resources/common.js`. - * Add a new entry to `valid_subresource_names` in `/common/security-features/tools/spec_validator.py`. - * Add a new entry to `subresource_schema` in `spec.src.json`. - * Update `source_context_schema` to specify in which source context the subresource can be used. - -* Add a new subresource redirection type - - * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18939](https://github.com/web-platform-tests/wpt/pull/18939) - -* Add a new subresource origin type - - * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18940](https://github.com/web-platform-tests/wpt/pull/18940) - -* Add a new source context (e.g. "module sharedworker global scope") - - * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18904](https://github.com/web-platform-tests/wpt/pull/18904) - -* Add a new source context list (e.g. "subresource request from a dedicated worker in a ` - invoker: invokeFromIframe, - }, - "iframe": { // - invoker: invokeFromIframe, - }, - "iframe-blank": { // - invoker: invokeFromIframe, - }, - "worker-classic": { - // Classic dedicated worker loaded from same-origin. - invoker: invokeFromWorker.bind(undefined, "worker", false, {}), - }, - "worker-classic-data": { - // Classic dedicated worker loaded from data: URL. - invoker: invokeFromWorker.bind(undefined, "worker", true, {}), - }, - "worker-module": { - // Module dedicated worker loaded from same-origin. - invoker: invokeFromWorker.bind(undefined, "worker", false, {type: 'module'}), - }, - "worker-module-data": { - // Module dedicated worker loaded from data: URL. - invoker: invokeFromWorker.bind(undefined, "worker", true, {type: 'module'}), - }, - "sharedworker-classic": { - // Classic shared worker loaded from same-origin. - invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {}), - }, - "sharedworker-classic-data": { - // Classic shared worker loaded from data: URL. - invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {}), - }, - "sharedworker-module": { - // Module shared worker loaded from same-origin. - invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {type: 'module'}), - }, - "sharedworker-module-data": { - // Module shared worker loaded from data: URL. - invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {type: 'module'}), - }, - }; - - return sourceContextMap[sourceContextList[0].sourceContextType].invoker( - subresource, sourceContextList); -} - -// Quick hack to expose invokeRequest when common.sub.js is loaded either -// as a classic or module script. -self.invokeRequest = invokeRequest; - -/** - invokeFrom*() functions are helper functions with the same parameters - and return values as invokeRequest(), that are tied to specific types - of top-most environment settings objects. - For example, invokeFromIframe() is the helper function for the cases where - sourceContextList[0] is an iframe. -*/ - -/** - @param {string} workerType - "worker" (for dedicated worker) or "sharedworker". - @param {boolean} isDataUrl - true if the worker script is loaded from data: URL. - Otherwise, the script is loaded from same-origin. - @param {object} workerOptions - The `options` argument for Worker constructor. - - Other parameters and return values are the same as those of invokeRequest(). -*/ -function invokeFromWorker(workerType, isDataUrl, workerOptions, - subresource, sourceContextList) { - const currentSourceContext = sourceContextList[0]; - let workerUrl = - "/common/security-features/scope/worker.py?policyDeliveries=" + - encodeURIComponent(JSON.stringify( - currentSourceContext.policyDeliveries || [])); - if (workerOptions.type === 'module') { - workerUrl += "&type=module"; - } - - let promise; - if (isDataUrl) { - promise = fetch(workerUrl) - .then(r => r.text()) - .then(source => { - return 'data:text/javascript;base64,' + btoa(source); - }); - } else { - promise = Promise.resolve(workerUrl); - } - - return promise - .then(url => { - if (workerType === "worker") { - const worker = new Worker(url, workerOptions); - worker.postMessage({subresource: subresource, - sourceContextList: sourceContextList.slice(1)}); - return bindEvents2(worker, "message", worker, "error", window, "error"); - } else if (workerType === "sharedworker") { - const worker = new SharedWorker(url, workerOptions); - worker.port.start(); - worker.port.postMessage({subresource: subresource, - sourceContextList: sourceContextList.slice(1)}); - return bindEvents2(worker.port, "message", worker, "error", window, "error"); - } else { - throw new Error('Invalid worker type: ' + workerType); - } - }) - .then(event => { - if (event.data.error) - return Promise.reject(event.data.error); - return event.data; - }); -} - -function invokeFromIframe(subresource, sourceContextList) { - const currentSourceContext = sourceContextList[0]; - const frameUrl = - "/common/security-features/scope/document.py?policyDeliveries=" + - encodeURIComponent(JSON.stringify( - currentSourceContext.policyDeliveries || [])); - - let iframe; - let promise; - if (currentSourceContext.sourceContextType === 'srcdoc') { - promise = fetch(frameUrl) - .then(r => r.text()) - .then(srcdoc => { - iframe = createElement( - "iframe", {srcdoc: srcdoc}, document.body, true); - return iframe.eventPromise; - }); - } else if (currentSourceContext.sourceContextType === 'iframe') { - iframe = createElement("iframe", {src: frameUrl}, document.body, true); - promise = iframe.eventPromise; - } else if (currentSourceContext.sourceContextType === 'iframe-blank') { - let frameContent; - promise = fetch(frameUrl) - .then(r => r.text()) - .then(t => { - frameContent = t; - iframe = createElement("iframe", {}, document.body, true); - return iframe.eventPromise; - }) - .then(() => { - // Reinitialize `iframe.eventPromise` with a new promise - // that catches the load event for the document.write() below. - bindEvents(iframe); - - iframe.contentDocument.write(frameContent); - iframe.contentDocument.close(); - return iframe.eventPromise; - }); - } - - return promise - .then(() => { - const promise = bindEvents2( - window, "message", iframe, "error", window, "error"); - iframe.contentWindow.postMessage( - {subresource: subresource, - sourceContextList: sourceContextList.slice(1)}, - "*"); - return promise; - }) - .then(event => { - if (event.data.error) - return Promise.reject(event.data.error); - return event.data; - }); -} - -// SanityChecker does nothing in release mode. See sanity-checker.js for debug -// mode. -function SanityChecker() {} -SanityChecker.prototype.checkScenario = function() {}; -SanityChecker.prototype.setFailTimeout = function(test, timeout) {}; -SanityChecker.prototype.checkSubresourceResult = function() {}; diff --git a/test/fixtures/wpt/common/security-features/resources/common.sub.js.headers b/test/fixtures/wpt/common/security-features/resources/common.sub.js.headers deleted file mode 100644 index cb762eff806..00000000000 --- a/test/fixtures/wpt/common/security-features/resources/common.sub.js.headers +++ /dev/null @@ -1 +0,0 @@ -Access-Control-Allow-Origin: * diff --git a/test/fixtures/wpt/common/security-features/scope/__init__.py b/test/fixtures/wpt/common/security-features/scope/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/common/security-features/scope/document.py b/test/fixtures/wpt/common/security-features/scope/document.py deleted file mode 100644 index 9a9f045e640..00000000000 --- a/test/fixtures/wpt/common/security-features/scope/document.py +++ /dev/null @@ -1,36 +0,0 @@ -import os, sys, json - -from wptserve.utils import isomorphic_decode, isomorphic_encode - -import importlib -util = importlib.import_module("common.security-features.scope.util") - -def main(request, response): - policyDeliveries = json.loads(request.GET.first(b"policyDeliveries", b"[]")) - maybe_additional_headers = {} - meta = u'' - error = u'' - for delivery in policyDeliveries: - if delivery[u'deliveryType'] == u'meta': - if delivery[u'key'] == u'referrerPolicy': - meta += u'' % delivery[u'value'] - else: - error = u'invalid delivery key' - elif delivery[u'deliveryType'] == u'http-rp': - if delivery[u'key'] == u'referrerPolicy': - maybe_additional_headers[b'Referrer-Policy'] = isomorphic_encode(delivery[u'value']) - else: - error = u'invalid delivery key' - else: - error = u'invalid deliveryType' - - handler = lambda: util.get_template(u"document.html.template") % ({ - u"meta": meta, - u"error": error - }) - util.respond( - request, - response, - payload_generator=handler, - content_type=b"text/html", - maybe_additional_headers=maybe_additional_headers) diff --git a/test/fixtures/wpt/common/security-features/scope/template/document.html.template b/test/fixtures/wpt/common/security-features/scope/template/document.html.template deleted file mode 100644 index 37e29f8e97f..00000000000 --- a/test/fixtures/wpt/common/security-features/scope/template/document.html.template +++ /dev/null @@ -1,30 +0,0 @@ - - - - %(meta)s - - - - diff --git a/test/fixtures/wpt/common/security-features/scope/template/worker.js.template b/test/fixtures/wpt/common/security-features/scope/template/worker.js.template deleted file mode 100644 index 7a2a6e05c44..00000000000 --- a/test/fixtures/wpt/common/security-features/scope/template/worker.js.template +++ /dev/null @@ -1,29 +0,0 @@ -%(import)s - -if ('DedicatedWorkerGlobalScope' in self && - self instanceof DedicatedWorkerGlobalScope) { - self.onmessage = event => onMessageFromParent(event, self); -} else if ('SharedWorkerGlobalScope' in self && - self instanceof SharedWorkerGlobalScope) { - onconnect = event => { - const port = event.ports[0]; - port.onmessage = event => onMessageFromParent(event, port); - }; -} - -// Receive a message from the parent and start the test. -function onMessageFromParent(event, port) { - const configurationError = "%(error)s"; - if (configurationError.length > 0) { - port.postMessage({error: configurationError}); - return; - } - - invokeRequest(event.data.subresource, - event.data.sourceContextList) - .then(result => port.postMessage(result)) - .catch(e => { - const message = (e.error && e.error.stack) || e.message || "Error"; - port.postMessage({error: message}); - }); -} diff --git a/test/fixtures/wpt/common/security-features/scope/util.py b/test/fixtures/wpt/common/security-features/scope/util.py deleted file mode 100644 index da5aacf35e9..00000000000 --- a/test/fixtures/wpt/common/security-features/scope/util.py +++ /dev/null @@ -1,43 +0,0 @@ -import os - -from wptserve.utils import isomorphic_decode - -def get_template(template_basename): - script_directory = os.path.dirname(os.path.abspath(isomorphic_decode(__file__))) - template_directory = os.path.abspath( - os.path.join(script_directory, u"template")) - template_filename = os.path.join(template_directory, template_basename) - - with open(template_filename, "r") as f: - return f.read() - - -def __noop(request, response): - return u"" - - -def respond(request, - response, - status_code=200, - content_type=b"text/html", - payload_generator=__noop, - cache_control=b"no-cache; must-revalidate", - access_control_allow_origin=b"*", - maybe_additional_headers=None): - response.add_required_headers = False - response.writer.write_status(status_code) - - if access_control_allow_origin != None: - response.writer.write_header(b"access-control-allow-origin", - access_control_allow_origin) - response.writer.write_header(b"content-type", content_type) - response.writer.write_header(b"cache-control", cache_control) - - additional_headers = maybe_additional_headers or {} - for header, value in additional_headers.items(): - response.writer.write_header(header, value) - - response.writer.end_headers() - - payload = payload_generator() - response.writer.write(payload) diff --git a/test/fixtures/wpt/common/security-features/scope/worker.py b/test/fixtures/wpt/common/security-features/scope/worker.py deleted file mode 100644 index 6b321e7de17..00000000000 --- a/test/fixtures/wpt/common/security-features/scope/worker.py +++ /dev/null @@ -1,44 +0,0 @@ -import os, sys, json - -from wptserve.utils import isomorphic_decode, isomorphic_encode -import importlib -util = importlib.import_module("common.security-features.scope.util") - -def main(request, response): - policyDeliveries = json.loads(request.GET.first(b'policyDeliveries', b'[]')) - worker_type = request.GET.first(b'type', b'classic') - commonjs_url = u'%s://%s:%s/common/security-features/resources/common.sub.js' % ( - request.url_parts.scheme, request.url_parts.hostname, - request.url_parts.port) - if worker_type == b'classic': - import_line = u'importScripts("%s");' % commonjs_url - else: - import_line = u'import "%s";' % commonjs_url - - maybe_additional_headers = {} - error = u'' - for delivery in policyDeliveries: - if delivery[u'deliveryType'] == u'meta': - error = u' cannot be used in WorkerGlobalScope' - elif delivery[u'deliveryType'] == u'http-rp': - if delivery[u'key'] == u'referrerPolicy': - maybe_additional_headers[b'Referrer-Policy'] = isomorphic_encode(delivery[u'value']) - elif delivery[u'key'] == u'mixedContent' and delivery[u'value'] == u'opt-in': - maybe_additional_headers[b'Content-Security-Policy'] = b'block-all-mixed-content' - elif delivery[u'key'] == u'upgradeInsecureRequests' and delivery[u'value'] == u'upgrade': - maybe_additional_headers[b'Content-Security-Policy'] = b'upgrade-insecure-requests' - else: - error = u'invalid delivery key for http-rp: %s' % delivery[u'key'] - else: - error = u'invalid deliveryType: %s' % delivery[u'deliveryType'] - - handler = lambda: util.get_template(u'worker.js.template') % ({ - u'import': import_line, - u'error': error - }) - util.respond( - request, - response, - payload_generator=handler, - content_type=b'text/javascript', - maybe_additional_headers=maybe_additional_headers) diff --git a/test/fixtures/wpt/common/security-features/subresource/__init__.py b/test/fixtures/wpt/common/security-features/subresource/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/common/security-features/subresource/audio.py b/test/fixtures/wpt/common/security-features/subresource/audio.py deleted file mode 100644 index f16a0f7fbbe..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/audio.py +++ /dev/null @@ -1,18 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(request, server_data): - file = os.path.join(request.doc_root, u"webaudio", u"resources", - u"sin_440Hz_-6dBFS_1s.wav") - return open(file, "rb").read() - - -def main(request, response): - handler = lambda data: generate_payload(request, data) - subresource.respond(request, - response, - payload_generator = handler, - access_control_allow_origin = b"*", - content_type = b"audio/wav") diff --git a/test/fixtures/wpt/common/security-features/subresource/document.py b/test/fixtures/wpt/common/security-features/subresource/document.py deleted file mode 100644 index 52b684a4d9f..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/document.py +++ /dev/null @@ -1,12 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(server_data): - return subresource.get_template(u"document.html.template") % server_data - -def main(request, response): - subresource.respond(request, - response, - payload_generator = generate_payload) diff --git a/test/fixtures/wpt/common/security-features/subresource/empty.py b/test/fixtures/wpt/common/security-features/subresource/empty.py deleted file mode 100644 index 312e12cbed9..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/empty.py +++ /dev/null @@ -1,14 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(server_data): - return u'' - -def main(request, response): - subresource.respond(request, - response, - payload_generator = generate_payload, - access_control_allow_origin = b"*", - content_type = b"text/plain") diff --git a/test/fixtures/wpt/common/security-features/subresource/font.py b/test/fixtures/wpt/common/security-features/subresource/font.py deleted file mode 100644 index 7900079cdf3..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/font.py +++ /dev/null @@ -1,76 +0,0 @@ -import os, sys -from base64 import decodebytes - -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - - -def generate_payload(request, server_data): - data = (u'{"headers": %(headers)s}') % server_data - if b"id" in request.GET: - request.server.stash.put(request.GET[b"id"], data) - # Simple base64 encoded .tff font - return decodebytes(b"AAEAAAANAIAAAwBQRkZUTU6u6MkAAAXcAAAAHE9TLzJWYW" - b"QKAAABWAAAAFZjbWFwAA8D7wAAAcAAAAFCY3Z0IAAhAnkA" - b"AAMEAAAABGdhc3D//wADAAAF1AAAAAhnbHlmCC6aTwAAAx" - b"QAAACMaGVhZO8ooBcAAADcAAAANmhoZWEIkAV9AAABFAAA" - b"ACRobXR4EZQAhQAAAbAAAAAQbG9jYQBwAFQAAAMIAAAACm" - b"1heHAASQA9AAABOAAAACBuYW1lehAVOgAAA6AAAAIHcG9z" - b"dP+uADUAAAWoAAAAKgABAAAAAQAAMhPyuV8PPPUACwPoAA" - b"AAAMU4Lm0AAAAAxTgubQAh/5wFeAK8AAAACAACAAAAAAAA" - b"AAEAAAK8/5wAWgXcAAAAAAV4AAEAAAAAAAAAAAAAAAAAAA" - b"AEAAEAAAAEAAwAAwAAAAAAAgAAAAEAAQAAAEAALgAAAAAA" - b"AQXcAfQABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAIABg" - b"kAAAAAAAAAAAABAAAAAAAAAAAAAAAAUGZFZABAAEEAQQMg" - b"/zgAWgK8AGQAAAABAAAAAAAABdwAIQAAAAAF3AAABdwAZA" - b"AAAAMAAAADAAAAHAABAAAAAAA8AAMAAQAAABwABAAgAAAA" - b"BAAEAAEAAABB//8AAABB////wgABAAAAAAAAAQYAAAEAAA" - b"AAAAAAAQIAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - b"AAAAAAAAAAAAAAAAAAAhAnkAAAAqACoAKgBGAAAAAgAhAA" - b"ABKgKaAAMABwAusQEALzyyBwQA7TKxBgXcPLIDAgDtMgCx" - b"AwAvPLIFBADtMrIHBgH8PLIBAgDtMjMRIREnMxEjIQEJ6M" - b"fHApr9ZiECWAAAAwBk/5wFeAK8AAMABwALAAABNSEVATUh" - b"FQE1IRUB9AH0/UQDhPu0BRQB9MjI/tTIyP7UyMgAAAAAAA" - b"4ArgABAAAAAAAAACYATgABAAAAAAABAAUAgQABAAAAAAAC" - b"AAYAlQABAAAAAAADACEA4AABAAAAAAAEAAUBDgABAAAAAA" - b"AFABABNgABAAAAAAAGAAUBUwADAAEECQAAAEwAAAADAAEE" - b"CQABAAoAdQADAAEECQACAAwAhwADAAEECQADAEIAnAADAA" - b"EECQAEAAoBAgADAAEECQAFACABFAADAAEECQAGAAoBRwBD" - b"AG8AcAB5AHIAaQBnAGgAdAAgACgAYwApACAAMgAwADAAOA" - b"AgAE0AbwB6AGkAbABsAGEAIABDAG8AcgBwAG8AcgBhAHQA" - b"aQBvAG4AAENvcHlyaWdodCAoYykgMjAwOCBNb3ppbGxhIE" - b"NvcnBvcmF0aW9uAABNAGEAcgBrAEEAAE1hcmtBAABNAGUA" - b"ZABpAHUAbQAATWVkaXVtAABGAG8AbgB0AEYAbwByAGcAZQ" - b"AgADIALgAwACAAOgAgAE0AYQByAGsAQQAgADoAIAA1AC0A" - b"MQAxAC0AMgAwADAAOAAARm9udEZvcmdlIDIuMCA6IE1hcm" - b"tBIDogNS0xMS0yMDA4AABNAGEAcgBrAEEAAE1hcmtBAABW" - b"AGUAcgBzAGkAbwBuACAAMAAwADEALgAwADAAMAAgAABWZX" - b"JzaW9uIDAwMS4wMDAgAABNAGEAcgBrAEEAAE1hcmtBAAAA" - b"AgAAAAAAAP+DADIAAAABAAAAAAAAAAAAAAAAAAAAAAAEAA" - b"AAAQACACQAAAAAAAH//wACAAAAAQAAAADEPovuAAAAAMU4" - b"Lm0AAAAAxTgubQ==") - -def generate_report_headers_payload(request, server_data): - stashed_data = request.server.stash.take(request.GET[b"id"]) - return stashed_data - -def main(request, response): - handler = lambda data: generate_payload(request, data) - content_type = b'application/x-font-truetype' - - if b"report-headers" in request.GET: - handler = lambda data: generate_report_headers_payload(request, data) - content_type = b'application/json' - - subresource.respond(request, - response, - payload_generator = handler, - content_type = content_type, - access_control_allow_origin = b"*") diff --git a/test/fixtures/wpt/common/security-features/subresource/image.py b/test/fixtures/wpt/common/security-features/subresource/image.py deleted file mode 100644 index 5c9a0c063c3..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/image.py +++ /dev/null @@ -1,116 +0,0 @@ -import os, sys, array, math - -from io import BytesIO - -from wptserve.utils import isomorphic_decode - -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -class Image: - """This class partially implements the interface of the PIL.Image.Image. - One day in the future WPT might support the PIL module or another imaging - library, so this hacky BMP implementation will no longer be required. - """ - def __init__(self, width, height): - self.width = width - self.height = height - self.img = bytearray([0 for i in range(3 * width * height)]) - - @staticmethod - def new(mode, size, color=0): - return Image(size[0], size[1]) - - def _int_to_bytes(self, number): - packed_bytes = [0, 0, 0, 0] - for i in range(4): - packed_bytes[i] = number & 0xFF - number >>= 8 - - return packed_bytes - - def putdata(self, color_data): - for y in range(self.height): - for x in range(self.width): - i = x + y * self.width - if i > len(color_data) - 1: - return - - self.img[i * 3: i * 3 + 3] = color_data[i][::-1] - - def save(self, f, type): - assert type == "BMP" - # 54 bytes of preambule + image color data. - filesize = 54 + 3 * self.width * self.height - # 14 bytes of header. - bmpfileheader = bytearray([ord('B'), ord('M')] + self._int_to_bytes(filesize) + - [0, 0, 0, 0, 54, 0, 0, 0]) - # 40 bytes of info. - bmpinfoheader = bytearray([40, 0, 0, 0] + - self._int_to_bytes(self.width) + - self._int_to_bytes(self.height) + - [1, 0, 24] + (25 * [0])) - - padlength = (4 - (self.width * 3) % 4) % 4 - bmppad = bytearray([0, 0, 0]) - padding = bmppad[0 : padlength] - - f.write(bmpfileheader) - f.write(bmpinfoheader) - - for i in range(self.height): - offset = self.width * (self.height - i - 1) * 3 - f.write(self.img[offset : offset + 3 * self.width]) - f.write(padding) - -def encode_string_as_bmp_image(string_data): - data_bytes = array.array("B", string_data.encode("utf-8")) - - num_bytes = len(data_bytes) - - # Encode data bytes to color data (RGB), one bit per channel. - # This is to avoid errors due to different color spaces used in decoding. - color_data = [] - for byte in data_bytes: - p = [int(x) * 255 for x in '{0:08b}'.format(byte)] - color_data.append((p[0], p[1], p[2])) - color_data.append((p[3], p[4], p[5])) - color_data.append((p[6], p[7], 0)) - - # Render image. - num_pixels = len(color_data) - sqrt = int(math.ceil(math.sqrt(num_pixels))) - img = Image.new("RGB", (sqrt, sqrt), "black") - img.putdata(color_data) - - # Flush image to string. - f = BytesIO() - img.save(f, "BMP") - f.seek(0) - - return f.read() - -def generate_payload(request, server_data): - data = (u'{"headers": %(headers)s}') % server_data - if b"id" in request.GET: - request.server.stash.put(request.GET[b"id"], data) - data = encode_string_as_bmp_image(data) - return data - -def generate_report_headers_payload(request, server_data): - stashed_data = request.server.stash.take(request.GET[b"id"]) - return stashed_data - -def main(request, response): - handler = lambda data: generate_payload(request, data) - content_type = b'image/bmp' - - if b"report-headers" in request.GET: - handler = lambda data: generate_report_headers_payload(request, data) - content_type = b'application/json' - - subresource.respond(request, - response, - payload_generator = handler, - content_type = content_type, - access_control_allow_origin = b"*") diff --git a/test/fixtures/wpt/common/security-features/subresource/referrer.py b/test/fixtures/wpt/common/security-features/subresource/referrer.py deleted file mode 100644 index e36631479e6..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/referrer.py +++ /dev/null @@ -1,4 +0,0 @@ -def main(request, response): - referrer = request.headers.get(b"referer", b"") - response_headers = [(b"Content-Type", b"text/javascript")] - return (200, response_headers, b"window.referrer = '" + referrer + b"'") diff --git a/test/fixtures/wpt/common/security-features/subresource/script.py b/test/fixtures/wpt/common/security-features/subresource/script.py deleted file mode 100644 index 9701816b9fa..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/script.py +++ /dev/null @@ -1,14 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode - -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(server_data): - return subresource.get_template(u"script.js.template") % server_data - -def main(request, response): - subresource.respond(request, - response, - payload_generator = generate_payload, - content_type = b"application/javascript") diff --git a/test/fixtures/wpt/common/security-features/subresource/shared-worker.py b/test/fixtures/wpt/common/security-features/subresource/shared-worker.py deleted file mode 100644 index bdfb61bbb3f..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/shared-worker.py +++ /dev/null @@ -1,13 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(server_data): - return subresource.get_template(u"shared-worker.js.template") % server_data - -def main(request, response): - subresource.respond(request, - response, - payload_generator = generate_payload, - content_type = b"application/javascript") diff --git a/test/fixtures/wpt/common/security-features/subresource/static-import.py b/test/fixtures/wpt/common/security-features/subresource/static-import.py deleted file mode 100644 index 3c3a6f6871d..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/static-import.py +++ /dev/null @@ -1,61 +0,0 @@ -import os, sys, json -from urllib.parse import unquote - -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def get_csp_value(value): - ''' - Returns actual CSP header values (e.g. "worker-src 'self'") for the - given string used in PolicyDelivery's value (e.g. "worker-src-self"). - ''' - - # script-src - # Test-related scripts like testharness.js and inline scripts containing - # test bodies. - # 'unsafe-inline' is added as a workaround here. This is probably not so - # bad, as it shouldn't intefere non-inline-script requests that we want to - # test. - if value == 'script-src-wildcard': - return "script-src * 'unsafe-inline'" - if value == 'script-src-self': - return "script-src 'self' 'unsafe-inline'" - # Workaround for "script-src 'none'" would be more complicated, because - # - "script-src 'none' 'unsafe-inline'" is handled somehow differently from - # "script-src 'none'", i.e. - # https://w3c.github.io/webappsec-csp/#match-url-to-source-list Step 3 - # handles the latter but not the former. - # - We need nonce- or path-based additional values to allow same-origin - # test scripts like testharness.js. - # Therefore, we disable 'script-src-none' tests for now in - # `/content-security-policy/spec.src.json`. - if value == 'script-src-none': - return "script-src 'none'" - - # worker-src - if value == 'worker-src-wildcard': - return 'worker-src *' - if value == 'worker-src-self': - return "worker-src 'self'" - if value == 'worker-src-none': - return "worker-src 'none'" - raise Exception('Invalid delivery_value: %s' % value) - -def generate_payload(request): - import_url = unquote(isomorphic_decode(request.GET[b'import_url'])) - return subresource.get_template(u"static-import.js.template") % { - u"import_url": import_url - } - -def main(request, response): - def payload_generator(_): return generate_payload(request) - maybe_additional_headers = {} - if b'contentSecurityPolicy' in request.GET: - csp = unquote(isomorphic_decode(request.GET[b'contentSecurityPolicy'])) - maybe_additional_headers[b'Content-Security-Policy'] = get_csp_value(csp) - subresource.respond(request, - response, - payload_generator = payload_generator, - content_type = b"application/javascript", - maybe_additional_headers = maybe_additional_headers) diff --git a/test/fixtures/wpt/common/security-features/subresource/stylesheet.py b/test/fixtures/wpt/common/security-features/subresource/stylesheet.py deleted file mode 100644 index 05db249250d..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/stylesheet.py +++ /dev/null @@ -1,61 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(request, server_data): - data = (u'{"headers": %(headers)s}') % server_data - type = b'image' - if b"type" in request.GET: - type = request.GET[b"type"] - - if b"id" in request.GET: - request.server.stash.put(request.GET[b"id"], data) - - if type == b'image': - return subresource.get_template(u"image.css.template") % {u"id": isomorphic_decode(request.GET[b"id"])} - - elif type == b'font': - return subresource.get_template(u"font.css.template") % {u"id": isomorphic_decode(request.GET[b"id"])} - - elif type == b'svg': - return subresource.get_template(u"svg.css.template") % { - u"id": isomorphic_decode(request.GET[b"id"]), - u"property": isomorphic_decode(request.GET[b"property"])} - - # A `'stylesheet-only'`-type stylesheet has no nested resources; this is - # useful in tests that cover referrers for stylesheet fetches (e.g. fetches - # triggered by `@import` statements). - elif type == b'stylesheet-only': - return u'' - -def generate_import_rule(request, server_data): - return u"@import url('%(url)s');" % { - u"url": subresource.create_url(request, swap_origin=True, - query_parameter_to_remove=u"import-rule") - } - -def generate_report_headers_payload(request, server_data): - stashed_data = request.server.stash.take(request.GET[b"id"]) - return stashed_data - -def main(request, response): - payload_generator = lambda data: generate_payload(request, data) - content_type = b"text/css" - referrer_policy = b"unsafe-url" - if b"import-rule" in request.GET: - payload_generator = lambda data: generate_import_rule(request, data) - - if b"report-headers" in request.GET: - payload_generator = lambda data: generate_report_headers_payload(request, data) - content_type = b'application/json' - - if b"referrer-policy" in request.GET: - referrer_policy = request.GET[b"referrer-policy"] - - subresource.respond( - request, - response, - payload_generator = payload_generator, - content_type = content_type, - maybe_additional_headers = { b"Referrer-Policy": referrer_policy }) diff --git a/test/fixtures/wpt/common/security-features/subresource/subresource.py b/test/fixtures/wpt/common/security-features/subresource/subresource.py deleted file mode 100644 index b3c055a93a5..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/subresource.py +++ /dev/null @@ -1,199 +0,0 @@ -import os, json -from urllib.parse import parse_qsl, SplitResult, urlencode, urlsplit, urlunsplit - -from wptserve.utils import isomorphic_decode, isomorphic_encode - -def get_template(template_basename): - script_directory = os.path.dirname(os.path.abspath(isomorphic_decode(__file__))) - template_directory = os.path.abspath(os.path.join(script_directory, - u"template")) - template_filename = os.path.join(template_directory, template_basename) - - with open(template_filename, "r") as f: - return f.read() - - -def redirect(url, response): - response.add_required_headers = False - response.writer.write_status(301) - response.writer.write_header(b"access-control-allow-origin", b"*") - response.writer.write_header(b"location", isomorphic_encode(url)) - response.writer.end_headers() - response.writer.write(u"") - - -# TODO(kristijanburnik): subdomain_prefix is a hardcoded value aligned with -# referrer-policy-test-case.js. The prefix should be configured in one place. -def __get_swapped_origin_netloc(netloc, subdomain_prefix = u"www1."): - if netloc.startswith(subdomain_prefix): - return netloc[len(subdomain_prefix):] - else: - return subdomain_prefix + netloc - - -# Creates a URL (typically a redirect target URL) that is the same as the -# current request URL `request.url`, except for: -# - When `swap_scheme` or `swap_origin` is True, its scheme/origin is changed -# to the other one. (http <-> https, ws <-> wss, etc.) -# - For `downgrade`, we redirect to a URL that would be successfully loaded -# if and only if upgrade-insecure-request is applied. -# - `query_parameter_to_remove` parameter is removed from query part. -# Its default is "redirection" to avoid redirect loops. -def create_url(request, - swap_scheme=False, - swap_origin=False, - downgrade=False, - query_parameter_to_remove=u"redirection"): - parsed = urlsplit(request.url) - destination_netloc = parsed.netloc - - scheme = parsed.scheme - if swap_scheme: - scheme = u"http" if parsed.scheme == u"https" else u"https" - hostname = parsed.netloc.split(u':')[0] - port = request.server.config[u"ports"][scheme][0] - destination_netloc = u":".join([hostname, str(port)]) - - if downgrade: - # These rely on some unintuitive cleverness due to WPT's test setup: - # 'Upgrade-Insecure-Requests' does not upgrade the port number, - # so we use URLs in the form `http://[domain]:[https-port]`, - # which will be upgraded to `https://[domain]:[https-port]`. - # If the upgrade fails, the load will fail, as we don't serve HTTP over - # the secure port. - if parsed.scheme == u"https": - scheme = u"http" - elif parsed.scheme == u"wss": - scheme = u"ws" - else: - raise ValueError(u"Downgrade redirection: Invalid scheme '%s'" % - parsed.scheme) - hostname = parsed.netloc.split(u':')[0] - port = request.server.config[u"ports"][parsed.scheme][0] - destination_netloc = u":".join([hostname, str(port)]) - - if swap_origin: - destination_netloc = __get_swapped_origin_netloc(destination_netloc) - - parsed_query = parse_qsl(parsed.query, keep_blank_values=True) - parsed_query = [x for x in parsed_query if x[0] != query_parameter_to_remove] - - destination_url = urlunsplit(SplitResult( - scheme = scheme, - netloc = destination_netloc, - path = parsed.path, - query = urlencode(parsed_query), - fragment = None)) - - return destination_url - - -def preprocess_redirection(request, response): - if b"redirection" not in request.GET: - return False - - redirection = request.GET[b"redirection"] - - if redirection == b"no-redirect": - return False - elif redirection == b"keep-scheme": - redirect_url = create_url(request, swap_scheme=False) - elif redirection == b"swap-scheme": - redirect_url = create_url(request, swap_scheme=True) - elif redirection == b"downgrade": - redirect_url = create_url(request, downgrade=True) - elif redirection == b"keep-origin": - redirect_url = create_url(request, swap_origin=False) - elif redirection == b"swap-origin": - redirect_url = create_url(request, swap_origin=True) - else: - raise ValueError(u"Invalid redirection type '%s'" % isomorphic_decode(redirection)) - - redirect(redirect_url, response) - return True - - -def preprocess_stash_action(request, response): - if b"action" not in request.GET: - return False - - action = request.GET[b"action"] - - key = request.GET[b"key"] - stash = request.server.stash - path = request.GET[b"path"] if b"path" in request.GET \ - else isomorphic_encode(request.url.split(u'?')[0]) - - if action == b"put": - value = isomorphic_decode(request.GET[b"value"]) - stash.take(key=key, path=path) - stash.put(key=key, value=value, path=path) - response_data = json.dumps({u"status": u"success", u"result": isomorphic_decode(key)}) - elif action == b"purge": - value = stash.take(key=key, path=path) - return False - elif action == b"take": - value = stash.take(key=key, path=path) - if value is None: - status = u"allowed" - else: - status = u"blocked" - response_data = json.dumps({u"status": status, u"result": value}) - else: - return False - - response.add_required_headers = False - response.writer.write_status(200) - response.writer.write_header(b"content-type", b"text/javascript") - response.writer.write_header(b"cache-control", b"no-cache; must-revalidate") - response.writer.end_headers() - response.writer.write(response_data) - return True - - -def __noop(request, response): - return u"" - - -def respond(request, - response, - status_code = 200, - content_type = b"text/html", - payload_generator = __noop, - cache_control = b"no-cache; must-revalidate", - access_control_allow_origin = b"*", - maybe_additional_headers = None): - if preprocess_redirection(request, response): - return - - if preprocess_stash_action(request, response): - return - - response.add_required_headers = False - response.writer.write_status(status_code) - - if access_control_allow_origin != None: - response.writer.write_header(b"access-control-allow-origin", - access_control_allow_origin) - response.writer.write_header(b"content-type", content_type) - response.writer.write_header(b"cache-control", cache_control) - - additional_headers = maybe_additional_headers or {} - for header, value in additional_headers.items(): - response.writer.write_header(header, value) - - response.writer.end_headers() - - new_headers = {} - new_val = [] - for key, val in request.headers.items(): - if len(val) == 1: - new_val = isomorphic_decode(val[0]) - else: - new_val = [isomorphic_decode(x) for x in val] - new_headers[isomorphic_decode(key)] = new_val - - server_data = {u"headers": json.dumps(new_headers, indent = 4)} - - payload = payload_generator(server_data) - response.writer.write(payload) diff --git a/test/fixtures/wpt/common/security-features/subresource/svg.py b/test/fixtures/wpt/common/security-features/subresource/svg.py deleted file mode 100644 index 9c569e3bf52..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/svg.py +++ /dev/null @@ -1,37 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(request, server_data): - data = (u'{"headers": %(headers)s}') % server_data - if b"id" in request.GET: - with request.server.stash.lock: - request.server.stash.take(request.GET[b"id"]) - request.server.stash.put(request.GET[b"id"], data) - return u"" - -def generate_payload_embedded(request, server_data): - return subresource.get_template(u"svg.embedded.template") % { - u"id": isomorphic_decode(request.GET[b"id"]), - u"property": isomorphic_decode(request.GET[b"property"])} - -def generate_report_headers_payload(request, server_data): - stashed_data = request.server.stash.take(request.GET[b"id"]) - return stashed_data - -def main(request, response): - handler = lambda data: generate_payload(request, data) - content_type = b'image/svg+xml' - - if b"embedded-svg" in request.GET: - handler = lambda data: generate_payload_embedded(request, data) - - if b"report-headers" in request.GET: - handler = lambda data: generate_report_headers_payload(request, data) - content_type = b'application/json' - - subresource.respond(request, - response, - payload_generator = handler, - content_type = content_type) diff --git a/test/fixtures/wpt/common/security-features/subresource/template/document.html.template b/test/fixtures/wpt/common/security-features/subresource/template/document.html.template deleted file mode 100644 index 141711c1483..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/document.html.template +++ /dev/null @@ -1,16 +0,0 @@ - - - - This page reports back it's request details to the parent frame - - - - - diff --git a/test/fixtures/wpt/common/security-features/subresource/template/font.css.template b/test/fixtures/wpt/common/security-features/subresource/template/font.css.template deleted file mode 100644 index 9d1e9c421cc..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/font.css.template +++ /dev/null @@ -1,9 +0,0 @@ -@font-face { - font-family: 'wpt'; - font-style: normal; - font-weight: normal; - src: url(/common/security-features/subresource/font.py?id=%(id)s) format('truetype'); -} -body { - font-family: 'wpt'; -} diff --git a/test/fixtures/wpt/common/security-features/subresource/template/image.css.template b/test/fixtures/wpt/common/security-features/subresource/template/image.css.template deleted file mode 100644 index dfe41f1bf16..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/image.css.template +++ /dev/null @@ -1,3 +0,0 @@ -div.styled::before { - content:url(/common/security-features/subresource/image.py?id=%(id)s) -} diff --git a/test/fixtures/wpt/common/security-features/subresource/template/script.js.template b/test/fixtures/wpt/common/security-features/subresource/template/script.js.template deleted file mode 100644 index e2edf21819d..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/script.js.template +++ /dev/null @@ -1,3 +0,0 @@ -postMessage({ - "headers": %(headers)s -}, "*"); diff --git a/test/fixtures/wpt/common/security-features/subresource/template/shared-worker.js.template b/test/fixtures/wpt/common/security-features/subresource/template/shared-worker.js.template deleted file mode 100644 index c3f109e4a90..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/shared-worker.js.template +++ /dev/null @@ -1,5 +0,0 @@ -onconnect = function(e) { - e.ports[0].postMessage({ - "headers": %(headers)s - }); -}; diff --git a/test/fixtures/wpt/common/security-features/subresource/template/static-import.js.template b/test/fixtures/wpt/common/security-features/subresource/template/static-import.js.template deleted file mode 100644 index 095459b5475..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/static-import.js.template +++ /dev/null @@ -1 +0,0 @@ -import '%(import_url)s'; diff --git a/test/fixtures/wpt/common/security-features/subresource/template/svg.css.template b/test/fixtures/wpt/common/security-features/subresource/template/svg.css.template deleted file mode 100644 index c2e509cc3b8..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/svg.css.template +++ /dev/null @@ -1,3 +0,0 @@ -path { - %(property)s: url(/common/security-features/subresource/svg.py?id=%(id)s#invalidFragment); -} diff --git a/test/fixtures/wpt/common/security-features/subresource/template/svg.embedded.template b/test/fixtures/wpt/common/security-features/subresource/template/svg.embedded.template deleted file mode 100644 index 5986c4800a7..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/svg.embedded.template +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/common/security-features/subresource/template/worker.js.template b/test/fixtures/wpt/common/security-features/subresource/template/worker.js.template deleted file mode 100644 index 817dd8c87ac..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/template/worker.js.template +++ /dev/null @@ -1,3 +0,0 @@ -postMessage({ - "headers": %(headers)s -}); diff --git a/test/fixtures/wpt/common/security-features/subresource/video.py b/test/fixtures/wpt/common/security-features/subresource/video.py deleted file mode 100644 index 9db8e9fbb5a..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/video.py +++ /dev/null @@ -1,17 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(request, server_data): - file = os.path.join(request.doc_root, u"media", u"movie_5.webm") - return open(file, "rb").read() - - -def main(request, response): - handler = lambda data: generate_payload(request, data) - subresource.respond(request, - response, - payload_generator = handler, - access_control_allow_origin = b"*", - content_type = b"video/webm") diff --git a/test/fixtures/wpt/common/security-features/subresource/worker.py b/test/fixtures/wpt/common/security-features/subresource/worker.py deleted file mode 100644 index f655633b5db..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/worker.py +++ /dev/null @@ -1,13 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(server_data): - return subresource.get_template(u"worker.js.template") % server_data - -def main(request, response): - subresource.respond(request, - response, - payload_generator = generate_payload, - content_type = b"application/javascript") diff --git a/test/fixtures/wpt/common/security-features/subresource/xhr.py b/test/fixtures/wpt/common/security-features/subresource/xhr.py deleted file mode 100644 index 75921e91569..00000000000 --- a/test/fixtures/wpt/common/security-features/subresource/xhr.py +++ /dev/null @@ -1,16 +0,0 @@ -import os, sys -from wptserve.utils import isomorphic_decode -import importlib -subresource = importlib.import_module("common.security-features.subresource.subresource") - -def generate_payload(server_data): - data = (u'{"headers": %(headers)s}') % server_data - return data - -def main(request, response): - subresource.respond(request, - response, - payload_generator = generate_payload, - access_control_allow_origin = b"*", - content_type = b"application/json", - cache_control = b"no-store") diff --git a/test/fixtures/wpt/common/security-features/tools/format_spec_src_json.py b/test/fixtures/wpt/common/security-features/tools/format_spec_src_json.py deleted file mode 100644 index d1bf5817ad2..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/format_spec_src_json.py +++ /dev/null @@ -1,24 +0,0 @@ -import collections -import json -import os - - -def main(): - '''Formats spec.src.json.''' - script_directory = os.path.dirname(os.path.abspath(__file__)) - for dir in [ - 'mixed-content', 'referrer-policy', 'referrer-policy/4K-1', - 'referrer-policy/4K', 'referrer-policy/4K+1', - 'upgrade-insecure-requests' - ]: - filename = os.path.join(script_directory, '..', '..', '..', dir, - 'spec.src.json') - spec = json.load( - open(filename, 'r'), object_pairs_hook=collections.OrderedDict) - with open(filename, 'w') as f: - f.write(json.dumps(spec, indent=2, separators=(',', ': '))) - f.write('\n') - - -if __name__ == '__main__': - main() diff --git a/test/fixtures/wpt/common/security-features/tools/generate.py b/test/fixtures/wpt/common/security-features/tools/generate.py deleted file mode 100755 index d1cd33147fc..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/generate.py +++ /dev/null @@ -1,462 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import collections -import copy -import json -import os -import sys - -import spec_validator -import util - - -def expand_pattern(expansion_pattern, test_expansion_schema): - expansion = {} - for artifact_key in expansion_pattern: - artifact_value = expansion_pattern[artifact_key] - if artifact_value == '*': - expansion[artifact_key] = test_expansion_schema[artifact_key] - elif isinstance(artifact_value, list): - expansion[artifact_key] = artifact_value - elif isinstance(artifact_value, dict): - # Flattened expansion. - expansion[artifact_key] = [] - values_dict = expand_pattern(artifact_value, - test_expansion_schema[artifact_key]) - for sub_key in values_dict.keys(): - expansion[artifact_key] += values_dict[sub_key] - else: - expansion[artifact_key] = [artifact_value] - - return expansion - - -def permute_expansion(expansion, - artifact_order, - selection={}, - artifact_index=0): - assert isinstance(artifact_order, list), "artifact_order should be a list" - - if artifact_index >= len(artifact_order): - yield selection - return - - artifact_key = artifact_order[artifact_index] - - for artifact_value in expansion[artifact_key]: - selection[artifact_key] = artifact_value - for next_selection in permute_expansion(expansion, artifact_order, - selection, artifact_index + 1): - yield next_selection - - -# Dumps the test config `selection` into a serialized JSON string. -def dump_test_parameters(selection): - return json.dumps( - selection, - indent=2, - separators=(',', ': '), - sort_keys=True, - cls=util.CustomEncoder) - - -def get_test_filename(spec_directory, spec_json, selection): - '''Returns the filname for the main test HTML file''' - - selection_for_filename = copy.deepcopy(selection) - # Use 'unset' rather than 'None' in test filenames. - if selection_for_filename['delivery_value'] is None: - selection_for_filename['delivery_value'] = 'unset' - - return os.path.join( - spec_directory, - spec_json['test_file_path_pattern'] % selection_for_filename) - - -def get_csp_value(value): - ''' - Returns actual CSP header values (e.g. "worker-src 'self'") for the - given string used in PolicyDelivery's value (e.g. "worker-src-self"). - ''' - - # script-src - # Test-related scripts like testharness.js and inline scripts containing - # test bodies. - # 'unsafe-inline' is added as a workaround here. This is probably not so - # bad, as it shouldn't intefere non-inline-script requests that we want to - # test. - if value == 'script-src-wildcard': - return "script-src * 'unsafe-inline'" - if value == 'script-src-self': - return "script-src 'self' 'unsafe-inline'" - # Workaround for "script-src 'none'" would be more complicated, because - # - "script-src 'none' 'unsafe-inline'" is handled somehow differently from - # "script-src 'none'", i.e. - # https://w3c.github.io/webappsec-csp/#match-url-to-source-list Step 3 - # handles the latter but not the former. - # - We need nonce- or path-based additional values to allow same-origin - # test scripts like testharness.js. - # Therefore, we disable 'script-src-none' tests for now in - # `/content-security-policy/spec.src.json`. - if value == 'script-src-none': - return "script-src 'none'" - - # worker-src - if value == 'worker-src-wildcard': - return 'worker-src *' - if value == 'worker-src-self': - return "worker-src 'self'" - if value == 'worker-src-none': - return "worker-src 'none'" - raise Exception('Invalid delivery_value: %s' % value) - -def handle_deliveries(policy_deliveries): - ''' - Generate elements and HTTP headers for the given list of - PolicyDelivery. - TODO(hiroshige): Merge duplicated code here, scope/document.py, etc. - ''' - - meta = '' - headers = {} - - for delivery in policy_deliveries: - if delivery.value is None: - continue - if delivery.key == 'referrerPolicy': - if delivery.delivery_type == 'meta': - meta += \ - '' % delivery.value - elif delivery.delivery_type == 'http-rp': - headers['Referrer-Policy'] = delivery.value - # TODO(kristijanburnik): Limit to WPT origins. - headers['Access-Control-Allow-Origin'] = '*' - else: - raise Exception( - 'Invalid delivery_type: %s' % delivery.delivery_type) - elif delivery.key == 'mixedContent': - assert (delivery.value == 'opt-in') - if delivery.delivery_type == 'meta': - meta += '' - elif delivery.delivery_type == 'http-rp': - headers['Content-Security-Policy'] = 'block-all-mixed-content' - else: - raise Exception( - 'Invalid delivery_type: %s' % delivery.delivery_type) - elif delivery.key == 'contentSecurityPolicy': - csp_value = get_csp_value(delivery.value) - if delivery.delivery_type == 'meta': - meta += '' - elif delivery.delivery_type == 'http-rp': - headers['Content-Security-Policy'] = csp_value - else: - raise Exception( - 'Invalid delivery_type: %s' % delivery.delivery_type) - elif delivery.key == 'upgradeInsecureRequests': - # https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery - assert (delivery.value == 'upgrade') - if delivery.delivery_type == 'meta': - meta += '' - elif delivery.delivery_type == 'http-rp': - headers[ - 'Content-Security-Policy'] = 'upgrade-insecure-requests' - else: - raise Exception( - 'Invalid delivery_type: %s' % delivery.delivery_type) - else: - raise Exception('Invalid delivery_key: %s' % delivery.key) - return {"meta": meta, "headers": headers} - - -def generate_selection(spec_json, selection): - ''' - Returns a scenario object (with a top-level source_context_list entry, - which will be removed in generate_test_file() later). - ''' - - target_policy_delivery = util.PolicyDelivery(selection['delivery_type'], - selection['delivery_key'], - selection['delivery_value']) - del selection['delivery_type'] - del selection['delivery_key'] - del selection['delivery_value'] - - # Parse source context list and policy deliveries of source contexts. - # `util.ShouldSkip()` exceptions are raised if e.g. unsuppported - # combinations of source contexts and policy deliveries are used. - source_context_list_scheme = spec_json['source_context_list_schema'][ - selection['source_context_list']] - selection['source_context_list'] = [ - util.SourceContext.from_json(source_context, target_policy_delivery, - spec_json['source_context_schema']) - for source_context in source_context_list_scheme['sourceContextList'] - ] - - # Check if the subresource is supported by the innermost source context. - innermost_source_context = selection['source_context_list'][-1] - supported_subresource = spec_json['source_context_schema'][ - 'supported_subresource'][innermost_source_context.source_context_type] - if supported_subresource != '*': - if selection['subresource'] not in supported_subresource: - raise util.ShouldSkip() - - # Parse subresource policy deliveries. - selection[ - 'subresource_policy_deliveries'] = util.PolicyDelivery.list_from_json( - source_context_list_scheme['subresourcePolicyDeliveries'], - target_policy_delivery, spec_json['subresource_schema'] - ['supported_delivery_type'][selection['subresource']]) - - # Generate per-scenario test description. - selection['test_description'] = spec_json[ - 'test_description_template'] % selection - - return selection - - -def generate_test_file(spec_directory, test_helper_filenames, - test_html_template_basename, test_filename, scenarios): - ''' - Generates a test HTML file (and possibly its associated .headers file) - from `scenarios`. - ''' - - # Scenarios for the same file should have the same `source_context_list`, - # including the top-level one. - # Note: currently, non-top-level source contexts aren't necessarily required - # to be the same, but we set this requirement as it will be useful e.g. when - # we e.g. reuse a worker among multiple scenarios. - for scenario in scenarios: - assert (scenario['source_context_list'] == scenarios[0] - ['source_context_list']) - - # We process the top source context below, and do not include it in - # the JSON objects (i.e. `scenarios`) in generated HTML files. - top_source_context = scenarios[0]['source_context_list'].pop(0) - assert (top_source_context.source_context_type == 'top') - for scenario in scenarios[1:]: - assert (scenario['source_context_list'].pop(0) == top_source_context) - - parameters = {} - - # Sort scenarios, to avoid unnecessary diffs due to different orders in - # `scenarios`. - serialized_scenarios = sorted( - [dump_test_parameters(scenario) for scenario in scenarios]) - - parameters['scenarios'] = ",\n".join(serialized_scenarios).replace( - "\n", "\n" + " " * 10) - - test_directory = os.path.dirname(test_filename) - - parameters['helper_js'] = "" - for test_helper_filename in test_helper_filenames: - parameters['helper_js'] += ' \n' % ( - os.path.relpath(test_helper_filename, test_directory)) - parameters['sanity_checker_js'] = os.path.relpath( - os.path.join(spec_directory, 'generic', 'sanity-checker.js'), - test_directory) - parameters['spec_json_js'] = os.path.relpath( - os.path.join(spec_directory, 'generic', 'spec_json.js'), - test_directory) - - test_headers_filename = test_filename + ".headers" - - test_html_template = util.get_template(test_html_template_basename) - disclaimer_template = util.get_template('disclaimer.template') - - html_template_filename = os.path.join(util.template_directory, - test_html_template_basename) - generated_disclaimer = disclaimer_template \ - % {'generating_script_filename': os.path.relpath(sys.argv[0], - util.test_root_directory), - 'spec_directory': os.path.relpath(spec_directory, - util.test_root_directory)} - - # Adjust the template for the test invoking JS. Indent it to look nice. - parameters['generated_disclaimer'] = generated_disclaimer.rstrip() - - # Directory for the test files. - try: - os.makedirs(test_directory) - except: - pass - - delivery = handle_deliveries(top_source_context.policy_deliveries) - - if len(delivery['headers']) > 0: - with open(test_headers_filename, "w") as f: - for header in delivery['headers']: - f.write('%s: %s\n' % (header, delivery['headers'][header])) - - parameters['meta_delivery_method'] = delivery['meta'] - # Obey the lint and pretty format. - if len(parameters['meta_delivery_method']) > 0: - parameters['meta_delivery_method'] = "\n " + \ - parameters['meta_delivery_method'] - - # Write out the generated HTML file. - util.write_file(test_filename, test_html_template % parameters) - - -def generate_test_source_files(spec_directory, test_helper_filenames, - spec_json, target): - test_expansion_schema = spec_json['test_expansion_schema'] - specification = spec_json['specification'] - - if target == "debug": - spec_json_js_template = util.get_template('spec_json.js.template') - util.write_file( - os.path.join(spec_directory, "generic", "spec_json.js"), - spec_json_js_template % {'spec_json': json.dumps(spec_json)}) - util.write_file( - os.path.join(spec_directory, "generic", - "debug-output.spec.src.json"), - json.dumps(spec_json, indent=2, separators=(',', ': '))) - - # Choose a debug/release template depending on the target. - html_template = "test.%s.html.template" % target - - artifact_order = list(test_expansion_schema.keys()) - artifact_order.remove('expansion') - - excluded_selection_pattern = '' - for key in artifact_order: - excluded_selection_pattern += '%(' + key + ')s/' - - # Create list of excluded tests. - exclusion_dict = set() - for excluded_pattern in spec_json['excluded_tests']: - excluded_expansion = \ - expand_pattern(excluded_pattern, test_expansion_schema) - for excluded_selection in permute_expansion(excluded_expansion, - artifact_order): - excluded_selection['delivery_key'] = spec_json['delivery_key'] - exclusion_dict.add(excluded_selection_pattern % excluded_selection) - - # `scenarios[filename]` represents the list of scenario objects to be - # generated into `filename`. - scenarios = {} - - for spec in specification: - # Used to make entries with expansion="override" override preceding - # entries with the same |selection_path|. - output_dict = {} - - for expansion_pattern in spec['test_expansion']: - expansion = expand_pattern(expansion_pattern, - test_expansion_schema) - for selection in permute_expansion(expansion, artifact_order): - selection['delivery_key'] = spec_json['delivery_key'] - selection_path = spec_json['selection_pattern'] % selection - if selection_path in output_dict: - if expansion_pattern['expansion'] != 'override': - print("Error: expansion is default in:") - print(dump_test_parameters(selection)) - print("but overrides:") - print(dump_test_parameters( - output_dict[selection_path])) - sys.exit(1) - output_dict[selection_path] = copy.deepcopy(selection) - - for selection_path in output_dict: - selection = output_dict[selection_path] - if (excluded_selection_pattern % selection) in exclusion_dict: - print('Excluding selection:', selection_path) - continue - try: - test_filename = get_test_filename(spec_directory, spec_json, - selection) - scenario = generate_selection(spec_json, selection) - scenarios[test_filename] = scenarios.get(test_filename, - []) + [scenario] - except util.ShouldSkip: - continue - - for filename in scenarios: - generate_test_file(spec_directory, test_helper_filenames, - html_template, filename, scenarios[filename]) - - -def merge_json(base, child): - for key in child: - if key not in base: - base[key] = child[key] - continue - # `base[key]` and `child[key]` both exists. - if isinstance(base[key], list) and isinstance(child[key], list): - base[key].extend(child[key]) - elif isinstance(base[key], dict) and isinstance(child[key], dict): - merge_json(base[key], child[key]) - else: - base[key] = child[key] - - -def main(): - parser = argparse.ArgumentParser( - description='Test suite generator utility') - parser.add_argument( - '-t', - '--target', - type=str, - choices=("release", "debug"), - default="release", - help='Sets the appropriate template for generating tests') - parser.add_argument( - '-s', - '--spec', - type=str, - default=os.getcwd(), - help='Specify a file used for describing and generating the tests') - # TODO(kristijanburnik): Add option for the spec_json file. - args = parser.parse_args() - - spec_directory = os.path.abspath(args.spec) - - # Read `spec.src.json` files, starting from `spec_directory`, and - # continuing to parent directories as long as `spec.src.json` exists. - spec_filenames = [] - test_helper_filenames = [] - spec_src_directory = spec_directory - while len(spec_src_directory) >= len(util.test_root_directory): - spec_filename = os.path.join(spec_src_directory, "spec.src.json") - if not os.path.exists(spec_filename): - break - spec_filenames.append(spec_filename) - test_filename = os.path.join(spec_src_directory, 'generic', - 'test-case.sub.js') - assert (os.path.exists(test_filename)) - test_helper_filenames.append(test_filename) - spec_src_directory = os.path.abspath( - os.path.join(spec_src_directory, "..")) - - spec_filenames = list(reversed(spec_filenames)) - test_helper_filenames = list(reversed(test_helper_filenames)) - - if len(spec_filenames) == 0: - print('Error: No spec.src.json is found at %s.' % spec_directory) - return - - # Load the default spec JSON file, ... - default_spec_filename = os.path.join(util.script_directory, - 'spec.src.json') - spec_json = collections.OrderedDict() - if os.path.exists(default_spec_filename): - spec_json = util.load_spec_json(default_spec_filename) - - # ... and then make spec JSON files in subdirectories override the default. - for spec_filename in spec_filenames: - child_spec_json = util.load_spec_json(spec_filename) - merge_json(spec_json, child_spec_json) - - spec_validator.assert_valid_spec_json(spec_json) - generate_test_source_files(spec_directory, test_helper_filenames, - spec_json, args.target) - - -if __name__ == '__main__': - main() diff --git a/test/fixtures/wpt/common/security-features/tools/spec.src.json b/test/fixtures/wpt/common/security-features/tools/spec.src.json deleted file mode 100644 index 4a84493f475..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/spec.src.json +++ /dev/null @@ -1,533 +0,0 @@ -{ - "selection_pattern": "%(source_context_list)s.%(delivery_type)s/%(delivery_value)s/%(subresource)s/%(origin)s.%(redirection)s.%(source_scheme)s", - "test_file_path_pattern": "gen/%(source_context_list)s.%(delivery_type)s/%(delivery_value)s/%(subresource)s.%(source_scheme)s.html", - "excluded_tests": [ - { - // Workers are same-origin only - "expansion": "*", - "source_scheme": "*", - "source_context_list": "*", - "delivery_type": "*", - "delivery_value": "*", - "redirection": "*", - "subresource": [ - "worker-classic", - "worker-module", - "sharedworker-classic", - "sharedworker-module" - ], - "origin": [ - "cross-https", - "cross-http", - "cross-http-downgrade", - "cross-wss", - "cross-ws", - "cross-ws-downgrade" - ], - "expectation": "*" - }, - { - // Workers are same-origin only (redirects) - "expansion": "*", - "source_scheme": "*", - "source_context_list": "*", - "delivery_type": "*", - "delivery_value": "*", - "redirection": [ - "swap-origin", - "swap-scheme" - ], - "subresource": [ - "worker-classic", - "worker-module", - "sharedworker-classic", - "sharedworker-module" - ], - "origin": "*", - "expectation": "*" - }, - { - // Websockets are ws/wss-only - "expansion": "*", - "source_scheme": "*", - "source_context_list": "*", - "delivery_type": "*", - "delivery_value": "*", - "redirection": "*", - "subresource": "websocket", - "origin": [ - "same-https", - "same-http", - "same-http-downgrade", - "cross-https", - "cross-http", - "cross-http-downgrade" - ], - "expectation": "*" - }, - { - // Redirects are intentionally forbidden in browsers: - // https://fetch.spec.whatwg.org/#concept-websocket-establish - // Websockets are no-redirect only - "expansion": "*", - "source_scheme": "*", - "source_context_list": "*", - "delivery_type": "*", - "delivery_value": "*", - "redirection": [ - "keep-origin", - "swap-origin", - "keep-scheme", - "swap-scheme", - "downgrade" - ], - "subresource": "websocket", - "origin": "*", - "expectation": "*" - }, - { - // ws/wss are websocket-only - "expansion": "*", - "source_scheme": "*", - "source_context_list": "*", - "delivery_type": "*", - "delivery_value": "*", - "redirection": "*", - "subresource": [ - "a-tag", - "area-tag", - "audio-tag", - "beacon", - "fetch", - "iframe-tag", - "img-tag", - "link-css-tag", - "link-prefetch-tag", - "object-tag", - "picture-tag", - "script-tag", - "script-tag-dynamic-import", - "sharedworker-classic", - "sharedworker-import", - "sharedworker-import-data", - "sharedworker-module", - "video-tag", - "worker-classic", - "worker-import", - "worker-import-data", - "worker-module", - "worklet-animation", - "worklet-animation-import-data", - "worklet-audio", - "worklet-audio-import-data", - "worklet-layout", - "worklet-layout-import-data", - "worklet-paint", - "worklet-paint-import-data", - "xhr" - ], - "origin": [ - "same-wss", - "same-ws", - "same-ws-downgrade", - "cross-wss", - "cross-ws", - "cross-ws-downgrade" - ], - "expectation": "*" - }, - { - // Worklets are HTTPS contexts only - "expansion": "*", - "source_scheme": "http", - "source_context_list": "*", - "delivery_type": "*", - "delivery_value": "*", - "redirection": "*", - "subresource": [ - "worklet-animation", - "worklet-animation-import-data", - "worklet-audio", - "worklet-audio-import-data", - "worklet-layout", - "worklet-layout-import-data", - "worklet-paint", - "worklet-paint-import-data" - ], - "origin": "*", - "expectation": "*" - } - ], - "source_context_schema": { - "supported_subresource": { - "top": "*", - "iframe": "*", - "iframe-blank": "*", - "srcdoc": "*", - "worker-classic": [ - "xhr", - "fetch", - "websocket", - "worker-classic", - "worker-module" - ], - "worker-module": [ - "xhr", - "fetch", - "websocket", - "worker-classic", - "worker-module" - ], - "worker-classic-data": [ - "xhr", - "fetch", - "websocket" - ], - "worker-module-data": [ - "xhr", - "fetch", - "websocket" - ], - "sharedworker-classic": [ - "xhr", - "fetch", - "websocket" - ], - "sharedworker-module": [ - "xhr", - "fetch", - "websocket" - ], - "sharedworker-classic-data": [ - "xhr", - "fetch", - "websocket" - ], - "sharedworker-module-data": [ - "xhr", - "fetch", - "websocket" - ] - } - }, - "source_context_list_schema": { - // Warning: Currently, some nested patterns of contexts have different - // inheritance rules for different kinds of policies. - // The generated tests will be used to test/investigate the policy - // inheritance rules, and eventually the policy inheritance rules will - // be unified (https://github.com/w3ctag/design-principles/issues/111). - "top": { - "description": "Policy set by the top-level Document", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "policy" - ] - } - ], - "subresourcePolicyDeliveries": [] - }, - "req": { - "description": "Subresource request's policy should override Document's policy", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "anotherPolicy" - ] - } - ], - "subresourcePolicyDeliveries": [ - "nonNullPolicy" - ] - }, - "srcdoc-inherit": { - "description": "srcdoc iframe without its own policy should inherit parent Document's policy", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "policy" - ] - }, - { - "sourceContextType": "srcdoc" - } - ], - "subresourcePolicyDeliveries": [] - }, - "srcdoc": { - "description": "srcdoc iframe's policy should override parent Document's policy", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "anotherPolicy" - ] - }, - { - "sourceContextType": "srcdoc", - "policyDeliveries": [ - "nonNullPolicy" - ] - } - ], - "subresourcePolicyDeliveries": [] - }, - "iframe": { - "description": "external iframe's policy should override parent Document's policy", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "anotherPolicy" - ] - }, - { - "sourceContextType": "iframe", - "policyDeliveries": [ - "policy" - ] - } - ], - "subresourcePolicyDeliveries": [] - }, - "iframe-blank-inherit": { - "description": "blank iframe should inherit parent Document's policy", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "policy" - ] - }, - { - "sourceContextType": "iframe-blank" - } - ], - "subresourcePolicyDeliveries": [] - }, - "worker-classic": { - // This is applicable to referrer-policy tests. - // Use "worker-classic-inherit" for CSP (mixed-content, etc.). - "description": "dedicated workers shouldn't inherit its parent's policy.", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "anotherPolicy" - ] - }, - { - "sourceContextType": "worker-classic", - "policyDeliveries": [ - "policy" - ] - } - ], - "subresourcePolicyDeliveries": [] - }, - "worker-classic-data": { - "description": "data: dedicated workers should inherit its parent's policy.", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "policy" - ] - }, - { - "sourceContextType": "worker-classic-data", - "policyDeliveries": [] - } - ], - "subresourcePolicyDeliveries": [] - }, - "worker-module": { - // This is applicable to referrer-policy tests. - "description": "dedicated workers shouldn't inherit its parent's policy.", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "anotherPolicy" - ] - }, - { - "sourceContextType": "worker-module", - "policyDeliveries": [ - "policy" - ] - } - ], - "subresourcePolicyDeliveries": [] - }, - "worker-module-data": { - "description": "data: dedicated workers should inherit its parent's policy.", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "policy" - ] - }, - { - "sourceContextType": "worker-module-data", - "policyDeliveries": [] - } - ], - "subresourcePolicyDeliveries": [] - }, - "sharedworker-classic": { - "description": "shared workers shouldn't inherit its parent's policy.", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "anotherPolicy" - ] - }, - { - "sourceContextType": "sharedworker-classic", - "policyDeliveries": [ - "policy" - ] - } - ], - "subresourcePolicyDeliveries": [] - }, - "sharedworker-classic-data": { - "description": "data: shared workers should inherit its parent's policy.", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "policy" - ] - }, - { - "sourceContextType": "sharedworker-classic-data", - "policyDeliveries": [] - } - ], - "subresourcePolicyDeliveries": [] - }, - "sharedworker-module": { - "description": "shared workers shouldn't inherit its parent's policy.", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "anotherPolicy" - ] - }, - { - "sourceContextType": "sharedworker-module", - "policyDeliveries": [ - "policy" - ] - } - ], - "subresourcePolicyDeliveries": [] - }, - "sharedworker-module-data": { - "description": "data: shared workers should inherit its parent's policy.", - "sourceContextList": [ - { - "sourceContextType": "top", - "policyDeliveries": [ - "policy" - ] - }, - { - "sourceContextType": "sharedworker-module-data", - "policyDeliveries": [] - } - ], - "subresourcePolicyDeliveries": [] - } - }, - "test_expansion_schema": { - "expansion": [ - "default", - "override" - ], - "source_scheme": [ - "http", - "https" - ], - "source_context_list": [ - "top", - "req", - "srcdoc-inherit", - "srcdoc", - "iframe", - "iframe-blank-inherit", - "worker-classic", - "worker-classic-data", - "worker-module", - "worker-module-data", - "sharedworker-classic", - "sharedworker-classic-data", - "sharedworker-module", - "sharedworker-module-data" - ], - "redirection": [ - "no-redirect", - "keep-origin", - "swap-origin", - "keep-scheme", - "swap-scheme", - "downgrade" - ], - "origin": [ - "same-https", - "same-http", - "same-http-downgrade", - "cross-https", - "cross-http", - "cross-http-downgrade", - "same-wss", - "same-ws", - "same-ws-downgrade", - "cross-wss", - "cross-ws", - "cross-ws-downgrade" - ], - "subresource": [ - "a-tag", - "area-tag", - "audio-tag", - "beacon", - "fetch", - "iframe-tag", - "img-tag", - "link-css-tag", - "link-prefetch-tag", - "object-tag", - "picture-tag", - "script-tag", - "script-tag-dynamic-import", - "sharedworker-classic", - "sharedworker-import", - "sharedworker-import-data", - "sharedworker-module", - "video-tag", - "websocket", - "worker-classic", - "worker-import", - "worker-import-data", - "worker-module", - "worklet-animation", - "worklet-animation-import-data", - "worklet-audio", - "worklet-audio-import-data", - "worklet-layout", - "worklet-layout-import-data", - "worklet-paint", - "worklet-paint-import-data", - "xhr" - ] - } -} diff --git a/test/fixtures/wpt/common/security-features/tools/spec_validator.py b/test/fixtures/wpt/common/security-features/tools/spec_validator.py deleted file mode 100755 index e4c9e145f83..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/spec_validator.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python3 - -import json, sys - - -def assert_non_empty_string(obj, field): - assert field in obj, 'Missing field "%s"' % field - assert isinstance(obj[field], str), \ - 'Field "%s" must be a string' % field - assert len(obj[field]) > 0, 'Field "%s" must not be empty' % field - - -def assert_non_empty_list(obj, field): - assert isinstance(obj[field], list), \ - '%s must be a list' % field - assert len(obj[field]) > 0, \ - '%s list must not be empty' % field - - -def assert_non_empty_dict(obj, field): - assert isinstance(obj[field], dict), \ - '%s must be a dict' % field - assert len(obj[field]) > 0, \ - '%s dict must not be empty' % field - - -def assert_contains(obj, field): - assert field in obj, 'Must contain field "%s"' % field - - -def assert_value_from(obj, field, items): - assert obj[field] in items, \ - 'Field "%s" must be from: %s' % (field, str(items)) - - -def assert_atom_or_list_items_from(obj, field, items): - if isinstance(obj[field], str) or isinstance( - obj[field], int) or obj[field] is None: - assert_value_from(obj, field, items) - return - - assert isinstance(obj[field], list), '%s must be a list' % field - for allowed_value in obj[field]: - assert allowed_value != '*', "Wildcard is not supported for lists!" - assert allowed_value in items, \ - 'Field "%s" must be from: %s' % (field, str(items)) - - -def assert_contains_only_fields(obj, expected_fields): - for expected_field in expected_fields: - assert_contains(obj, expected_field) - - for actual_field in obj: - assert actual_field in expected_fields, \ - 'Unexpected field "%s".' % actual_field - - -def leaf_values(schema): - if isinstance(schema, list): - return schema - ret = [] - for _, sub_schema in schema.iteritems(): - ret += leaf_values(sub_schema) - return ret - - -def assert_value_unique_in(value, used_values): - assert value not in used_values, 'Duplicate value "%s"!' % str(value) - used_values[value] = True - - -def assert_valid_artifact(exp_pattern, artifact_key, schema): - if isinstance(schema, list): - assert_atom_or_list_items_from(exp_pattern, artifact_key, - ["*"] + schema) - return - - for sub_artifact_key, sub_schema in schema.iteritems(): - assert_valid_artifact(exp_pattern[artifact_key], sub_artifact_key, - sub_schema) - - -def validate(spec_json, details): - """ Validates the json specification for generating tests. """ - - details['object'] = spec_json - assert_contains_only_fields(spec_json, [ - "selection_pattern", "test_file_path_pattern", - "test_description_template", "test_page_title_template", - "specification", "delivery_key", "subresource_schema", - "source_context_schema", "source_context_list_schema", - "test_expansion_schema", "excluded_tests" - ]) - assert_non_empty_list(spec_json, "specification") - assert_non_empty_dict(spec_json, "test_expansion_schema") - assert_non_empty_list(spec_json, "excluded_tests") - - specification = spec_json['specification'] - test_expansion_schema = spec_json['test_expansion_schema'] - excluded_tests = spec_json['excluded_tests'] - - valid_test_expansion_fields = test_expansion_schema.keys() - - # Should be consistent with `sourceContextMap` in - # `/common/security-features/resources/common.sub.js`. - valid_source_context_names = [ - "top", "iframe", "iframe-blank", "srcdoc", "worker-classic", - "worker-module", "worker-classic-data", "worker-module-data", - "sharedworker-classic", "sharedworker-module", - "sharedworker-classic-data", "sharedworker-module-data" - ] - - valid_subresource_names = [ - "a-tag", "area-tag", "audio-tag", "form-tag", "iframe-tag", "img-tag", - "link-css-tag", "link-prefetch-tag", "object-tag", "picture-tag", - "script-tag", "script-tag-dynamic-import", "video-tag" - ] + ["beacon", "fetch", "xhr", "websocket"] + [ - "worker-classic", "worker-module", "worker-import", - "worker-import-data", "sharedworker-classic", "sharedworker-module", - "sharedworker-import", "sharedworker-import-data", - "serviceworker-classic", "serviceworker-module", - "serviceworker-import", "serviceworker-import-data" - ] + [ - "worklet-animation", "worklet-audio", "worklet-layout", - "worklet-paint", "worklet-animation-import", "worklet-audio-import", - "worklet-layout-import", "worklet-paint-import", - "worklet-animation-import-data", "worklet-audio-import-data", - "worklet-layout-import-data", "worklet-paint-import-data" - ] - - # Validate each single spec. - for spec in specification: - details['object'] = spec - - # Validate required fields for a single spec. - assert_contains_only_fields(spec, [ - 'title', 'description', 'specification_url', 'test_expansion' - ]) - assert_non_empty_string(spec, 'title') - assert_non_empty_string(spec, 'description') - assert_non_empty_string(spec, 'specification_url') - assert_non_empty_list(spec, 'test_expansion') - - for spec_exp in spec['test_expansion']: - details['object'] = spec_exp - assert_contains_only_fields(spec_exp, valid_test_expansion_fields) - - for artifact in test_expansion_schema: - details['test_expansion_field'] = artifact - assert_valid_artifact(spec_exp, artifact, - test_expansion_schema[artifact]) - del details['test_expansion_field'] - - # Validate source_context_schema. - details['object'] = spec_json['source_context_schema'] - assert_contains_only_fields( - spec_json['source_context_schema'], - ['supported_delivery_type', 'supported_subresource']) - assert_contains_only_fields( - spec_json['source_context_schema']['supported_delivery_type'], - valid_source_context_names) - for source_context in spec_json['source_context_schema'][ - 'supported_delivery_type']: - assert_valid_artifact( - spec_json['source_context_schema']['supported_delivery_type'], - source_context, test_expansion_schema['delivery_type']) - assert_contains_only_fields( - spec_json['source_context_schema']['supported_subresource'], - valid_source_context_names) - for source_context in spec_json['source_context_schema'][ - 'supported_subresource']: - assert_valid_artifact( - spec_json['source_context_schema']['supported_subresource'], - source_context, leaf_values(test_expansion_schema['subresource'])) - - # Validate subresource_schema. - details['object'] = spec_json['subresource_schema'] - assert_contains_only_fields(spec_json['subresource_schema'], - ['supported_delivery_type']) - assert_contains_only_fields( - spec_json['subresource_schema']['supported_delivery_type'], - leaf_values(test_expansion_schema['subresource'])) - for subresource in spec_json['subresource_schema'][ - 'supported_delivery_type']: - assert_valid_artifact( - spec_json['subresource_schema']['supported_delivery_type'], - subresource, test_expansion_schema['delivery_type']) - - # Validate the test_expansion schema members. - details['object'] = test_expansion_schema - assert_contains_only_fields(test_expansion_schema, [ - 'expansion', 'source_scheme', 'source_context_list', 'delivery_type', - 'delivery_value', 'redirection', 'subresource', 'origin', 'expectation' - ]) - assert_atom_or_list_items_from(test_expansion_schema, 'expansion', - ['default', 'override']) - assert_atom_or_list_items_from(test_expansion_schema, 'source_scheme', - ['http', 'https']) - assert_atom_or_list_items_from( - test_expansion_schema, 'source_context_list', - spec_json['source_context_list_schema'].keys()) - - # Should be consistent with `preprocess_redirection` in - # `/common/security-features/subresource/subresource.py`. - assert_atom_or_list_items_from(test_expansion_schema, 'redirection', [ - 'no-redirect', 'keep-origin', 'swap-origin', 'keep-scheme', - 'swap-scheme', 'downgrade' - ]) - for subresource in leaf_values(test_expansion_schema['subresource']): - assert subresource in valid_subresource_names, "Invalid subresource %s" % subresource - # Should be consistent with getSubresourceOrigin() in - # `/common/security-features/resources/common.sub.js`. - assert_atom_or_list_items_from(test_expansion_schema, 'origin', [ - 'same-http', 'same-https', 'same-ws', 'same-wss', 'cross-http', - 'cross-https', 'cross-ws', 'cross-wss', 'same-http-downgrade', - 'cross-http-downgrade', 'same-ws-downgrade', 'cross-ws-downgrade' - ]) - - # Validate excluded tests. - details['object'] = excluded_tests - for excluded_test_expansion in excluded_tests: - assert_contains_only_fields(excluded_test_expansion, - valid_test_expansion_fields) - details['object'] = excluded_test_expansion - for artifact in test_expansion_schema: - details['test_expansion_field'] = artifact - assert_valid_artifact(excluded_test_expansion, artifact, - test_expansion_schema[artifact]) - del details['test_expansion_field'] - - del details['object'] - - -def assert_valid_spec_json(spec_json): - error_details = {} - try: - validate(spec_json, error_details) - except AssertionError as err: - print('ERROR:', err) - print(json.dumps(error_details, indent=4)) - sys.exit(1) - - -def main(): - spec_json = load_spec_json() - assert_valid_spec_json(spec_json) - print("Spec JSON is valid.") - - -if __name__ == '__main__': - main() diff --git a/test/fixtures/wpt/common/security-features/tools/template/disclaimer.template b/test/fixtures/wpt/common/security-features/tools/template/disclaimer.template deleted file mode 100644 index ba9458cb312..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/template/disclaimer.template +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/fixtures/wpt/common/security-features/tools/template/spec_json.js.template b/test/fixtures/wpt/common/security-features/tools/template/spec_json.js.template deleted file mode 100644 index e4cbd034259..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/template/spec_json.js.template +++ /dev/null @@ -1 +0,0 @@ -var SPEC_JSON = %(spec_json)s; diff --git a/test/fixtures/wpt/common/security-features/tools/template/test.debug.html.template b/test/fixtures/wpt/common/security-features/tools/template/test.debug.html.template deleted file mode 100644 index b6be088f611..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/template/test.debug.html.template +++ /dev/null @@ -1,26 +0,0 @@ - -%(generated_disclaimer)s - - - - %(meta_delivery_method)s - - - - - - - -%(helper_js)s - - -
- - diff --git a/test/fixtures/wpt/common/security-features/tools/template/test.release.html.template b/test/fixtures/wpt/common/security-features/tools/template/test.release.html.template deleted file mode 100644 index bac2d5b5a4d..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/template/test.release.html.template +++ /dev/null @@ -1,22 +0,0 @@ - -%(generated_disclaimer)s - - - - %(meta_delivery_method)s - - - -%(helper_js)s - - -
- - diff --git a/test/fixtures/wpt/common/security-features/tools/util.py b/test/fixtures/wpt/common/security-features/tools/util.py deleted file mode 100644 index 5da06f9d51e..00000000000 --- a/test/fixtures/wpt/common/security-features/tools/util.py +++ /dev/null @@ -1,228 +0,0 @@ -import os, sys, json, json5, re -import collections - -script_directory = os.path.dirname(os.path.abspath(__file__)) -template_directory = os.path.abspath( - os.path.join(script_directory, 'template')) -test_root_directory = os.path.abspath( - os.path.join(script_directory, '..', '..', '..')) - - -def get_template(basename): - with open(os.path.join(template_directory, basename), "r") as f: - return f.read() - - -def write_file(filename, contents): - with open(filename, "w") as f: - f.write(contents) - - -def read_nth_line(fp, line_number): - fp.seek(0) - for i, line in enumerate(fp): - if (i + 1) == line_number: - return line - - -def load_spec_json(path_to_spec): - re_error_location = re.compile('line ([0-9]+) column ([0-9]+)') - with open(path_to_spec, "r") as f: - try: - return json5.load(f, object_pairs_hook=collections.OrderedDict) - except ValueError as ex: - print(ex.message) - match = re_error_location.search(ex.message) - if match: - line_number, column = int(match.group(1)), int(match.group(2)) - print(read_nth_line(f, line_number).rstrip()) - print(" " * (column - 1) + "^") - sys.exit(1) - - -class ShouldSkip(Exception): - ''' - Raised when the given combination of subresource type, source context type, - delivery type etc. are not supported and we should skip that configuration. - ShouldSkip is expected in normal generator execution (and thus subsequent - generation continues), as we first enumerate a broad range of configurations - first, and later raise ShouldSkip to filter out unsupported combinations. - - ShouldSkip is distinguished from other general errors that cause immediate - termination of the generator and require fix. - ''' - def __init__(self): - pass - - -class PolicyDelivery(object): - ''' - See `@typedef PolicyDelivery` comments in - `common/security-features/resources/common.sub.js`. - ''' - - def __init__(self, delivery_type, key, value): - self.delivery_type = delivery_type - self.key = key - self.value = value - - def __eq__(self, other): - return type(self) is type(other) and self.__dict__ == other.__dict__ - - @classmethod - def list_from_json(cls, list, target_policy_delivery, - supported_delivery_types): - # type: (dict, PolicyDelivery, typing.List[str]) -> typing.List[PolicyDelivery] - ''' - Parses a JSON object `list` that represents a list of `PolicyDelivery` - and returns a list of `PolicyDelivery`, plus supporting placeholders - (see `from_json()` comments below or - `common/security-features/README.md`). - - Can raise `ShouldSkip`. - ''' - if list is None: - return [] - - out = [] - for obj in list: - policy_delivery = PolicyDelivery.from_json( - obj, target_policy_delivery, supported_delivery_types) - # Drop entries with null values. - if policy_delivery.value is None: - continue - out.append(policy_delivery) - return out - - @classmethod - def from_json(cls, obj, target_policy_delivery, supported_delivery_types): - # type: (dict, PolicyDelivery, typing.List[str]) -> PolicyDelivery - ''' - Parses a JSON object `obj` and returns a `PolicyDelivery` object. - In addition to dicts (in the same format as to_json() outputs), - this method accepts the following placeholders: - "policy": - `target_policy_delivery` - "policyIfNonNull": - `target_policy_delivery` if its value is not None. - "anotherPolicy": - A PolicyDelivery that has the same key as - `target_policy_delivery` but a different value. - The delivery type is selected from `supported_delivery_types`. - - Can raise `ShouldSkip`. - ''' - - if obj == "policy": - policy_delivery = target_policy_delivery - elif obj == "nonNullPolicy": - if target_policy_delivery.value is None: - raise ShouldSkip() - policy_delivery = target_policy_delivery - elif obj == "anotherPolicy": - if len(supported_delivery_types) == 0: - raise ShouldSkip() - policy_delivery = target_policy_delivery.get_another_policy( - supported_delivery_types[0]) - elif isinstance(obj, dict): - policy_delivery = PolicyDelivery(obj['deliveryType'], obj['key'], - obj['value']) - else: - raise Exception('policy delivery is invalid: ' + obj) - - # Omit unsupported combinations of source contexts and delivery type. - if policy_delivery.delivery_type not in supported_delivery_types: - raise ShouldSkip() - - return policy_delivery - - def to_json(self): - # type: () -> dict - return { - "deliveryType": self.delivery_type, - "key": self.key, - "value": self.value - } - - def get_another_policy(self, delivery_type): - # type: (str) -> PolicyDelivery - if self.key == 'referrerPolicy': - # Return 'unsafe-url' (i.e. more unsafe policy than `self.value`) - # as long as possible, to make sure the tests to fail if the - # returned policy is used unexpectedly instead of `self.value`. - # Using safer policy wouldn't be distinguishable from acceptable - # arbitrary policy enforcement by user agents, as specified at - # Step 7 of - # https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer: - # "The user agent MAY alter referrerURL or referrerOrigin at this - # point to enforce arbitrary policy considerations in the - # interests of minimizing data leakage." - # See also the comments at `referrerUrlResolver` in - # `wpt/referrer-policy/generic/test-case.sub.js`. - if self.value != 'unsafe-url': - return PolicyDelivery(delivery_type, self.key, 'unsafe-url') - else: - return PolicyDelivery(delivery_type, self.key, 'no-referrer') - elif self.key == 'mixedContent': - if self.value == 'opt-in': - return PolicyDelivery(delivery_type, self.key, None) - else: - return PolicyDelivery(delivery_type, self.key, 'opt-in') - elif self.key == 'contentSecurityPolicy': - if self.value is not None: - return PolicyDelivery(delivery_type, self.key, None) - else: - return PolicyDelivery(delivery_type, self.key, 'worker-src-none') - elif self.key == 'upgradeInsecureRequests': - if self.value == 'upgrade': - return PolicyDelivery(delivery_type, self.key, None) - else: - return PolicyDelivery(delivery_type, self.key, 'upgrade') - else: - raise Exception('delivery key is invalid: ' + self.key) - - -class SourceContext(object): - def __init__(self, source_context_type, policy_deliveries): - # type: (unicode, typing.List[PolicyDelivery]) -> None - self.source_context_type = source_context_type - self.policy_deliveries = policy_deliveries - - def __eq__(self, other): - return type(self) is type(other) and self.__dict__ == other.__dict__ - - @classmethod - def from_json(cls, obj, target_policy_delivery, source_context_schema): - ''' - Parses a JSON object `obj` and returns a `SourceContext` object. - - `target_policy_delivery` and `source_context_schema` are used for - policy delivery placeholders and filtering out unsupported - delivery types. - - Can raise `ShouldSkip`. - ''' - source_context_type = obj.get('sourceContextType') - policy_deliveries = PolicyDelivery.list_from_json( - obj.get('policyDeliveries'), target_policy_delivery, - source_context_schema['supported_delivery_type'] - [source_context_type]) - return SourceContext(source_context_type, policy_deliveries) - - def to_json(self): - return { - "sourceContextType": self.source_context_type, - "policyDeliveries": [x.to_json() for x in self.policy_deliveries] - } - - -class CustomEncoder(json.JSONEncoder): - ''' - Used to dump dicts containing `SourceContext`/`PolicyDelivery` into JSON. - ''' - def default(self, obj): - if isinstance(obj, SourceContext): - return obj.to_json() - if isinstance(obj, PolicyDelivery): - return obj.to_json() - return json.JSONEncoder.default(self, obj) diff --git a/test/fixtures/wpt/common/security-features/types.md b/test/fixtures/wpt/common/security-features/types.md deleted file mode 100644 index 17079916c1e..00000000000 --- a/test/fixtures/wpt/common/security-features/types.md +++ /dev/null @@ -1,62 +0,0 @@ -# Types around the generator and generated tests - -This document describes types and concepts used across JavaScript and Python parts of this test framework. -Please refer to the JSDoc in `common.sub.js` or docstrings in Python scripts (if any). - -## Scenario - -### Properties - -- All keys of `test_expansion_schema` in `spec.src.json`, except for `expansion`, `delivery_type`, `delivery_value`, and `source_context_list`. Their values are **string**s specified in `test_expansion_schema`. -- `source_context_list` -- `subresource_policy_deliveries` - -### Types - -- Generator (`spec.src.json`): JSON object -- Generator (Python): `dict` -- Runtime (JS): JSON object -- Runtime (Python): N/A - -## `PolicyDelivery` - -### Types - -- Generator (`spec.src.json`): JSON object -- Generator (Python): `util.PolicyDelivery` -- Runtime (JS): JSON object (`@typedef PolicyDelivery` in `common.sub.js`) -- Runtime (Python): N/A - -## `SourceContext` - -Subresource requests can be possibly sent from various kinds of fetch client's environment settings objects. For example: - -- top-level windows, -- ` - - diff --git a/test/fixtures/wpt/eventsource/eventsource-cross-origin.window.js b/test/fixtures/wpt/eventsource/eventsource-cross-origin.window.js deleted file mode 100644 index 23bd27a7dce..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-cross-origin.window.js +++ /dev/null @@ -1,51 +0,0 @@ -// META: title=EventSource: cross-origin - - const crossdomain = location.href.replace('://', '://élève.').replace(/\/[^\/]*$/, '/'), - origin = location.origin.replace('://', '://xn--lve-6lad.'); - - - function doCORS(url, title) { - async_test(document.title + " " + title).step(function() { - var source = new EventSource(url, { withCredentials: true }) - source.onmessage = this.step_func_done(e => { - assert_equals(e.data, "data"); - assert_equals(e.origin, origin); - source.close(); - }) - }) - } - - doCORS(crossdomain + "resources/cors.py?run=message", - "basic use") - doCORS(crossdomain + "resources/cors.py?run=redirect&location=/eventsource/resources/cors.py?run=message", - "redirect use") - doCORS(crossdomain + "resources/cors.py?run=status-reconnect&status=200", - "redirect use recon") - - function failCORS(url, title) { - async_test(document.title + " " + title).step(function() { - var source = new EventSource(url) - source.onerror = this.step_func(function(e) { - assert_equals(source.readyState, source.CLOSED, 'readyState') - assert_false(e.hasOwnProperty('data')) - source.close() - this.done() - }) - - /* Shouldn't happen */ - source.onmessage = this.step_func(function(e) { - assert_unreached("shouldn't fire message event") - }) - source.onopen = this.step_func(function(e) { - assert_unreached("shouldn't fire open event") - }) - }) - } - - failCORS(crossdomain + "resources/cors.py?run=message&origin=http://example.org", - "allow-origin: http://example.org should fail") - failCORS(crossdomain + "resources/cors.py?run=message&origin=", - "allow-origin:'' should fail") - failCORS(crossdomain + "resources/message.py", - "No allow-origin should fail") - diff --git a/test/fixtures/wpt/eventsource/eventsource-eventtarget.any.js b/test/fixtures/wpt/eventsource/eventsource-eventtarget.any.js deleted file mode 100644 index b0d0017dd25..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-eventtarget.any.js +++ /dev/null @@ -1,16 +0,0 @@ -// META: title=EventSource: addEventListener() - - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py") - source.addEventListener("message", listener, false) - }) - function listener(e) { - test.step(function() { - assert_equals("data", e.data) - this.close() - }, this) - test.done() - } - - diff --git a/test/fixtures/wpt/eventsource/eventsource-onmessage-realm.htm b/test/fixtures/wpt/eventsource/eventsource-onmessage-realm.htm deleted file mode 100644 index db2218b5168..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-onmessage-realm.htm +++ /dev/null @@ -1,25 +0,0 @@ - - -EventSource: message event Realm - - - - - - - diff --git a/test/fixtures/wpt/eventsource/eventsource-onmessage-trusted.any.js b/test/fixtures/wpt/eventsource/eventsource-onmessage-trusted.any.js deleted file mode 100644 index d0be4d03e8b..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-onmessage-trusted.any.js +++ /dev/null @@ -1,12 +0,0 @@ -// META: title=EventSource message events are trusted - -"use strict"; - -async_test(t => { - const source = new EventSource("resources/message.py"); - - source.onmessage = t.step_func_done(e => { - source.close(); - assert_equals(e.isTrusted, true); - }); -}, "EventSource message events are trusted"); diff --git a/test/fixtures/wpt/eventsource/eventsource-onmessage.any.js b/test/fixtures/wpt/eventsource/eventsource-onmessage.any.js deleted file mode 100644 index 391fa4b1933..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-onmessage.any.js +++ /dev/null @@ -1,14 +0,0 @@ -// META: title=EventSource: onmessage - - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py") - source.onmessage = function(e) { - test.step(function() { - assert_equals("data", e.data) - source.close() - }) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/eventsource-onopen.any.js b/test/fixtures/wpt/eventsource/eventsource-onopen.any.js deleted file mode 100644 index 3977cb176e0..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-onopen.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// META: title=EventSource: onopen (announcing the connection) - - var test = async_test() - test.step(function() { - source = new EventSource("resources/message.py") - source.onopen = function(e) { - test.step(function() { - assert_equals(source.readyState, source.OPEN) - assert_false(e.hasOwnProperty('data')) - assert_false(e.bubbles) - assert_false(e.cancelable) - this.close() - }, this) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/eventsource-prototype.any.js b/test/fixtures/wpt/eventsource/eventsource-prototype.any.js deleted file mode 100644 index b7aefb32f44..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-prototype.any.js +++ /dev/null @@ -1,10 +0,0 @@ -// META: title=EventSource: prototype et al - - test(function() { - EventSource.prototype.ReturnTrue = function() { return true } - var source = new EventSource("resources/message.py") - assert_true(source.ReturnTrue()) - assert_own_property(self, "EventSource") - source.close() - }) - diff --git a/test/fixtures/wpt/eventsource/eventsource-reconnect.window.js b/test/fixtures/wpt/eventsource/eventsource-reconnect.window.js deleted file mode 100644 index 551fbdc88b2..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-reconnect.window.js +++ /dev/null @@ -1,47 +0,0 @@ -// META: title=EventSource: reconnection - - function doReconn(url, title) { - var test = async_test(document.title + " " + title) - test.step(function() { - var source = new EventSource(url) - source.onmessage = test.step_func(function(e) { - assert_equals(e.data, "data") - source.close() - test.done() - }) - }) - } - - doReconn("resources/status-reconnect.py?status=200", - "200") - - - var t = async_test(document.title + ", test reconnection events"); - t.step(function() { - var opened = false, reconnected = false, - source = new EventSource("resources/status-reconnect.py?status=200&ok_first&id=2"); - - source.onerror = t.step_func(function(e) { - assert_equals(e.type, 'error'); - assert_equals(source.readyState, source.CONNECTING, "readyState"); - assert_true(opened, "connection is opened earlier"); - - reconnected = true; - }); - - source.onmessage = t.step_func(function(e) { - if (!opened) { - opened = true; - assert_false(reconnected, "have reconnected before first message"); - assert_equals(e.data, "ok"); - } - else { - assert_true(reconnected, "Got reconnection event"); - assert_equals(e.data, "data"); - source.close() - t.done() - } - }); - }); - - diff --git a/test/fixtures/wpt/eventsource/eventsource-request-cancellation.window.js b/test/fixtures/wpt/eventsource/eventsource-request-cancellation.window.js deleted file mode 100644 index 1cee9b742ea..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-request-cancellation.window.js +++ /dev/null @@ -1,21 +0,0 @@ -// META: title=EventSource: request cancellation - - var t = async_test(); - onload = t.step_func(function() { - var url = "resources/message.py?sleep=1000&message=" + encodeURIComponent("retry:1000\ndata:abc\n\n"); - var es = new EventSource(url); - es.onerror = t.step_func(function() { - assert_equals(es.readyState, EventSource.CLOSED) - t.step_timeout(function () { - assert_equals(es.readyState, EventSource.CLOSED, - "After stopping the eventsource readyState should be CLOSED") - t.done(); - }, 1000); - }); - - t.step_timeout(function() { - window.stop() - es.onopen = t.unreached_func("Got open event"); - es.onmessage = t.unreached_func("Got message after closing source"); - }, 0); - }); diff --git a/test/fixtures/wpt/eventsource/eventsource-url.any.js b/test/fixtures/wpt/eventsource/eventsource-url.any.js deleted file mode 100644 index 92207ea78a1..00000000000 --- a/test/fixtures/wpt/eventsource/eventsource-url.any.js +++ /dev/null @@ -1,8 +0,0 @@ -// META: title=EventSource: url - - test(function() { - var url = "resources/message.py", - source = new EventSource(url) - assert_equals(source.url.substr(-(url.length)), url) - source.close() - }) diff --git a/test/fixtures/wpt/eventsource/format-bom-2.any.js b/test/fixtures/wpt/eventsource/format-bom-2.any.js deleted file mode 100644 index 8b7be8402c0..00000000000 --- a/test/fixtures/wpt/eventsource/format-bom-2.any.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: title=EventSource: Double BOM - - var test = async_test(), - hasbeenone = false, - hasbeentwo = false - test.step(function() { - var source = new EventSource("resources/message.py?message=%EF%BB%BF%EF%BB%BFdata%3A1%0A%0Adata%3A2%0A%0Adata%3A3") - source.addEventListener("message", listener, false) - }) - function listener(e) { - test.step(function() { - if(e.data == "1") - hasbeenone = true - if(e.data == "2") - hasbeentwo = true - if(e.data == "3") { - assert_false(hasbeenone) - assert_true(hasbeentwo) - this.close() - test.done() - } - }, this) - } - diff --git a/test/fixtures/wpt/eventsource/format-bom.any.js b/test/fixtures/wpt/eventsource/format-bom.any.js deleted file mode 100644 index 05d1abd18b1..00000000000 --- a/test/fixtures/wpt/eventsource/format-bom.any.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: title=EventSource: BOM - - var test = async_test(), - hasbeenone = false, - hasbeentwo = false - test.step(function() { - var source = new EventSource("resources/message.py?message=%EF%BB%BFdata%3A1%0A%0A%EF%BB%BFdata%3A2%0A%0Adata%3A3") - source.addEventListener("message", listener, false) - }) - function listener(e) { - test.step(function() { - if(e.data == "1") - hasbeenone = true - if(e.data == "2") - hasbeentwo = true - if(e.data == "3") { - assert_true(hasbeenone) - assert_false(hasbeentwo) - this.close() - test.done() - } - }, this) - } - diff --git a/test/fixtures/wpt/eventsource/format-comments.any.js b/test/fixtures/wpt/eventsource/format-comments.any.js deleted file mode 100644 index 186e4714ba3..00000000000 --- a/test/fixtures/wpt/eventsource/format-comments.any.js +++ /dev/null @@ -1,16 +0,0 @@ -// META: title=EventSource: comment fest - - var test = async_test() - test.step(function() { - var longstring = (new Array(2*1024+1)).join("x"), // cannot make the string too long; causes timeout - message = encodeURI("data:1\r:\0\n:\r\ndata:2\n:" + longstring + "\rdata:3\n:data:fail\r:" + longstring + "\ndata:4\n"), - source = new EventSource("resources/message.py?message=" + message + "&newline=none") - source.onmessage = function(e) { - test.step(function() { - assert_equals("1\n2\n3\n4", e.data) - source.close() - }) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-data-before-final-empty-line.any.js b/test/fixtures/wpt/eventsource/format-data-before-final-empty-line.any.js deleted file mode 100644 index 5a4d84d28d3..00000000000 --- a/test/fixtures/wpt/eventsource/format-data-before-final-empty-line.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// META: title=EventSource: a data before final empty line - - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?newline=none&message=" + encodeURIComponent("retry:1000\ndata:test1\n\nid:test\ndata:test2")) - var count = 0; - source.onmessage = function(e) { - if (++count === 2) { - test.step(function() { - assert_equals(e.lastEventId, "", "lastEventId") - assert_equals(e.data, "test1", "data") - source.close() - }) - test.done() - } - } - }) diff --git a/test/fixtures/wpt/eventsource/format-field-data.any.js b/test/fixtures/wpt/eventsource/format-field-data.any.js deleted file mode 100644 index bea9be17424..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-data.any.js +++ /dev/null @@ -1,23 +0,0 @@ -// META: title=EventSource: data field parsing - - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?message=data%3A%0A%0Adata%0Adata%0A%0Adata%3Atest"), - counter = 0 - source.onmessage = function(e) { - test.step(function() { - if(counter == 0) { - assert_equals("", e.data) - } else if(counter == 1) { - assert_equals("\n", e.data) - } else if(counter == 2) { - assert_equals("test", e.data) - source.close() - test.done() - } else { - assert_unreached() - } - counter++ - }) - } - }) diff --git a/test/fixtures/wpt/eventsource/format-field-event-empty.any.js b/test/fixtures/wpt/eventsource/format-field-event-empty.any.js deleted file mode 100644 index ada8e5725fe..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-event-empty.any.js +++ /dev/null @@ -1,13 +0,0 @@ -// META: title=EventSource: empty "event" field - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?message=event%3A%20%0Adata%3Adata") - source.onmessage = function(e) { - test.step(function() { - assert_equals("data", e.data) - this.close() - }, this) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-field-event.any.js b/test/fixtures/wpt/eventsource/format-field-event.any.js deleted file mode 100644 index 0c7d1fc2662..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-event.any.js +++ /dev/null @@ -1,15 +0,0 @@ -// META: title=EventSource: custom event name - var test = async_test(), - dispatchedtest = false - test.step(function() { - var source = new EventSource("resources/message.py?message=event%3Atest%0Adata%3Ax%0A%0Adata%3Ax") - source.addEventListener("test", function() { test.step(function() { dispatchedtest = true }) }, false) - source.onmessage = function() { - test.step(function() { - assert_true(dispatchedtest) - this.close() - }, this) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-field-id-2.any.js b/test/fixtures/wpt/eventsource/format-field-id-2.any.js deleted file mode 100644 index 9933f46b875..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-id-2.any.js +++ /dev/null @@ -1,25 +0,0 @@ -// META: title=EventSource: Last-Event-ID (2) - var test = async_test() - test.step(function() { - var source = new EventSource("resources/last-event-id.py"), - counter = 0 - source.onmessage = function(e) { - test.step(function() { - if(e.data == "hello" && counter == 0) { - counter++ - assert_equals(e.lastEventId, "…") - } else if(counter == 1) { - counter++ - assert_equals("…", e.data) - assert_equals("…", e.lastEventId) - } else if(counter == 2) { - counter++ - assert_equals("…", e.data) - assert_equals("…", e.lastEventId) - source.close() - test.done() - } else - assert_unreached() - }) - } - }) diff --git a/test/fixtures/wpt/eventsource/format-field-id-3.window.js b/test/fixtures/wpt/eventsource/format-field-id-3.window.js deleted file mode 100644 index 3766fbf7bb1..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-id-3.window.js +++ /dev/null @@ -1,56 +0,0 @@ -const ID_PERSISTS = 1, -ID_RESETS_1 = 2, -ID_RESETS_2 = 3; - -async_test(testPersist, "EventSource: lastEventId persists"); -async_test(testReset(ID_RESETS_1), "EventSource: lastEventId resets"); -async_test(testReset(ID_RESETS_2), "EventSource: lastEventId resets (id without colon)"); - -function testPersist(t) { - const source = new EventSource("resources/last-event-id2.py?type=" + ID_PERSISTS); - let counter = 0; - t.add_cleanup(() => source.close()); - source.onmessage = t.step_func(e => { - counter++; - if (counter === 1) { - assert_equals(e.lastEventId, "1"); - assert_equals(e.data, "1"); - } else if (counter === 2) { - assert_equals(e.lastEventId, "1"); - assert_equals(e.data, "2"); - } else if (counter === 3) { - assert_equals(e.lastEventId, "2"); - assert_equals(e.data, "3"); - } else if (counter === 4) { - assert_equals(e.lastEventId, "2"); - assert_equals(e.data, "4"); - t.done(); - } else { - assert_unreached(); - } - }); -} - -function testReset(type) { - return function (t) { - const source = new EventSource("resources/last-event-id2.py?type=" + type); - let counter = 0; - t.add_cleanup(() => source.close()); - source.onmessage = t.step_func(e => { - counter++; - if (counter === 1) { - assert_equals(e.lastEventId, "1"); - assert_equals(e.data, "1"); - } else if (counter === 2) { - assert_equals(e.lastEventId, ""); - assert_equals(e.data, "2"); - } else if (counter === 3) { - assert_equals(e.lastEventId, ""); - assert_equals(e.data, "3"); - t.done(); - } else { - assert_unreached(); - } - }); - } -} diff --git a/test/fixtures/wpt/eventsource/format-field-id-null.window.js b/test/fixtures/wpt/eventsource/format-field-id-null.window.js deleted file mode 100644 index 6d564dde0f2..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-id-null.window.js +++ /dev/null @@ -1,25 +0,0 @@ -[ - "\u0000\u0000", - "x\u0000", - "\u0000x", - "x\u0000x", - " \u0000" -].forEach(idValue => { - const encodedIdValue = encodeURIComponent(idValue); - async_test(t => { - const source = new EventSource("resources/last-event-id.py?idvalue=" + encodedIdValue); - t.add_cleanup(() => source.close()); - let seenhello = false; - source.onmessage = t.step_func(e => { - if (e.data == "hello" && !seenhello) { - seenhello = true; - assert_equals(e.lastEventId, ""); - } else if(seenhello) { - assert_equals(e.data, "hello"); - assert_equals(e.lastEventId, ""); - t.done(); - } else - assert_unreached(); - }); - }, "EventSource: id field set to " + encodedIdValue); -}); diff --git a/test/fixtures/wpt/eventsource/format-field-id.any.js b/test/fixtures/wpt/eventsource/format-field-id.any.js deleted file mode 100644 index 26f1aea7091..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-id.any.js +++ /dev/null @@ -1,21 +0,0 @@ -// META: title=EventSource: Last-Event-ID - var test = async_test() - test.step(function() { - var source = new EventSource("resources/last-event-id.py"), - seenhello = false - source.onmessage = function(e) { - test.step(function() { - if(e.data == "hello" && !seenhello) { - seenhello = true - assert_equals(e.lastEventId, "…") - } else if(seenhello) { - assert_equals("…", e.data) - assert_equals("…", e.lastEventId) - source.close() - test.done() - } else - assert_unreached() - }) - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-field-parsing.any.js b/test/fixtures/wpt/eventsource/format-field-parsing.any.js deleted file mode 100644 index 9b05187153a..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-parsing.any.js +++ /dev/null @@ -1,14 +0,0 @@ -// META: title=EventSource: field parsing - var test = async_test() - test.step(function() { - var message = encodeURI("data:\0\ndata: 2\rData:1\ndata\0:2\ndata:1\r\0data:4\nda-ta:3\rdata_5\ndata:3\rdata:\r\n data:32\ndata:4\n"), - source = new EventSource("resources/message.py?message=" + message + "&newline=none") - source.onmessage = function(e) { - test.step(function() { - assert_equals(e.data, "\0\n 2\n1\n3\n\n4") - source.close() - }) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-field-retry-bogus.any.js b/test/fixtures/wpt/eventsource/format-field-retry-bogus.any.js deleted file mode 100644 index 86d9b9ea409..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-retry-bogus.any.js +++ /dev/null @@ -1,19 +0,0 @@ -// META: title=EventSource: "retry" field (bogus) - var test = async_test() - test.step(function() { - var timeoutms = 3000, - source = new EventSource("resources/message.py?message=retry%3A3000%0Aretry%3A1000x%0Adata%3Ax"), - opened = 0 - source.onopen = function() { - test.step(function() { - if(opened == 0) - opened = new Date().getTime() - else { - var diff = (new Date().getTime()) - opened - assert_true(Math.abs(1 - diff / timeoutms) < 0.25) // allow 25% difference - this.close(); - test.done() - } - }, this) - } - }) diff --git a/test/fixtures/wpt/eventsource/format-field-retry-empty.any.js b/test/fixtures/wpt/eventsource/format-field-retry-empty.any.js deleted file mode 100644 index e7d5e76a134..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-retry-empty.any.js +++ /dev/null @@ -1,13 +0,0 @@ -// META: title=EventSource: empty retry field - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?message=retry%0Adata%3Atest") - source.onmessage = function(e) { - test.step(function() { - assert_equals("test", e.data) - source.close() - }) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-field-retry.any.js b/test/fixtures/wpt/eventsource/format-field-retry.any.js deleted file mode 100644 index 819241dbd40..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-retry.any.js +++ /dev/null @@ -1,21 +0,0 @@ -// META: title=EventSource: "retry" field - var test = async_test(); - test.step(function() { - var timeoutms = 3000, - timeoutstr = "03000", // 1536 in octal, but should be 3000 - source = new EventSource("resources/message.py?message=retry%3A" + timeoutstr + "%0Adata%3Ax"), - opened = 0 - source.onopen = function() { - test.step(function() { - if(opened == 0) - opened = new Date().getTime() - else { - var diff = (new Date().getTime()) - opened - assert_true(Math.abs(1 - diff / timeoutms) < 0.25) // allow 25% difference - this.close(); - test.done() - } - }, this) - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-field-unknown.any.js b/test/fixtures/wpt/eventsource/format-field-unknown.any.js deleted file mode 100644 index f702ed8565d..00000000000 --- a/test/fixtures/wpt/eventsource/format-field-unknown.any.js +++ /dev/null @@ -1,13 +0,0 @@ -// META: title=EventSource: unknown fields and parsing fun - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?message=data%3Atest%0A%20data%0Adata%0Afoobar%3Axxx%0Ajustsometext%0A%3Athisisacommentyay%0Adata%3Atest") - source.onmessage = function(e) { - test.step(function() { - assert_equals("test\n\ntest", e.data) - source.close() - }) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-leading-space.any.js b/test/fixtures/wpt/eventsource/format-leading-space.any.js deleted file mode 100644 index 0ddfd9b32bb..00000000000 --- a/test/fixtures/wpt/eventsource/format-leading-space.any.js +++ /dev/null @@ -1,14 +0,0 @@ -// META: title=EventSource: leading space - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?message=data%3A%09test%0Ddata%3A%20%0Adata%3Atest") - source.onmessage = function(e) { - test.step(function() { - assert_equals("\ttest\n\ntest", e.data) - source.close() - }) - test.done() - } - }) - // also used a CR as newline once - diff --git a/test/fixtures/wpt/eventsource/format-mime-bogus.any.js b/test/fixtures/wpt/eventsource/format-mime-bogus.any.js deleted file mode 100644 index 18c7c7d4a49..00000000000 --- a/test/fixtures/wpt/eventsource/format-mime-bogus.any.js +++ /dev/null @@ -1,25 +0,0 @@ -// META: title=EventSource: bogus MIME type - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?mime=x%20bogus") - source.onmessage = function() { - test.step(function() { - assert_unreached() - source.close() - }) - test.done() - } - source.onerror = function(e) { - test.step(function() { - assert_equals(this.readyState, this.CLOSED) - assert_false(e.hasOwnProperty('data')) - assert_false(e.bubbles) - assert_false(e.cancelable) - this.close() - }, this) - test.done() - } - }) - // This tests "fails the connection" as well as making sure a simple - // event is dispatched and not a MessageEvent - diff --git a/test/fixtures/wpt/eventsource/format-mime-trailing-semicolon.any.js b/test/fixtures/wpt/eventsource/format-mime-trailing-semicolon.any.js deleted file mode 100644 index 55a314bf524..00000000000 --- a/test/fixtures/wpt/eventsource/format-mime-trailing-semicolon.any.js +++ /dev/null @@ -1,20 +0,0 @@ -// META: title=EventSource: MIME type with trailing ; - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?mime=text/event-stream%3B") - source.onopen = function() { - test.step(function() { - assert_equals(source.readyState, source.OPEN) - source.close() - }) - test.done() - } - source.onerror = function() { - test.step(function() { - assert_unreached() - source.close() - }) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-mime-valid-bogus.any.js b/test/fixtures/wpt/eventsource/format-mime-valid-bogus.any.js deleted file mode 100644 index 355ba6c524f..00000000000 --- a/test/fixtures/wpt/eventsource/format-mime-valid-bogus.any.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: title=EventSource: incorrect valid MIME type - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?mime=text/x-bogus") - source.onmessage = function() { - test.step(function() { - assert_unreached() - source.close() - }) - test.done() - } - source.onerror = function(e) { - test.step(function() { - assert_equals(source.readyState, source.CLOSED) - assert_false(e.hasOwnProperty('data')) - assert_false(e.bubbles) - assert_false(e.cancelable) - }) - test.done() - } - }) - // This tests "fails the connection" as well as making sure a simple - // event is dispatched and not a MessageEvent - diff --git a/test/fixtures/wpt/eventsource/format-newlines.any.js b/test/fixtures/wpt/eventsource/format-newlines.any.js deleted file mode 100644 index 0768171d333..00000000000 --- a/test/fixtures/wpt/eventsource/format-newlines.any.js +++ /dev/null @@ -1,13 +0,0 @@ -// META: title=EventSource: newline fest - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?message=data%3Atest%0D%0Adata%0Adata%3Atest%0D%0A%0D&newline=none") - source.onmessage = function(e) { - test.step(function() { - assert_equals("test\n\ntest", e.data) - source.close() - }) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-null-character.any.js b/test/fixtures/wpt/eventsource/format-null-character.any.js deleted file mode 100644 index 943628d2c02..00000000000 --- a/test/fixtures/wpt/eventsource/format-null-character.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// META: title=EventSource: null character in response - var test = async_test() - test.step(function() { - var source = new EventSource("resources/message.py?message=data%3A%00%0A%0A") - source.onmessage = function(e) { - test.step(function() { - assert_equals("\x00", e.data) - source.close() - }, this) - test.done() - } - source.onerror = function() { - test.step(function() { assert_unreached() }) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/format-utf-8.any.js b/test/fixtures/wpt/eventsource/format-utf-8.any.js deleted file mode 100644 index 7976abfb55d..00000000000 --- a/test/fixtures/wpt/eventsource/format-utf-8.any.js +++ /dev/null @@ -1,12 +0,0 @@ -// META: title=EventSource always UTF-8 -async_test().step(function() { - var source = new EventSource("resources/message.py?mime=text/event-stream%3bcharset=windows-1252&message=data%3Aok%E2%80%A6") - source.onmessage = this.step_func(function(e) { - assert_equals('ok…', e.data, 'decoded data') - source.close() - this.done() - }) - source.onerror = this.step_func(function() { - assert_unreached("Got error event") - }) -}) diff --git a/test/fixtures/wpt/eventsource/request-accept.any.js b/test/fixtures/wpt/eventsource/request-accept.any.js deleted file mode 100644 index 2e181735564..00000000000 --- a/test/fixtures/wpt/eventsource/request-accept.any.js +++ /dev/null @@ -1,13 +0,0 @@ -// META: title=EventSource: Accept header - var test = async_test() - test.step(function() { - var source = new EventSource("resources/accept.event_stream?pipe=sub") - source.onmessage = function(e) { - test.step(function() { - assert_equals(e.data, "text/event-stream") - this.close() - }, this) - test.done() - } - }) - diff --git a/test/fixtures/wpt/eventsource/request-cache-control.any.js b/test/fixtures/wpt/eventsource/request-cache-control.any.js deleted file mode 100644 index 95b71d7a589..00000000000 --- a/test/fixtures/wpt/eventsource/request-cache-control.any.js +++ /dev/null @@ -1,35 +0,0 @@ -// META: title=EventSource: Cache-Control - var crossdomain = location.href - .replace('://', '://www2.') - .replace(/\/[^\/]*$/, '/') - - // running it twice to check whether it stays consistent - function cacheTest(url) { - var test = async_test(url + "1") - // Recursive test. This avoids test that timeout - var test2 = async_test(url + "2") - test.step(function() { - var source = new EventSource(url) - source.onmessage = function(e) { - test.step(function() { - assert_equals(e.data, "no-cache") - this.close() - test2.step(function() { - var source2 = new EventSource(url) - source2.onmessage = function(e) { - test2.step(function() { - assert_equals(e.data, "no-cache") - this.close() - }, this) - test2.done() - } - }) - }, this) - test.done() - } - }) - } - - cacheTest("resources/cache-control.event_stream?pipe=sub") - cacheTest(crossdomain + "resources/cors.py?run=cache-control") - diff --git a/test/fixtures/wpt/eventsource/request-credentials.window.js b/test/fixtures/wpt/eventsource/request-credentials.window.js deleted file mode 100644 index d7c554aa4a2..00000000000 --- a/test/fixtures/wpt/eventsource/request-credentials.window.js +++ /dev/null @@ -1,37 +0,0 @@ -// META: title=EventSource: credentials - var crossdomain = location.href - .replace('://', '://www2.') - .replace(/\/[^\/]*$/, '/') - - function testCookie(desc, success, props, id) { - var test = async_test(document.title + ': credentials ' + desc) - test.step(function() { - var source = new EventSource(crossdomain + "resources/cors-cookie.py?ident=" + id, props) - - source.onmessage = test.step_func(function(e) { - if(e.data.indexOf("first") == 0) { - assert_equals(e.data, "first NO_COOKIE", "cookie status") - } - else if(e.data.indexOf("second") == 0) { - if (success) - assert_equals(e.data, "second COOKIE", "cookie status") - else - assert_equals(e.data, "second NO_COOKIE", "cookie status") - - source.close() - test.done() - } - else { - assert_unreached("unrecognized data returned: " + e.data) - source.close() - test.done() - } - }) - }) - } - - testCookie('enabled', true, { withCredentials: true }, '1_' + new Date().getTime()) - testCookie('disabled', false, { withCredentials: false }, '2_' + new Date().getTime()) - testCookie('default', false, { }, '3_' + new Date().getTime()) - - diff --git a/test/fixtures/wpt/eventsource/request-redirect.window.js b/test/fixtures/wpt/eventsource/request-redirect.window.js deleted file mode 100644 index 3788dd84502..00000000000 --- a/test/fixtures/wpt/eventsource/request-redirect.window.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: title=EventSource: redirect - function redirectTest(status) { - var test = async_test(document.title + " (" + status +")") - test.step(function() { - var source = new EventSource("/common/redirect.py?location=/eventsource/resources/message.py&status=" + status) - source.onopen = function() { - test.step(function() { - assert_equals(this.readyState, this.OPEN) - this.close() - }, this) - test.done() - } - source.onerror = function() { - test.step(function() { assert_unreached() }) - test.done() - } - }) - } - - redirectTest("301") - redirectTest("302") - redirectTest("303") - redirectTest("307") - diff --git a/test/fixtures/wpt/eventsource/request-status-error.window.js b/test/fixtures/wpt/eventsource/request-status-error.window.js deleted file mode 100644 index 8632d8e8c6b..00000000000 --- a/test/fixtures/wpt/eventsource/request-status-error.window.js +++ /dev/null @@ -1,27 +0,0 @@ -// META: title=EventSource: incorrect HTTP status code - function statusTest(status) { - var test = async_test(document.title + " (" + status +")") - test.step(function() { - var source = new EventSource("resources/status-error.py?status=" + status) - source.onmessage = function() { - test.step(function() { - assert_unreached() - }) - test.done() - } - source.onerror = function() { - test.step(function() { - assert_equals(this.readyState, this.CLOSED) - }, this) - test.done() - } - }) - } - statusTest("204") - statusTest("205") - statusTest("210") - statusTest("299") - statusTest("404") - statusTest("410") - statusTest("503") - diff --git a/test/fixtures/wpt/eventsource/resources/accept.event_stream b/test/fixtures/wpt/eventsource/resources/accept.event_stream deleted file mode 100644 index 24da5482678..00000000000 --- a/test/fixtures/wpt/eventsource/resources/accept.event_stream +++ /dev/null @@ -1,2 +0,0 @@ -data: {{headers[accept]}} - diff --git a/test/fixtures/wpt/eventsource/resources/cache-control.event_stream b/test/fixtures/wpt/eventsource/resources/cache-control.event_stream deleted file mode 100644 index aa9f2d6c090..00000000000 --- a/test/fixtures/wpt/eventsource/resources/cache-control.event_stream +++ /dev/null @@ -1,2 +0,0 @@ -data: {{headers[cache-control]}} - diff --git a/test/fixtures/wpt/eventsource/resources/cors-cookie.py b/test/fixtures/wpt/eventsource/resources/cors-cookie.py deleted file mode 100644 index 9eaab9b95a0..00000000000 --- a/test/fixtures/wpt/eventsource/resources/cors-cookie.py +++ /dev/null @@ -1,31 +0,0 @@ -from datetime import datetime - -def main(request, response): - last_event_id = request.headers.get(b"Last-Event-Id", b"") - ident = request.GET.first(b'ident', b"test") - cookie = b"COOKIE" if ident in request.cookies else b"NO_COOKIE" - origin = request.GET.first(b'origin', request.headers[b"origin"]) - credentials = request.GET.first(b'credentials', b'true') - - headers = [] - - if origin != b'none': - headers.append((b"Access-Control-Allow-Origin", origin)); - - if credentials != b'none': - headers.append((b"Access-Control-Allow-Credentials", credentials)); - - if last_event_id == b'': - headers.append((b"Content-Type", b"text/event-stream")) - response.set_cookie(ident, b"COOKIE") - data = b"id: 1\nretry: 200\ndata: first %s\n\n" % cookie - elif last_event_id == b'1': - headers.append((b"Content-Type", b"text/event-stream")) - long_long_time_ago = datetime.now().replace(year=2001, month=7, day=27) - response.set_cookie(ident, b"COOKIE", expires=long_long_time_ago) - data = b"id: 2\ndata: second %s\n\n" % cookie - else: - headers.append((b"Content-Type", b"stop")) - data = b"data: " + last_event_id + cookie + b"\n\n"; - - return headers, data diff --git a/test/fixtures/wpt/eventsource/resources/cors.py b/test/fixtures/wpt/eventsource/resources/cors.py deleted file mode 100644 index 6ed31f2cd7d..00000000000 --- a/test/fixtures/wpt/eventsource/resources/cors.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -from wptserve import pipes - -from wptserve.utils import isomorphic_decode - -def run_other(request, response, path): - #This is a terrible hack - environ = {u"__file__": path} - exec(compile(open(path, u"r").read(), path, u'exec'), environ, environ) - rv = environ[u"main"](request, response) - return rv - -def main(request, response): - origin = request.GET.first(b"origin", request.headers[b"origin"]) - credentials = request.GET.first(b"credentials", b"true") - - response.headers.update([(b"Access-Control-Allow-Origin", origin), - (b"Access-Control-Allow-Credentials", credentials)]) - - handler = request.GET.first(b'run') - if handler in [b"status-reconnect", - b"message", - b"redirect", - b"cache-control"]: - if handler == b"cache-control": - response.headers.set(b"Content-Type", b"text/event-stream") - rv = open(os.path.join(request.doc_root, u"eventsource", u"resources", u"cache-control.event_stream"), u"r").read() - response.content = rv - pipes.sub(request, response) - return - elif handler == b"redirect": - return run_other(request, response, os.path.join(request.doc_root, u"common", u"redirect.py")) - else: - return run_other(request, response, os.path.join(os.path.dirname(isomorphic_decode(__file__)), isomorphic_decode(handler) + u".py")) - else: - return diff --git a/test/fixtures/wpt/eventsource/resources/eventsource-onmessage-realm.htm b/test/fixtures/wpt/eventsource/resources/eventsource-onmessage-realm.htm deleted file mode 100644 index 63e6d012b4d..00000000000 --- a/test/fixtures/wpt/eventsource/resources/eventsource-onmessage-realm.htm +++ /dev/null @@ -1,2 +0,0 @@ - -This page is just used to grab an EventSource constructor diff --git a/test/fixtures/wpt/eventsource/resources/init.htm b/test/fixtures/wpt/eventsource/resources/init.htm deleted file mode 100644 index 7c56d88800d..00000000000 --- a/test/fixtures/wpt/eventsource/resources/init.htm +++ /dev/null @@ -1,9 +0,0 @@ - - - - support init file - - - - - diff --git a/test/fixtures/wpt/eventsource/resources/last-event-id.py b/test/fixtures/wpt/eventsource/resources/last-event-id.py deleted file mode 100644 index a2cb7264457..00000000000 --- a/test/fixtures/wpt/eventsource/resources/last-event-id.py +++ /dev/null @@ -1,9 +0,0 @@ -def main(request, response): - response.headers.set(b"Content-Type", b"text/event-stream") - - last_event_id = request.headers.get(b"Last-Event-ID", b"") - if last_event_id: - return b"data: " + last_event_id + b"\n\n" - else: - idvalue = request.GET.first(b"idvalue", u"\u2026".encode("utf-8")) - return b"id: " + idvalue + b"\nretry: 200\ndata: hello\n\n" diff --git a/test/fixtures/wpt/eventsource/resources/last-event-id2.py b/test/fixtures/wpt/eventsource/resources/last-event-id2.py deleted file mode 100644 index 4f133d707d1..00000000000 --- a/test/fixtures/wpt/eventsource/resources/last-event-id2.py +++ /dev/null @@ -1,23 +0,0 @@ -ID_PERSISTS = 1 -ID_RESETS_1 = 2 -ID_RESETS_2 = 3 - -def main(request, response): - response.headers.set(b"Content-Type", b"text/event-stream") - try: - test_type = int(request.GET.first(b"type", ID_PERSISTS)) - except: - test_type = ID_PERSISTS - - if test_type == ID_PERSISTS: - return b"id: 1\ndata: 1\n\ndata: 2\n\nid: 2\ndata:3\n\ndata:4\n\n" - - elif test_type == ID_RESETS_1: - return b"id: 1\ndata: 1\n\nid:\ndata:2\n\ndata:3\n\n" - - # empty id field without colon character (:) should also reset - elif test_type == ID_RESETS_2: - return b"id: 1\ndata: 1\n\nid\ndata:2\n\ndata:3\n\n" - - else: - return b"data: invalid_test\n\n" diff --git a/test/fixtures/wpt/eventsource/resources/message.py b/test/fixtures/wpt/eventsource/resources/message.py deleted file mode 100644 index 468564f4df0..00000000000 --- a/test/fixtures/wpt/eventsource/resources/message.py +++ /dev/null @@ -1,14 +0,0 @@ -import time - -def main(request, response): - mime = request.GET.first(b"mime", b"text/event-stream") - message = request.GET.first(b"message", b"data: data"); - newline = b"" if request.GET.first(b"newline", None) == b"none" else b"\n\n"; - sleep = int(request.GET.first(b"sleep", b"0")) - - headers = [(b"Content-Type", mime)] - body = message + newline + b"\n" - if sleep != 0: - time.sleep(sleep/1000) - - return headers, body diff --git a/test/fixtures/wpt/eventsource/resources/message2.py b/test/fixtures/wpt/eventsource/resources/message2.py deleted file mode 100644 index 8515e7b25eb..00000000000 --- a/test/fixtures/wpt/eventsource/resources/message2.py +++ /dev/null @@ -1,33 +0,0 @@ -import time - -def main(request, response): - response.headers.set(b'Content-Type', b'text/event-stream') - response.headers.set(b'Cache-Control', b'no-cache') - - response.write_status_headers() - - while True: - response.writer.write(u"data:msg") - response.writer.write(u"\n") - response.writer.write(u"data: msg") - response.writer.write(u"\n\n") - - response.writer.write(u":") - response.writer.write(u"\n") - - response.writer.write(u"falsefield:msg") - response.writer.write(u"\n\n") - - response.writer.write(u"falsefield:msg") - response.writer.write(u"\n") - - response.writer.write(u"Data:data") - response.writer.write(u"\n\n") - - response.writer.write(u"data") - response.writer.write(u"\n\n") - - response.writer.write(u"data:end") - response.writer.write(u"\n\n") - - time.sleep(2) diff --git a/test/fixtures/wpt/eventsource/resources/reconnect-fail.py b/test/fixtures/wpt/eventsource/resources/reconnect-fail.py deleted file mode 100644 index 12b07700cd0..00000000000 --- a/test/fixtures/wpt/eventsource/resources/reconnect-fail.py +++ /dev/null @@ -1,24 +0,0 @@ -def main(request, response): - name = b"recon_fail_" + request.GET.first(b"id") - - headers = [(b"Content-Type", b"text/event-stream")] - cookie = request.cookies.first(name, None) - state = cookie.value if cookie is not None else None - - if state == b'opened': - status = (200, b"RECONNECT") - response.set_cookie(name, b"reconnected"); - body = b"data: reconnected\n\n"; - - elif state == b'reconnected': - status = (204, b"NO CONTENT (CLOSE)") - response.delete_cookie(name); - body = b"data: closed\n\n" # Will never get through - - else: - status = (200, b"OPEN"); - response.set_cookie(name, b"opened"); - body = b"retry: 2\ndata: opened\n\n"; - - return status, headers, body - diff --git a/test/fixtures/wpt/eventsource/resources/status-error.py b/test/fixtures/wpt/eventsource/resources/status-error.py deleted file mode 100644 index ed5687b6c2b..00000000000 --- a/test/fixtures/wpt/eventsource/resources/status-error.py +++ /dev/null @@ -1,15 +0,0 @@ -def main(request, response): - status = (request.GET.first(b"status", b"404"), b"HAHAHAHA") - headers = [(b"Content-Type", b"text/event-stream")] - - # According to RFC7231, HTTP responses bearing status code 204 or 205 must - # not specify a body. The expected browser behavior for this condition is not - # currently defined--see the following for further discussion: - # - # https://github.com/web-platform-tests/wpt/pull/5227 - if status[0] in [b"204", b"205"]: - body = b"" - else: - body = b"data: data\n\n" - - return status, headers, body diff --git a/test/fixtures/wpt/eventsource/resources/status-reconnect.py b/test/fixtures/wpt/eventsource/resources/status-reconnect.py deleted file mode 100644 index a59f751fc36..00000000000 --- a/test/fixtures/wpt/eventsource/resources/status-reconnect.py +++ /dev/null @@ -1,21 +0,0 @@ -def main(request, response): - status_code = request.GET.first(b"status", b"204") - name = request.GET.first(b"id", status_code) - - headers = [(b"Content-Type", b"text/event-stream")] - - cookie_name = b"request" + name - - if request.cookies.first(cookie_name, b"") == status_code: - status = 200 - response.delete_cookie(cookie_name) - body = b"data: data\n\n" - else: - response.set_cookie(cookie_name, status_code); - status = (int(status_code), b"TEST") - body = b"retry: 2\n" - if b"ok_first" in request.GET: - body += b"data: ok\n\n" - - return status, headers, body - diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.htm deleted file mode 100644 index 30fbc309ab6..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.htm +++ /dev/null @@ -1,24 +0,0 @@ - - - - shared worker - EventSource: close() - - - - -
- - - \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.js deleted file mode 100644 index 8d160b605f2..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.js +++ /dev/null @@ -1,12 +0,0 @@ -onconnect = function(e) { -try { - var port = e.ports[0] - var source = new EventSource("../resources/message.py") - source.onopen = function(e) { - this.close() - port.postMessage([true, this.readyState]) - } -} catch(e) { - port.postMessage([false, String(e)]) -} -} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.htm deleted file mode 100644 index 690cde36002..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.htm +++ /dev/null @@ -1,34 +0,0 @@ - - - - shared worker - EventSource: constructor (act as if there is a network error) - - - - - -
- - - - diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.js deleted file mode 100644 index a68dc5b0b7d..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.js +++ /dev/null @@ -1,13 +0,0 @@ -onconnect = function(e) { -try { - var port = e.ports[0] - var url = decodeURIComponent(location.hash.substr(1)) - var source = new EventSource(url) - source.onerror = function(e) { - port.postMessage([true, this.readyState, 'data' in e]) - this.close(); - } -} catch(e) { - port.postMessage([false, String(e)]) -} -} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-url-bogus.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-url-bogus.js deleted file mode 100644 index 80847357b55..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-url-bogus.js +++ /dev/null @@ -1,10 +0,0 @@ -onconnect = function(e) { -try { - var port = e.ports[0] - var source = new EventSource("http://this is invalid/") - port.postMessage([false, 'no exception thrown']) - source.close() -} catch(e) { - port.postMessage([true, e.code]) -} -} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.htm deleted file mode 100644 index f25509dfd4a..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.htm +++ /dev/null @@ -1,24 +0,0 @@ - - - - shared worker - EventSource: addEventListener() - - - - -
- - - \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.js deleted file mode 100644 index 761165118ac..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.js +++ /dev/null @@ -1,13 +0,0 @@ -onconnect = function(e) { -try { - var port = e.ports[0] - var source = new EventSource("../resources/message.py") - source.addEventListener("message", listener, false) - function listener(e) { - port.postMessage([true, e.data]) - this.close() - } -} catch(e) { - port.postMessage([false, String(e)]) -} -} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmesage.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmesage.js deleted file mode 100644 index f5e2c898df0..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmesage.js +++ /dev/null @@ -1,12 +0,0 @@ -onconnect = function(e) { -try { - var port = e.ports[0] - var source = new EventSource("../resources/message.py") - source.onmessage = function(e) { - port.postMessage([true, e.data]) - this.close() - } -} catch(e) { - port.postMessage([false, String(e)]) -} -} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmessage.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmessage.htm deleted file mode 100644 index bcd6093454d..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmessage.htm +++ /dev/null @@ -1,24 +0,0 @@ - - - - shared worker - EventSource: onmessage - - - - -
- - - \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.htm deleted file mode 100644 index 752a6e449f9..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.htm +++ /dev/null @@ -1,27 +0,0 @@ - - - - shared worker - EventSource: onopen (announcing the connection) - - - - -
- - - \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.js deleted file mode 100644 index 6dc9424a213..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.js +++ /dev/null @@ -1,12 +0,0 @@ -onconnect = function(e) { -try { - var port = e.ports[0] - var source = new EventSource("../resources/message.py") - source.onopen = function(e) { - port.postMessage([true, source.readyState, 'data' in e, e.bubbles, e.cancelable]) - this.close() - } -} catch(e) { - port.postMessage([false, String(e)]) -} -} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.htm deleted file mode 100644 index 16c932a3384..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.htm +++ /dev/null @@ -1,25 +0,0 @@ - - - - shared worker - EventSource: prototype et al - - - - -
- - - \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.js deleted file mode 100644 index f4c809a9b3e..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.js +++ /dev/null @@ -1,11 +0,0 @@ -onconnect = function(e) { -try { - var port = e.ports[0] - EventSource.prototype.ReturnTrue = function() { return true } - var source = new EventSource("../resources/message.py") - port.postMessage([true, source.ReturnTrue(), 'EventSource' in self]) - source.close() -} catch(e) { - port.postMessage([false, String(e)]) -} -} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.htm deleted file mode 100644 index a1c9ca8455f..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.htm +++ /dev/null @@ -1,25 +0,0 @@ - - - - shared worker - EventSource: url - - - - -
- - - \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.js deleted file mode 100644 index 491dbac3332..00000000000 --- a/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.js +++ /dev/null @@ -1,10 +0,0 @@ -onconnect = function(e) { -try { - var port = e.ports[0] - var source = new EventSource("../resources/message.py") - port.postMessage([true, source.url]) - source.close() -} catch(e) { - port.postMessage([false, String(e)]) -} -} \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/META.yml b/test/fixtures/wpt/fetch/META.yml deleted file mode 100644 index 81432ff5f5c..00000000000 --- a/test/fixtures/wpt/fetch/META.yml +++ /dev/null @@ -1,7 +0,0 @@ -spec: https://fetch.spec.whatwg.org/ -suggested_reviewers: - - jdm - - youennf - - annevk - - mnot - - yutakahirano diff --git a/test/fixtures/wpt/fetch/README.md b/test/fixtures/wpt/fetch/README.md deleted file mode 100644 index dcaad0219d5..00000000000 --- a/test/fixtures/wpt/fetch/README.md +++ /dev/null @@ -1,6 +0,0 @@ -Tests for the [Fetch Standard](https://fetch.spec.whatwg.org/). - -More Fetch tests can be found in - -* /cors -* /xhr diff --git a/test/fixtures/wpt/fetch/api/abort/cache.https.any.js b/test/fixtures/wpt/fetch/api/abort/cache.https.any.js deleted file mode 100644 index bdaf0e69e58..00000000000 --- a/test/fixtures/wpt/fetch/api/abort/cache.https.any.js +++ /dev/null @@ -1,47 +0,0 @@ -// META: title=Request signals & the cache API -// META: global=window,worker - -promise_test(async () => { - await caches.delete('test'); - const controller = new AbortController(); - const signal = controller.signal; - const request = new Request('../resources/data.json', { signal }); - - const cache = await caches.open('test'); - await cache.put(request, new Response('')); - - const requests = await cache.keys(); - - assert_equals(requests.length, 1, 'Ensuring cleanup worked'); - - const [cachedRequest] = requests; - - controller.abort(); - - assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted"); - - const data = await fetch(cachedRequest).then(r => r.json()); - assert_equals(data.key, 'value', 'Fetch fully completes'); -}, "Signals are not stored in the cache API"); - -promise_test(async () => { - await caches.delete('test'); - const controller = new AbortController(); - const signal = controller.signal; - const request = new Request('../resources/data.json', { signal }); - controller.abort(); - - const cache = await caches.open('test'); - await cache.put(request, new Response('')); - - const requests = await cache.keys(); - - assert_equals(requests.length, 1, 'Ensuring cleanup worked'); - - const [cachedRequest] = requests; - - assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted"); - - const data = await fetch(cachedRequest).then(r => r.json()); - assert_equals(data.key, 'value', 'Fetch fully completes'); -}, "Signals are not stored in the cache API, even if they're already aborted"); diff --git a/test/fixtures/wpt/fetch/api/abort/destroyed-context.html b/test/fixtures/wpt/fetch/api/abort/destroyed-context.html deleted file mode 100644 index 161d39bd9ce..00000000000 --- a/test/fixtures/wpt/fetch/api/abort/destroyed-context.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/test/fixtures/wpt/fetch/api/abort/general.any.js b/test/fixtures/wpt/fetch/api/abort/general.any.js deleted file mode 100644 index 139f08947b1..00000000000 --- a/test/fixtures/wpt/fetch/api/abort/general.any.js +++ /dev/null @@ -1,572 +0,0 @@ -// META: timeout=long -// META: global=window,worker -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=../request/request-error.js - -const BODY_METHODS = ['arrayBuffer', 'blob', 'bytes', 'formData', 'json', 'text']; - -const error1 = new Error('error1'); -error1.name = 'error1'; - -// This is used to close connections that weren't correctly closed during the tests, -// otherwise you can end up running out of HTTP connections. -let requestAbortKeys = []; - -function abortRequests() { - const keys = requestAbortKeys; - requestAbortKeys = []; - return Promise.all( - keys.map(key => fetch(`../resources/stash-put.py?key=${key}&value=close`)) - ); -} - -const hostInfo = get_host_info(); -const urlHostname = hostInfo.REMOTE_HOST; - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const fetchPromise = fetch('../resources/data.json', { signal }); - - await promise_rejects_dom(t, "AbortError", fetchPromise); -}, "Aborting rejects with AbortError"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(error1); - - const fetchPromise = fetch('../resources/data.json', { signal }); - - await promise_rejects_exactly(t, error1, fetchPromise, 'fetch() should reject with abort reason'); -}, "Aborting rejects with abort reason"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const url = new URL('../resources/data.json', location); - url.hostname = urlHostname; - - const fetchPromise = fetch(url, { - signal, - mode: 'no-cors' - }); - - await promise_rejects_dom(t, "AbortError", fetchPromise); -}, "Aborting rejects with AbortError - no-cors"); - -// Test that errors thrown from the request constructor take priority over abort errors. -// badRequestArgTests is from response-error.js -for (const { args, testName } of badRequestArgTests) { - promise_test(async t => { - try { - // If this doesn't throw, we'll effectively skip the test. - // It'll fail properly in ../request/request-error.html - new Request(...args); - } - catch (err) { - const controller = new AbortController(); - controller.abort(); - - // Add signal to 2nd arg - args[1] = args[1] || {}; - args[1].signal = controller.signal; - await promise_rejects_js(t, TypeError, fetch(...args)); - } - }, `TypeError from request constructor takes priority - ${testName}`); -} - -test(() => { - const request = new Request(''); - assert_true(Boolean(request.signal), "Signal member is present & truthy"); - assert_equals(request.signal.constructor, AbortSignal); -}, "Request objects have a signal property"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const request = new Request('../resources/data.json', { signal }); - - assert_true(Boolean(request.signal), "Signal member is present & truthy"); - assert_equals(request.signal.constructor, AbortSignal); - assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference'); - assert_true(request.signal.aborted, `Request's signal has aborted`); - - const fetchPromise = fetch(request); - - await promise_rejects_dom(t, "AbortError", fetchPromise); -}, "Signal on request object"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(error1); - - const request = new Request('../resources/data.json', { signal }); - - assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference'); - assert_true(request.signal.aborted, `Request's signal has aborted`); - assert_equals(request.signal.reason, error1, `Request's signal's abort reason is error1`); - - const fetchPromise = fetch(request); - - await promise_rejects_exactly(t, error1, fetchPromise, "fetch() should reject with abort reason"); -}, "Signal on request object should also have abort reason"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const request = new Request('../resources/data.json', { signal }); - const requestFromRequest = new Request(request); - - const fetchPromise = fetch(requestFromRequest); - - await promise_rejects_dom(t, "AbortError", fetchPromise); -}, "Signal on request object created from request object"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const request = new Request('../resources/data.json'); - const requestFromRequest = new Request(request, { signal }); - - const fetchPromise = fetch(requestFromRequest); - - await promise_rejects_dom(t, "AbortError", fetchPromise); -}, "Signal on request object created from request object, with signal on second request"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const request = new Request('../resources/data.json', { signal: new AbortController().signal }); - const requestFromRequest = new Request(request, { signal }); - - const fetchPromise = fetch(requestFromRequest); - - await promise_rejects_dom(t, "AbortError", fetchPromise); -}, "Signal on request object created from request object, with signal on second request overriding another"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const request = new Request('../resources/data.json', { signal }); - - const fetchPromise = fetch(request, {method: 'POST'}); - - await promise_rejects_dom(t, "AbortError", fetchPromise); -}, "Signal retained after unrelated properties are overridden by fetch"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const request = new Request('../resources/data.json', { signal }); - - const data = await fetch(request, { signal: null }).then(r => r.json()); - assert_equals(data.key, 'value', 'Fetch fully completes'); -}, "Signal removed by setting to null"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const log = []; - - await Promise.all([ - fetch('../resources/data.json', { signal }).then( - () => assert_unreached("Fetch must not resolve"), - () => log.push('fetch-reject') - ), - Promise.resolve().then(() => log.push('next-microtask')) - ]); - - assert_array_equals(log, ['fetch-reject', 'next-microtask']); -}, "Already aborted signal rejects immediately"); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const request = new Request('../resources/data.json', { - signal, - method: 'POST', - body: 'foo', - headers: { 'Content-Type': 'text/plain' } - }); - - await fetch(request).catch(() => {}); - - assert_true(request.bodyUsed, "Body has been used"); -}, "Request is still 'used' if signal is aborted before fetching"); - -for (const bodyMethod of BODY_METHODS) { - promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - - const log = []; - const response = await fetch('../resources/data.json', { signal }); - - controller.abort(); - - const bodyPromise = response[bodyMethod](); - - await Promise.all([ - bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)), - Promise.resolve().then(() => log.push('next-microtask')) - ]); - - await promise_rejects_dom(t, "AbortError", bodyPromise); - - assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']); - }, `response.${bodyMethod}() rejects if already aborted`); -} - -promise_test(async (t) => { - const controller = new AbortController(); - const signal = controller.signal; - - const res = await fetch('../resources/data.json', { signal }); - controller.abort(); - - await promise_rejects_dom(t, 'AbortError', res.text()); - await promise_rejects_dom(t, 'AbortError', res.text()); -}, 'Call text() twice on aborted response'); - -promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - const stateKey = token(); - const abortKey = token(); - requestAbortKeys.push(abortKey); - controller.abort(); - - await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }).catch(() => {}); - - // I'm hoping this will give the browser enough time to (incorrectly) make the request - // above, if it intends to. - await fetch('../resources/data.json').then(r => r.json()); - - const response = await fetch(`../resources/stash-take.py?key=${stateKey}`); - const data = await response.json(); - - assert_equals(data, null, "Request hasn't been made to the server"); -}, "Already aborted signal does not make request"); - -promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const fetches = []; - - for (let i = 0; i < 3; i++) { - const abortKey = token(); - requestAbortKeys.push(abortKey); - - fetches.push( - fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal }) - ); - } - - for (const fetchPromise of fetches) { - await promise_rejects_dom(t, "AbortError", fetchPromise); - } -}, "Already aborted signal can be used for many fetches"); - -promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - - await fetch('../resources/data.json', { signal }).then(r => r.json()); - - controller.abort(); - - const fetches = []; - - for (let i = 0; i < 3; i++) { - const abortKey = token(); - requestAbortKeys.push(abortKey); - - fetches.push( - fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal }) - ); - } - - for (const fetchPromise of fetches) { - await promise_rejects_dom(t, "AbortError", fetchPromise); - } -}, "Signal can be used to abort other fetches, even if another fetch succeeded before aborting"); - -promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - const stateKey = token(); - const abortKey = token(); - requestAbortKeys.push(abortKey); - - await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); - - const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); - assert_equals(beforeAbortResult, "open", "Connection is open"); - - controller.abort(); - - // The connection won't close immediately, but it should close at some point: - const start = Date.now(); - - while (true) { - // Stop spinning if 10 seconds have passed - if (Date.now() - start > 10000) throw Error('Timed out'); - - const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); - if (afterAbortResult == 'closed') break; - } -}, "Underlying connection is closed when aborting after receiving response"); - -promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - const stateKey = token(); - const abortKey = token(); - requestAbortKeys.push(abortKey); - - const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location); - url.hostname = urlHostname; - - await fetch(url, { - signal, - mode: 'no-cors' - }); - - const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location); - stashTakeURL.hostname = urlHostname; - - const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json()); - assert_equals(beforeAbortResult, "open", "Connection is open"); - - controller.abort(); - - // The connection won't close immediately, but it should close at some point: - const start = Date.now(); - - while (true) { - // Stop spinning if 10 seconds have passed - if (Date.now() - start > 10000) throw Error('Timed out'); - - const afterAbortResult = await fetch(stashTakeURL).then(r => r.json()); - if (afterAbortResult == 'closed') break; - } -}, "Underlying connection is closed when aborting after receiving response - no-cors"); - -for (const bodyMethod of BODY_METHODS) { - promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - const stateKey = token(); - const abortKey = token(); - requestAbortKeys.push(abortKey); - - const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); - - const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); - assert_equals(beforeAbortResult, "open", "Connection is open"); - - const bodyPromise = response[bodyMethod](); - - controller.abort(); - - await promise_rejects_dom(t, "AbortError", bodyPromise); - - const start = Date.now(); - - while (true) { - // Stop spinning if 10 seconds have passed - if (Date.now() - start > 10000) throw Error('Timed out'); - - const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); - if (afterAbortResult == 'closed') break; - } - }, `Fetch aborted & connection closed when aborted after calling response.${bodyMethod}()`); -} - -promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - const stateKey = token(); - const abortKey = token(); - requestAbortKeys.push(abortKey); - - const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); - const reader = response.body.getReader(); - - controller.abort(); - - await promise_rejects_dom(t, "AbortError", reader.read()); - await promise_rejects_dom(t, "AbortError", reader.closed); - - // The connection won't close immediately, but it should close at some point: - const start = Date.now(); - - while (true) { - // Stop spinning if 10 seconds have passed - if (Date.now() - start > 10000) throw Error('Timed out'); - - const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); - if (afterAbortResult == 'closed') break; - } -}, "Stream errors once aborted. Underlying connection closed."); - -promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - const stateKey = token(); - const abortKey = token(); - requestAbortKeys.push(abortKey); - - const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }); - const reader = response.body.getReader(); - - await reader.read(); - - controller.abort(); - - await promise_rejects_dom(t, "AbortError", reader.read()); - await promise_rejects_dom(t, "AbortError", reader.closed); - - // The connection won't close immediately, but it should close at some point: - const start = Date.now(); - - while (true) { - // Stop spinning if 10 seconds have passed - if (Date.now() - start > 10000) throw Error('Timed out'); - - const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json()); - if (afterAbortResult == 'closed') break; - } -}, "Stream errors once aborted, after reading. Underlying connection closed."); - -promise_test(async t => { - await abortRequests(); - - const controller = new AbortController(); - const signal = controller.signal; - - const response = await fetch(`../resources/empty.txt`, { signal }); - - // Read whole response to ensure close signal has sent. - await response.clone().text(); - - const reader = response.body.getReader(); - - controller.abort(); - - const item = await reader.read(); - - assert_true(item.done, "Stream is done"); -}, "Stream will not error if body is empty. It's closed with an empty queue before it errors."); - -promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - let cancelReason; - - const body = new ReadableStream({ - pull(controller) { - controller.enqueue(new Uint8Array([42])); - }, - cancel(reason) { - cancelReason = reason; - } - }); - - const fetchPromise = fetch('../resources/empty.txt', { - body, signal, - method: 'POST', - duplex: 'half', - headers: { - 'Content-Type': 'text/plain' - } - }); - - assert_true(!!cancelReason, 'Cancel called sync'); - assert_equals(cancelReason.constructor, DOMException); - assert_equals(cancelReason.name, 'AbortError'); - - await promise_rejects_dom(t, "AbortError", fetchPromise); - - const fetchErr = await fetchPromise.catch(e => e); - - assert_equals(cancelReason, fetchErr, "Fetch rejects with same error instance"); -}, "Readable stream synchronously cancels with AbortError if aborted before reading"); - -test(() => { - const controller = new AbortController(); - const signal = controller.signal; - controller.abort(); - - const request = new Request('.', { signal }); - const requestSignal = request.signal; - - const clonedRequest = request.clone(); - - assert_equals(requestSignal, request.signal, "Original request signal the same after cloning"); - assert_true(request.signal.aborted, "Original request signal aborted"); - assert_not_equals(clonedRequest.signal, request.signal, "Cloned request has different signal"); - assert_true(clonedRequest.signal.aborted, "Cloned request signal aborted"); -}, "Signal state is cloned"); - -test(() => { - const controller = new AbortController(); - const signal = controller.signal; - - const request = new Request('.', { signal }); - const clonedRequest = request.clone(); - - const log = []; - - request.signal.addEventListener('abort', () => log.push('original-aborted')); - clonedRequest.signal.addEventListener('abort', () => log.push('clone-aborted')); - - controller.abort(); - - assert_array_equals(log, ['original-aborted', 'clone-aborted'], "Abort events fired in correct order"); - assert_true(request.signal.aborted, 'Signal aborted'); - assert_true(clonedRequest.signal.aborted, 'Signal aborted'); -}, "Clone aborts with original controller"); diff --git a/test/fixtures/wpt/fetch/api/abort/keepalive.html b/test/fixtures/wpt/fetch/api/abort/keepalive.html deleted file mode 100644 index db12df0d289..00000000000 --- a/test/fixtures/wpt/fetch/api/abort/keepalive.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/abort/request.any.js b/test/fixtures/wpt/fetch/api/abort/request.any.js deleted file mode 100644 index dcc7803abe5..00000000000 --- a/test/fixtures/wpt/fetch/api/abort/request.any.js +++ /dev/null @@ -1,85 +0,0 @@ -// META: timeout=long -// META: global=window,worker - -const BODY_FUNCTION_AND_DATA = { - arrayBuffer: null, - blob: null, - formData: new FormData(), - json: new Blob(["{}"]), - text: null, -}; - -for (const [bodyFunction, body] of Object.entries(BODY_FUNCTION_AND_DATA)) { - promise_test(async () => { - const controller = new AbortController(); - const signal = controller.signal; - const request = new Request("../resources/data.json", { - method: "post", - signal, - body, - }); - - controller.abort(); - await request[bodyFunction](); - assert_true( - true, - `An aborted request should still be able to run ${bodyFunction}()` - ); - }, `Calling ${bodyFunction}() on an aborted request`); - - promise_test(async () => { - const controller = new AbortController(); - const signal = controller.signal; - const request = new Request("../resources/data.json", { - method: "post", - signal, - body, - }); - - const p = request[bodyFunction](); - controller.abort(); - await p; - assert_true( - true, - `An aborted request should still be able to run ${bodyFunction}()` - ); - }, `Aborting a request after calling ${bodyFunction}()`); - - if (!body) { - promise_test(async () => { - const controller = new AbortController(); - const signal = controller.signal; - const request = new Request("../resources/data.json", { - method: "post", - signal, - body, - }); - - // consuming happens synchronously, so don't wait - fetch(request).catch(() => {}); - - controller.abort(); - await request[bodyFunction](); - assert_true( - true, - `An aborted consumed request should still be able to run ${bodyFunction}() when empty` - ); - }, `Calling ${bodyFunction}() on an aborted consumed empty request`); - } - - promise_test(async t => { - const controller = new AbortController(); - const signal = controller.signal; - const request = new Request("../resources/data.json", { - method: "post", - signal, - body: body || new Blob(["foo"]), - }); - - // consuming happens synchronously, so don't wait - fetch(request).catch(() => {}); - - controller.abort(); - await promise_rejects_js(t, TypeError, request[bodyFunction]()); - }, `Calling ${bodyFunction}() on an aborted consumed nonempty request`); -} diff --git a/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html b/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html deleted file mode 100644 index 1867e205bb6..00000000000 --- a/test/fixtures/wpt/fetch/api/abort/serviceworker-intercepted.https.html +++ /dev/null @@ -1,212 +0,0 @@ - - - - - Aborting fetch when intercepted by a service worker - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/basic/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/basic/WEB_FEATURES.yml deleted file mode 100644 index d6cd4adaeff..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/WEB_FEATURES.yml +++ /dev/null @@ -1,4 +0,0 @@ -features: -- name: fetch-request-streams - files: - - request-upload* diff --git a/test/fixtures/wpt/fetch/api/basic/accept-header.any.js b/test/fixtures/wpt/fetch/api/basic/accept-header.any.js deleted file mode 100644 index cd54cf2a03e..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/accept-header.any.js +++ /dev/null @@ -1,34 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -promise_test(function() { - return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept").then(function(response) { - assert_equals(response.status, 200, "HTTP status is 200"); - assert_equals(response.type , "basic", "Response's type is basic"); - assert_equals(response.headers.get("x-request-accept"), "*/*", "Request has accept header with value '*/*'"); - }); -}, "Request through fetch should have 'accept' header with value '*/*'"); - -promise_test(function() { - return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept", {"headers": [["Accept", "custom/*"]]}).then(function(response) { - assert_equals(response.status, 200, "HTTP status is 200"); - assert_equals(response.type , "basic", "Response's type is basic"); - assert_equals(response.headers.get("x-request-accept"), "custom/*", "Request has accept header with value 'custom/*'"); - }); -}, "Request through fetch should have 'accept' header with value 'custom/*'"); - -promise_test(function() { - return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language").then(function(response) { - assert_equals(response.status, 200, "HTTP status is 200"); - assert_equals(response.type , "basic", "Response's type is basic"); - assert_true(response.headers.has("x-request-accept-language")); - }); -}, "Request through fetch should have a 'accept-language' header"); - -promise_test(function() { - return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language", {"headers": [["Accept-Language", "bzh"]]}).then(function(response) { - assert_equals(response.status, 200, "HTTP status is 200"); - assert_equals(response.type , "basic", "Response's type is basic"); - assert_equals(response.headers.get("x-request-accept-language"), "bzh", "Request has accept header with value 'bzh'"); - }); -}, "Request through fetch should have 'accept-language' header with value 'bzh'"); diff --git a/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html b/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html deleted file mode 100644 index afc2bbbafb0..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/block-mime-as-script.html +++ /dev/null @@ -1,43 +0,0 @@ - - -Block mime type as script - - -
- diff --git a/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js b/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js deleted file mode 100644 index 2f9fa81c02b..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/conditional-get.any.js +++ /dev/null @@ -1,38 +0,0 @@ -// META: title=Request ETag -// META: global=window,worker -// META: script=/common/utils.js - -promise_test(function() { - var cacheBuster = token(); // ensures first request is uncached - var url = "../resources/cache.py?v=" + cacheBuster; - var etag; - - // make the first request - return fetch(url).then(function(response) { - // ensure we're getting the regular, uncached response - assert_equals(response.status, 200); - assert_equals(response.headers.get("X-HTTP-STATUS"), null) - - return response.text(); // consuming the body, just to be safe - }).then(function(body) { - // make a second request - return fetch(url); - }).then(function(response) { - // while the server responds with 304 if our browser sent the correct - // If-None-Match request header, at the JavaScript level this surfaces - // as 200 - assert_equals(response.status, 200); - assert_equals(response.headers.get("X-HTTP-STATUS"), "304") - - etag = response.headers.get("ETag") - - return response.text(); // consuming the body, just to be safe - }).then(function(body) { - // make a third request, explicitly setting If-None-Match request header - var headers = { "If-None-Match": etag } - return fetch(url, { headers: headers }) - }).then(function(response) { - // 304 now surfaces thanks to the explicit If-None-Match request header - assert_equals(response.status, 304); - }); -}, "Testing conditional GET with ETags"); diff --git a/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js b/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js deleted file mode 100644 index f7114425f95..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/error-after-response.any.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: title=Fetch: network timeout after receiving the HTTP response headers -// META: global=window,worker -// META: timeout=long -// META: script=../resources/utils.js - -function checkReader(test, reader, promiseToTest) -{ - return reader.read().then((value) => { - validateBufferFromString(value.value, "TEST_CHUNK", "Should receive first chunk"); - return promise_rejects_js(test, TypeError, promiseToTest(reader)); - }); -} - -promise_test((test) => { - return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => { - return checkReader(test, response.body.getReader(), reader => reader.read()); - }); -}, "Response reader read() promise should reject after a network error happening after resolving fetch promise"); - -promise_test((test) => { - return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => { - return checkReader(test, response.body.getReader(), reader => reader.closed); - }); -}, "Response reader closed promise should reject after a network error happening after resolving fetch promise"); diff --git a/test/fixtures/wpt/fetch/api/basic/gc.any.js b/test/fixtures/wpt/fetch/api/basic/gc.any.js deleted file mode 100644 index 70362ff39ce..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/gc.any.js +++ /dev/null @@ -1,19 +0,0 @@ -// META: global=window,worker -// META: script=/common/gc.js - -promise_test(async () => { - let i = 0; - const repeat = 5; - const buffer = await new Response(new ReadableStream({ - pull(c) { - if (i >= repeat) { - c.close(); - return; - } - ++i; - c.enqueue(new Uint8Array([0])) - garbageCollect(); - } - })).arrayBuffer(); - assert_equals(buffer.byteLength, repeat, `The buffer should be ${repeat}-byte long`); -}, "GC/CC should not abruptly close the stream while being consumed by Response"); diff --git a/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js b/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js deleted file mode 100644 index bb70d87d250..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/header-value-combining.any.js +++ /dev/null @@ -1,15 +0,0 @@ -// META: global=window,worker - -[ - ["content-length", "0", "header-content-length"], - ["content-length", "0, 0", "header-content-length-twice"], - ["double-trouble", ", ", "headers-double-empty"], - ["foo-test", "1, 2, 3", "headers-basic"], - ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"], - ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"], -].forEach(testValues => { - promise_test(async t => { - const response = await fetch("../../../xhr/resources/" + testValues[2] + ".asis"); - assert_equals(response.headers.get(testValues[0]), testValues[1]); - }, "response.headers.get('" + testValues[0] + "') expects " + testValues[1]); -}); diff --git a/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js b/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js deleted file mode 100644 index 741d83bf7aa..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/header-value-null-byte.any.js +++ /dev/null @@ -1,5 +0,0 @@ -// META: global=window,worker - -promise_test(t => { - return promise_rejects_js(t, TypeError, fetch("../../../xhr/resources/parse-headers.py?my-custom-header="+encodeURIComponent("x\0x"))); -}, "Ensure fetch() rejects null bytes in headers"); diff --git a/test/fixtures/wpt/fetch/api/basic/historical.any.js b/test/fixtures/wpt/fetch/api/basic/historical.any.js deleted file mode 100644 index c8081262168..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/historical.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// META: global=window,worker - -test(() => { - assert_false("getAll" in new Headers()); - assert_false("getAll" in Headers.prototype); -}, "Headers object no longer has a getAll() method"); - -test(() => { - assert_false("type" in new Request("about:blank")); - assert_false("type" in Request.prototype); -}, "'type' getter should not exist on Request objects"); - -// See https://github.com/whatwg/fetch/pull/979 for the removal -test(() => { - assert_false("trailer" in new Response()); - assert_false("trailer" in Response.prototype); -}, "Response object no longer has a trailer getter"); diff --git a/test/fixtures/wpt/fetch/api/basic/http-response-code.any.js b/test/fixtures/wpt/fetch/api/basic/http-response-code.any.js deleted file mode 100644 index 1fd312a3e9f..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/http-response-code.any.js +++ /dev/null @@ -1,14 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js - -promise_test(async (test) => { - const resp = await fetch( - "/fetch/connection-pool/resources/network-partition-key.py?" - + `status=425&uuid=${token()}&partition_id=${get_host_info().ORIGIN}` - + `&dispatch=check_partition&addcounter=true`); - assert_equals(resp.status, 425); - const text = await resp.text(); - assert_equals(text, "ok. Request was sent 1 times. 1 connections were created."); -}, "Fetch on 425 response should not be retried for non TLS early data."); diff --git a/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js b/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js deleted file mode 100644 index e3cfd1b2f6e..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/integrity.sub.any.js +++ /dev/null @@ -1,87 +0,0 @@ -// META: global=window,dedicatedworker,sharedworker -// META: script=../resources/utils.js - -function integrity(desc, url, integrity, initRequestMode, shouldPass) { - var fetchRequestInit = {'integrity': integrity} - if (!!initRequestMode && initRequestMode !== "") { - fetchRequestInit.mode = initRequestMode; - } - - if (shouldPass) { - promise_test(function(test) { - return fetch(url, fetchRequestInit).then(function(resp) { - if (initRequestMode !== "no-cors") { - assert_equals(resp.status, 200, "Response's status is 200"); - } else { - assert_equals(resp.status, 0, "Opaque response's status is 0"); - assert_equals(resp.type, "opaque"); - } - }); - }, desc); - } else { - promise_test(function(test) { - return promise_rejects_js(test, TypeError, fetch(url, fetchRequestInit)); - }, desc); - } -} - -const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk="; -const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL"; -const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg=="; -const topSha512wrongpadding = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg"; -const topSha512base64url = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg=="; -const topSha512base64url_nopadding = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg"; -const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I="; -const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg=="; - -const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt"; -const url = path; -const corsUrl = - `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`; -const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}` - -integrity("Empty string integrity", url, "", /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("SHA-512 integrity with missing padding", url, topSha512wrongpadding, - /* initRequestMode */ undefined, /* shouldPass */ true); -integrity("SHA-512 integrity base64url encoded", url, topSha512base64url, - /* initRequestMode */ undefined, /* shouldPass */ true); -integrity("SHA-512 integrity base64url encoded with missing padding", url, - topSha512base64url_nopadding, /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("Invalid integrity", url, invalidSha256, - /* initRequestMode */ undefined, /* shouldPass */ false); -integrity("Multiple integrities: valid stronger than invalid", url, - invalidSha256 + " " + topSha384, /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("Multiple integrities: invalid stronger than valid", - url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined, - /* shouldPass */ false); -integrity("Multiple integrities: invalid as strong as valid", url, - invalidSha512 + " " + topSha512, /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("Multiple integrities: both are valid", url, - topSha384 + " " + topSha512, /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("Multiple integrities: both are invalid", url, - invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined, - /* shouldPass */ false); -integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined, - /* shouldPass */ true); -integrity("CORS SHA-512 integrity", corsUrl, topSha512, - /* initRequestMode */ undefined, /* shouldPass */ true); -integrity("CORS invalid integrity", corsUrl, invalidSha512, - /* initRequestMode */ undefined, /* shouldPass */ false); - -integrity("Empty string integrity for opaque response", corsUrl2, "", - /* initRequestMode */ "no-cors", /* shouldPass */ true); -integrity("SHA-* integrity for opaque response", corsUrl2, topSha512, - /* initRequestMode */ "no-cors", /* shouldPass */ false); - -done(); diff --git a/test/fixtures/wpt/fetch/api/basic/keepalive.any.js b/test/fixtures/wpt/fetch/api/basic/keepalive.any.js deleted file mode 100644 index d4e831b9631..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/keepalive.any.js +++ /dev/null @@ -1,77 +0,0 @@ -// META: global=window -// META: timeout=long -// META: title=Fetch API: keepalive handling -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=../resources/keepalive-helper.js - -'use strict'; - -const { - HTTP_NOTSAMESITE_ORIGIN, - HTTP_REMOTE_ORIGIN, - HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT -} = get_host_info(); - -/** - * In a different-site iframe, test to fetch a keepalive URL on the specified - * document event. - */ -function keepaliveSimpleRequestTest(method) { - for (const evt of ['load', 'unload', 'pagehide']) { - const desc = - `[keepalive] simple ${method} request on '${evt}' [no payload]`; - promise_test(async (test) => { - const token1 = token(); - const iframe = document.createElement('iframe'); - iframe.src = getKeepAliveIframeUrl(token1, method, {sendOn: evt}); - document.body.appendChild(iframe); - await iframeLoaded(iframe); - if (evt != 'load') { - iframe.remove(); - } - - assertStashedTokenAsync(desc, token1); - }, `${desc}; setting up`); - } -} - -for (const method of ['GET', 'POST']) { - keepaliveSimpleRequestTest(method); -} - -// verifies fetch keepalive requests from a worker -function keepaliveSimpleWorkerTest() { - const desc = - `simple keepalive test for web workers`; - promise_test(async (test) => { - const TOKEN = token(); - const FRAME_ORIGIN = new URL(location.href).origin; - const TEST_URL = get_host_info().HTTP_ORIGIN + `/fetch/api/resources/stash-put.py?key=${TOKEN}&value=on` - + `&frame_origin=${FRAME_ORIGIN}`; - // start a worker which sends keepalive request and immediately terminates - const worker = new Worker(`/fetch/api/resources/keepalive-worker.js?param=${TEST_URL}`); - - const keepAliveWorkerPromise = new Promise((resolve, reject) => { - worker.onmessage = (event) => { - if (event.data === 'started') { - resolve(); - } else { - reject(new Error("Unexpected message received from worker")); - } - }; - worker.onerror = (error) => { - reject(error); - }; - }); - - // wait until the worker has been initialized (indicated by the "started" message) - await keepAliveWorkerPromise; - // verifies if the token sent in fetch request has been updated in the server - assertStashedTokenAsync(desc, TOKEN); - - }, `${desc};`); - -} - -keepaliveSimpleWorkerTest(); diff --git a/test/fixtures/wpt/fetch/api/basic/mediasource.window.js b/test/fixtures/wpt/fetch/api/basic/mediasource.window.js deleted file mode 100644 index 1f89595393d..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/mediasource.window.js +++ /dev/null @@ -1,5 +0,0 @@ -promise_test(t => { - const mediaSource = new MediaSource(), - mediaSourceURL = URL.createObjectURL(mediaSource); - return promise_rejects_js(t, TypeError, fetch(mediaSourceURL)); -}, "Cannot fetch blob: URL from a MediaSource"); diff --git a/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js b/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js deleted file mode 100644 index a4abcac55f3..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/mode-no-cors.sub.any.js +++ /dev/null @@ -1,29 +0,0 @@ -// META: script=../resources/utils.js - -function fetchNoCors(url, isOpaqueFiltered) { - var urlQuery = "?pipe=header(x-is-filtered,value)" - promise_test(function(test) { - if (isOpaqueFiltered) - return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) { - assert_equals(resp.status, 0, "Opaque filter: status is 0"); - assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\""); - assert_equals(resp.url, "", "Opaque filter: url is \"\""); - assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque"); - assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered"); - }); - else - return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered"); - }); - }, "Fetch "+ url + " with no-cors mode"); -} - -fetchNoCors(RESOURCES_DIR + "top.txt", false); -fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false); -fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true); -fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true); - -done(); - diff --git a/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js b/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js deleted file mode 100644 index 1457702f1b1..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/mode-same-origin.any.js +++ /dev/null @@ -1,28 +0,0 @@ -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function fetchSameOrigin(url, shouldPass) { - promise_test(function(test) { - if (shouldPass) - return fetch(url , {"mode": "same-origin"}).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type, "basic", "response type is basic"); - }); - else - return promise_rejects_js(test, TypeError, fetch(url, {mode: "same-origin"})); - }, "Fetch "+ url + " with same-origin mode"); -} - -var host_info = get_host_info(); - -fetchSameOrigin(RESOURCES_DIR + "top.txt", true); -fetchSameOrigin(host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true); -fetchSameOrigin(host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false); -fetchSameOrigin(host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false); - -var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location="; - -fetchSameOrigin(redirPath + RESOURCES_DIR + "top.txt", true); -fetchSameOrigin(redirPath + host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true); -fetchSameOrigin(redirPath + host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false); -fetchSameOrigin(redirPath + host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false); diff --git a/test/fixtures/wpt/fetch/api/basic/referrer.any.js b/test/fixtures/wpt/fetch/api/basic/referrer.any.js deleted file mode 100644 index 85745e692a2..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/referrer.any.js +++ /dev/null @@ -1,29 +0,0 @@ -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function runTest(url, init, expectedReferrer, title) { - promise_test(function(test) { - url += (url.indexOf('?') !== -1 ? '&' : '?') + "headers=referer&cors"; - - return fetch(url , init).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.headers.get("x-request-referer"), expectedReferrer, "Request's referrer is correct"); - }); - }, title); -} - -var fetchedUrl = RESOURCES_DIR + "inspect-headers.py"; -var corsFetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py"; -var redirectUrl = RESOURCES_DIR + "redirect.py?location=" ; -var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location="; - -runTest(fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, location.toString(), "origin-when-cross-origin policy on a same-origin URL"); -runTest(corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL"); -runTest(redirectUrl + corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection"); -runTest(corsRedirectUrl + fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection"); - - -var referrerUrlWithCredentials = get_host_info().HTTP_ORIGIN.replace("http://", "http://username:password@"); -runTest(fetchedUrl, {referrer: referrerUrlWithCredentials}, get_host_info().HTTP_ORIGIN + "/", "Referrer with credentials should be stripped"); -var referrerUrlWithFragmentIdentifier = get_host_info().HTTP_ORIGIN + "#fragmentIdentifier"; -runTest(fetchedUrl, {referrer: referrerUrlWithFragmentIdentifier}, get_host_info().HTTP_ORIGIN + "/", "Referrer with fragment ID should be stripped"); diff --git a/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js b/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js deleted file mode 100644 index d7560f03a23..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-forbidden-headers.any.js +++ /dev/null @@ -1,82 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function requestValidOverrideHeaders(desc, validHeaders) { - var url = RESOURCES_DIR + "inspect-headers.py"; - var requestInit = {"headers": validHeaders} - var urlParameters = "?headers=" + Object.keys(validHeaders).join("|"); - - promise_test(function(test){ - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - for (var header in validHeaders) - assert_equals(resp.headers.get("x-request-" + header), validHeaders[header], header + "is not skipped for non-forbidden methods"); - }); - }, desc); -} - -requestForbiddenHeaders("Accept-Charset is a forbidden request header", {"Accept-Charset": "utf-8"}); -requestForbiddenHeaders("Accept-Encoding is a forbidden request header", {"Accept-Encoding": ""}); - -requestForbiddenHeaders("Access-Control-Request-Headers is a forbidden request header", {"Access-Control-Request-Headers": ""}); -requestForbiddenHeaders("Access-Control-Request-Method is a forbidden request header", {"Access-Control-Request-Method": ""}); -requestForbiddenHeaders("Connection is a forbidden request header", {"Connection": "close"}); -requestForbiddenHeaders("Content-Length is a forbidden request header", {"Content-Length": "42"}); -requestForbiddenHeaders("Cookie is a forbidden request header", {"Cookie": "cookie=none"}); -requestForbiddenHeaders("Cookie2 is a forbidden request header", {"Cookie2": "cookie2=none"}); -requestForbiddenHeaders("Date is a forbidden request header", {"Date": "Wed, 04 May 1988 22:22:22 GMT"}); -requestForbiddenHeaders("DNT is a forbidden request header", {"DNT": "4"}); -requestForbiddenHeaders("Expect is a forbidden request header", {"Expect": "100-continue"}); -requestForbiddenHeaders("Host is a forbidden request header", {"Host": "http://wrong-host.com"}); -requestForbiddenHeaders("Keep-Alive is a forbidden request header", {"Keep-Alive": "timeout=15"}); -requestForbiddenHeaders("Origin is a forbidden request header", {"Origin": "http://wrong-origin.com"}); -requestForbiddenHeaders("Referer is a forbidden request header", {"Referer": "http://wrong-referer.com"}); -requestForbiddenHeaders("TE is a forbidden request header", {"TE": "trailers"}); -requestForbiddenHeaders("Trailer is a forbidden request header", {"Trailer": "Accept"}); -requestForbiddenHeaders("Transfer-Encoding is a forbidden request header", {"Transfer-Encoding": "chunked"}); -requestForbiddenHeaders("Upgrade is a forbidden request header", {"Upgrade": "HTTP/2.0"}); -requestForbiddenHeaders("Via is a forbidden request header", {"Via": "1.1 nowhere.com"}); -requestForbiddenHeaders("Proxy- is a forbidden request header", {"Proxy-": "value"}); -requestForbiddenHeaders("Proxy-Test is a forbidden request header", {"Proxy-Test": "value"}); -requestForbiddenHeaders("Sec- is a forbidden request header", {"Sec-": "value"}); -requestForbiddenHeaders("Sec-Test is a forbidden request header", {"Sec-Test": "value"}); - -let forbiddenMethods = [ - "TRACE", - "TRACK", - "CONNECT", - "trace", - "track", - "connect", - "trace,", - "GET,track ", - " connect", -]; - -let overrideHeaders = [ - "x-http-method-override", - "x-http-method", - "x-method-override", - "X-HTTP-METHOD-OVERRIDE", - "X-HTTP-METHOD", - "X-METHOD-OVERRIDE", -]; - -for (forbiddenMethod of forbiddenMethods) { - for (overrideHeader of overrideHeaders) { - requestForbiddenHeaders(`header ${overrideHeader} is forbidden to use value ${forbiddenMethod}`, {[overrideHeader]: forbiddenMethod}); - } -} - -let permittedValues = [ - "GETTRACE", - "GET", - "\",TRACE\",", -]; - -for (permittedValue of permittedValues) { - for (overrideHeader of overrideHeaders) { - requestValidOverrideHeaders(`header ${overrideHeader} is allowed to use value ${permittedValue}`, {[overrideHeader]: permittedValue}); - } -} diff --git a/test/fixtures/wpt/fetch/api/basic/request-head.any.js b/test/fixtures/wpt/fetch/api/basic/request-head.any.js deleted file mode 100644 index e0b6afa079a..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-head.any.js +++ /dev/null @@ -1,6 +0,0 @@ -// META: global=window,worker - -promise_test(function(test) { - var requestInit = {"method": "HEAD", "body": "test"}; - return promise_rejects_js(test, TypeError, fetch(".", requestInit)); -}, "Fetch with HEAD with body"); diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js deleted file mode 100644 index 4c10e717f8c..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-headers-case.any.js +++ /dev/null @@ -1,13 +0,0 @@ -// META: global=window,worker - -promise_test(() => { - return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-is-A-test", 1], ["THIS-IS-A-TEST", 2]] }).then(res => res.text()).then(body => { - assert_regexp_match(body, /THIS-is-A-test: 1, 2/) - }) -}, "Multiple headers with the same name, different case (THIS-is-A-test first)") - -promise_test(() => { - return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-IS-A-TEST", 1], ["THIS-is-A-test", 2]] }).then(res => res.text()).then(body => { - assert_regexp_match(body, /THIS-IS-A-TEST: 1, 2/) - }) -}, "Multiple headers with the same name, different case (THIS-IS-A-TEST first)") diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js deleted file mode 100644 index 4a9a8011385..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-headers-nonascii.any.js +++ /dev/null @@ -1,29 +0,0 @@ -// META: global=window,worker - -// This tests characters that are not -// https://infra.spec.whatwg.org/#ascii-code-point -// but are still -// https://infra.spec.whatwg.org/#byte-value -// in request header values. -// Such request header values are valid and thus sent to servers. -// Characters outside the #byte-value range are tested e.g. in -// fetch/api/headers/headers-errors.html. - -promise_test(() => { - return fetch( - "../resources/inspect-headers.py?headers=accept|x-test", - {headers: { - "Accept": "before-æøå-after", - "X-Test": "before-ß-after" - }}) - .then(res => { - assert_equals( - res.headers.get("x-request-accept"), - "before-æøå-after", - "Accept Header"); - assert_equals( - res.headers.get("x-request-x-test"), - "before-ß-after", - "X-Test Header"); - }); -}, "Non-ascii bytes in request headers"); diff --git a/test/fixtures/wpt/fetch/api/basic/request-headers.any.js b/test/fixtures/wpt/fetch/api/basic/request-headers.any.js deleted file mode 100644 index f6a7fe1494b..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-headers.any.js +++ /dev/null @@ -1,83 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function checkContentType(contentType, body) -{ - if (self.FormData && body instanceof self.FormData) { - assert_true(contentType.startsWith("multipart/form-data; boundary="), "Request should have header content-type starting with multipart/form-data; boundary=, but got " + contentType); - return; - } - - var expectedContentType = "text/plain;charset=UTF-8"; - if(body === null || body instanceof ArrayBuffer || body.buffer instanceof ArrayBuffer) - expectedContentType = null; - else if (body instanceof Blob) - expectedContentType = body.type ? body.type : null; - else if (body instanceof URLSearchParams) - expectedContentType = "application/x-www-form-urlencoded;charset=UTF-8"; - - assert_equals(contentType , expectedContentType, "Request should have header content-type: " + expectedContentType); -} - -function requestHeaders(desc, url, method, body, expectedOrigin, expectedContentLength) { - var urlParameters = "?headers=origin|user-agent|accept-charset|content-length|content-type"; - var requestInit = {"method": method} - promise_test(function(test){ - if (typeof body === "function") - body = body(); - if (body) - requestInit["body"] = body; - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - assert_true(resp.headers.has("x-request-user-agent"), "Request has header user-agent"); - assert_false(resp.headers.has("accept-charset"), "Request has header accept-charset"); - assert_equals(resp.headers.get("x-request-origin") , expectedOrigin, "Request should have header origin: " + expectedOrigin); - if (expectedContentLength !== undefined) - assert_equals(resp.headers.get("x-request-content-length") , expectedContentLength, "Request should have header content-length: " + expectedContentLength); - checkContentType(resp.headers.get("x-request-content-type"), body); - }); - }, desc); -} - -var url = RESOURCES_DIR + "inspect-headers.py" - -requestHeaders("Fetch with GET", url, "GET", null, null, null); -requestHeaders("Fetch with HEAD", url, "HEAD", null, null, null); -requestHeaders("Fetch with PUT without body", url, "POST", null, location.origin, "0"); -requestHeaders("Fetch with PUT with body", url, "PUT", "Request's body", location.origin, "14"); -requestHeaders("Fetch with POST without body", url, "POST", null, location.origin, "0"); -requestHeaders("Fetch with POST with text body", url, "POST", "Request's body", location.origin, "14"); -requestHeaders("Fetch with POST with FormData body", url, "POST", function() { return new FormData(); }, location.origin); -requestHeaders("Fetch with POST with URLSearchParams body", url, "POST", function() { return new URLSearchParams("name=value"); }, location.origin, "10"); -requestHeaders("Fetch with POST with Blob body", url, "POST", new Blob(["Test"]), location.origin, "4"); -requestHeaders("Fetch with POST with ArrayBuffer body", url, "POST", new ArrayBuffer(4), location.origin, "4"); -requestHeaders("Fetch with POST with Uint8Array body", url, "POST", new Uint8Array(4), location.origin, "4"); -requestHeaders("Fetch with POST with Int8Array body", url, "POST", new Int8Array(4), location.origin, "4"); -requestHeaders("Fetch with POST with Float16Array body", url, "POST", () => new Float16Array(1), location.origin, "2"); -requestHeaders("Fetch with POST with Float32Array body", url, "POST", new Float32Array(1), location.origin, "4"); -requestHeaders("Fetch with POST with Float64Array body", url, "POST", new Float64Array(1), location.origin, "8"); -requestHeaders("Fetch with POST with DataView body", url, "POST", new DataView(new ArrayBuffer(8), 0, 4), location.origin, "4"); -requestHeaders("Fetch with POST with Blob body with mime type", url, "POST", new Blob(["Test"], { type: "text/maybe" }), location.origin, "4"); -requestHeaders("Fetch with Chicken", url, "Chicken", null, location.origin, null); -requestHeaders("Fetch with Chicken with body", url, "Chicken", "Request's body", location.origin, "14"); - -function requestOriginHeader(method, mode, needsOrigin) { - promise_test(function(test){ - return fetch(url + "?headers=origin", {method:method, mode:mode}).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - if(needsOrigin) - assert_equals(resp.headers.get("x-request-origin") , location.origin, "Request should have an Origin header with origin: " + location.origin); - else - assert_equals(resp.headers.get("x-request-origin"), null, "Request should not have an Origin header") - }); - }, "Fetch with " + method + " and mode \"" + mode + "\" " + (needsOrigin ? "needs" : "does not need") + " an Origin header"); -} - -requestOriginHeader("GET", "cors", false); -requestOriginHeader("POST", "same-origin", true); -requestOriginHeader("POST", "no-cors", true); -requestOriginHeader("PUT", "same-origin", true); -requestOriginHeader("TacO", "same-origin", true); -requestOriginHeader("TacO", "cors", true); diff --git a/test/fixtures/wpt/fetch/api/basic/request-private-network-headers.tentative.any.js b/test/fixtures/wpt/fetch/api/basic/request-private-network-headers.tentative.any.js deleted file mode 100644 index 9662a91c177..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-private-network-headers.tentative.any.js +++ /dev/null @@ -1,18 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -requestForbiddenHeaders( - 'Access-Control-Request-Private-Network is a forbidden request header', - {'Access-Control-Request-Private-Network': ''}); - -var invalidRequestHeaders = [ - ["Access-Control-Request-Private-Network", "KO"], -]; - -invalidRequestHeaders.forEach(function(header) { - test(function() { - var request = new Request(""); - request.headers.set(header[0], header[1]); - assert_equals(request.headers.get(header[0]), null); - }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\""); -}); diff --git a/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html b/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html deleted file mode 100644 index bdea1e18531..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-referrer-redirected-worker.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Fetch in worker: referrer header - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js b/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js deleted file mode 100644 index 0c3357642d6..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-referrer.any.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function testReferrer(referrer, expected, desc) { - promise_test(function(test) { - var url = RESOURCES_DIR + "inspect-headers.py?headers=referer" - var req = new Request(url, { referrer: referrer }); - return fetch(req).then(function(resp) { - var actual = resp.headers.get("x-request-referer"); - if (expected) { - assert_equals(actual, expected, "request's referer should be: " + expected); - return; - } - if (actual) { - assert_equals(actual, "", "request's referer should be empty"); - } - }); - }, desc); -} - -testReferrer("about:client", self.location.href, 'about:client referrer'); - -var fooURL = new URL("./foo", self.location).href; -testReferrer(fooURL, fooURL, 'url referrer'); diff --git a/test/fixtures/wpt/fetch/api/basic/request-upload.any.js b/test/fixtures/wpt/fetch/api/basic/request-upload.any.js deleted file mode 100644 index 0c4813bb531..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-upload.any.js +++ /dev/null @@ -1,139 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js - -function testUpload(desc, url, method, createBody, expectedBody) { - const requestInit = {method}; - promise_test(function(test){ - const body = createBody(); - if (body) { - requestInit["body"] = body; - requestInit.duplex = "half"; - } - return fetch(url, requestInit).then(function(resp) { - return resp.text().then((text)=> { - assert_equals(text, expectedBody); - }); - }); - }, desc); -} - -function testUploadFailure(desc, url, method, createBody) { - const requestInit = {method}; - promise_test(t => { - const body = createBody(); - if (body) { - requestInit["body"] = body; - } - return promise_rejects_js(t, TypeError, fetch(url, requestInit)); - }, desc); -} - -const url = RESOURCES_DIR + "echo-content.py" - -testUpload("Fetch with PUT with body", url, - "PUT", - () => "Request's body", - "Request's body"); -testUpload("Fetch with POST with text body", url, - "POST", - () => "Request's body", - "Request's body"); -testUpload("Fetch with POST with URLSearchParams body", url, - "POST", - () => new URLSearchParams("name=value"), - "name=value"); -testUpload("Fetch with POST with Blob body", url, - "POST", - () => new Blob(["Test"]), - "Test"); -testUpload("Fetch with POST with ArrayBuffer body", url, - "POST", - () => new ArrayBuffer(4), - "\0\0\0\0"); -testUpload("Fetch with POST with Uint8Array body", url, - "POST", - () => new Uint8Array(4), - "\0\0\0\0"); -testUpload("Fetch with POST with Int8Array body", url, - "POST", - () => new Int8Array(4), - "\0\0\0\0"); -testUpload("Fetch with POST with Float16Array body", url, - "POST", - () => new Float16Array(2), - "\0\0\0\0"); -testUpload("Fetch with POST with Float32Array body", url, - "POST", - () => new Float32Array(1), - "\0\0\0\0"); -testUpload("Fetch with POST with Float64Array body", url, - "POST", - () => new Float64Array(1), - "\0\0\0\0\0\0\0\0"); -testUpload("Fetch with POST with DataView body", url, - "POST", - () => new DataView(new ArrayBuffer(8), 0, 4), - "\0\0\0\0"); -testUpload("Fetch with POST with Blob body with mime type", url, - "POST", - () => new Blob(["Test"], { type: "text/maybe" }), - "Test"); - -testUploadFailure("Fetch with POST with ReadableStream containing String", url, - "POST", - () => { - return new ReadableStream({start: controller => { - controller.enqueue("Test"); - controller.close(); - }}) - }); -testUploadFailure("Fetch with POST with ReadableStream containing null", url, - "POST", - () => { - return new ReadableStream({start: controller => { - controller.enqueue(null); - controller.close(); - }}) - }); -testUploadFailure("Fetch with POST with ReadableStream containing number", url, - "POST", - () => { - return new ReadableStream({start: controller => { - controller.enqueue(99); - controller.close(); - }}) - }); -testUploadFailure("Fetch with POST with ReadableStream containing ArrayBuffer", url, - "POST", - () => { - return new ReadableStream({start: controller => { - controller.enqueue(new ArrayBuffer()); - controller.close(); - }}) - }); -testUploadFailure("Fetch with POST with ReadableStream containing Blob", url, - "POST", - () => { - return new ReadableStream({start: controller => { - controller.enqueue(new Blob()); - controller.close(); - }}) - }); - -promise_test(async (test) => { - const resp = await fetch( - "/fetch/connection-pool/resources/network-partition-key.py?" - + `status=421&uuid=${token()}&partition_id=${get_host_info().ORIGIN}` - + `&dispatch=check_partition&addcounter=true`, - {method: "POST", body: "foobar"}); - assert_equals(resp.status, 421); - const text = await resp.text(); - assert_equals(text, "ok. Request was sent 2 times. 2 connections were created."); -}, "Fetch with POST with text body on 421 response should be retried once on new connection."); - -promise_test(async (test) => { - const body = new ReadableStream({start: c => c.close()}); - await promise_rejects_js(test, TypeError, fetch('/', {method: 'POST', body})); -}, "Streaming upload shouldn't work on Http/1.1."); diff --git a/test/fixtures/wpt/fetch/api/basic/request-upload.h2.any.js b/test/fixtures/wpt/fetch/api/basic/request-upload.h2.any.js deleted file mode 100644 index 68122278ccd..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/request-upload.h2.any.js +++ /dev/null @@ -1,209 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js - -const duplex = "half"; - -async function assertUpload(url, method, createBody, expectedBody) { - const requestInit = {method}; - const body = createBody(); - if (body) { - requestInit["body"] = body; - requestInit.duplex = "half"; - } - const resp = await fetch(url, requestInit); - const text = await resp.text(); - assert_equals(text, expectedBody); -} - -function testUpload(desc, url, method, createBody, expectedBody) { - promise_test(async () => { - await assertUpload(url, method, createBody, expectedBody); - }, desc); -} - -function createStream(chunks) { - return new ReadableStream({ - start: (controller) => { - for (const chunk of chunks) { - controller.enqueue(chunk); - } - controller.close(); - } - }); -} - -const url = RESOURCES_DIR + "echo-content.h2.py" - -testUpload("Fetch with POST with empty ReadableStream", url, - "POST", - () => { - return new ReadableStream({start: controller => { - controller.close(); - }}) - }, - ""); - -testUpload("Fetch with POST with ReadableStream", url, - "POST", - () => { - return new ReadableStream({start: controller => { - const encoder = new TextEncoder(); - controller.enqueue(encoder.encode("Test")); - controller.close(); - }}) - }, - "Test"); - -promise_test(async (test) => { - const body = new ReadableStream({start: controller => { - const encoder = new TextEncoder(); - controller.enqueue(encoder.encode("Test")); - controller.close(); - }}); - const resp = await fetch( - "/fetch/connection-pool/resources/network-partition-key.py?" - + `status=421&uuid=${token()}&partition_id=${self.origin}` - + `&dispatch=check_partition&addcounter=true`, - {method: "POST", body: body, duplex}); - assert_equals(resp.status, 421); - const text = await resp.text(); - assert_equals(text, "ok. Request was sent 1 times. 1 connections were created."); -}, "Fetch with POST with ReadableStream on 421 response should return the response and not retry."); - -promise_test(async (test) => { - const request = new Request('', { - body: new ReadableStream(), - method: 'POST', - duplex, - }); - - assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`); - - const response = await fetch('data:a/a;charset=utf-8,test', { - method: 'POST', - body: new ReadableStream(), - duplex, - }); - - assert_equals(await response.text(), 'test', `Response has correct body`); -}, "Feature detect for POST with ReadableStream"); - -promise_test(async (test) => { - const request = new Request('data:a/a;charset=utf-8,test', { - body: new ReadableStream(), - method: 'POST', - duplex, - }); - - assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`); - const response = await fetch(request); - assert_equals(await response.text(), 'test', `Response has correct body`); -}, "Feature detect for POST with ReadableStream, using request object"); - -test(() => { - let duplexAccessed = false; - - const request = new Request("", { - body: new ReadableStream(), - method: "POST", - get duplex() { - duplexAccessed = true; - return "half"; - }, - }); - - assert_equals( - request.headers.get("Content-Type"), - null, - `Request should not have a content-type set` - ); - assert_true(duplexAccessed, `duplex dictionary property should be accessed`); -}, "Synchronous feature detect"); - -// The asserts the synchronousFeatureDetect isn't broken by a partial implementation. -// An earlier feature detect was broken by Safari implementing streaming bodies as part of Request, -// but it failed when passed to fetch(). -// This tests ensures that UAs must not implement RequestInit.duplex and streaming request bodies without also implementing the fetch() parts. -promise_test(async () => { - let duplexAccessed = false; - - const request = new Request("", { - body: new ReadableStream(), - method: "POST", - get duplex() { - duplexAccessed = true; - return "half"; - }, - }); - - const supported = - request.headers.get("Content-Type") === null && duplexAccessed; - - // If the feature detect fails, assume the browser is being truthful (other tests pick up broken cases here) - if (!supported) return false; - - await assertUpload( - url, - "POST", - () => - new ReadableStream({ - start: (controller) => { - const encoder = new TextEncoder(); - controller.enqueue(encoder.encode("Test")); - controller.close(); - }, - }), - "Test" - ); -}, "Synchronous feature detect fails if feature unsupported"); - -promise_test(async (t) => { - const body = createStream(["hello"]); - const method = "POST"; - await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); -}, "Streaming upload with body containing a String"); - -promise_test(async (t) => { - const body = createStream([null]); - const method = "POST"; - await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); -}, "Streaming upload with body containing null"); - -promise_test(async (t) => { - const body = createStream([33]); - const method = "POST"; - await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); -}, "Streaming upload with body containing a number"); - -promise_test(async (t) => { - const url = "/fetch/api/resources/authentication.py?realm=test"; - const body = createStream([]); - const method = "POST"; - await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); -}, "Streaming upload should fail on a 401 response"); - -promise_test(async (t) => { - const abortMessage = 'foo abort'; - let streamCancelPromise = new Promise(async res => { - var stream = new ReadableStream({ - cancel: function(reason) { - res(reason); - } - }); - let abortController = new AbortController(); - let fetchPromise = promise_rejects_exactly(t, abortMessage, fetch('', { - method: 'POST', - body: stream, - duplex: 'half', - signal: abortController.signal - })); - abortController.abort(abortMessage); - await fetchPromise; - }); - - let cancelReason = await streamCancelPromise; - assert_equals( - cancelReason, abortMessage, 'ReadableStream.cancel should be called.'); -}, 'ReadbleStream should be closed on signal.abort'); diff --git a/test/fixtures/wpt/fetch/api/basic/response-null-body.any.js b/test/fixtures/wpt/fetch/api/basic/response-null-body.any.js deleted file mode 100644 index bb058926572..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/response-null-body.any.js +++ /dev/null @@ -1,38 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -const nullBodyStatus = [204, 205, 304]; -const methods = ["GET", "POST", "OPTIONS"]; - -for (const status of nullBodyStatus) { - for (const method of methods) { - promise_test( - async () => { - const url = - `${RESOURCES_DIR}status.py?code=${status}&content=hello-world`; - const resp = await fetch(url, { method }); - assert_equals(resp.status, status); - assert_equals(resp.body, null, "the body should be null"); - const text = await resp.text(); - assert_equals(text, "", "null bodies result in empty text"); - }, - `Response.body is null for responses with status=${status} (method=${method})`, - ); - } -} - -promise_test(async () => { - const url = `${RESOURCES_DIR}status.py?code=200&content=hello-world`; - const resp = await fetch(url, { method: "HEAD" }); - assert_equals(resp.status, 200); - assert_equals(resp.body, null, "the body should be null"); - const text = await resp.text(); - assert_equals(text, "", "null bodies result in empty text"); -}, `Response.body is null for responses with method=HEAD`); - -promise_test(async (t) => { - const integrity = "sha384-UT6f7WCFp32YJnp1is4l/ZYnOeQKpE8xjmdkLOwZ3nIP+tmT2aMRFQGJomjVf5cE"; - const url = `${RESOURCES_DIR}status.py?code=204&content=hello-world`; - const promise = fetch(url, { method: "GET", integrity }); - promise_rejects_js(t, TypeError, promise); -}, "Null body status with subresource integrity should abort"); diff --git a/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js b/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js deleted file mode 100644 index 0d123c42944..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/response-url.sub.any.js +++ /dev/null @@ -1,16 +0,0 @@ -function checkResponseURL(fetchedURL, expectedURL) -{ - promise_test(function() { - return fetch(fetchedURL).then(function(response) { - assert_equals(response.url, expectedURL); - }); - }, "Testing response url getter with " +fetchedURL); -} - -var baseURL = "http://{{host}}:{{ports[http][0]}}"; -checkResponseURL(baseURL + "/ada", baseURL + "/ada"); -checkResponseURL(baseURL + "/#", baseURL + "/"); -checkResponseURL(baseURL + "/#ada", baseURL + "/"); -checkResponseURL(baseURL + "#ada", baseURL + "/"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js deleted file mode 100644 index 9ef44183c17..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/scheme-about.any.js +++ /dev/null @@ -1,26 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function checkNetworkError(url, method) { - method = method || "GET"; - const desc = "Fetching " + url.substring(0, 45) + " with method " + method + " is KO" - promise_test(function(test) { - var promise = fetch(url, { method: method }); - return promise_rejects_js(test, TypeError, promise); - }, desc); -} - -checkNetworkError("about:blank", "GET"); -checkNetworkError("about:blank", "PUT"); -checkNetworkError("about:blank", "POST"); -checkNetworkError("about:invalid.com"); -checkNetworkError("about:config"); -checkNetworkError("about:unicorn"); - -promise_test(function(test) { - var promise = fetch("about:blank", { - "method": "GET", - "Range": "bytes=1-10" - }); - return promise_rejects_js(test, TypeError, promise); -}, "Fetching about:blank with range header does not affect behavior"); diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js deleted file mode 100644 index 8afdc033c9d..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/scheme-blob.sub.any.js +++ /dev/null @@ -1,125 +0,0 @@ -// META: script=../resources/utils.js - -function checkFetchResponse(url, data, mime, size, desc) { - promise_test(function(test) { - size = size.toString(); - return fetch(url).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type, "basic", "response type is basic"); - assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type")); - assert_equals(resp.headers.get("Content-Length"), size, "Content-Length is " + resp.headers.get("Content-Length")); - return resp.text(); - }).then(function(bodyAsText) { - assert_equals(bodyAsText, data, "Response's body is " + data); - }); - }, desc); -} - -var blob = new Blob(["Blob's data"], { "type" : "text/plain" }); -checkFetchResponse(URL.createObjectURL(blob), "Blob's data", "text/plain", blob.size, - "Fetching [GET] URL.createObjectURL(blob) is OK"); - -function checkKoUrl(url, method, desc) { - promise_test(function(test) { - var promise = fetch(url, {"method": method}); - return promise_rejects_js(test, TypeError, promise); - }, desc); -} - -var blob2 = new Blob(["Blob's data"], { "type" : "text/plain" }); -checkKoUrl("blob:http://{{domains[www]}}:{{ports[http][0]}}/", "GET", - "Fetching [GET] blob:http://{{domains[www]}}:{{ports[http][0]}}/ is KO"); - -var invalidRequestMethods = [ - "POST", - "OPTIONS", - "HEAD", - "PUT", - "DELETE", - "INVALID", -]; -invalidRequestMethods.forEach(function(method) { - checkKoUrl(URL.createObjectURL(blob2), method, "Fetching [" + method + "] URL.createObjectURL(blob) is KO"); -}); - -checkKoUrl("blob:not-backed-by-a-blob/", "GET", - "Fetching [GET] blob:not-backed-by-a-blob/ is KO"); - -let empty_blob = new Blob([]); -checkFetchResponse(URL.createObjectURL(empty_blob), "", "", 0, - "Fetching URL.createObjectURL(empty_blob) is OK"); - -let empty_type_blob = new Blob([], {type: ""}); -checkFetchResponse(URL.createObjectURL(empty_type_blob), "", "", 0, - "Fetching URL.createObjectURL(empty_type_blob) is OK"); - -let empty_data_blob = new Blob([], {type: "text/plain"}); -checkFetchResponse(URL.createObjectURL(empty_data_blob), "", "text/plain", 0, - "Fetching URL.createObjectURL(empty_data_blob) is OK"); - -let invalid_type_blob = new Blob([], {type: "invalid"}); -checkFetchResponse(URL.createObjectURL(invalid_type_blob), "", "", 0, - "Fetching URL.createObjectURL(invalid_type_blob) is OK"); - -promise_test(function(test) { - return fetch("/images/blue.png").then(function(resp) { - return resp.arrayBuffer(); - }).then(function(image_buffer) { - let blob = new Blob([image_buffer]); - return fetch(URL.createObjectURL(blob)).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type, "basic", "response type is basic"); - assert_equals(resp.headers.get("Content-Type"), "", "Content-Type is " + resp.headers.get("Content-Type")); - }) - }); -}, "Blob content is not sniffed for a content type [image/png]"); - -let simple_xml_string = ''; -let xml_blob_no_type = new Blob([simple_xml_string]); -checkFetchResponse(URL.createObjectURL(xml_blob_no_type), simple_xml_string, "", 45, - "Blob content is not sniffed for a content type [text/xml]"); - -let simple_text_string = 'Hello, World!'; -promise_test(function(test) { - let blob = new Blob([simple_text_string], {"type": "text/plain"}); - let slice = blob.slice(7, simple_text_string.length, "\0"); - return fetch(URL.createObjectURL(slice)).then(function (resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type, "basic", "response type is basic"); - assert_equals(resp.headers.get("Content-Type"), ""); - assert_equals(resp.headers.get("Content-Length"), "6"); - return resp.text(); - }).then(function(bodyAsText) { - assert_equals(bodyAsText, "World!"); - }); -}, "Set content type to the empty string for slice with invalid content type"); - -promise_test(function(test) { - let blob = new Blob([simple_text_string], {"type": "text/plain"}); - let slice = blob.slice(7, simple_text_string.length, "\0"); - return fetch(URL.createObjectURL(slice)).then(function (resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type, "basic", "response type is basic"); - assert_equals(resp.headers.get("Content-Type"), ""); - assert_equals(resp.headers.get("Content-Length"), "6"); - return resp.text(); - }).then(function(bodyAsText) { - assert_equals(bodyAsText, "World!"); - }); -}, "Set content type to the empty string for slice with no content type "); - -promise_test(function(test) { - let blob = new Blob([simple_xml_string]); - let slice = blob.slice(0, 38); - return fetch(URL.createObjectURL(slice)).then(function (resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type, "basic", "response type is basic"); - assert_equals(resp.headers.get("Content-Type"), ""); - assert_equals(resp.headers.get("Content-Length"), "38"); - return resp.text(); - }).then(function(bodyAsText) { - assert_equals(bodyAsText, ''); - }); -}, "Blob.slice should not sniff the content for a content type"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js deleted file mode 100644 index 55df43bd503..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/scheme-data.any.js +++ /dev/null @@ -1,43 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function checkFetchResponse(url, data, mime, fetchMode, method) { - var cut = (url.length >= 40) ? "[...]" : ""; - var desc = "Fetching " + (method ? "[" + method + "] " : "") + url.substring(0, 40) + cut + " is OK"; - var init = {"method": method || "GET"}; - if (fetchMode) { - init.mode = fetchMode; - desc += " (" + fetchMode + ")"; - } - promise_test(function(test) { - return fetch(url, init).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.statusText, "OK", "HTTP statusText is OK"); - assert_equals(resp.type, "basic", "response type is basic"); - assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type")); - return resp.text(); - }).then(function(body) { - assert_equals(body, data, "Response's body is correct"); - }); - }, desc); -} - -checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII"); -checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "same-origin"); -checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "cors"); -checkFetchResponse("data:text/plain;base64,cmVzcG9uc2UncyBib2R5", "response's body", "text/plain"); -checkFetchResponse("data:image/png;base64,cmVzcG9uc2UncyBib2R5", - "response's body", - "image/png"); -checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", null, "POST"); -checkFetchResponse("data:,response%27s%20body", "", "text/plain;charset=US-ASCII", null, "HEAD"); - -function checkKoUrl(url, method, desc) { - var cut = (url.length >= 40) ? "[...]" : ""; - desc = "Fetching [" + method + "] " + url.substring(0, 45) + cut + " is KO" - promise_test(function(test) { - return promise_rejects_js(test, TypeError, fetch(url, {"method": method})); - }, desc); -} - -checkKoUrl("data:notAdataUrl.com", "GET"); diff --git a/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js b/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js deleted file mode 100644 index 550f69c41b5..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/scheme-others.sub.any.js +++ /dev/null @@ -1,31 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function checkKoUrl(url, desc) { - if (!desc) - desc = "Fetching " + url.substring(0, 45) + " is KO" - promise_test(function(test) { - var promise = fetch(url); - return promise_rejects_js(test, TypeError, promise); - }, desc); -} - -var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/"; -checkKoUrl("aaa" + urlWithoutScheme); -checkKoUrl("cap" + urlWithoutScheme); -checkKoUrl("cid" + urlWithoutScheme); -checkKoUrl("dav" + urlWithoutScheme); -checkKoUrl("dict" + urlWithoutScheme); -checkKoUrl("dns" + urlWithoutScheme); -checkKoUrl("geo" + urlWithoutScheme); -checkKoUrl("im" + urlWithoutScheme); -checkKoUrl("imap" + urlWithoutScheme); -checkKoUrl("ipp" + urlWithoutScheme); -checkKoUrl("ldap" + urlWithoutScheme); -checkKoUrl("mailto" + urlWithoutScheme); -checkKoUrl("nfs" + urlWithoutScheme); -checkKoUrl("pop" + urlWithoutScheme); -checkKoUrl("rtsp" + urlWithoutScheme); -checkKoUrl("snmp" + urlWithoutScheme); - -done(); diff --git a/test/fixtures/wpt/fetch/api/basic/status.h2.any.js b/test/fixtures/wpt/fetch/api/basic/status.h2.any.js deleted file mode 100644 index 99fec88f505..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/status.h2.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// See also /xhr/status.h2.window.js - -[ - 200, - 210, - 400, - 404, - 410, - 500, - 502 -].forEach(status => { - promise_test(async t => { - const response = await fetch("/xhr/resources/status.py?code=" + status); - assert_equals(response.status, status, "status should be " + status); - assert_equals(response.statusText, "", "statusText should be the empty string"); - }, "statusText over H2 for status " + status + " should be the empty string"); -}); diff --git a/test/fixtures/wpt/fetch/api/basic/stream-response.any.js b/test/fixtures/wpt/fetch/api/basic/stream-response.any.js deleted file mode 100644 index d964dda717c..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/stream-response.any.js +++ /dev/null @@ -1,40 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function streamBody(reader, test, count = 0) { - return reader.read().then(function(data) { - if (!data.done && count < 2) { - count += 1; - return streamBody(reader, test, count); - } else { - test.step(function() { - assert_true(count >= 2, "Retrieve body progressively"); - }); - } - }); -} - -//simulate streaming: -//count is large enough to let the UA deliver the body before it is completely retrieved -promise_test(function(test) { - return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(resp) { - if (resp.body) - return streamBody(resp.body.getReader(), test); - else - test.step(function() { - assert_unreached( "Body does not exist in response"); - }); - }); -}, "Stream response's body when content-type is present"); - -// This test makes sure that the response body is not buffered if no content type is provided. -promise_test(function(test) { - return fetch(RESOURCES_DIR + "trickle.py?ms=300&count=10¬ype=true").then(function(resp) { - if (resp.body) - return streamBody(resp.body.getReader(), test); - else - test.step(function() { - assert_unreached( "Body does not exist in response"); - }); - }); -}, "Stream response's body when content-type is not present"); diff --git a/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js b/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js deleted file mode 100644 index 382efc1a8b4..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/stream-safe-creation.any.js +++ /dev/null @@ -1,54 +0,0 @@ -// META: global=window,worker - -// These tests verify that stream creation is not affected by changes to -// Object.prototype. - -const creationCases = { - fetch: async () => fetch(location.href), - request: () => new Request(location.href, {method: 'POST', body: 'hi'}), - response: () => new Response('bye'), - consumeEmptyResponse: () => new Response().text(), - consumeNonEmptyResponse: () => new Response(new Uint8Array([64])).text(), - consumeEmptyRequest: () => new Request(location.href).text(), - consumeNonEmptyRequest: () => new Request(location.href, - {method: 'POST', body: 'yes'}).arrayBuffer(), -}; - -for (const creationCase of Object.keys(creationCases)) { - for (const accessorName of ['start', 'type', 'size', 'highWaterMark']) { - promise_test(async t => { - Object.defineProperty(Object.prototype, accessorName, { - get() { throw Error(`Object.prototype.${accessorName} was accessed`); }, - configurable: true - }); - t.add_cleanup(() => { - delete Object.prototype[accessorName]; - return Promise.resolve(); - }); - await creationCases[creationCase](); - }, `throwing Object.prototype.${accessorName} accessor should not affect ` + - `stream creation by '${creationCase}'`); - - promise_test(async t => { - // -1 is a convenient value which is invalid, and should cause the - // constructor to throw, for all four fields. - Object.prototype[accessorName] = -1; - t.add_cleanup(() => { - delete Object.prototype[accessorName]; - return Promise.resolve(); - }); - await creationCases[creationCase](); - }, `Object.prototype.${accessorName} accessor returning invalid value ` + - `should not affect stream creation by '${creationCase}'`); - } - - promise_test(async t => { - Object.prototype.start = controller => controller.error(new Error('start')); - t.add_cleanup(() => { - delete Object.prototype.start; - return Promise.resolve(); - }); - await creationCases[creationCase](); - }, `Object.prototype.start function which errors the stream should not ` + - `affect stream creation by '${creationCase}'`); -} diff --git a/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js b/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js deleted file mode 100644 index 05c8c88825d..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/text-utf8.any.js +++ /dev/null @@ -1,74 +0,0 @@ -// META: title=Fetch: Request and Response text() should decode as UTF-8 -// META: global=window,worker -// META: script=../resources/utils.js - -function testTextDecoding(body, expectedText, urlParameter, title) -{ - var arrayBuffer = stringToArray(body); - - promise_test(function(test) { - var request = new Request("", {method: "POST", body: arrayBuffer}); - return request.text().then(function(value) { - assert_equals(value, expectedText, "Request.text() should decode data as UTF-8"); - }); - }, title + " with Request.text()"); - - promise_test(function(test) { - var response = new Response(arrayBuffer); - return response.text().then(function(value) { - assert_equals(value, expectedText, "Response.text() should decode data as UTF-8"); - }); - }, title + " with Response.text()"); - - promise_test(function(test) { - return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-8&content=" + urlParameter).then(function(response) { - return response.text().then(function(value) { - assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8"); - }); - }); - }, title + " with fetched data (UTF-8 charset)"); - - promise_test(function(test) { - return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-16&content=" + urlParameter).then(function(response) { - return response.text().then(function(value) { - assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8"); - }); - }); - }, title + " with fetched data (UTF-16 charset)"); - - promise_test(function(test) { - return new Response(body).arrayBuffer().then(function(buffer) { - assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Response.arrayBuffer() should contain data encoded as UTF-8"); - }); - }, title + " (Response object)"); - - promise_test(function(test) { - return new Request("", {method: "POST", body: body}).arrayBuffer().then(function(buffer) { - assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Request.arrayBuffer() should contain data encoded as UTF-8"); - }); - }, title + " (Request object)"); - -} - -var utf8WithBOM = "\xef\xbb\xbf\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90"; -var utf8WithBOMAsURLParameter = "%EF%BB%BF%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90"; -var utf8WithoutBOM = "\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90"; -var utf8WithoutBOMAsURLParameter = "%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90"; -var utf8Decoded = "三村かな子"; -testTextDecoding(utf8WithBOM, utf8Decoded, utf8WithBOMAsURLParameter, "UTF-8 with BOM"); -testTextDecoding(utf8WithoutBOM, utf8Decoded, utf8WithoutBOMAsURLParameter, "UTF-8 without BOM"); - -var utf16BEWithBOM = "\xfe\xff\x4e\x09\x67\x51\x30\x4b\x30\x6a\x5b\x50"; -var utf16BEWithBOMAsURLParameter = "%fe%ff%4e%09%67%51%30%4b%30%6a%5b%50"; -var utf16BEWithBOMDecodedAsUTF8 = "��N\tgQ0K0j[P"; -testTextDecoding(utf16BEWithBOM, utf16BEWithBOMDecodedAsUTF8, utf16BEWithBOMAsURLParameter, "UTF-16BE with BOM decoded as UTF-8"); - -var utf16LEWithBOM = "\xff\xfe\x09\x4e\x51\x67\x4b\x30\x6a\x30\x50\x5b"; -var utf16LEWithBOMAsURLParameter = "%ff%fe%09%4e%51%67%4b%30%6a%30%50%5b"; -var utf16LEWithBOMDecodedAsUTF8 = "��\tNQgK0j0P["; -testTextDecoding(utf16LEWithBOM, utf16LEWithBOMDecodedAsUTF8, utf16LEWithBOMAsURLParameter, "UTF-16LE with BOM decoded as UTF-8"); - -var utf16WithoutBOM = "\xe6\x00\xf8\x00\xe5\x00\x0a\x00\xc6\x30\xb9\x30\xc8\x30\x0a\x00"; -var utf16WithoutBOMAsURLParameter = "%E6%00%F8%00%E5%00%0A%00%C6%30%B9%30%C8%30%0A%00"; -var utf16WithoutBOMDecoded = "\ufffd\u0000\ufffd\u0000\ufffd\u0000\u000a\u0000\ufffd\u0030\ufffd\u0030\ufffd\u0030\u000a\u0000"; -testTextDecoding(utf16WithoutBOM, utf16WithoutBOMDecoded, utf16WithoutBOMAsURLParameter, "UTF-16 without BOM decoded as UTF-8"); diff --git a/test/fixtures/wpt/fetch/api/basic/url-parsing.sub.html b/test/fixtures/wpt/fetch/api/basic/url-parsing.sub.html deleted file mode 100644 index fa47b29473a..00000000000 --- a/test/fixtures/wpt/fetch/api/basic/url-parsing.sub.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - -
- diff --git a/test/fixtures/wpt/fetch/api/body/cloned-any.js b/test/fixtures/wpt/fetch/api/body/cloned-any.js deleted file mode 100644 index 2bca96c7043..00000000000 --- a/test/fixtures/wpt/fetch/api/body/cloned-any.js +++ /dev/null @@ -1,50 +0,0 @@ -// Changing the body after it have been passed to Response/Request -// should not change the outcome of the consumed body - -const url = 'http://a'; -const method = 'post'; - -promise_test(async t => { - const body = new FormData(); - body.set('a', '1'); - const res = new Response(body); - const req = new Request(url, { method, body }); - body.set('a', '2'); - assert_true((await res.formData()).get('a') === '1'); - assert_true((await req.formData()).get('a') === '1'); -}, 'FormData is cloned'); - -promise_test(async t => { - const body = new URLSearchParams({a: '1'}); - const res = new Response(body); - const req = new Request(url, { method, body }); - body.set('a', '2'); - assert_true((await res.formData()).get('a') === '1'); - assert_true((await req.formData()).get('a') === '1'); -}, 'URLSearchParams is cloned'); - -promise_test(async t => { - const body = new Uint8Array([97]); // a - const res = new Response(body); - const req = new Request(url, { method, body }); - body[0] = 98; // b - assert_true(await res.text() === 'a'); - assert_true(await req.text() === 'a'); -}, 'TypedArray is cloned'); - -promise_test(async t => { - const body = new Uint8Array([97]); // a - const res = new Response(body.buffer); - const req = new Request(url, { method, body: body.buffer }); - body[0] = 98; // b - assert_true(await res.text() === 'a'); - assert_true(await req.text() === 'a'); -}, 'ArrayBuffer is cloned'); - -promise_test(async t => { - const body = new Blob(['a']); - const res = new Response(body); - const req = new Request(url, { method, body }); - assert_true(await res.blob() !== body); - assert_true(await req.blob() !== body); -}, 'Blob is cloned'); diff --git a/test/fixtures/wpt/fetch/api/body/formdata.any.js b/test/fixtures/wpt/fetch/api/body/formdata.any.js deleted file mode 100644 index 6733fa0ed70..00000000000 --- a/test/fixtures/wpt/fetch/api/body/formdata.any.js +++ /dev/null @@ -1,25 +0,0 @@ -promise_test(async t => { - const res = new Response(new FormData()); - const fd = await res.formData(); - assert_true(fd instanceof FormData); -}, 'Consume empty response.formData() as FormData'); - -promise_test(async t => { - const req = new Request('about:blank', { - method: 'POST', - body: new FormData() - }); - const fd = await req.formData(); - assert_true(fd instanceof FormData); -}, 'Consume empty request.formData() as FormData'); - -promise_test(async t => { - let formdata = new FormData(); - formdata.append('foo', new Blob([JSON.stringify({ bar: "baz", })], { type: "application/json" })); - let blob = await new Response(formdata).blob(); - let body = await blob.text(); - blob = new Blob([body.toLowerCase()], { type: blob.type.toLowerCase() }); - let formdataWithLowercaseBody = await new Response(blob).formData(); - assert_true(formdataWithLowercaseBody.has("foo")); - assert_equals(formdataWithLowercaseBody.get("foo").type, "application/json"); -}, 'Consume multipart/form-data headers case-insensitively'); diff --git a/test/fixtures/wpt/fetch/api/body/mime-type.any.js b/test/fixtures/wpt/fetch/api/body/mime-type.any.js deleted file mode 100644 index ed19309bdb2..00000000000 --- a/test/fixtures/wpt/fetch/api/body/mime-type.any.js +++ /dev/null @@ -1,127 +0,0 @@ -[ - () => new Request("about:blank", { headers: { "Content-Type": "text/plain" } }), - () => new Response("", { headers: { "Content-Type": "text/plain" } }) -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); - const newMIMEType = "test/test"; - bodyContainer.headers.set("Content-Type", newMIMEType); - const blob = await bodyContainer.blob(); - assert_equals(blob.type, newMIMEType); - }, `${bodyContainer.constructor.name}: overriding explicit Content-Type`); -}); - -[ - () => new Request("about:blank", { body: new URLSearchParams(), method: "POST" }), - () => new Response(new URLSearchParams()), -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - assert_equals(bodyContainer.headers.get("Content-Type"), "application/x-www-form-urlencoded;charset=UTF-8"); - bodyContainer.headers.delete("Content-Type"); - const blob = await bodyContainer.blob(); - assert_equals(blob.type, ""); - }, `${bodyContainer.constructor.name}: removing implicit Content-Type`); -}); - -[ - () => new Request("about:blank", { body: new ArrayBuffer(), method: "POST" }), - () => new Response(new ArrayBuffer()), -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - assert_equals(bodyContainer.headers.get("Content-Type"), null); - const newMIMEType = "test/test"; - bodyContainer.headers.set("Content-Type", newMIMEType); - const blob = await bodyContainer.blob(); - assert_equals(blob.type, newMIMEType); - }, `${bodyContainer.constructor.name}: setting missing Content-Type`); -}); - -[ - () => new Request("about:blank", { method: "POST" }), - () => new Response(), -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - const blob = await bodyContainer.blob(); - assert_equals(blob.type, ""); - }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body`); -}); - -[ - () => new Request("about:blank", { method: "POST", headers: [["Content-Type", "Mytext/Plain"]] }), - () => new Response("", { headers: [["Content-Type", "Mytext/Plain"]] }) -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - const blob = await bodyContainer.blob(); - assert_equals(blob.type, 'mytext/plain'); - }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body with Content-Type`); -}); - -[ - () => new Request("about:blank", { body: new Blob([""]), method: "POST" }), - () => new Response(new Blob([""])) -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - const blob = await bodyContainer.blob(); - assert_equals(blob.type, ""); - assert_equals(bodyContainer.headers.get("Content-Type"), null); - }, `${bodyContainer.constructor.name}: MIME type for Blob`); -}); - -[ - () => new Request("about:blank", { body: new Blob([""], { type: "Text/Plain" }), method: "POST" }), - () => new Response(new Blob([""], { type: "Text/Plain" })) -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - const blob = await bodyContainer.blob(); - assert_equals(blob.type, "text/plain"); - assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); - }, `${bodyContainer.constructor.name}: MIME type for Blob with non-empty type`); -}); - -[ - () => new Request("about:blank", { method: "POST", body: new Blob([""], { type: "Text/Plain" }), headers: [["Content-Type", "Text/Html"]] }), - () => new Response(new Blob([""], { type: "Text/Plain" }), { headers: [["Content-Type", "Text/Html"]] }) -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - const cloned = bodyContainer.clone(); - promise_test(async t => { - const blobs = [await bodyContainer.blob(), await cloned.blob()]; - assert_equals(blobs[0].type, "text/html"); - assert_equals(blobs[1].type, "text/html"); - assert_equals(bodyContainer.headers.get("Content-Type"), "Text/Html"); - assert_equals(cloned.headers.get("Content-Type"), "Text/Html"); - }, `${bodyContainer.constructor.name}: Extract a MIME type with clone`); -}); - -[ - () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST", headers: [["Content-Type", "text/html"]] }), - () => new Response(new Blob([], { type: "text/plain" }), { headers: [["Content-Type", "text/html"]] }), -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - assert_equals(bodyContainer.headers.get("Content-Type"), "text/html"); - const blob = await bodyContainer.blob(); - assert_equals(blob.type, "text/html"); - }, `${bodyContainer.constructor.name}: Content-Type in headers wins Blob"s type`); -}); - -[ - () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST" }), - () => new Response(new Blob([], { type: "text/plain" })), -].forEach(bodyContainerCreator => { - const bodyContainer = bodyContainerCreator(); - promise_test(async t => { - assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); - const newMIMEType = "text/html"; - bodyContainer.headers.set("Content-Type", newMIMEType); - const blob = await bodyContainer.blob(); - assert_equals(blob.type, newMIMEType); - }, `${bodyContainer.constructor.name}: setting missing Content-Type in headers and it wins Blob"s type`); -}); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js b/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js deleted file mode 100644 index 95de0af2d8f..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-basic.any.js +++ /dev/null @@ -1,43 +0,0 @@ -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -const { - HTTPS_ORIGIN, - HTTP_ORIGIN_WITH_DIFFERENT_PORT, - HTTP_REMOTE_ORIGIN, - HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, - HTTPS_REMOTE_ORIGIN, -} = get_host_info(); - -function cors(desc, origin) { - const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`; - const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`; - - promise_test((test) => { - return fetch(urlAllowCors, {'mode': 'no-cors'}).then((resp) => { - assert_equals(resp.status, 0, "Opaque filter: status is 0"); - assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\""); - assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque"); - return resp.text().then((value) => { - assert_equals(value, "", "Opaque response should have an empty body"); - }); - }); - }, `${desc} [no-cors mode]`); - - promise_test((test) => { - return promise_rejects_js(test, TypeError, fetch(url, {'mode': 'cors'})); - }, `${desc} [server forbid CORS]`); - - promise_test((test) => { - return fetch(urlAllowCors, {'mode': 'cors'}).then((resp) => { - assert_equals(resp.status, 200, "Fetch's response's status is 200"); - assert_equals(resp.type , "cors", "CORS response's type is cors"); - }); - }, `${desc} [cors mode]`); -} - -cors('Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT); -cors('Same domain different protocol different port', HTTPS_ORIGIN); -cors('Cross domain basic usage', HTTP_REMOTE_ORIGIN); -cors('Cross domain different port', HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT); -cors('Cross domain different protocol', HTTPS_REMOTE_ORIGIN); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js deleted file mode 100644 index f5217b42460..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-cookies-redirect.any.js +++ /dev/null @@ -1,49 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; -var urlSetCookies1 = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; -var urlSetCookies2 = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; -var urlCheckCookies = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie"; - -var urlSetCookiesParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; -urlSetCookiesParameters += "|header(Access-Control-Allow-Credentials,true)"; - -urlSetCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1)"; -urlSetCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2)"; - -urlClearCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1%3B%20max-age=0)"; -urlClearCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2%3B%20max-age=0)"; - -promise_test(async (test) => { - await fetch(urlSetCookies1 + urlSetCookiesParameters1, {"credentials": "include", "mode": "cors"}); - await fetch(urlSetCookies2 + urlSetCookiesParameters2, {"credentials": "include", "mode": "cors"}); -}, "Set cookies"); - -function doTest(usePreflight) { - promise_test(async (test) => { - var url = redirectUrl; - var uuid_token = token(); - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - urlParameters += "&redirect_status=301"; - urlParameters += "&location=" + encodeURIComponent(urlCheckCookies); - urlParameters += "&allow_headers=a&headers=Cookie"; - headers = []; - if (usePreflight) - headers.push(["a", "b"]); - - var requestInit = {"credentials": "include", "mode": "cors", "headers": headers}; - var response = await fetch(url + urlParameters, requestInit); - - assert_equals(response.headers.get("x-request-cookie") , "a=2", "Request includes cookie(s)"); - }, "Testing credentials after cross-origin redirection with CORS and " + (usePreflight ? "" : "no ") + "preflight"); -} - -doTest(false); -doTest(true); - -promise_test(async (test) => { - await fetch(urlSetCookies1 + urlClearCookiesParameters1, {"credentials": "include", "mode": "cors"}); - await fetch(urlSetCookies2 + urlClearCookiesParameters2, {"credentials": "include", "mode": "cors"}); -}, "Clean cookies"); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js b/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js deleted file mode 100644 index 8c666e4782f..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-cookies.any.js +++ /dev/null @@ -1,56 +0,0 @@ -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function corsCookies(desc, baseURL1, baseURL2, credentialsMode, cookies) { - var urlSetCookie = baseURL1 + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; - var urlCheckCookies = baseURL2 + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie"; - //enable cors with credentials - var urlParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; - urlParameters += "|header(Access-Control-Allow-Credentials,true)"; - - var urlCleanParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; - urlCleanParameters += "|header(Access-Control-Allow-Credentials,true)"; - if (cookies) { - urlParameters += "|header(Set-Cookie,"; - urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)"; - urlCleanParameters += "|header(Set-Cookie,"; - urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)"; - } - - var requestInit = {"credentials": credentialsMode, "mode": "cors"}; - - promise_test(function(test){ - return fetch(urlSetCookie + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - //check cookies sent - return fetch(urlCheckCookies, requestInit); - }).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response"); - if (credentialsMode === "include" && baseURL1 === baseURL2) { - assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request includes cookie(s)"); - } - else { - assert_false(resp.headers.has("x-request-cookie") , "Request should have no cookie"); - } - //clean cookies - return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}); - }).catch(function(e) { - return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}).then(function(resp) { - throw e; - }) - }); - }, desc); -} - -var local = get_host_info().HTTP_ORIGIN; -var remote = get_host_info().HTTP_REMOTE_ORIGIN; -// FIXME: otherRemote might not be accessible on some test environments. -var otherRemote = local.replace("http://", "http://www."); - -corsCookies("Omit mode: no cookie sent", local, local, "omit", ["g=7"]); -corsCookies("Include mode: 1 cookie", remote, remote, "include", ["a=1"]); -corsCookies("Include mode: local cookies are not sent with remote request", local, remote, "include", ["c=3"]); -corsCookies("Include mode: remote cookies are not sent with local request", remote, local, "include", ["d=4"]); -corsCookies("Same-origin mode: cookies are discarded in cors request", remote, remote, "same-origin", ["f=6"]); -corsCookies("Include mode: remote cookies are not sent with other remote request", remote, otherRemote, "include", ["e=5"]); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js deleted file mode 100644 index 340e99ab5f9..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-expose-star.sub.any.js +++ /dev/null @@ -1,41 +0,0 @@ -// META: script=../resources/utils.js - -const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt", - sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|" - -promise_test(() => { - const headers = "header(Access-Control-Allow-Origin,*)" - return fetch(url + sharedHeaders + headers).then(resp => { - assert_equals(resp.status, 200) - assert_equals(resp.type , "cors") - assert_equals(resp.headers.get("test"), "X") - assert_equals(resp.headers.get("set-cookie"), null) - assert_equals(resp.headers.get("*"), "whoa") - }) -}, "Basic Access-Control-Expose-Headers: * support") - -promise_test(() => { - const origin = location.origin, // assuming an ASCII origin - headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)" - return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => { - assert_equals(resp.status, 200) - assert_equals(resp.type , "cors") - assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted - assert_equals(resp.headers.get("test"), null) - assert_equals(resp.headers.get("set-cookie"), null) - assert_equals(resp.headers.get("*"), "whoa") - }) -}, "* for credentialed fetches only matches literally") - -promise_test(() => { - const headers = "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)" - return fetch(url + sharedHeaders + headers).then(resp => { - assert_equals(resp.status, 200) - assert_equals(resp.type , "cors") - assert_equals(resp.headers.get("test"), "X") - assert_equals(resp.headers.get("set-cookie"), null) - assert_equals(resp.headers.get("*"), "whoa") - }) -}, "* can be one of several values") - -done(); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js deleted file mode 100644 index 5f9492487f1..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-filtering.sub.any.js +++ /dev/null @@ -1,65 +0,0 @@ -// META: script=../resources/utils.js - -function corsFilter(corsUrl, headerName, headerValue, isFiltered) { - var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|header(Access-Control-Allow-Origin,*)"; - promise_test(function(test) { - return fetch(url).then(function(resp) { - assert_equals(resp.status, 200, "Fetch success with code 200"); - assert_equals(resp.type , "cors", "CORS fetch's response has cors type"); - if (!isFiltered) { - assert_equals(resp.headers.get(headerName), headerValue, - headerName + " header should be included in response with value: " + headerValue); - } else { - assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response"); - } - }); - }, "CORS filter on " + headerName + " header"); -} - -function corsExposeFilter(corsUrl, headerName, headerValue, isForbidden, withCredentials) { - var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|" + - "header(Access-Control-Allow-Origin, http://{{host}}:{{ports[http][0]}})" + - "header(Access-Control-Allow-Credentials, true)" + - "header(Access-Control-Expose-Headers," + headerName + ")"; - - var title = "CORS filter on " + headerName + " header, header is " + (isForbidden ? "forbidden" : "exposed"); - if (withCredentials) - title+= "(credentials = include)"; - promise_test(function(test) { - return fetch(new Request(url, { credentials: withCredentials ? "include" : "omit" })).then(function(resp) { - assert_equals(resp.status, 200, "Fetch success with code 200"); - assert_equals(resp.type , "cors", "CORS fetch's response has cors type"); - if (!isForbidden) { - assert_equals(resp.headers.get(headerName), headerValue, - headerName + " header should be included in response with value: " + headerValue); - } else { - assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response"); - } - }); - }, title); -} - -var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; - -corsFilter(url, "Cache-Control", "no-cache", false); -corsFilter(url, "Content-Language", "fr", false); -corsFilter(url, "Content-Type", "text/html", false); -corsFilter(url, "Expires","04 May 1988 22:22:22 GMT" , false); -corsFilter(url, "Last-Modified", "04 May 1988 22:22:22 GMT", false); -corsFilter(url, "Pragma", "no-cache", false); -corsFilter(url, "Content-Length", "3" , false); // top.txt contains "top" - -corsFilter(url, "Age", "27", true); -corsFilter(url, "Server", "wptServe" , true); -corsFilter(url, "Warning", "Mind the gap" , true); -corsFilter(url, "Set-Cookie", "name=value; max-age=0", true); -corsFilter(url, "Set-Cookie2", "name=value; max-age=0", true); - -corsExposeFilter(url, "Age", "27", false); -corsExposeFilter(url, "Server", "wptServe" , false); -corsExposeFilter(url, "Warning", "Mind the gap" , false); - -corsExposeFilter(url, "Set-Cookie", "name=value; max-age=0" , true); -corsExposeFilter(url, "Set-Cookie2", "name=value; max-age=0" , true); -corsExposeFilter(url, "Set-Cookie", "name=value; max-age=0" , true, true); -corsExposeFilter(url, "Set-Cookie2", "name=value; max-age=0" , true, true); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-keepalive.any.js b/test/fixtures/wpt/fetch/api/cors/cors-keepalive.any.js deleted file mode 100644 index f54bf4f9b60..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-keepalive.any.js +++ /dev/null @@ -1,116 +0,0 @@ -// META: global=window -// META: timeout=long -// META: title=Fetch API: keepalive handling -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=../resources/keepalive-helper.js -// META: script=../resources/utils.js - -'use strict'; - -const { - HTTP_NOTSAMESITE_ORIGIN, - HTTPS_ORIGIN, - HTTP_ORIGIN_WITH_DIFFERENT_PORT, - HTTP_REMOTE_ORIGIN, - HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, - HTTPS_REMOTE_ORIGIN, -} = get_host_info(); - -/** - * Tests to cover the basic behaviors of keepalive + cors/no-cors mode requests - * to different `origin` when the initiator document is still alive. They should - * behave the same as without setting keepalive. - */ -function keepaliveCorsBasicTest(desc, origin) { - const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`; - const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`; - - promise_test((test) => { - return fetch(urlAllowCors, {keepalive: true, 'mode': 'no-cors'}) - .then((resp) => { - assert_equals(resp.status, 0, 'Opaque filter: status is 0'); - assert_equals(resp.statusText, '', 'Opaque filter: statusText is ""'); - assert_equals( - resp.type, 'opaque', 'Opaque filter: response\'s type is opaque'); - return resp.text().then((value) => { - assert_equals( - value, '', 'Opaque response should have an empty body'); - }); - }); - }, `${desc} [no-cors mode]`); - - promise_test((test) => { - return promise_rejects_js( - test, TypeError, fetch(url, {keepalive: true, 'mode': 'cors'})); - }, `${desc} [cors mode, server forbid CORS]`); - - promise_test((test) => { - return fetch(urlAllowCors, {keepalive: true, 'mode': 'cors'}) - .then((resp) => { - assert_equals(resp.status, 200, 'Fetch\'s response\'s status is 200'); - assert_equals(resp.type, 'cors', 'CORS response\'s type is cors'); - }); - }, `${desc} [cors mode]`); -} - -keepaliveCorsBasicTest( - `[keepalive] Same domain different port`, HTTP_ORIGIN_WITH_DIFFERENT_PORT); -keepaliveCorsBasicTest( - `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN); -keepaliveCorsBasicTest( - `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN); -keepaliveCorsBasicTest( - `[keepalive] Cross domain different port`, - HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT); -keepaliveCorsBasicTest( - `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN); - -/** - * In a same-site iframe, and in `unload` event handler, test to fetch - * a keepalive URL that involves in different cors modes. - */ -function keepaliveCorsInUnloadTest(description, origin, method) { - const evt = 'unload'; - for (const mode of ['no-cors', 'cors']) { - for (const disallowCrossOrigin of [false, true]) { - const desc = `${description} ${method} request in ${evt} [${mode} mode` + - (disallowCrossOrigin ? ']' : ', server forbid CORS]'); - const expectTokenExist = !disallowCrossOrigin || mode === 'no-cors'; - promise_test(async (test) => { - const token1 = token(); - const iframe = document.createElement('iframe'); - iframe.src = getKeepAliveIframeUrl(token1, method, { - frameOrigin: '', - requestOrigin: origin, - sendOn: evt, - mode: mode, - disallowCrossOrigin - }); - document.body.appendChild(iframe); - await iframeLoaded(iframe); - iframe.remove(); - assert_equals(await getTokenFromMessage(), token1); - - assertStashedTokenAsync(desc, token1, {expectTokenExist}); - }, `${desc}; setting up`); - } - } -} - -for (const method of ['GET', 'POST']) { - keepaliveCorsInUnloadTest( - '[keepalive] Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT, - method); - keepaliveCorsInUnloadTest( - `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN, - method); - keepaliveCorsInUnloadTest( - `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN, method); - keepaliveCorsInUnloadTest( - `[keepalive] Cross domain different port`, - HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, method); - keepaliveCorsInUnloadTest( - `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN, - method); -} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js b/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js deleted file mode 100644 index b3abb922841..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-multiple-origins.sub.any.js +++ /dev/null @@ -1,22 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function corsMultipleOrigins(originList) { - var urlParameters = "?origin=" + encodeURIComponent(originList.join(", ")); - var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - - promise_test(function(test) { - return promise_rejects_js(test, TypeError, fetch(url + urlParameters)); - }, "Listing multiple origins is illegal: " + originList); -} -/* Actual origin */ -var origin = "http://{{host}}:{{ports[http][0]}}"; - -corsMultipleOrigins(["\"\"", "http://example.com", origin]); -corsMultipleOrigins(["\"\"", "http://example.com", "*"]); -corsMultipleOrigins(["\"\"", origin, origin]); -corsMultipleOrigins(["*", "http://example.com", "*"]); -corsMultipleOrigins(["*", "http://example.com", origin]); -corsMultipleOrigins(["", "http://example.com", "https://example2.com"]); - -done(); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js deleted file mode 100644 index 7a0269aae4e..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-no-preflight.any.js +++ /dev/null @@ -1,41 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function corsNoPreflight(desc, baseURL, method, headerName, headerValue) { - - var uuid_token = token(); - var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - var requestInit = {"mode": "cors", "method": method, "headers":{}}; - if (headerName) - requestInit["headers"][headerName] = headerValue; - - promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - assert_equals(resp.status, 200, "Clean stash response's status is 200"); - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made"); - }); - }); - }, desc); -} - -var host_info = get_host_info(); - -corsNoPreflight("Cross domain basic usage [GET]", host_info.HTTP_REMOTE_ORIGIN, "GET"); -corsNoPreflight("Same domain different port [GET]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET"); -corsNoPreflight("Cross domain different port [GET]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET"); -corsNoPreflight("Cross domain different protocol [GET]", host_info.HTTPS_REMOTE_ORIGIN, "GET"); -corsNoPreflight("Same domain different protocol different port [GET]", host_info.HTTPS_ORIGIN, "GET"); -corsNoPreflight("Cross domain [POST]", host_info.HTTP_REMOTE_ORIGIN, "POST"); -corsNoPreflight("Cross domain [HEAD]", host_info.HTTP_REMOTE_ORIGIN, "HEAD"); -corsNoPreflight("Cross domain [GET] [Accept: */*]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept", "*/*"); -corsNoPreflight("Cross domain [GET] [Accept-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept-Language", "fr"); -corsNoPreflight("Cross domain [GET] [Content-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Language", "fr"); -corsNoPreflight("Cross domain [GET] [Content-Type: application/x-www-form-urlencoded]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "application/x-www-form-urlencoded"); -corsNoPreflight("Cross domain [GET] [Content-Type: multipart/form-data]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "multipart/form-data"); -corsNoPreflight("Cross domain [GET] [Content-Type: text/plain]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain"); -corsNoPreflight("Cross domain [GET] [Content-Type: text/plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain;charset=utf-8"); -corsNoPreflight("Cross domain [GET] [Content-Type: Text/Plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "Text/Plain;charset=utf-8"); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js b/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js deleted file mode 100644 index 30a02d910fd..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-origin.any.js +++ /dev/null @@ -1,51 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -/* If origin is undefined, it is set to fetched url's origin*/ -function corsOrigin(desc, baseURL, method, origin, shouldPass) { - if (!origin) - origin = baseURL; - - var uuid_token = token(); - var urlParameters = "?token=" + uuid_token + "&max_age=0&origin=" + encodeURIComponent(origin) + "&allow_methods=" + method; - var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - var requestInit = {"mode": "cors", "method": method}; - - promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - assert_equals(resp.status, 200, "Clean stash response's status is 200"); - if (shouldPass) { - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - }); - } else { - return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); - } - }); - }, desc); - -} - -var host_info = get_host_info(); - -/* Actual origin */ -var origin = host_info.HTTP_ORIGIN; - -corsOrigin("Cross domain different subdomain [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "GET", origin, true); -corsOrigin("Cross domain different subdomain [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", undefined, false); -corsOrigin("Same domain different port [origin OK]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true); -corsOrigin("Same domain different port [origin KO]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false); -corsOrigin("Cross domain different port [origin OK]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true); -corsOrigin("Cross domain different port [origin KO]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false); -corsOrigin("Cross domain different protocol [origin OK]", host_info.HTTPS_REMOTE_ORIGIN, "GET", origin, true); -corsOrigin("Cross domain different protocol [origin KO]", host_info.HTTPS_REMOTE_ORIGIN, "GET", undefined, false); -corsOrigin("Same domain different protocol different port [origin OK]", host_info.HTTPS_ORIGIN, "GET", origin, true); -corsOrigin("Same domain different protocol different port [origin KO]", host_info.HTTPS_ORIGIN, "GET", undefined, false); -corsOrigin("Cross domain [POST] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "POST", origin, true); -corsOrigin("Cross domain [POST] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "POST", undefined, false); -corsOrigin("Cross domain [HEAD] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", origin, true); -corsOrigin("Cross domain [HEAD] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", undefined, false); -corsOrigin("CORS preflight [PUT] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "PUT", origin, true); -corsOrigin("CORS preflight [PUT] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "PUT", undefined, false); -corsOrigin("Allowed origin: \"\" [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", "" , false); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js deleted file mode 100644 index ce6a169d814..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-preflight-cache.any.js +++ /dev/null @@ -1,46 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -var cors_url = get_host_info().HTTP_REMOTE_ORIGIN + - dirname(location.pathname) + - RESOURCES_DIR + - "preflight.py"; - -promise_test((test) => { - var uuid_token = token(); - var request_url = - cors_url + "?token=" + uuid_token + "&max_age=12000&allow_methods=POST" + - "&allow_headers=x-test-header"; - return fetch(cors_url + "?token=" + uuid_token + "&clear-stash") - .then(() => { - return fetch( - new Request(request_url, - { - mode: "cors", - method: "POST", - headers: [["x-test-header", "test1"]] - })); - }) - .then((resp) => { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); - return fetch(cors_url + "?token=" + uuid_token + "&clear-stash"); - }) - .then((res) => res.text()) - .then((txt) => { - assert_equals(txt, "1", "Server stash must be cleared."); - return fetch( - new Request(request_url, - { - mode: "cors", - method: "POST", - headers: [["x-test-header", "test2"]] - })); - }) - .then((resp) => { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.headers.get("x-did-preflight"), "0", "Preflight request has not been made"); - return fetch(cors_url + "?token=" + uuid_token + "&clear-stash"); - }); -}); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js deleted file mode 100644 index b2747ccd5bc..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js +++ /dev/null @@ -1,19 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=resources/corspreflight.js - -const corsURL = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - -promise_test(() => fetch("resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…"); - -function runTests(testArray) { - testArray.forEach(testItem => { - const [headerName, headerValue] = testItem; - corsPreflight("Need CORS-preflight for " + headerName + "/" + headerValue + " header", - corsURL, - "GET", - true, - [[headerName, headerValue]]); - }); -} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js deleted file mode 100644 index 15f7659abd2..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-preflight-redirect.any.js +++ /dev/null @@ -1,37 +0,0 @@ -// META: global=window,worker -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function corsPreflightRedirect(desc, redirectUrl, redirectLocation, redirectStatus, redirectPreflight) { - var uuid_token = token(); - var url = redirectUrl; - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - urlParameters += "&redirect_status=" + redirectStatus; - urlParameters += "&location=" + encodeURIComponent(redirectLocation); - - if (redirectPreflight) - urlParameters += "&redirect_preflight"; - var requestInit = {"mode": "cors", "redirect": "follow"}; - - /* Force preflight */ - requestInit["headers"] = {"x-force-preflight": ""}; - urlParameters += "&allow_headers=x-force-preflight"; - - promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - assert_equals(resp.status, 200, "Clean stash response's status is 200"); - return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); - }); - }, desc); -} - -var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; -var locationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - -for (var code of [301, 302, 303, 307, 308]) { - /* preflight should not follow the redirection */ - corsPreflightRedirect("Redirection " + code + " on preflight failed", redirectUrl, locationUrl, code, true); - /* preflight is done before redirection: preflight force redirect to error */ - corsPreflightRedirect("Redirection " + code + " after preflight failed", redirectUrl, locationUrl, code, false); -} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js deleted file mode 100644 index 5df9fcf1429..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-preflight-referrer.any.js +++ /dev/null @@ -1,51 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function corsPreflightReferrer(desc, corsUrl, referrerPolicy, referrer, expectedReferrer) { - var uuid_token = token(); - var url = corsUrl; - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - var requestInit = {"mode": "cors", "referrerPolicy": referrerPolicy}; - - if (referrer) - requestInit.referrer = referrer; - - /* Force preflight */ - requestInit["headers"] = {"x-force-preflight": ""}; - urlParameters += "&allow_headers=x-force-preflight"; - - promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - assert_equals(resp.status, 200, "Clean stash response's status is 200"); - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); - assert_equals(resp.headers.get("x-preflight-referrer"), expectedReferrer, "Preflight's referrer is correct"); - assert_equals(resp.headers.get("x-referrer"), expectedReferrer, "Request's referrer is correct"); - assert_equals(resp.headers.get("x-control-request-headers"), "", "Access-Control-Allow-Headers value"); - }); - }); - }, desc + " and referrer: " + (referrer ? "'" + referrer + "'" : "default")); -} - -var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; -var origin = get_host_info().HTTP_ORIGIN + "/"; - -corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", undefined, ""); -corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", "myreferrer", ""); - -corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", undefined, origin); -corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", "myreferrer", origin); - -corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", undefined, location.toString()) -corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", "myreferrer", new URL("myreferrer", location).toString()); - -corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", undefined, origin); -corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", "myreferrer", origin); - -corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", undefined, origin); -corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", "myreferrer", origin); - -corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", undefined, location.toString()); -corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", "myreferrer", new URL("myreferrer", location).toString()); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js deleted file mode 100644 index 718e351c1d3..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-preflight-response-validation.any.js +++ /dev/null @@ -1,33 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function corsPreflightResponseValidation(desc, corsUrl, allowHeaders, allowMethods) { - var uuid_token = token(); - var url = corsUrl; - var requestInit = {"mode": "cors"}; - /* Force preflight */ - requestInit["headers"] = {"x-force-preflight": ""}; - - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - urlParameters += "&allow_headers=x-force-preflight"; - if (allowHeaders) - urlParameters += "," + allowHeaders; - if (allowMethods) - urlParameters += "&allow_methods="+ allowMethods; - - promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(async function(resp) { - assert_equals(resp.status, 200, "Clean stash response's status is 200"); - await promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); - - return fetch(url + urlParameters).then(function(resp) { - assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); - }); - }); - }, desc); -} - -var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; -corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Headers", corsUrl, "Bad value", null); -corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Methods", corsUrl, null, "Bad value"); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js deleted file mode 100644 index f9fb20469cf..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-preflight-star.any.js +++ /dev/null @@ -1,86 +0,0 @@ -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -const url = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py", - origin = location.origin // assuming an ASCII origin - -function preflightTest(succeeds, withCredentials, allowMethod, allowHeader, useMethod, useHeader) { - return promise_test(t => { - let testURL = url + "?", - requestInit = {} - if (withCredentials) { - testURL += "origin=" + origin + "&" - testURL += "credentials&" - requestInit.credentials = "include" - } - if (useMethod) { - requestInit.method = useMethod - } - if (useHeader.length > 0) { - requestInit.headers = [useHeader] - } - testURL += "allow_methods=" + allowMethod + "&" - testURL += "allow_headers=" + allowHeader + "&" - - if (succeeds) { - return fetch(testURL, requestInit).then(resp => { - assert_equals(resp.headers.get("x-origin"), origin) - }) - } else { - return promise_rejects_js(t, TypeError, fetch(testURL, requestInit)) - } - }, "CORS that " + (succeeds ? "succeeds" : "fails") + " with credentials: " + withCredentials + "; method: " + useMethod + " (allowed: " + allowMethod + "); header: " + useHeader + " (allowed: " + allowHeader + ")") -} - -// "GET" does not pass the case-sensitive method check, but in the safe list. -preflightTest(true, false, "get", "x-test", "GET", ["X-Test", "1"]) -// Headers check is case-insensitive, and "*" works as any for method. -preflightTest(true, false, "*", "x-test", "SUPER", ["X-Test", "1"]) -// "*" works as any only without credentials. -preflightTest(true, false, "*", "*", "OK", ["X-Test", "1"]) -preflightTest(false, true, "*", "*", "OK", ["X-Test", "1"]) -preflightTest(false, true, "*", "", "PUT", []) -preflightTest(false, true, "get", "*", "GET", ["X-Test", "1"]) -preflightTest(false, true, "*", "*", "GET", ["X-Test", "1"]) -// Exact character match works even for "*" with credentials. -preflightTest(true, true, "*", "*", "*", ["*", "1"]) - -// The following methods are upper-cased for init["method"] by -// https://fetch.spec.whatwg.org/#concept-method-normalize -// but not in Access-Control-Allow-Methods response. -// But they are https://fetch.spec.whatwg.org/#cors-safelisted-method, -// CORS anyway passes regardless of the cases. -for (const METHOD of ['GET', 'HEAD', 'POST']) { - const method = METHOD.toLowerCase(); - preflightTest(true, true, METHOD, "*", METHOD, []) - preflightTest(true, true, METHOD, "*", method, []) - preflightTest(true, true, method, "*", METHOD, []) - preflightTest(true, true, method, "*", method, []) -} - -// The following methods are upper-cased for init["method"] by -// https://fetch.spec.whatwg.org/#concept-method-normalize -// but not in Access-Control-Allow-Methods response. -// As they are not https://fetch.spec.whatwg.org/#cors-safelisted-method, -// Access-Control-Allow-Methods should contain upper-cased methods, -// while init["method"] can be either in upper or lower case. -for (const METHOD of ['DELETE', 'PUT']) { - const method = METHOD.toLowerCase(); - preflightTest(true, true, METHOD, "*", METHOD, []) - preflightTest(true, true, METHOD, "*", method, []) - preflightTest(false, true, method, "*", METHOD, []) - preflightTest(false, true, method, "*", method, []) -} - -// "PATCH" is NOT upper-cased in both places because it is not listed in -// https://fetch.spec.whatwg.org/#concept-method-normalize. -// So Access-Control-Allow-Methods value and init["method"] should match -// case-sensitively. -preflightTest(true, true, "PATCH", "*", "PATCH", []) -preflightTest(false, true, "PATCH", "*", "patch", []) -preflightTest(false, true, "patch", "*", "PATCH", []) -preflightTest(true, true, "patch", "*", "patch", []) - -// "Authorization" header can't be wildcarded. -preflightTest(false, false, "*", "*", "POST", ["Authorization", "123"]) -preflightTest(true, false, "*", "*, Authorization", "POST", ["Authorization", "123"]) diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js deleted file mode 100644 index a4467a6087b..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-preflight-status.any.js +++ /dev/null @@ -1,37 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -/* Check preflight is ok if status is ok status (200 to 299)*/ -function corsPreflightStatus(desc, corsUrl, preflightStatus) { - var uuid_token = token(); - var url = corsUrl; - var requestInit = {"mode": "cors"}; - /* Force preflight */ - requestInit["headers"] = {"x-force-preflight": ""}; - - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - urlParameters += "&allow_headers=x-force-preflight"; - urlParameters += "&preflight_status=" + preflightStatus; - - promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - assert_equals(resp.status, 200, "Clean stash response's status is 200"); - if (200 <= preflightStatus && 299 >= preflightStatus) { - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); - }); - } else { - return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); - } - }); - }, desc); -} - -var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; -for (status of [200, 201, 202, 203, 204, 205, 206, - 300, 301, 302, 303, 304, 305, 306, 307, 308, - 400, 401, 402, 403, 404, 405, - 501, 502, 503, 504, 505]) - corsPreflightStatus("Preflight answered with status " + status, corsUrl, status); diff --git a/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js deleted file mode 100644 index 045422f40b1..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-preflight.any.js +++ /dev/null @@ -1,62 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=resources/corspreflight.js - -var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - -corsPreflight("CORS [DELETE], server allows", corsUrl, "DELETE", true); -corsPreflight("CORS [DELETE], server refuses", corsUrl, "DELETE", false); -corsPreflight("CORS [PUT], server allows", corsUrl, "PUT", true); -corsPreflight("CORS [PUT], server allows, check preflight has user agent", corsUrl + "?checkUserAgentHeaderInPreflight", "PUT", true); -corsPreflight("CORS [PUT], server refuses", corsUrl, "PUT", false); -corsPreflight("CORS [PATCH], server allows", corsUrl, "PATCH", true); -corsPreflight("CORS [PATCH], server refuses", corsUrl, "PATCH", false); -corsPreflight("CORS [patcH], server allows", corsUrl, "patcH", true); -corsPreflight("CORS [patcH], server refuses", corsUrl, "patcH", false); -corsPreflight("CORS [NEW], server allows", corsUrl, "NEW", true); -corsPreflight("CORS [NEW], server refuses", corsUrl, "NEW", false); -corsPreflight("CORS [chicken], server allows", corsUrl, "chicken", true); -corsPreflight("CORS [chicken], server refuses", corsUrl, "chicken", false); - -corsPreflight("CORS [GET] [x-test-header: allowed], server allows", corsUrl, "GET", true, [["x-test-header1", "allowed"]]); -corsPreflight("CORS [GET] [x-test-header: refused], server refuses", corsUrl, "GET", false, [["x-test-header1", "refused"]]); - -var headers = [ - ["x-test-header1", "allowedOrRefused"], - ["x-test-header2", "allowedOrRefused"], - ["X-test-header3", "allowedOrRefused"], - ["x-test-header-b", "allowedOrRefused"], - ["x-test-header-D", "allowedOrRefused"], - ["x-test-header-C", "allowedOrRefused"], - ["x-test-header-a", "allowedOrRefused"], - ["Content-Type", "allowedOrRefused"], -]; -var safeHeaders= [ - ["Accept", "*"], - ["Accept-Language", "bzh"], - ["Content-Language", "eu"], -]; - -corsPreflight("CORS [GET] [several headers], server allows", corsUrl, "GET", true, headers, safeHeaders); -corsPreflight("CORS [GET] [several headers], server refuses", corsUrl, "GET", false, headers, safeHeaders); -corsPreflight("CORS [PUT] [several headers], server allows", corsUrl, "PUT", true, headers, safeHeaders); -corsPreflight("CORS [PUT] [several headers], server refuses", corsUrl, "PUT", false, headers, safeHeaders); - -corsPreflight("CORS [PUT] [only safe headers], server allows", corsUrl, "PUT", true, null, safeHeaders); - -promise_test(async t => { - const url = `${corsUrl}?allow_headers=*`; - await promise_rejects_js(t, TypeError, fetch(url, { - headers: { - authorization: 'foobar' - } - })); -}, '"authorization" should not be covered by the wildcard symbol'); - -promise_test(async t => { - const url = `${corsUrl}?allow_headers=authorization`; - await fetch(url, { headers: { - authorization: 'foobar' - }}); -}, '"authorization" should be covered by "authorization"'); \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js deleted file mode 100644 index 2aff3134063..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-redirect-credentials.any.js +++ /dev/null @@ -1,52 +0,0 @@ -// META: timeout=long -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function corsRedirectCredentials(desc, redirectUrl, redirectLocation, redirectStatus, locationCredentials) { - var url = redirectUrl - var urlParameters = "?redirect_status=" + redirectStatus; - urlParameters += "&location=" + redirectLocation.replace("://", "://" + locationCredentials + "@"); - - var requestInit = {"mode": "cors", "redirect": "follow"}; - - promise_test(t => { - const result = fetch(url + urlParameters, requestInit) - if(locationCredentials === "") { - return result; - } else { - return promise_rejects_js(t, TypeError, result); - } - }, desc); -} - -var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; -var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - -var host_info = get_host_info(); - -var localRedirect = host_info.HTTP_ORIGIN + redirPath; -var remoteRedirect = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + redirPath; - -var localLocation = host_info.HTTP_ORIGIN + preflightPath; -var remoteLocation = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; -var remoteLocation2 = host_info.HTTP_REMOTE_ORIGIN + preflightPath; - -for (var code of [301, 302, 303, 307, 308]) { - corsRedirectCredentials("Redirect " + code + " from same origin to remote without user and password", localRedirect, remoteLocation, code, ""); - - corsRedirectCredentials("Redirect " + code + " from same origin to remote with user and password", localRedirect, remoteLocation, code, "user:password"); - corsRedirectCredentials("Redirect " + code + " from same origin to remote with user", localRedirect, remoteLocation, code, "user:"); - corsRedirectCredentials("Redirect " + code + " from same origin to remote with password", localRedirect, remoteLocation, code, ":password"); - - corsRedirectCredentials("Redirect " + code + " from remote to same origin with user and password", remoteRedirect, localLocation, code, "user:password"); - corsRedirectCredentials("Redirect " + code + " from remote to same origin with user", remoteRedirect, localLocation, code, "user:"); - corsRedirectCredentials("Redirect " + code + " from remote to same origin with password", remoteRedirect, localLocation, code, ":password"); - - corsRedirectCredentials("Redirect " + code + " from remote to same remote with user and password", remoteRedirect, remoteLocation, code, "user:password"); - corsRedirectCredentials("Redirect " + code + " from remote to same remote with user", remoteRedirect, remoteLocation, code, "user:"); - corsRedirectCredentials("Redirect " + code + " from remote to same remote with password", remoteRedirect, remoteLocation, code, ":password"); - - corsRedirectCredentials("Redirect " + code + " from remote to another remote with user and password", remoteRedirect, remoteLocation2, code, "user:password"); - corsRedirectCredentials("Redirect " + code + " from remote to another remote with user", remoteRedirect, remoteLocation2, code, "user:"); - corsRedirectCredentials("Redirect " + code + " from remote to another remote with password", remoteRedirect, remoteLocation2, code, ":password"); -} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js deleted file mode 100644 index 50848170d0d..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-redirect-preflight.any.js +++ /dev/null @@ -1,46 +0,0 @@ -// META: timeout=long -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectSuccess) { - var urlBaseParameters = "&redirect_status=" + redirectStatus; - var urlParametersSuccess = urlBaseParameters + "&allow_headers=x-w3c&location=" + encodeURIComponent(redirectLocation + "?allow_headers=x-w3c"); - var urlParametersFailure = urlBaseParameters + "&location=" + encodeURIComponent(redirectLocation); - - var requestInit = {"mode": "cors", "redirect": "follow", "headers" : [["x-w3c", "test"]]}; - - promise_test(function(test) { - var uuid_token = token(); - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - return fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersSuccess, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); - }); - }); - }, desc + " (preflight after redirection success case)"); - promise_test(function(test) { - var uuid_token = token(); - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - return promise_rejects_js(test, TypeError, fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersFailure, requestInit)); - }); - }, desc + " (preflight after redirection failure case)"); -} - -var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; -var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - -var host_info = get_host_info(); - -var localRedirect = host_info.HTTP_ORIGIN + redirPath; -var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath; - -var localLocation = host_info.HTTP_ORIGIN + preflightPath; -var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath; -var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; - -for (var code of [301, 302, 303, 307, 308]) { - corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code); - corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code); - corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code); -} diff --git a/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js b/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js deleted file mode 100644 index cdf4097d566..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/cors-redirect.any.js +++ /dev/null @@ -1,42 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) { - var uuid_token = token(); - var url = redirectUrl; - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - urlParameters += "&redirect_status=" + redirectStatus; - urlParameters += "&location=" + encodeURIComponent(redirectLocation); - - var requestInit = {"mode": "cors", "redirect": "follow"}; - - return promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made"); - assert_equals(resp.headers.get("x-origin"), expectedOrigin, "Origin is correctly set after redirect"); - }); - }); - }, desc); -} - -var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; -var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; - -var host_info = get_host_info(); - -var localRedirect = host_info.HTTP_ORIGIN + redirPath; -var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath; - -var localLocation = host_info.HTTP_ORIGIN + preflightPath; -var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath; -var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; - -for (var code of [301, 302, 303, 307, 308]) { - corsRedirect("Redirect " + code + ": cors to same cors", remoteRedirect, remoteLocation, code, location.origin); - corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code, "null"); - corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code, location.origin); - corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code, "null"); -} diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html b/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html deleted file mode 100644 index 217baa3c46b..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/data-url-iframe.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html b/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html deleted file mode 100644 index d69748ab261..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/data-url-shared-worker.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/api/cors/data-url-worker.html b/test/fixtures/wpt/fetch/api/cors/data-url-worker.html deleted file mode 100644 index 13113e62621..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/data-url-worker.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js b/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js deleted file mode 100644 index 18b8f6dfa28..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/resources/corspreflight.js +++ /dev/null @@ -1,58 +0,0 @@ -function headerNames(headers) { - let names = []; - for (let header of headers) { - names.push(header[0].toLowerCase()); - } - return names; -} - -/* - Check preflight is done - Control if server allows method and headers and check accordingly - Check control access headers added by UA (for method and headers) -*/ -function corsPreflight(desc, corsUrl, method, allowed, headers, safeHeaders) { - return promise_test(function(test) { - var uuid_token = token(); - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(response) { - var url = corsUrl + (corsUrl.indexOf("?") === -1 ? "?" : "&"); - var urlParameters = "token=" + uuid_token + "&max_age=0"; - var requestInit = {"mode": "cors", "method": method}; - var requestHeaders = []; - if (headers) - requestHeaders.push.apply(requestHeaders, headers); - if (safeHeaders) - requestHeaders.push.apply(requestHeaders, safeHeaders); - requestInit["headers"] = requestHeaders; - - if (allowed) { - urlParameters += "&allow_methods=" + method + "&control_request_headers"; - if (headers) { - //Make the server allow the headers - urlParameters += "&allow_headers=" + headerNames(headers).join("%20%2C"); - } - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); - if (headers) { - var actualHeaders = resp.headers.get("x-control-request-headers").toLowerCase().split(","); - for (var i in actualHeaders) - actualHeaders[i] = actualHeaders[i].trim(); - for (var header of headers) - assert_in_array(header[0].toLowerCase(), actualHeaders, "Preflight asked permission for header: " + header); - - let accessControlAllowHeaders = headerNames(headers).sort().join(","); - assert_equals(resp.headers.get("x-control-request-headers"), accessControlAllowHeaders, "Access-Control-Allow-Headers value"); - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token); - } else { - assert_equals(resp.headers.get("x-control-request-headers"), null, "Access-Control-Request-Headers should be omitted") - } - }); - } else { - return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)).then(function(){ - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token); - }); - } - }); - }, desc); -} diff --git a/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json b/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json deleted file mode 100644 index 945dc0f93ba..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/resources/not-cors-safelisted.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - ["accept", "\""], - ["accept", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"], - ["accept-language", "\u0001"], - ["accept-language", "@"], - ["authorization", "basics"], - ["content-language", "\u0001"], - ["content-language", "@"], - ["content-type", "text/html"], - ["content-type", "text/plain; long=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"], - ["range", "bytes 0-"], - ["test", "hi"] -] diff --git a/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html b/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html deleted file mode 100644 index feb9f1f2e5b..00000000000 --- a/test/fixtures/wpt/fetch/api/cors/sandboxed-iframe.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/crashtests/aborted-fetch-response.https.html b/test/fixtures/wpt/fetch/api/crashtests/aborted-fetch-response.https.html deleted file mode 100644 index fa1ad1717f0..00000000000 --- a/test/fixtures/wpt/fetch/api/crashtests/aborted-fetch-response.https.html +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/test/fixtures/wpt/fetch/api/crashtests/body-window-destroy.html b/test/fixtures/wpt/fetch/api/crashtests/body-window-destroy.html deleted file mode 100644 index 646d3c5f8ce..00000000000 --- a/test/fixtures/wpt/fetch/api/crashtests/body-window-destroy.html +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/test/fixtures/wpt/fetch/api/crashtests/huge-fetch.any.js b/test/fixtures/wpt/fetch/api/crashtests/huge-fetch.any.js deleted file mode 100644 index 1b09925d855..00000000000 --- a/test/fixtures/wpt/fetch/api/crashtests/huge-fetch.any.js +++ /dev/null @@ -1,16 +0,0 @@ -// META: global=window,worker - -'use strict'; - -promise_test(async t => { - const response = await fetch('../resources/huge-response.py'); - const reader = response.body.getReader(); - // Read one chunk just to show willing. - const { value, done } = await reader.read(); - assert_false(done, 'there should be some data'); - assert_greater_than(value.byteLength, 0, 'the chunk should be non-empty'); - // Wait 2 seconds to give it a chance to crash. - await new Promise(resolve => t.step_timeout(resolve, 2000)); - // If we get here without crashing we passed the test. - reader.cancel(); -}, 'fetching a huge cacheable file but not reading it should not crash'); diff --git a/test/fixtures/wpt/fetch/api/crashtests/request.html b/test/fixtures/wpt/fetch/api/crashtests/request.html deleted file mode 100644 index 2d21930c3bb..00000000000 --- a/test/fixtures/wpt/fetch/api/crashtests/request.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js b/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js deleted file mode 100644 index 31ccc386977..00000000000 --- a/test/fixtures/wpt/fetch/api/credentials/authentication-basic.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// META: global=window,worker - -function basicAuth(desc, user, pass, mode, status) { - promise_test(function(test) { - var headers = { "Authorization": "Basic " + btoa(user + ":" + pass)}; - var requestInit = {"credentials": mode, "headers": headers}; - return fetch("../resources/authentication.py?realm=test", requestInit).then(function(resp) { - assert_equals(resp.status, status, "HTTP status is " + status); - assert_equals(resp.type , "basic", "Response's type is basic"); - }); - }, desc); -} - -basicAuth("User-added Authorization header with include mode", "user", "password", "include", 200); -basicAuth("User-added Authorization header with same-origin mode", "user", "password", "same-origin", 200); -basicAuth("User-added Authorization header with omit mode", "user", "password", "omit", 200); -basicAuth("User-added bogus Authorization header with omit mode", "notuser", "notpassword", "omit", 401); diff --git a/test/fixtures/wpt/fetch/api/credentials/authentication-redirection.any.js b/test/fixtures/wpt/fetch/api/credentials/authentication-redirection.any.js deleted file mode 100644 index 5a155074378..00000000000 --- a/test/fixtures/wpt/fetch/api/credentials/authentication-redirection.any.js +++ /dev/null @@ -1,29 +0,0 @@ -// META: global=window,worker -// META: script=/common/get-host-info.sub.js - -const authorizationValue = "Basic " + btoa("user:pass"); -async function getAuthorizationHeaderValue(url) -{ - const headers = { "Authorization": authorizationValue}; - const requestInit = {"headers": headers}; - const response = await fetch(url, requestInit); - return response.text(); -} - -promise_test(async test => { - const result = await getAuthorizationHeaderValue("/fetch/api/resources/dump-authorization-header.py"); - assert_equals(result, authorizationValue); -}, "getAuthorizationHeaderValue - no redirection"); - -promise_test(async test => { - result = await getAuthorizationHeaderValue("/fetch/api/resources/redirect.py?location=" + encodeURIComponent("/fetch/api/resources/dump-authorization-header.py")); - assert_equals(result, authorizationValue); - - result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/dump-authorization-header.py")); - assert_equals(result, authorizationValue); -}, "getAuthorizationHeaderValue - same origin redirection"); - -promise_test(async (test) => { - const result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_ORIGIN + "/fetch/api/resources/dump-authorization-header.py?strip_auth_header=true")); - assert_equals(result, "none"); -}, "getAuthorizationHeaderValue - cross origin redirection"); diff --git a/test/fixtures/wpt/fetch/api/credentials/cookies.any.js b/test/fixtures/wpt/fetch/api/credentials/cookies.any.js deleted file mode 100644 index de30e477655..00000000000 --- a/test/fixtures/wpt/fetch/api/credentials/cookies.any.js +++ /dev/null @@ -1,49 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -function cookies(desc, credentials1, credentials2 ,cookies) { - var url = RESOURCES_DIR + "top.txt" - var urlParameters = ""; - var urlCleanParameters = ""; - if (cookies) { - urlParameters +="?pipe=header(Set-Cookie,"; - urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)"; - urlCleanParameters +="?pipe=header(Set-Cookie,"; - urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)"; - } - - var requestInit = {"credentials": credentials1} - promise_test(function(test){ - var requestInit = {"credentials": credentials1} - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - //check cookies sent - return fetch(RESOURCES_DIR + "inspect-headers.py?headers=cookie" , {"credentials": credentials2}); - }).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response"); - if (credentials1 != "omit" && credentials2 != "omit") { - assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request include cookie(s)"); - } - else { - assert_false(resp.headers.has("x-request-cookie") , "Request does not have cookie(s)"); - } - //clean cookies - return fetch(url + urlCleanParameters, {"credentials": "include"}); - }).catch(function(e) { - return fetch(url + urlCleanParameters, {"credentials": "include"}).then(function() { - return Promise.reject(e); - }); - }); - }, desc); -} - -cookies("Include mode: 1 cookie", "include", "include", ["a=1"]); -cookies("Include mode: 2 cookies", "include", "include", ["b=2", "c=3"]); -cookies("Omit mode: discard cookies", "omit", "omit", ["d=4"]); -cookies("Omit mode: no cookie is stored", "omit", "include", ["e=5"]); -cookies("Omit mode: no cookie is sent", "include", "omit", ["f=6"]); -cookies("Same-origin mode: 1 cookie", "same-origin", "same-origin", ["a=1"]); -cookies("Same-origin mode: 2 cookies", "same-origin", "same-origin", ["b=2", "c=3"]); diff --git a/test/fixtures/wpt/fetch/api/headers/header-setcookie.any.js b/test/fixtures/wpt/fetch/api/headers/header-setcookie.any.js deleted file mode 100644 index cafb780c2c7..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/header-setcookie.any.js +++ /dev/null @@ -1,266 +0,0 @@ -// META: title=Headers set-cookie special cases -// META: global=window,worker - -const headerList = [ - ["set-cookie", "foo=bar"], - ["Set-Cookie", "fizz=buzz; domain=example.com"], -]; - -const setCookie2HeaderList = [ - ["set-cookie2", "foo2=bar2"], - ["Set-Cookie2", "fizz2=buzz2; domain=example2.com"], -]; - -function assert_nested_array_equals(actual, expected) { - assert_equals(actual.length, expected.length, "Array length is not equal"); - for (let i = 0; i < expected.length; i++) { - assert_array_equals(actual[i], expected[i]); - } -} - -test(function () { - const headers = new Headers(headerList); - assert_equals( - headers.get("set-cookie"), - "foo=bar, fizz=buzz; domain=example.com", - ); -}, "Headers.prototype.get combines set-cookie headers in order"); - -test(function () { - const headers = new Headers(headerList); - const list = [...headers]; - assert_nested_array_equals(list, [ - ["set-cookie", "foo=bar"], - ["set-cookie", "fizz=buzz; domain=example.com"], - ]); -}, "Headers iterator does not combine set-cookie headers"); - -test(function () { - const headers = new Headers(setCookie2HeaderList); - const list = [...headers]; - assert_nested_array_equals(list, [ - ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], - ]); -}, "Headers iterator does not special case set-cookie2 headers"); - -test(function () { - const headers = new Headers([...headerList, ...setCookie2HeaderList]); - const list = [...headers]; - assert_nested_array_equals(list, [ - ["set-cookie", "foo=bar"], - ["set-cookie", "fizz=buzz; domain=example.com"], - ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], - ]); -}, "Headers iterator does not combine set-cookie & set-cookie2 headers"); - -test(function () { - // Values are in non alphabetic order, and the iterator should yield in the - // headers in the exact order of the input. - const headers = new Headers([ - ["set-cookie", "z=z"], - ["set-cookie", "a=a"], - ["set-cookie", "n=n"], - ]); - const list = [...headers]; - assert_nested_array_equals(list, [ - ["set-cookie", "z=z"], - ["set-cookie", "a=a"], - ["set-cookie", "n=n"], - ]); -}, "Headers iterator preserves set-cookie ordering"); - -test( - function () { - const headers = new Headers([ - ["xylophone-header", "1"], - ["best-header", "2"], - ["set-cookie", "3"], - ["a-cool-header", "4"], - ["set-cookie", "5"], - ["a-cool-header", "6"], - ["best-header", "7"], - ]); - const list = [...headers]; - assert_nested_array_equals(list, [ - ["a-cool-header", "4, 6"], - ["best-header", "2, 7"], - ["set-cookie", "3"], - ["set-cookie", "5"], - ["xylophone-header", "1"], - ]); - }, - "Headers iterator preserves per header ordering, but sorts keys alphabetically", -); - -test( - function () { - const headers = new Headers([ - ["xylophone-header", "7"], - ["best-header", "6"], - ["set-cookie", "5"], - ["a-cool-header", "4"], - ["set-cookie", "3"], - ["a-cool-header", "2"], - ["best-header", "1"], - ]); - const list = [...headers]; - assert_nested_array_equals(list, [ - ["a-cool-header", "4, 2"], - ["best-header", "6, 1"], - ["set-cookie", "5"], - ["set-cookie", "3"], - ["xylophone-header", "7"], - ]); - }, - "Headers iterator preserves per header ordering, but sorts keys alphabetically (and ignores value ordering)", -); - -test(function () { - const headers = new Headers([["fizz", "buzz"], ["X-Header", "test"]]); - const iterator = headers[Symbol.iterator](); - assert_array_equals(iterator.next().value, ["fizz", "buzz"]); - headers.append("Set-Cookie", "a=b"); - assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]); - headers.append("Accept", "text/html"); - assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]); - assert_array_equals(iterator.next().value, ["x-header", "test"]); - headers.append("set-cookie", "c=d"); - assert_array_equals(iterator.next().value, ["x-header", "test"]); - assert_true(iterator.next().done); -}, "Headers iterator is correctly updated with set-cookie changes"); - -test(function () { - const headers = new Headers([ - ["set-cookie", "a"], - ["set-cookie", "b"], - ["set-cookie", "c"] - ]); - const iterator = headers[Symbol.iterator](); - assert_array_equals(iterator.next().value, ["set-cookie", "a"]); - headers.delete("set-cookie"); - headers.append("set-cookie", "d"); - headers.append("set-cookie", "e"); - headers.append("set-cookie", "f"); - assert_array_equals(iterator.next().value, ["set-cookie", "e"]); - assert_array_equals(iterator.next().value, ["set-cookie", "f"]); - assert_true(iterator.next().done); -}, "Headers iterator is correctly updated with set-cookie changes #2"); - -test(function () { - const headers = new Headers(headerList); - assert_true(headers.has("sEt-cOoKiE")); -}, "Headers.prototype.has works for set-cookie"); - -test(function () { - const headers = new Headers(setCookie2HeaderList); - headers.append("set-Cookie", "foo=bar"); - headers.append("sEt-cOoKiE", "fizz=buzz"); - const list = [...headers]; - assert_nested_array_equals(list, [ - ["set-cookie", "foo=bar"], - ["set-cookie", "fizz=buzz"], - ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], - ]); -}, "Headers.prototype.append works for set-cookie"); - -test(function () { - const headers = new Headers(headerList); - headers.set("set-cookie", "foo2=bar2"); - const list = [...headers]; - assert_nested_array_equals(list, [ - ["set-cookie", "foo2=bar2"], - ]); -}, "Headers.prototype.set works for set-cookie"); - -test(function () { - const headers = new Headers(headerList); - headers.delete("set-Cookie"); - const list = [...headers]; - assert_nested_array_equals(list, []); -}, "Headers.prototype.delete works for set-cookie"); - -test(function () { - const headers = new Headers(); - assert_array_equals(headers.getSetCookie(), []); -}, "Headers.prototype.getSetCookie with no headers present"); - -test(function () { - const headers = new Headers([headerList[0]]); - assert_array_equals(headers.getSetCookie(), ["foo=bar"]); -}, "Headers.prototype.getSetCookie with one header"); - -test(function () { - const headers = new Headers({ "Set-Cookie": "foo=bar" }); - assert_array_equals(headers.getSetCookie(), ["foo=bar"]); -}, "Headers.prototype.getSetCookie with one header created from an object"); - -test(function () { - const headers = new Headers(headerList); - assert_array_equals(headers.getSetCookie(), [ - "foo=bar", - "fizz=buzz; domain=example.com", - ]); -}, "Headers.prototype.getSetCookie with multiple headers"); - -test(function () { - const headers = new Headers([["set-cookie", ""]]); - assert_array_equals(headers.getSetCookie(), [""]); -}, "Headers.prototype.getSetCookie with an empty header"); - -test(function () { - const headers = new Headers([["set-cookie", "x"], ["set-cookie", "x"]]); - assert_array_equals(headers.getSetCookie(), ["x", "x"]); -}, "Headers.prototype.getSetCookie with two equal headers"); - -test(function () { - const headers = new Headers([ - ["set-cookie2", "x"], - ["set-cookie", "y"], - ["set-cookie2", "z"], - ]); - assert_array_equals(headers.getSetCookie(), ["y"]); -}, "Headers.prototype.getSetCookie ignores set-cookie2 headers"); - -test(function () { - // Values are in non alphabetic order, and the iterator should yield in the - // headers in the exact order of the input. - const headers = new Headers([ - ["set-cookie", "z=z"], - ["set-cookie", "a=a"], - ["set-cookie", "n=n"], - ]); - assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]); -}, "Headers.prototype.getSetCookie preserves header ordering"); - -test(function () { - const headers = new Headers({"Set-Cookie": " a=b\n"}); - headers.append("set-cookie", "\n\rc=d "); - assert_nested_array_equals([...headers], [ - ["set-cookie", "a=b"], - ["set-cookie", "c=d"] - ]); - headers.set("set-cookie", "\te=f "); - assert_nested_array_equals([...headers], [["set-cookie", "e=f"]]); -}, "Adding Set-Cookie headers normalizes their value"); - -test(function () { - assert_throws_js(TypeError, () => { - new Headers({"set-cookie": "\0"}); - }); - - const headers = new Headers(); - assert_throws_js(TypeError, () => { - headers.append("Set-Cookie", "a\nb"); - }); - assert_throws_js(TypeError, () => { - headers.set("Set-Cookie", "a\rb"); - }); -}, "Adding invalid Set-Cookie headers throws"); - -test(function () { - const response = new Response(); - response.headers.append("Set-Cookie", "foo=bar"); - assert_array_equals(response.headers.getSetCookie(), []); - response.headers.append("sEt-cOokIe", "bar=baz"); - assert_array_equals(response.headers.getSetCookie(), []); -}, "Set-Cookie is a forbidden response header"); diff --git a/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js b/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js deleted file mode 100644 index 5710554ada9..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/header-values-normalize.any.js +++ /dev/null @@ -1,72 +0,0 @@ -// META: title=Header value normalizing test -// META: global=window,worker -// META: timeout=long - -"use strict"; - -for(let i = 0; i < 0x21; i++) { - let fail = false, - strip = false - - // REMOVE 0x0B/0x0C exception once https://github.com/web-platform-tests/wpt/issues/8372 is fixed - if(i === 0x0B || i === 0x0C) - continue - - if(i === 0) { - fail = true - } - - if(i === 0x09 || i === 0x0A || i === 0x0D || i === 0x20) { - strip = true - } - - let url = "../resources/inspect-headers.py?headers=val1|val2|val3", - val = String.fromCharCode(i), - expectedVal = strip ? "" : val, - val1 = val, - expectedVal1 = expectedVal, - val2 = "x" + val, - expectedVal2 = "x" + expectedVal, - val3 = val + "x", - expectedVal3 = expectedVal + "x" - - // XMLHttpRequest is not available in service workers - if (!self.GLOBAL.isWorker()) { - async_test((t) => { - let xhr = new XMLHttpRequest() - xhr.open("POST", url) - if(fail) { - assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val1", val1)) - assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val2", val2)) - assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val3", val3)) - t.done() - } else { - xhr.setRequestHeader("val1", val1) - xhr.setRequestHeader("val2", val2) - xhr.setRequestHeader("val3", val3) - xhr.onload = t.step_func_done(() => { - assert_equals(xhr.getResponseHeader("x-request-val1"), expectedVal1) - assert_equals(xhr.getResponseHeader("x-request-val2"), expectedVal2) - assert_equals(xhr.getResponseHeader("x-request-val3"), expectedVal3) - }) - xhr.send() - } - }, "XMLHttpRequest with value " + encodeURI(val)) - } - - promise_test((t) => { - if(fail) { - return Promise.all([ - promise_rejects_js(t, TypeError, fetch(url, { headers: {"val1": val1} })), - promise_rejects_js(t, TypeError, fetch(url, { headers: {"val2": val2} })), - promise_rejects_js(t, TypeError, fetch(url, { headers: {"val3": val3} })) - ]) - } else { - return fetch(url, { headers: {"val1": val1, "val2": val2, "val3": val3} }).then((res) => { - assert_equals(res.headers.get("x-request-val1"), expectedVal1) - assert_equals(res.headers.get("x-request-val2"), expectedVal2) - assert_equals(res.headers.get("x-request-val3"), expectedVal3) - }) - } - }, "fetch() with value " + encodeURI(val)) -} diff --git a/test/fixtures/wpt/fetch/api/headers/header-values.any.js b/test/fixtures/wpt/fetch/api/headers/header-values.any.js deleted file mode 100644 index bb7570c5a37..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/header-values.any.js +++ /dev/null @@ -1,63 +0,0 @@ -// META: title=Header value test -// META: global=window,worker -// META: timeout=long - -"use strict"; - -// Invalid values -[0, 0x0A, 0x0D].forEach(val => { - val = "x" + String.fromCharCode(val) + "x" - - // XMLHttpRequest is not available in service workers - if (!self.GLOBAL.isWorker()) { - test(() => { - let xhr = new XMLHttpRequest() - xhr.open("POST", "/") - assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("value-test", val)) - }, "XMLHttpRequest with value " + encodeURI(val) + " needs to throw") - } - - promise_test(t => promise_rejects_js(t, TypeError, fetch("/", { headers: {"value-test": val} })), "fetch() with value " + encodeURI(val) + " needs to throw") -}) - -// Valid values -let headerValues =[] -for(let i = 0; i < 0x100; i++) { - if(i === 0 || i === 0x0A || i === 0x0D) { - continue - } - headerValues.push("x" + String.fromCharCode(i) + "x") -} -var url = "../resources/inspect-headers.py?headers=" -headerValues.forEach((_, i) => { - url += "val" + i + "|" -}) - -// XMLHttpRequest is not available in service workers -if (!self.GLOBAL.isWorker()) { - async_test((t) => { - let xhr = new XMLHttpRequest() - xhr.open("POST", url) - headerValues.forEach((val, i) => { - xhr.setRequestHeader("val" + i, val) - }) - xhr.onload = t.step_func_done(() => { - headerValues.forEach((val, i) => { - assert_equals(xhr.getResponseHeader("x-request-val" + i), val) - }) - }) - xhr.send() - }, "XMLHttpRequest with all valid values") -} - -promise_test((t) => { - const headers = new Headers - headerValues.forEach((val, i) => { - headers.append("val" + i, val) - }) - return fetch(url, { headers }).then((res) => { - headerValues.forEach((val, i) => { - assert_equals(res.headers.get("x-request-val" + i), val) - }) - }) -}, "fetch() with all valid values") diff --git a/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js b/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js deleted file mode 100644 index ead1047645a..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/headers-basic.any.js +++ /dev/null @@ -1,275 +0,0 @@ -// META: title=Headers structure -// META: global=window,worker - -"use strict"; - -test(function() { - new Headers(); -}, "Create headers from no parameter"); - -test(function() { - new Headers(undefined); -}, "Create headers from undefined parameter"); - -test(function() { - new Headers({}); -}, "Create headers from empty object"); - -var parameters = [null, 1]; -parameters.forEach(function(parameter) { - test(function() { - assert_throws_js(TypeError, function() { new Headers(parameter) }); - }, "Create headers with " + parameter + " should throw"); -}); - -var headerDict = {"name1": "value1", - "name2": "value2", - "name3": "value3", - "name4": null, - "name5": undefined, - "name6": 1, - "Content-Type": "value4" -}; - -var headerSeq = []; -for (var name in headerDict) - headerSeq.push([name, headerDict[name]]); - -test(function() { - var headers = new Headers(headerSeq); - for (name in headerDict) { - assert_equals(headers.get(name), String(headerDict[name]), - "name: " + name + " has value: " + headerDict[name]); - } - assert_equals(headers.get("length"), null, "init should be treated as a sequence, not as a dictionary"); -}, "Create headers with sequence"); - -test(function() { - var headers = new Headers(headerDict); - for (name in headerDict) { - assert_equals(headers.get(name), String(headerDict[name]), - "name: " + name + " has value: " + headerDict[name]); - } -}, "Create headers with record"); - -test(function() { - var headers = new Headers(headerDict); - var headers2 = new Headers(headers); - for (name in headerDict) { - assert_equals(headers2.get(name), String(headerDict[name]), - "name: " + name + " has value: " + headerDict[name]); - } -}, "Create headers with existing headers"); - -test(function() { - var headers = new Headers() - headers[Symbol.iterator] = function *() { - yield ["test", "test"] - } - var headers2 = new Headers(headers) - assert_equals(headers2.get("test"), "test") -}, "Create headers with existing headers with custom iterator"); - -test(function() { - var headers = new Headers(); - for (name in headerDict) { - headers.append(name, headerDict[name]); - assert_equals(headers.get(name), String(headerDict[name]), - "name: " + name + " has value: " + headerDict[name]); - } -}, "Check append method"); - -test(function() { - var headers = new Headers(); - for (name in headerDict) { - headers.set(name, headerDict[name]); - assert_equals(headers.get(name), String(headerDict[name]), - "name: " + name + " has value: " + headerDict[name]); - } -}, "Check set method"); - -test(function() { - var headers = new Headers(headerDict); - for (name in headerDict) - assert_true(headers.has(name),"headers has name " + name); - - assert_false(headers.has("nameNotInHeaders"),"headers do not have header: nameNotInHeaders"); -}, "Check has method"); - -test(function() { - var headers = new Headers(headerDict); - for (name in headerDict) { - assert_true(headers.has(name),"headers have a header: " + name); - headers.delete(name) - assert_true(!headers.has(name),"headers do not have anymore a header: " + name); - } -}, "Check delete method"); - -test(function() { - var headers = new Headers(headerDict); - for (name in headerDict) - assert_equals(headers.get(name), String(headerDict[name]), - "name: " + name + " has value: " + headerDict[name]); - - assert_equals(headers.get("nameNotInHeaders"), null, "header: nameNotInHeaders has no value"); -}, "Check get method"); - -var headerEntriesDict = {"name1": "value1", - "Name2": "value2", - "name": "value3", - "content-Type": "value4", - "Content-Typ": "value5", - "Content-Types": "value6" -}; -var sortedHeaderDict = {}; -var headerValues = []; -var sortedHeaderKeys = Object.keys(headerEntriesDict).map(function(value) { - sortedHeaderDict[value.toLowerCase()] = headerEntriesDict[value]; - headerValues.push(headerEntriesDict[value]); - return value.toLowerCase(); -}).sort(); - -var iteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())); -function checkIteratorProperties(iterator) { - var prototype = Object.getPrototypeOf(iterator); - assert_equals(Object.getPrototypeOf(prototype), iteratorPrototype); - - var descriptor = Object.getOwnPropertyDescriptor(prototype, "next"); - assert_true(descriptor.configurable, "configurable"); - assert_true(descriptor.enumerable, "enumerable"); - assert_true(descriptor.writable, "writable"); -} - -test(function() { - var headers = new Headers(headerEntriesDict); - var actual = headers.keys(); - checkIteratorProperties(actual); - - sortedHeaderKeys.forEach(function(key) { - const entry = actual.next(); - assert_false(entry.done); - assert_equals(entry.value, key); - }); - assert_true(actual.next().done); - assert_true(actual.next().done); - - for (const key of headers.keys()) - assert_true(sortedHeaderKeys.indexOf(key) != -1); -}, "Check keys method"); - -test(function() { - var headers = new Headers(headerEntriesDict); - var actual = headers.values(); - checkIteratorProperties(actual); - - sortedHeaderKeys.forEach(function(key) { - const entry = actual.next(); - assert_false(entry.done); - assert_equals(entry.value, sortedHeaderDict[key]); - }); - assert_true(actual.next().done); - assert_true(actual.next().done); - - for (const value of headers.values()) - assert_true(headerValues.indexOf(value) != -1); -}, "Check values method"); - -test(function() { - var headers = new Headers(headerEntriesDict); - var actual = headers.entries(); - checkIteratorProperties(actual); - - sortedHeaderKeys.forEach(function(key) { - const entry = actual.next(); - assert_false(entry.done); - assert_equals(entry.value[0], key); - assert_equals(entry.value[1], sortedHeaderDict[key]); - }); - assert_true(actual.next().done); - assert_true(actual.next().done); - - for (const entry of headers.entries()) - assert_equals(entry[1], sortedHeaderDict[entry[0]]); -}, "Check entries method"); - -test(function() { - var headers = new Headers(headerEntriesDict); - var actual = headers[Symbol.iterator](); - - sortedHeaderKeys.forEach(function(key) { - const entry = actual.next(); - assert_false(entry.done); - assert_equals(entry.value[0], key); - assert_equals(entry.value[1], sortedHeaderDict[key]); - }); - assert_true(actual.next().done); - assert_true(actual.next().done); -}, "Check Symbol.iterator method"); - -test(function() { - var headers = new Headers(headerEntriesDict); - var reference = sortedHeaderKeys[Symbol.iterator](); - headers.forEach(function(value, key, container) { - assert_equals(headers, container); - const entry = reference.next(); - assert_false(entry.done); - assert_equals(key, entry.value); - assert_equals(value, sortedHeaderDict[entry.value]); - }); - assert_true(reference.next().done); -}, "Check forEach method"); - -test(() => { - const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0"}); - const actualKeys = []; - const actualValues = []; - for (const [header, value] of headers) { - actualKeys.push(header); - actualValues.push(value); - headers.delete("foo"); - } - assert_array_equals(actualKeys, ["bar", "baz"]); - assert_array_equals(actualValues, ["0", "1"]); -}, "Iteration skips elements removed while iterating"); - -test(() => { - const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"}); - const actualKeys = []; - const actualValues = []; - for (const [header, value] of headers) { - actualKeys.push(header); - actualValues.push(value); - if (header === "baz") - headers.delete("bar"); - } - assert_array_equals(actualKeys, ["bar", "baz", "quux"]); - assert_array_equals(actualValues, ["0", "1", "3"]); -}, "Removing elements already iterated over causes an element to be skipped during iteration"); - -test(() => { - const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"}); - const actualKeys = []; - const actualValues = []; - for (const [header, value] of headers) { - actualKeys.push(header); - actualValues.push(value); - if (header === "baz") - headers.append("X-yZ", "4"); - } - assert_array_equals(actualKeys, ["bar", "baz", "foo", "quux", "x-yz"]); - assert_array_equals(actualValues, ["0", "1", "2", "3", "4"]); -}, "Appending a value pair during iteration causes it to be reached during iteration"); - -test(() => { - const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"}); - const actualKeys = []; - const actualValues = []; - for (const [header, value] of headers) { - actualKeys.push(header); - actualValues.push(value); - if (header === "baz") - headers.append("abc", "-1"); - } - assert_array_equals(actualKeys, ["bar", "baz", "baz", "foo", "quux"]); - assert_array_equals(actualValues, ["0", "1", "1", "2", "3"]); -}, "Prepending a value pair before the current element position causes it to be skipped during iteration and adds the current element a second time"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js b/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js deleted file mode 100644 index 20b8a9d375a..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/headers-casing.any.js +++ /dev/null @@ -1,54 +0,0 @@ -// META: title=Headers case management -// META: global=window,worker - -"use strict"; - -var headerDictCase = {"UPPERCASE": "value1", - "lowercase": "value2", - "mixedCase": "value3", - "Content-TYPE": "value4" - }; - -function checkHeadersCase(originalName, headersToCheck, expectedDict) { - var lowCaseName = originalName.toLowerCase(); - var upCaseName = originalName.toUpperCase(); - var expectedValue = expectedDict[originalName]; - assert_equals(headersToCheck.get(originalName), expectedValue, - "name: " + originalName + " has value: " + expectedValue); - assert_equals(headersToCheck.get(lowCaseName), expectedValue, - "name: " + lowCaseName + " has value: " + expectedValue); - assert_equals(headersToCheck.get(upCaseName), expectedValue, - "name: " + upCaseName + " has value: " + expectedValue); -} - -test(function() { - var headers = new Headers(headerDictCase); - for (const name in headerDictCase) - checkHeadersCase(name, headers, headerDictCase) -}, "Create headers, names use characters with different case"); - -test(function() { - var headers = new Headers(); - for (const name in headerDictCase) { - headers.append(name, headerDictCase[name]); - checkHeadersCase(name, headers, headerDictCase); - } -}, "Check append method, names use characters with different case"); - -test(function() { - var headers = new Headers(); - for (const name in headerDictCase) { - headers.set(name, headerDictCase[name]); - checkHeadersCase(name, headers, headerDictCase); - } -}, "Check set method, names use characters with different case"); - -test(function() { - var headers = new Headers(); - for (const name in headerDictCase) - headers.set(name, headerDictCase[name]); - for (const name in headerDictCase) - headers.delete(name.toLowerCase()); - for (const name in headerDictCase) - assert_false(headers.has(name), "header " + name + " should have been deleted"); -}, "Check delete method, names use characters with different case"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js b/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js deleted file mode 100644 index 4f3b6d11df9..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/headers-combine.any.js +++ /dev/null @@ -1,66 +0,0 @@ -// META: title=Headers have combined (and sorted) values -// META: global=window,worker - -"use strict"; - -var headerSeqCombine = [["single", "singleValue"], - ["double", "doubleValue1"], - ["double", "doubleValue2"], - ["triple", "tripleValue1"], - ["triple", "tripleValue2"], - ["triple", "tripleValue3"] -]; -var expectedDict = {"single": "singleValue", - "double": "doubleValue1, doubleValue2", - "triple": "tripleValue1, tripleValue2, tripleValue3" -}; - -test(function() { - var headers = new Headers(headerSeqCombine); - for (const name in expectedDict) - assert_equals(headers.get(name), expectedDict[name]); -}, "Create headers using same name for different values"); - -test(function() { - var headers = new Headers(headerSeqCombine); - for (const name in expectedDict) { - assert_true(headers.has(name), "name: " + name + " has value(s)"); - headers.delete(name); - assert_false(headers.has(name), "name: " + name + " has no value(s) anymore"); - } -}, "Check delete and has methods when using same name for different values"); - -test(function() { - var headers = new Headers(headerSeqCombine); - for (const name in expectedDict) { - headers.set(name,"newSingleValue"); - assert_equals(headers.get(name), "newSingleValue", "name: " + name + " has value: newSingleValue"); - } -}, "Check set methods when called with already used name"); - -test(function() { - var headers = new Headers(headerSeqCombine); - for (const name in expectedDict) { - var value = headers.get(name); - headers.append(name,"newSingleValue"); - assert_equals(headers.get(name), (value + ", " + "newSingleValue")); - } -}, "Check append methods when called with already used name"); - -test(() => { - const headers = new Headers([["1", "a"],["1", "b"]]); - for(let header of headers) { - assert_array_equals(header, ["1", "a, b"]); - } -}, "Iterate combined values"); - -test(() => { - const headers = new Headers([["2", "a"], ["1", "b"], ["2", "b"]]), - expected = [["1", "b"], ["2", "a, b"]]; - let i = 0; - for(let header of headers) { - assert_array_equals(header, expected[i]); - i++; - } - assert_equals(i, 2); -}, "Iterate combined values in sorted order") diff --git a/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js b/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js deleted file mode 100644 index 82dadd82340..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/headers-errors.any.js +++ /dev/null @@ -1,96 +0,0 @@ -// META: title=Headers errors -// META: global=window,worker - -"use strict"; - -test(function() { - assert_throws_js(TypeError, function() { new Headers([["name"]]); }); -}, "Create headers giving an array having one string as init argument"); - -test(function() { - assert_throws_js(TypeError, function() { new Headers([["invalid", "invalidValue1", "invalidValue2"]]); }); -}, "Create headers giving an array having three strings as init argument"); - -test(function() { - assert_throws_js(TypeError, function() { new Headers([["invalidĀ", "Value1"]]); }); -}, "Create headers giving bad header name as init argument"); - -test(function() { - assert_throws_js(TypeError, function() { new Headers([["name", "invalidValueĀ"]]); }); -}, "Create headers giving bad header value as init argument"); - -var badNames = ["invalidĀ", {}]; -var badValues = ["invalidĀ"]; - -badNames.forEach(function(name) { - test(function() { - var headers = new Headers(); - assert_throws_js(TypeError, function() { headers.get(name); }); - }, "Check headers get with an invalid name " + name); -}); - -badNames.forEach(function(name) { - test(function() { - var headers = new Headers(); - assert_throws_js(TypeError, function() { headers.delete(name); }); - }, "Check headers delete with an invalid name " + name); -}); - -badNames.forEach(function(name) { - test(function() { - var headers = new Headers(); - assert_throws_js(TypeError, function() { headers.has(name); }); - }, "Check headers has with an invalid name " + name); -}); - -badNames.forEach(function(name) { - test(function() { - var headers = new Headers(); - assert_throws_js(TypeError, function() { headers.set(name, "Value1"); }); - }, "Check headers set with an invalid name " + name); -}); - -badValues.forEach(function(value) { - test(function() { - var headers = new Headers(); - assert_throws_js(TypeError, function() { headers.set("name", value); }); - }, "Check headers set with an invalid value " + value); -}); - -badNames.forEach(function(name) { - test(function() { - var headers = new Headers(); - assert_throws_js(TypeError, function() { headers.append("invalidĀ", "Value1"); }); - }, "Check headers append with an invalid name " + name); -}); - -badValues.forEach(function(value) { - test(function() { - var headers = new Headers(); - assert_throws_js(TypeError, function() { headers.append("name", value); }); - }, "Check headers append with an invalid value " + value); -}); - -test(function() { - var headers = new Headers([["name", "value"]]); - assert_throws_js(TypeError, function() { headers.forEach(); }); - assert_throws_js(TypeError, function() { headers.forEach(undefined); }); - assert_throws_js(TypeError, function() { headers.forEach(1); }); -}, "Headers forEach throws if argument is not callable"); - -test(function() { - var headers = new Headers([["name1", "value1"], ["name2", "value2"], ["name3", "value3"]]); - var counter = 0; - try { - headers.forEach(function(value, name) { - counter++; - if (name == "name2") - throw "error"; - }); - } catch (e) { - assert_equals(counter, 2); - assert_equals(e, "error"); - return; - } - assert_unreached(); -}, "Headers forEach loop should stop if callback is throwing exception"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js b/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js deleted file mode 100644 index 60dbb9ef67a..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/headers-no-cors.any.js +++ /dev/null @@ -1,59 +0,0 @@ -// META: global=window,worker - -"use strict"; - -promise_test(() => fetch("../cors/resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…"); - -const longValue = "s".repeat(127); - -[ - { - "headers": ["accept", "accept-language", "content-language"], - "values": [longValue, "", longValue] - }, - { - "headers": ["accept", "accept-language", "content-language"], - "values": ["", longValue] - }, - { - "headers": ["content-type"], - "values": ["text/plain;" + "s".repeat(116), "text/plain"] - } -].forEach(testItem => { - testItem.headers.forEach(header => { - test(() => { - const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers; - testItem.values.forEach((value) => { - noCorsHeaders.append(header, value); - assert_equals(noCorsHeaders.get(header), testItem.values[0], '1'); - }); - noCorsHeaders.set(header, testItem.values.join(", ")); - assert_equals(noCorsHeaders.get(header), testItem.values[0], '2'); - noCorsHeaders.delete(header); - assert_false(noCorsHeaders.has(header)); - }, "\"no-cors\" Headers object cannot have " + header + " set to " + testItem.values.join(", ")); - }); -}); - -function runTests(testArray) { - testArray = testArray.concat([ - ["dpr", "2"], - ["rtt", "1.0"], - ["downlink", "-1.0"], - ["ect", "6g"], - ["save-data", "on"], - ["viewport-width", "100"], - ["width", "100"], - ["unknown", "doesitmatter"] - ]); - testArray.forEach(testItem => { - const [headerName, headerValue] = testItem; - test(() => { - const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers; - noCorsHeaders.append(headerName, headerValue); - assert_false(noCorsHeaders.has(headerName)); - noCorsHeaders.set(headerName, headerValue); - assert_false(noCorsHeaders.has(headerName)); - }, "\"no-cors\" Headers object cannot have " + headerName + "/" + headerValue + " as header"); - }); -} diff --git a/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js b/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js deleted file mode 100644 index 68cf5b85f3a..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/headers-normalize.any.js +++ /dev/null @@ -1,56 +0,0 @@ -// META: title=Headers normalize values -// META: global=window,worker - -"use strict"; - -const expectations = { - "name1": [" space ", "space"], - "name2": ["\ttab\t", "tab"], - "name3": [" spaceAndTab\t", "spaceAndTab"], - "name4": ["\r\n newLine", "newLine"], //obs-fold cases - "name5": ["newLine\r\n ", "newLine"], - "name6": ["\r\n\tnewLine", "newLine"], - "name7": ["\t\f\tnewLine\n", "\f\tnewLine"], - "name8": ["newLine\xa0", "newLine\xa0"], // \xa0 == non breaking space -}; - -test(function () { - const headerDict = Object.fromEntries( - Object.entries(expectations).map(([name, [actual]]) => [name, actual]), - ); - var headers = new Headers(headerDict); - for (const name in expectations) { - const expected = expectations[name][1]; - assert_equals( - headers.get(name), - expected, - "name: " + name + " has normalized value: " + expected, - ); - } -}, "Create headers with not normalized values"); - -test(function () { - var headers = new Headers(); - for (const name in expectations) { - headers.append(name, expectations[name][0]); - const expected = expectations[name][1]; - assert_equals( - headers.get(name), - expected, - "name: " + name + " has value: " + expected, - ); - } -}, "Check append method with not normalized values"); - -test(function () { - var headers = new Headers(); - for (const name in expectations) { - headers.set(name, expectations[name][0]); - const expected = expectations[name][1]; - assert_equals( - headers.get(name), - expected, - "name: " + name + " has value: " + expected, - ); - } -}, "Check set method with not normalized values"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-record.any.js b/test/fixtures/wpt/fetch/api/headers/headers-record.any.js deleted file mode 100644 index fa853914f48..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/headers-record.any.js +++ /dev/null @@ -1,357 +0,0 @@ -// META: global=window,worker - -"use strict"; - -var log = []; -function clearLog() { - log = []; -} -function addLogEntry(name, args) { - log.push([ name, ...args ]); -} - -var loggingHandler = { -}; - -setup(function() { - for (let prop of Object.getOwnPropertyNames(Reflect)) { - loggingHandler[prop] = function(...args) { - addLogEntry(prop, args); - return Reflect[prop](...args); - } - } -}); - -test(function() { - var h = new Headers(); - assert_equals([...h].length, 0); -}, "Passing nothing to Headers constructor"); - -test(function() { - var h = new Headers(undefined); - assert_equals([...h].length, 0); -}, "Passing undefined to Headers constructor"); - -test(function() { - assert_throws_js(TypeError, function() { - var h = new Headers(null); - }); -}, "Passing null to Headers constructor"); - -test(function() { - this.add_cleanup(clearLog); - var record = { a: "b" }; - var proxy = new Proxy(record, loggingHandler); - var h = new Headers(proxy); - - assert_equals(log.length, 4); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", record]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[3], ["get", record, "a", proxy]); - - // Check the results. - assert_equals([...h].length, 1); - assert_array_equals([...h.keys()], ["a"]); - assert_true(h.has("a")); - assert_equals(h.get("a"), "b"); -}, "Basic operation with one property"); - -test(function() { - this.add_cleanup(clearLog); - var recordProto = { c: "d" }; - var record = Object.create(recordProto, { a: { value: "b", enumerable: true } }); - var proxy = new Proxy(record, loggingHandler); - var h = new Headers(proxy); - - assert_equals(log.length, 4); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", record]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[3], ["get", record, "a", proxy]); - - // Check the results. - assert_equals([...h].length, 1); - assert_array_equals([...h.keys()], ["a"]); - assert_true(h.has("a")); - assert_equals(h.get("a"), "b"); -}, "Basic operation with one property and a proto"); - -test(function() { - this.add_cleanup(clearLog); - var record = { a: "b", c: "d" }; - var proxy = new Proxy(record, loggingHandler); - var h = new Headers(proxy); - - assert_equals(log.length, 6); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", record]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[3], ["get", record, "a", proxy]); - // Then the second [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]); - // Then the second [[Get]] from step 5.2. - assert_array_equals(log[5], ["get", record, "c", proxy]); - - // Check the results. - assert_equals([...h].length, 2); - assert_array_equals([...h.keys()], ["a", "c"]); - assert_true(h.has("a")); - assert_equals(h.get("a"), "b"); - assert_true(h.has("c")); - assert_equals(h.get("c"), "d"); -}, "Correct operation ordering with two properties"); - -test(function() { - this.add_cleanup(clearLog); - var record = { a: "b", "\uFFFF": "d" }; - var proxy = new Proxy(record, loggingHandler); - assert_throws_js(TypeError, function() { - var h = new Headers(proxy); - }); - - assert_equals(log.length, 5); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", record]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[3], ["get", record, "a", proxy]); - // Then the second [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "\uFFFF"]); - // The second [[Get]] never happens, because we convert the invalid name to a - // ByteString first and throw. -}, "Correct operation ordering with two properties one of which has an invalid name"); - -test(function() { - this.add_cleanup(clearLog); - var record = { a: "\uFFFF", c: "d" } - var proxy = new Proxy(record, loggingHandler); - assert_throws_js(TypeError, function() { - var h = new Headers(proxy); - }); - - assert_equals(log.length, 4); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", record]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[3], ["get", record, "a", proxy]); - // Nothing else after this, because converting the result of that [[Get]] to a - // ByteString throws. -}, "Correct operation ordering with two properties one of which has an invalid value"); - -test(function() { - this.add_cleanup(clearLog); - var record = {}; - Object.defineProperty(record, "a", { value: "b", enumerable: false }); - Object.defineProperty(record, "c", { value: "d", enumerable: true }); - Object.defineProperty(record, "e", { value: "f", enumerable: false }); - var proxy = new Proxy(record, loggingHandler); - var h = new Headers(proxy); - - assert_equals(log.length, 6); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", record]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); - // No [[Get]] because not enumerable - // Then the second [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[3], ["getOwnPropertyDescriptor", record, "c"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[4], ["get", record, "c", proxy]); - // Then the third [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "e"]); - // No [[Get]] because not enumerable - - // Check the results. - assert_equals([...h].length, 1); - assert_array_equals([...h.keys()], ["c"]); - assert_true(h.has("c")); - assert_equals(h.get("c"), "d"); -}, "Correct operation ordering with non-enumerable properties"); - -test(function() { - this.add_cleanup(clearLog); - var record = {a: "b", c: "d", e: "f"}; - var lyingHandler = { - getOwnPropertyDescriptor: function(target, name) { - if (name == "a" || name == "e") { - return undefined; - } - return Reflect.getOwnPropertyDescriptor(target, name); - } - }; - var lyingProxy = new Proxy(record, lyingHandler); - var proxy = new Proxy(lyingProxy, loggingHandler); - var h = new Headers(proxy); - - assert_equals(log.length, 6); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", lyingProxy]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", lyingProxy, "a"]); - // No [[Get]] because no descriptor - // Then the second [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[3], ["getOwnPropertyDescriptor", lyingProxy, "c"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[4], ["get", lyingProxy, "c", proxy]); - // Then the third [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[5], ["getOwnPropertyDescriptor", lyingProxy, "e"]); - // No [[Get]] because no descriptor - - // Check the results. - assert_equals([...h].length, 1); - assert_array_equals([...h.keys()], ["c"]); - assert_true(h.has("c")); - assert_equals(h.get("c"), "d"); -}, "Correct operation ordering with undefined descriptors"); - -test(function() { - this.add_cleanup(clearLog); - var record = {a: "b", c: "d"}; - var lyingHandler = { - ownKeys: function() { - return [ "a", "c", "a", "c" ]; - }, - }; - var lyingProxy = new Proxy(record, lyingHandler); - var proxy = new Proxy(lyingProxy, loggingHandler); - - // Returning duplicate keys from ownKeys() throws a TypeError. - assert_throws_js(TypeError, - function() { var h = new Headers(proxy); }); - - assert_equals(log.length, 2); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", lyingProxy]); -}, "Correct operation ordering with repeated keys"); - -test(function() { - this.add_cleanup(clearLog); - var record = { - a: "b", - [Symbol.toStringTag]: { - // Make sure the ToString conversion of the value happens - // after the ToString conversion of the key. - toString: function () { addLogEntry("toString", [this]); return "nope"; } - }, - c: "d" }; - var proxy = new Proxy(record, loggingHandler); - assert_throws_js(TypeError, - function() { var h = new Headers(proxy); }); - - assert_equals(log.length, 7); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", record]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[3], ["get", record, "a", proxy]); - // Then the second [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]); - // Then the second [[Get]] from step 5.2. - assert_array_equals(log[5], ["get", record, "c", proxy]); - // Then the third [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[6], ["getOwnPropertyDescriptor", record, - Symbol.toStringTag]); - // Then we throw an exception converting the Symbol to a string, before we do - // the third [[Get]]. -}, "Basic operation with Symbol keys"); - -test(function() { - this.add_cleanup(clearLog); - var record = { - a: { - toString: function() { addLogEntry("toString", [this]); return "b"; } - }, - [Symbol.toStringTag]: { - toString: function () { addLogEntry("toString", [this]); return "nope"; } - }, - c: { - toString: function() { addLogEntry("toString", [this]); return "d"; } - } - }; - // Now make that Symbol-named property not enumerable. - Object.defineProperty(record, Symbol.toStringTag, { enumerable: false }); - assert_array_equals(Reflect.ownKeys(record), - ["a", "c", Symbol.toStringTag]); - - var proxy = new Proxy(record, loggingHandler); - var h = new Headers(proxy); - - assert_equals(log.length, 9); - // The first thing is the [[Get]] of Symbol.iterator to figure out whether - // we're a sequence, during overload resolution. - assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]); - // Then we have the [[OwnPropertyKeys]] from - // https://webidl.spec.whatwg.org/#es-to-record step 4. - assert_array_equals(log[1], ["ownKeys", record]); - // Then the [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]); - // Then the [[Get]] from step 5.2. - assert_array_equals(log[3], ["get", record, "a", proxy]); - // Then the ToString on the value. - assert_array_equals(log[4], ["toString", record.a]); - // Then the second [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "c"]); - // Then the second [[Get]] from step 5.2. - assert_array_equals(log[6], ["get", record, "c", proxy]); - // Then the ToString on the value. - assert_array_equals(log[7], ["toString", record.c]); - // Then the third [[GetOwnProperty]] from step 5.1. - assert_array_equals(log[8], ["getOwnPropertyDescriptor", record, - Symbol.toStringTag]); - // No [[Get]] because not enumerable. - - // Check the results. - assert_equals([...h].length, 2); - assert_array_equals([...h.keys()], ["a", "c"]); - assert_true(h.has("a")); - assert_equals(h.get("a"), "b"); - assert_true(h.has("c")); - assert_equals(h.get("c"), "d"); -}, "Operation with non-enumerable Symbol keys"); diff --git a/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js b/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js deleted file mode 100644 index d826bcab2a0..00000000000 --- a/test/fixtures/wpt/fetch/api/headers/headers-structure.any.js +++ /dev/null @@ -1,20 +0,0 @@ -// META: title=Headers basic -// META: global=window,worker - -"use strict"; - -var headers = new Headers(); -var methods = ["append", - "delete", - "get", - "has", - "set", - //Headers is iterable - "entries", - "keys", - "values" - ]; -for (var idx in methods) - test(function() { - assert_true(methods[idx] in headers, "headers has " + methods[idx] + " method"); - }, "Headers has " + methods[idx] + " method"); diff --git a/test/fixtures/wpt/fetch/api/idlharness.any.js b/test/fixtures/wpt/fetch/api/idlharness.any.js deleted file mode 100644 index 7b3c694e16a..00000000000 --- a/test/fixtures/wpt/fetch/api/idlharness.any.js +++ /dev/null @@ -1,21 +0,0 @@ -// META: global=window,worker -// META: script=/resources/WebIDLParser.js -// META: script=/resources/idlharness.js -// META: timeout=long - -idl_test( - ['fetch'], - ['referrer-policy', 'html', 'dom'], - idl_array => { - idl_array.add_objects({ - Headers: ["new Headers()"], - Request: ["new Request('about:blank')"], - Response: ["new Response()"], - }); - if (self.GLOBAL.isWindow()) { - idl_array.add_objects({ Window: ['window'] }); - } else if (self.GLOBAL.isWorker()) { - idl_array.add_objects({ WorkerGlobalScope: ['self'] }); - } - } -); diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html b/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html deleted file mode 100644 index e8660dffa94..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/csp-blocked-worker.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Fetch in worker: blocked by CSP - - - - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.html b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html deleted file mode 100644 index 99e90dfcd8f..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/csp-blocked.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Fetch: blocked by CSP - - - - - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers b/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers deleted file mode 100644 index c8c1e9ffbd9..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/csp-blocked.html.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Security-Policy: connect-src 'none'; \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.js b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js deleted file mode 100644 index 28653fff85c..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/csp-blocked.js +++ /dev/null @@ -1,13 +0,0 @@ -if (this.document === undefined) { - importScripts("/resources/testharness.js"); - importScripts("../resources/utils.js"); -} - -//Content-Security-Policy: connect-src 'none'; cf .headers file -cspViolationUrl = RESOURCES_DIR + "top.txt"; - -promise_test(function(test) { - return promise_rejects_js(test, TypeError, fetch(cspViolationUrl)); -}, "Fetch is blocked by CSP, got a TypeError"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers b/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers deleted file mode 100644 index c8c1e9ffbd9..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/csp-blocked.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Security-Policy: connect-src 'none'; \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/nested-policy.js b/test/fixtures/wpt/fetch/api/policies/nested-policy.js deleted file mode 100644 index b0d17696c33..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/nested-policy.js +++ /dev/null @@ -1 +0,0 @@ -// empty, but referrer-policy set on this file diff --git a/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers b/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers deleted file mode 100644 index 7ffbf17d6be..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/nested-policy.js.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: no-referrer diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html deleted file mode 100644 index af898aa29f5..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-service-worker.https.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Fetch in service worker: referrer with no-referrer policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html deleted file mode 100644 index dbef9bb658f..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer-worker.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Fetch in worker: referrer with no-referrer policy - - - - - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html deleted file mode 100644 index 22a6f34c525..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Fetch: referrer with no-referrer policy - - - - - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers deleted file mode 100644 index 7ffbf17d6be..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.html.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: no-referrer diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js deleted file mode 100644 index 60600bf081c..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js +++ /dev/null @@ -1,19 +0,0 @@ -if (this.document === undefined) { - importScripts("/resources/testharness.js"); - importScripts("../resources/utils.js"); -} - -var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=origin"; - -promise_test(function(test) { - return fetch(fetchedUrl).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - var referrer = resp.headers.get("x-request-referer"); - //Either no referrer header is sent or it is empty - if (referrer) - assert_equals(referrer, "", "request's referrer is empty"); - }); -}, "Request's referrer is empty"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers deleted file mode 100644 index 7ffbf17d6be..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-no-referrer.js.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: no-referrer diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html deleted file mode 100644 index 4018b837816..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin-service-worker.https.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Fetch in service worker: referrer with no-referrer policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html deleted file mode 100644 index d87192e2271..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Fetch in service worker: referrer with origin-when-cross-origin policy - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html deleted file mode 100644 index f95ae8cf081..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin-worker.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Fetch in worker: referrer with origin-when-cross-origin policy - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html deleted file mode 100644 index 5cd79e4b536..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Fetch: referrer with origin-when-cross-origin policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers deleted file mode 100644 index ad768e63294..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.html.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: origin-when-cross-origin diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js deleted file mode 100644 index 0adadbc5508..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js +++ /dev/null @@ -1,21 +0,0 @@ -if (this.document === undefined) { - importScripts("/resources/testharness.js"); - importScripts("../resources/utils.js"); - importScripts("/common/get-host-info.sub.js"); - - // A nested importScripts() with a referrer-policy should have no effect - // on overall worker policy. - importScripts("nested-policy.js"); -} - -var referrerOrigin = location.origin + '/'; -var fetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; - -promise_test(function(test) { - return fetch(fetchedUrl).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); - }); -}, "Request's referrer is origin"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers deleted file mode 100644 index ad768e63294..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin-when-cross-origin.js.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: origin-when-cross-origin diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html deleted file mode 100644 index bb80dd54fbf..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin-worker.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Fetch in worker: referrer with origin policy - - - - - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.html b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html deleted file mode 100644 index b164afe01de..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Fetch: referrer with origin policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers deleted file mode 100644 index 5b29739bbdd..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin.html.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: origin diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.js b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js deleted file mode 100644 index 918f8f207c3..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin.js +++ /dev/null @@ -1,30 +0,0 @@ -if (this.document === undefined) { - importScripts("/resources/testharness.js"); - importScripts("../resources/utils.js"); - - // A nested importScripts() with a referrer-policy should have no effect - // on overall worker policy. - importScripts("nested-policy.js"); -} - -var referrerOrigin = (new URL("/", location.href)).href; -var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer"; - -promise_test(function(test) { - return fetch(fetchedUrl).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); - }); -}, "Request's referrer is origin"); - -promise_test(function(test) { - var referrerUrl = "https://{{domains[www]}}:{{ports[https][0]}}/"; - return fetch(fetchedUrl, { "referrer": referrerUrl }).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); - }); -}, "Cross-origin referrer is overridden by client origin"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers deleted file mode 100644 index 5b29739bbdd..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-origin.js.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: origin diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html deleted file mode 100644 index 634877edae8..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-service-worker.https.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Fetch in worker: referrer with unsafe-url policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html deleted file mode 100644 index 42045776b12..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url-worker.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Fetch in worker: referrer with unsafe-url policy - - - - - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html deleted file mode 100644 index 10dd79e3d35..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Fetch: referrer with unsafe-url policy - - - - - - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers deleted file mode 100644 index 8e23770bd60..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.html.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: unsafe-url diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js deleted file mode 100644 index 4d61172613e..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js +++ /dev/null @@ -1,21 +0,0 @@ -if (this.document === undefined) { - importScripts("/resources/testharness.js"); - importScripts("../resources/utils.js"); - - // A nested importScripts() with a referrer-policy should have no effect - // on overall worker policy. - importScripts("nested-policy.js"); -} - -var referrerUrl = location.href; -var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer"; - -promise_test(function(test) { - return fetch(fetchedUrl).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - assert_equals(resp.headers.get("x-request-referer"), referrerUrl, "request's referrer is " + referrerUrl); - }); -}, "Request's referrer is the full url of current document/worker"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers b/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers deleted file mode 100644 index 8e23770bd60..00000000000 --- a/test/fixtures/wpt/fetch/api/policies/referrer-unsafe-url.js.headers +++ /dev/null @@ -1 +0,0 @@ -Referrer-Policy: unsafe-url diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js deleted file mode 100644 index 74d731f2425..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-back-to-original-origin.any.js +++ /dev/null @@ -1,38 +0,0 @@ -// META: global=window,worker -// META: script=/common/get-host-info.sub.js - -const BASE = location.href; -const IS_HTTPS = new URL(BASE).protocol === 'https:'; -const REMOTE_HOST = get_host_info()['REMOTE_HOST']; -const REMOTE_PORT = - IS_HTTPS ? get_host_info()['HTTPS_PORT'] : get_host_info()['HTTP_PORT']; - -const REMOTE_ORIGIN = - new URL(`//${REMOTE_HOST}:${REMOTE_PORT}`, BASE).origin; -const DESTINATION = new URL('../resources/cors-top.txt', BASE); - -function CreateURL(url, BASE, params) { - const u = new URL(url, BASE); - for (const {name, value} of params) { - u.searchParams.append(name, value); - } - return u; -} - -const redirect = - CreateURL('/fetch/api/resources/redirect.py', REMOTE_ORIGIN, - [{name: 'redirect_status', value: 303}, - {name: 'location', value: DESTINATION.href}]); - -promise_test(async (test) => { - const res = await fetch(redirect.href, {mode: 'no-cors'}); - // This is discussed at https://github.com/whatwg/fetch/issues/737. - assert_equals(res.type, 'opaque'); -}, 'original => remote => original with mode: "no-cors"'); - -promise_test(async (test) => { - const res = await fetch(redirect.href, {mode: 'cors'}); - assert_equals(res.type, 'cors'); -}, 'original => remote => original with mode: "cors"'); - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js deleted file mode 100644 index 420f9c0dfcb..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-count.any.js +++ /dev/null @@ -1,51 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js -// META: script=/common/utils.js -// META: timeout=long - -/** - * Fetches a target that returns response with HTTP status code `statusCode` to - * redirect `maxCount` times. - */ -function redirectCountTest(maxCount, {statusCode, shouldPass = true} = {}) { - const desc = `Redirect ${statusCode} ${maxCount} times`; - - const fromUrl = `${RESOURCES_DIR}redirect.py`; - const toUrl = fromUrl; - const token1 = token(); - const url = `${fromUrl}?token=${token1}` + - `&max_age=0` + - `&redirect_status=${statusCode}` + - `&max_count=${maxCount}` + - `&location=${encodeURIComponent(toUrl)}`; - - const requestInit = {'redirect': 'follow'}; - - promise_test((test) => { - return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`) - .then((resp) => { - assert_equals( - resp.status, 200, 'Clean stash response\'s status is 200'); - - if (!shouldPass) - return promise_rejects_js(test, TypeError, fetch(url, requestInit)); - - return fetch(url, requestInit) - .then((resp) => { - assert_equals(resp.status, 200, 'Response\'s status is 200'); - return resp.text(); - }) - .then((body) => { - assert_equals( - body, maxCount.toString(), `Redirected ${maxCount} times`); - }); - }); - }, desc); -} - -for (const statusCode of [301, 302, 303, 307, 308]) { - redirectCountTest(20, {statusCode}); - redirectCountTest(21, {statusCode, shouldPass: false}); -} - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js deleted file mode 100644 index 487f4d42e92..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-empty-location.any.js +++ /dev/null @@ -1,21 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -// Tests receiving a redirect response with a Location header with an empty -// value. - -const url = RESOURCES_DIR + 'redirect-empty-location.py'; - -promise_test(t => { - return promise_rejects_js(t, TypeError, fetch(url, {redirect:'follow'})); -}, 'redirect response with empty Location, follow mode'); - -promise_test(t => { - return fetch(url, {redirect:'manual'}) - .then(resp => { - assert_equals(resp.type, 'opaqueredirect'); - assert_equals(resp.status, 0); - }); -}, 'redirect response with empty Location, manual mode'); - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.any.js deleted file mode 100644 index c9ac13f3dbb..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.any.js +++ /dev/null @@ -1,35 +0,0 @@ -// META: global=window -// META: timeout=long -// META: title=Fetch API: keepalive handling -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=../resources/keepalive-helper.js - -'use strict'; - -const { - HTTP_NOTSAMESITE_ORIGIN, - HTTP_REMOTE_ORIGIN, - HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT -} = get_host_info(); - - -keepaliveRedirectInUnloadTest('same-origin redirect'); -keepaliveRedirectInUnloadTest( - 'same-origin redirect + preflight', {withPreflight: true}); -keepaliveRedirectInUnloadTest('cross-origin redirect', { - origin1: HTTP_REMOTE_ORIGIN, - origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT -}); -keepaliveRedirectInUnloadTest('cross-origin redirect + preflight', { - origin1: HTTP_REMOTE_ORIGIN, - origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, - withPreflight: true -}); -keepaliveRedirectInUnloadTest( - 'redirect to file URL', - {url2: 'file://tmp/bar.txt', expectFetchSucceed: false}); -keepaliveRedirectInUnloadTest('redirect to data URL', { - url2: 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5', - expectFetchSucceed: false -}); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.https.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.https.any.js deleted file mode 100644 index 54e4bc31fa1..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-keepalive.https.any.js +++ /dev/null @@ -1,18 +0,0 @@ -// META: global=window -// META: title=Fetch API: keepalive handling -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=../resources/keepalive-helper.js - -'use strict'; - -const { - HTTP_NOTSAMESITE_ORIGIN, - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -keepaliveRedirectTest(`mixed content redirect`, { - origin1: HTTPS_NOTSAMESITE_ORIGIN, - origin2: HTTP_NOTSAMESITE_ORIGIN, - expectFetchSucceed: false -}); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js deleted file mode 100644 index 779ad705793..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-location-escape.tentative.any.js +++ /dev/null @@ -1,46 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -// See https://github.com/whatwg/fetch/issues/883 for the behavior covered by -// this test. As of writing, the Fetch spec has not been updated to cover these. - -// redirectLocation tests that a Location header of |locationHeader| is resolved -// to a URL which ends in |expectedUrlSuffix|. |locationHeader| is interpreted -// as a byte sequence via isomorphic encode, as described in [INFRA]. This -// allows the caller to specify byte sequences which are not valid UTF-8. -// However, this means, e.g., U+2603 must be passed in as "\xe2\x98\x83", its -// UTF-8 encoding, not "\u2603". -// -// [INFRA] https://infra.spec.whatwg.org/#isomorphic-encode -function redirectLocation( - desc, redirectUrl, locationHeader, expectedUrlSuffix) { - promise_test(function(test) { - // Note we use escape() instead of encodeURIComponent(), so that characters - // are escaped as bytes in the isomorphic encoding. - var url = redirectUrl + '?simple=1&location=' + escape(locationHeader); - - return fetch(url, {'redirect': 'follow'}).then(function(resp) { - assert_true( - resp.url.endsWith(expectedUrlSuffix), - resp.url + ' ends with ' + expectedUrlSuffix); - }); - }, desc); -} - -var redirUrl = RESOURCES_DIR + 'redirect.py'; -redirectLocation( - 'Redirect to escaped UTF-8', redirUrl, 'top.txt?%E2%98%83%e2%98%83', - 'top.txt?%E2%98%83%e2%98%83'); -redirectLocation( - 'Redirect to unescaped UTF-8', redirUrl, 'top.txt?\xe2\x98\x83', - 'top.txt?%E2%98%83'); -redirectLocation( - 'Redirect to escaped and unescaped UTF-8', redirUrl, - 'top.txt?\xe2\x98\x83%e2%98%83', 'top.txt?%E2%98%83%e2%98%83'); -redirectLocation( - 'Escaping produces double-percent', redirUrl, 'top.txt?%\xe2\x98\x83', - 'top.txt?%%E2%98%83'); -redirectLocation( - 'Redirect to invalid UTF-8', redirUrl, 'top.txt?\xff', 'top.txt?%FF'); - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js deleted file mode 100644 index 3d483bdcd49..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-location.any.js +++ /dev/null @@ -1,73 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -const VALID_URL = 'top.txt'; -const INVALID_URL = 'invalidurl:'; -const DATA_URL = 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5'; - -/** - * A test to fetch a URL that returns response redirecting to `toUrl` with - * `status` as its HTTP status code. `expectStatus` can be set to test the - * status code in fetch's Promise response. - */ -function redirectLocationTest(toUrlDesc, { - toUrl = undefined, - status, - expectStatus = undefined, - mode, - shouldPass = true -} = {}) { - toUrlDesc = toUrl ? `with ${toUrlDesc}` : `without`; - const desc = `Redirect ${status} in "${mode}" mode ${toUrlDesc} location`; - const url = `${RESOURCES_DIR}redirect.py?redirect_status=${status}` + - (toUrl ? `&location=${encodeURIComponent(toUrl)}` : ''); - const requestInit = {'redirect': mode}; - if (!expectStatus) - expectStatus = status; - - promise_test((test) => { - if (mode === 'error' || !shouldPass) - return promise_rejects_js(test, TypeError, fetch(url, requestInit)); - if (mode === 'manual') - return fetch(url, requestInit).then((resp) => { - assert_equals(resp.status, 0, "Response's status is 0"); - assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect"); - assert_equals(resp.statusText, '', `Response's statusText is ""`); - assert_true(resp.headers.entries().next().done, "Headers should be empty"); - }); - - if (mode === 'follow') - return fetch(url, requestInit).then((resp) => { - assert_equals( - resp.status, expectStatus, `Response's status is ${expectStatus}`); - }); - assert_unreached(`${mode} is not a valid redirect mode`); - }, desc); -} - -// FIXME: We may want to mix redirect-mode and cors-mode. -for (const status of [301, 302, 303, 307, 308]) { - redirectLocationTest('without location', {status, mode: 'follow'}); - redirectLocationTest('without location', {status, mode: 'manual'}); - // FIXME: Add tests for "error" redirect-mode without location. - - // When succeeded, `follow` mode should have followed all redirects. - redirectLocationTest( - 'valid', {toUrl: VALID_URL, status, expectStatus: 200, mode: 'follow'}); - redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'manual'}); - redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'error'}); - - redirectLocationTest( - 'invalid', - {toUrl: INVALID_URL, status, mode: 'follow', shouldPass: false}); - redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'manual'}); - redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'error'}); - - redirectLocationTest( - 'data', {toUrl: DATA_URL, status, mode: 'follow', shouldPass: false}); - // FIXME: Should this pass? - redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'manual'}); - redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'error'}); -} - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js deleted file mode 100644 index 9fe086a9db7..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-method.any.js +++ /dev/null @@ -1,112 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -// Creates a promise_test that fetches a URL that returns a redirect response. -// -// |opts| has additional options: -// |opts.body|: the request body as a string or blob (default is empty body) -// |opts.expectedBodyAsString|: the expected response body as a string. The -// server is expected to echo the request body. The default is the empty string -// if the request after redirection isn't POST; otherwise it's |opts.body|. -// |opts.expectedRequestContentType|: the expected Content-Type of redirected -// request. -function redirectMethod(desc, redirectUrl, redirectLocation, redirectStatus, method, expectedMethod, opts) { - let url = redirectUrl; - let urlParameters = "?redirect_status=" + redirectStatus; - urlParameters += "&location=" + encodeURIComponent(redirectLocation); - - let requestHeaders = { - "Content-Encoding": "Identity", - "Content-Language": "en-US", - "Content-Location": "foo", - }; - let requestInit = {"method": method, "redirect": "follow", "headers" : requestHeaders}; - opts = opts || {}; - if (opts.body) { - requestInit.body = opts.body; - } - - promise_test(function(test) { - return fetch(url + urlParameters, requestInit).then(function(resp) { - let expectedRequestContentType = "NO"; - if (opts.expectedRequestContentType) { - expectedRequestContentType = opts.expectedRequestContentType; - } - - assert_equals(resp.status, 200, "Response's status is 200"); - assert_equals(resp.type, "basic", "Response's type basic"); - assert_equals( - resp.headers.get("x-request-method"), - expectedMethod, - "Request method after redirection is " + expectedMethod); - let hasRequestBodyHeader = true; - if (opts.expectedStripRequestBodyHeader) { - hasRequestBodyHeader = !opts.expectedStripRequestBodyHeader; - } - assert_equals( - resp.headers.get("x-request-content-type"), - expectedRequestContentType, - "Request Content-Type after redirection is " + expectedRequestContentType); - [ - "Content-Encoding", - "Content-Language", - "Content-Location" - ].forEach(header => { - let xHeader = "x-request-" + header.toLowerCase(); - let expectedValue = hasRequestBodyHeader ? requestHeaders[header] : "NO"; - assert_equals( - resp.headers.get(xHeader), - expectedValue, - "Request " + header + " after redirection is " + expectedValue); - }); - assert_true(resp.redirected); - return resp.text().then(function(text) { - let expectedBody = ""; - if (expectedMethod == "POST") { - expectedBody = opts.expectedBodyAsString || requestInit.body; - } - let expectedContentLength = expectedBody ? expectedBody.length.toString() : "NO"; - assert_equals(text, expectedBody, "request body"); - assert_equals( - resp.headers.get("x-request-content-length"), - expectedContentLength, - "Request Content-Length after redirection is " + expectedContentLength); - }); - }); - }, desc); -} - -promise_test(function(test) { - assert_false(new Response().redirected); - return fetch(RESOURCES_DIR + "method.py").then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - assert_false(resp.redirected); - }); -}, "Response.redirected should be false on not-redirected responses"); - -var redirUrl = RESOURCES_DIR + "redirect.py"; -var locationUrl = "method.py"; - -const stringBody = "this is my body"; -const blobBody = new Blob(["it's me the blob!", " ", "and more blob!"]); -const blobBodyAsString = "it's me the blob! and more blob!"; - -redirectMethod("Redirect 301 with GET", redirUrl, locationUrl, 301, "GET", "GET"); -redirectMethod("Redirect 301 with POST", redirUrl, locationUrl, 301, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); -redirectMethod("Redirect 301 with HEAD", redirUrl, locationUrl, 301, "HEAD", "HEAD"); - -redirectMethod("Redirect 302 with GET", redirUrl, locationUrl, 302, "GET", "GET"); -redirectMethod("Redirect 302 with POST", redirUrl, locationUrl, 302, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); -redirectMethod("Redirect 302 with HEAD", redirUrl, locationUrl, 302, "HEAD", "HEAD"); - -redirectMethod("Redirect 303 with GET", redirUrl, locationUrl, 303, "GET", "GET"); -redirectMethod("Redirect 303 with POST", redirUrl, locationUrl, 303, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true }); -redirectMethod("Redirect 303 with HEAD", redirUrl, locationUrl, 303, "HEAD", "HEAD"); -redirectMethod("Redirect 303 with TESTING", redirUrl, locationUrl, 303, "TESTING", "GET", { expectedStripRequestBodyHeader: true }); - -redirectMethod("Redirect 307 with GET", redirUrl, locationUrl, 307, "GET", "GET"); -redirectMethod("Redirect 307 with POST (string body)", redirUrl, locationUrl, 307, "POST", "POST", { body: stringBody , expectedRequestContentType: "text/plain;charset=UTF-8"}); -redirectMethod("Redirect 307 with POST (blob body)", redirUrl, locationUrl, 307, "POST", "POST", { body: blobBody, expectedBodyAsString: blobBodyAsString }); -redirectMethod("Redirect 307 with HEAD", redirUrl, locationUrl, 307, "HEAD", "HEAD"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js deleted file mode 100644 index 9f1ff98c65a..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-mode.any.js +++ /dev/null @@ -1,59 +0,0 @@ -// META: script=/common/get-host-info.sub.js - -var redirectLocation = "cors-top.txt"; -const { ORIGIN, REMOTE_ORIGIN } = get_host_info(); - -function testRedirect(origin, redirectStatus, redirectMode, corsMode) { - var url = new URL("../resources/redirect.py", self.location); - if (origin === "cross-origin") { - url.host = get_host_info().REMOTE_HOST; - url.port = get_host_info().HTTP_PORT; - } - - var urlParameters = "?redirect_status=" + redirectStatus; - urlParameters += "&location=" + encodeURIComponent(redirectLocation); - - var requestInit = {redirect: redirectMode, mode: corsMode}; - - promise_test(function(test) { - if (redirectMode === "error" || - (corsMode === "no-cors" && redirectMode !== "follow" && origin !== "same-origin")) - return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); - if (redirectMode === "manual") - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 0, "Response's status is 0"); - assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect"); - assert_equals(resp.statusText, "", "Response's statusText is \"\""); - assert_equals(resp.url, url + urlParameters, "Response URL should be the original one"); - }); - if (redirectMode === "follow") - return fetch(url + urlParameters, requestInit).then(function(resp) { - if (corsMode !== "no-cors" || origin === "same-origin") { - assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), "Response's url should be the redirected one"); - assert_equals(resp.status, 200, "Response's status is 200"); - } else { - assert_equals(resp.type, "opaque", "Response is opaque"); - } - }); - assert_unreached(redirectMode + " is no a valid redirect mode"); - }, origin + " redirect " + redirectStatus + " in " + redirectMode + " redirect and " + corsMode + " mode"); -} - -for (var origin of ["same-origin", "cross-origin"]) { - for (var statusCode of [301, 302, 303, 307, 308]) { - for (var redirect of ["error", "manual", "follow"]) { - for (var mode of ["cors", "no-cors"]) - testRedirect(origin, statusCode, redirect, mode); - } - } -} - -promise_test(async (t) => { - const destination = `${ORIGIN}/common/blank.html`; - // We use /common/redirect.py intentionally, as we want a CORS error. - const url = - `${REMOTE_ORIGIN}/common/redirect.py?location=${destination}`; - await promise_rejects_js(t, TypeError, fetch(url, { redirect: "manual" })); -}, "manual redirect with a CORS error should be rejected"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js deleted file mode 100644 index 6001c509b1d..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-origin.any.js +++ /dev/null @@ -1,68 +0,0 @@ -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -const { - HTTP_ORIGIN, - HTTP_REMOTE_ORIGIN, -} = get_host_info(); - -/** - * Fetches `fromUrl` with 'cors' and 'follow' modes that returns response to - * redirect to `toUrl`. - */ -function testOriginAfterRedirection( - desc, method, fromUrl, toUrl, statusCode, expectedOrigin) { - desc = `[${method}] Redirect ${statusCode} ${desc}`; - const token1 = token(); - const url = `${fromUrl}?token=${token1}&max_age=0` + - `&redirect_status=${statusCode}` + - `&location=${encodeURIComponent(toUrl)}`; - - const requestInit = {method, 'mode': 'cors', 'redirect': 'follow'}; - - promise_test(function(test) { - return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`) - .then((cleanResponse) => { - assert_equals( - cleanResponse.status, 200, - `Clean stash response's status is 200`); - return fetch(url, requestInit).then((redirectResponse) => { - assert_equals( - redirectResponse.status, 200, - `Inspect header response's status is 200`); - assert_equals( - redirectResponse.headers.get('x-request-origin'), - expectedOrigin, 'Check origin header'); - }); - }); - }, desc); -} - -const FROM_URL = `${RESOURCES_DIR}redirect.py`; -const CORS_FROM_URL = - `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${FROM_URL}`; -const TO_URL = `${HTTP_ORIGIN}${dirname(location.pathname)}${ - RESOURCES_DIR}inspect-headers.py?headers=origin`; -const CORS_TO_URL = `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${ - RESOURCES_DIR}inspect-headers.py?cors&headers=origin`; - -for (const statusCode of [301, 302, 303, 307, 308]) { - for (const method of ['GET', 'POST']) { - testOriginAfterRedirection( - 'Same origin to same origin', method, FROM_URL, TO_URL, statusCode, - null); - testOriginAfterRedirection( - 'Same origin to other origin', method, FROM_URL, CORS_TO_URL, - statusCode, HTTP_ORIGIN); - testOriginAfterRedirection( - 'Other origin to other origin', method, CORS_FROM_URL, CORS_TO_URL, - statusCode, HTTP_ORIGIN); - // TODO(crbug.com/1432059): Fix broken tests. - testOriginAfterRedirection( - 'Other origin to same origin', method, CORS_FROM_URL, `${TO_URL}&cors`, - statusCode, 'null'); - } -} - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js deleted file mode 100644 index 337f8dd0698..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-referrer-override.any.js +++ /dev/null @@ -1,104 +0,0 @@ -// META: timeout=long -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function getExpectation(expectations, initPolicy, initScenario, redirectPolicy, redirectScenario) { - let policies = [ - expectations[initPolicy][initScenario], - expectations[redirectPolicy][redirectScenario] - ]; - - if (policies.includes("omitted")) { - return null; - } else if (policies.includes("origin")) { - return referrerOrigin; - } else { - // "stripped-referrer" - return referrerUrl; - } -} - -function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) { - var url = redirectUrl; - var urlParameters = "?location=" + encodeURIComponent(redirectLocation); - var description = desc + ", " + referrerPolicy + " init, " + redirectReferrerPolicy + " redirect header "; - - if (redirectReferrerPolicy) - urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy; - - var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy}; - promise_test(function(test) { - return fetch(url + urlParameters, requestInit).then(function(response) { - assert_equals(response.status, 200, "Inspect header response's status is 200"); - assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header"); - }); - }, description); -} - -var referrerOrigin = get_host_info().HTTP_ORIGIN + "/"; -var referrerUrl = location.href; - -var redirectUrl = RESOURCES_DIR + "redirect.py"; -var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer"; -var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; - -var expectations = { - "no-referrer": { - "same-origin": "omitted", - "cross-origin": "omitted" - }, - "no-referrer-when-downgrade": { - "same-origin": "stripped-referrer", - "cross-origin": "stripped-referrer" - }, - "origin": { - "same-origin": "origin", - "cross-origin": "origin" - }, - "origin-when-cross-origin": { - "same-origin": "stripped-referrer", - "cross-origin": "origin", - }, - "same-origin": { - "same-origin": "stripped-referrer", - "cross-origin": "omitted" - }, - "strict-origin": { - "same-origin": "origin", - "cross-origin": "origin" - }, - "strict-origin-when-cross-origin": { - "same-origin": "stripped-referrer", - "cross-origin": "origin" - }, - "unsafe-url": { - "same-origin": "stripped-referrer", - "cross-origin": "stripped-referrer" - } -}; - -for (var initPolicy in expectations) { - for (var redirectPolicy in expectations) { - - // Redirect to same-origin URL - testReferrerAfterRedirection( - "Same origin redirection", - redirectUrl, - locationUrl, - initPolicy, - redirectPolicy, - getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "same-origin")); - - // Redirect to cross-origin URL - testReferrerAfterRedirection( - "Cross origin redirection", - redirectUrl, - crossLocationUrl, - initPolicy, - redirectPolicy, - getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "cross-origin")); - } -} - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js deleted file mode 100644 index 99fda42e69b..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-referrer.any.js +++ /dev/null @@ -1,66 +0,0 @@ -// META: timeout=long -// META: script=/common/utils.js -// META: script=../resources/utils.js -// META: script=/common/get-host-info.sub.js - -function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) { - var url = redirectUrl; - var urlParameters = "?location=" + encodeURIComponent(redirectLocation); - - if (redirectReferrerPolicy) - urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy; - - var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy}; - - promise_test(function(test) { - return fetch(url + urlParameters, requestInit).then(function(response) { - assert_equals(response.status, 200, "Inspect header response's status is 200"); - assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header"); - }); - }, desc); -} - -var referrerOrigin = get_host_info().HTTP_ORIGIN + "/"; -var referrerUrl = location.href; - -var redirectUrl = RESOURCES_DIR + "redirect.py"; -var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer"; -var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; - -testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl); -testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl); -testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl); -testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin); -testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl); -testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null); -testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin); -testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl); - -testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl); -testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl); -testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl); -testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin); -testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl); -testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null); -testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin); -testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl); - -testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl); -testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl); -testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null); -testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin); -testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin); -testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null); -testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin); -testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin); - -testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl); -testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl); -testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null); -testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin); -testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin); -testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null); -testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin); -testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin); - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js deleted file mode 100644 index 31ec124fd6a..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-schemes.any.js +++ /dev/null @@ -1,19 +0,0 @@ -// META: title=Fetch: handling different schemes in redirects -// META: global=window,worker -// META: script=/common/get-host-info.sub.js - -// All non-HTTP(S) schemes cannot survive redirects -var url = "../resources/redirect.py?location="; -var tests = [ - url + "mailto:a@a.com", - url + "data:,HI", - url + "facetime:a@a.org", - url + "about:blank", - url + "about:unicorn", - url + "blob:djfksfjs" -]; -tests.forEach(function(url) { - promise_test(function(test) { - return promise_rejects_js(test, TypeError, fetch(url)) - }) -}) diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js deleted file mode 100644 index 9d0f147349c..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-to-dataurl.any.js +++ /dev/null @@ -1,28 +0,0 @@ -// META: global=window,worker -// META: script=/common/get-host-info.sub.js - -var dataURL = "data:text/plain;base64,cmVzcG9uc2UncyBib2R5"; -var body = "response's body"; -var contentType = "text/plain"; - -function redirectDataURL(desc, redirectUrl, mode) { - var url = redirectUrl + "?cors&location=" + encodeURIComponent(dataURL); - - var requestInit = {"mode": mode}; - - promise_test(function(test) { - return promise_rejects_js(test, TypeError, fetch(url, requestInit)); - }, desc); -} - -var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py"; -var corsRedirUrl = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py"; - -redirectDataURL("Testing data URL loading after same-origin redirection (cors mode)", redirUrl, "cors"); -redirectDataURL("Testing data URL loading after same-origin redirection (no-cors mode)", redirUrl, "no-cors"); -redirectDataURL("Testing data URL loading after same-origin redirection (same-origin mode)", redirUrl, "same-origin"); - -redirectDataURL("Testing data URL loading after cross-origin redirection (cors mode)", corsRedirUrl, "cors"); -redirectDataURL("Testing data URL loading after cross-origin redirection (no-cors mode)", corsRedirUrl, "no-cors"); - -done(); diff --git a/test/fixtures/wpt/fetch/api/redirect/redirect-upload.h2.any.js b/test/fixtures/wpt/fetch/api/redirect/redirect-upload.h2.any.js deleted file mode 100644 index 521bd3adc28..00000000000 --- a/test/fixtures/wpt/fetch/api/redirect/redirect-upload.h2.any.js +++ /dev/null @@ -1,33 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js - -const redirectUrl = RESOURCES_DIR + "redirect.h2.py"; -const redirectLocation = "top.txt"; - -async function fetchStreamRedirect(statusCode) { - const url = RESOURCES_DIR + "redirect.h2.py" + - `?redirect_status=${statusCode}&location=${redirectLocation}`; - const requestInit = {method: "POST"}; - requestInit["body"] = new ReadableStream({start: controller => { - const encoder = new TextEncoder(); - controller.enqueue(encoder.encode("Test")); - controller.close(); - }}); - requestInit.duplex = "half"; - return fetch(url, requestInit); -} - -promise_test(async () => { - const resp = await fetchStreamRedirect(303); - assert_equals(resp.status, 200); - assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), - "Response's url should be the redirected one"); -}, "Fetch upload streaming should be accepted on 303"); - -for (const statusCode of [301, 302, 307, 308]) { - promise_test(t => { - return promise_rejects_js(t, TypeError, fetchStreamRedirect(statusCode)); - }, `Fetch upload streaming should fail on ${statusCode}`); -} diff --git a/test/fixtures/wpt/fetch/api/request/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/api/request/WEB_FEATURES.yml deleted file mode 100644 index 69b2ea582a2..00000000000 --- a/test/fixtures/wpt/fetch/api/request/WEB_FEATURES.yml +++ /dev/null @@ -1,4 +0,0 @@ -features: -- name: fetch-priority - files: - - request-init-priority.any.js diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html deleted file mode 100644 index f3f9f7856d5..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-frame.https.html +++ /dev/null @@ -1,51 +0,0 @@ - -Fetch destination tests for resources with no load event - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html deleted file mode 100644 index 1aa5a5613b1..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-iframe.https.html +++ /dev/null @@ -1,51 +0,0 @@ - -Fetch destination tests for resources with no load event - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html deleted file mode 100644 index 2fb4aaebc04..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-no-load-event.https.html +++ /dev/null @@ -1,138 +0,0 @@ - -Fetch destination tests for resources with no load event - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html deleted file mode 100644 index db99202df87..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-prefetch.https.html +++ /dev/null @@ -1,46 +0,0 @@ - -Fetch destination test for prefetching - - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html deleted file mode 100644 index 5935c1ff31e..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination-worker.https.html +++ /dev/null @@ -1,60 +0,0 @@ - -Fetch destination tests for resources with no load event - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html b/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html deleted file mode 100644 index 1b6cf169141..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/fetch-destination.https.html +++ /dev/null @@ -1,485 +0,0 @@ - -Fetch destination tests - - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.css b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers deleted file mode 100644 index 9bb8badcad4..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.es.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/event-stream diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.html b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.json b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png deleted file mode 100644 index 01c9666a8de..00000000000 Binary files a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.png and /dev/null differ diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.ttf b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.ttf deleted file mode 100644 index 9023592ef5a..00000000000 Binary files a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy.ttf and /dev/null differ diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 deleted file mode 100644 index 0091330f1ec..00000000000 Binary files a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.mp3 and /dev/null differ diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga deleted file mode 100644 index 239ad2bd08c..00000000000 Binary files a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_audio.oga and /dev/null differ diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 deleted file mode 100644 index 7022e75c15e..00000000000 Binary files a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.mp4 and /dev/null differ diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.webm b/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.webm deleted file mode 100644 index c3d433a3e02..00000000000 Binary files a/test/fixtures/wpt/fetch/api/request/destination/resources/dummy_video.webm and /dev/null differ diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/empty.https.html b/test/fixtures/wpt/fetch/api/request/destination/resources/empty.https.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-frame.js b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-frame.js deleted file mode 100644 index b69de0b7df9..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-frame.js +++ /dev/null @@ -1,20 +0,0 @@ -self.addEventListener('fetch', function(event) { - if (event.request.url.includes('dummy')) { - event.waitUntil(async function() { - let destination = new URL(event.request.url).searchParams.get("dest"); - let clients = await self.clients.matchAll({"includeUncontrolled": true}); - clients.forEach(function(client) { - if (client.url.includes("fetch-destination-frame")) { - if (event.request.destination == destination) { - client.postMessage("PASS"); - } else { - client.postMessage("FAIL"); - } - } - }) - }()); - } - event.respondWith(fetch(event.request)); -}); - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js deleted file mode 100644 index 76345839ead..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js +++ /dev/null @@ -1,20 +0,0 @@ -self.addEventListener('fetch', function(event) { - if (event.request.url.includes('dummy')) { - event.waitUntil(async function() { - let destination = new URL(event.request.url).searchParams.get("dest"); - let clients = await self.clients.matchAll({"includeUncontrolled": true}); - clients.forEach(function(client) { - if (client.url.includes("fetch-destination-iframe")) { - if (event.request.destination == destination) { - client.postMessage("PASS"); - } else { - client.postMessage("FAIL"); - } - } - }) - }()); - } - event.respondWith(fetch(event.request)); -}); - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js deleted file mode 100644 index a583b1272a1..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js +++ /dev/null @@ -1,20 +0,0 @@ -self.addEventListener('fetch', function(event) { - const url = event.request.url; - if (url.includes('dummy') && url.includes('?')) { - event.waitUntil(async function() { - let destination = new URL(url).searchParams.get("dest"); - var result = "FAIL"; - if (event.request.destination == destination || - (event.request.destination == "empty" && destination == "")) { - result = "PASS"; - } - let cl = await clients.matchAll({includeUncontrolled: true}); - for (i = 0; i < cl.length; i++) { - cl[i].postMessage(result); - } - }()) - } - event.respondWith(fetch(event.request)); -}); - - diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker.js b/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker.js deleted file mode 100644 index 904009c1721..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/fetch-destination-worker.js +++ /dev/null @@ -1,12 +0,0 @@ -self.addEventListener('fetch', function(event) { - if (event.request.url.includes('dummy')) { - let destination = new URL(event.request.url).searchParams.get("dest"); - if (event.request.destination == destination || - (event.request.destination == "empty" && destination == "")) { - event.respondWith(fetch(event.request)); - } else { - event.respondWith(Response.error()); - } - } -}); - diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-css.js b/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-css.js deleted file mode 100644 index 3c8cf1f44b7..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-css.js +++ /dev/null @@ -1 +0,0 @@ -import "./dummy.css?dest=style" with { type: "css" }; diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-json.js b/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-json.js deleted file mode 100644 index b2d964dd824..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/import-declaration-type-json.js +++ /dev/null @@ -1 +0,0 @@ -import "./dummy.json?dest=json" with { type: "json" }; diff --git a/test/fixtures/wpt/fetch/api/request/destination/resources/importer.js b/test/fixtures/wpt/fetch/api/request/destination/resources/importer.js deleted file mode 100644 index 9568474d505..00000000000 --- a/test/fixtures/wpt/fetch/api/request/destination/resources/importer.js +++ /dev/null @@ -1 +0,0 @@ -importScripts("dummy?t=importScripts&dest=script"); diff --git a/test/fixtures/wpt/fetch/api/request/forbidden-method.any.js b/test/fixtures/wpt/fetch/api/request/forbidden-method.any.js deleted file mode 100644 index eb13f37f0b5..00000000000 --- a/test/fixtures/wpt/fetch/api/request/forbidden-method.any.js +++ /dev/null @@ -1,13 +0,0 @@ -// META: global=window,worker - -// https://fetch.spec.whatwg.org/#forbidden-method -for (const method of [ - 'CONNECT', 'TRACE', 'TRACK', - 'connect', 'trace', 'track' - ]) { - test(function() { - assert_throws_js(TypeError, - function() { new Request('./', {method: method}); } - ); - }, 'Request() with a forbidden method ' + method + ' must throw.'); -} diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/construct-in-detached-frame.window.js b/test/fixtures/wpt/fetch/api/request/multi-globals/construct-in-detached-frame.window.js deleted file mode 100644 index b0d6ba5b80d..00000000000 --- a/test/fixtures/wpt/fetch/api/request/multi-globals/construct-in-detached-frame.window.js +++ /dev/null @@ -1,11 +0,0 @@ -// This is a regression test for Chromium issue https://crbug.com/1427266. -test(() => { - const iframe = document.createElement('iframe'); - document.body.append(iframe); - const otherRequest = iframe.contentWindow.Request; - iframe.remove(); - const r1 = new otherRequest('resource', { method: 'POST', body: 'string' }); - const r2 = new otherRequest(r1); - assert_true(r1.bodyUsed); - assert_false(r2.bodyUsed); -}, 'creating a request from another request in a detached realm should work'); diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/current/current.html b/test/fixtures/wpt/fetch/api/request/multi-globals/current/current.html deleted file mode 100644 index 9bb6e0bbf3f..00000000000 --- a/test/fixtures/wpt/fetch/api/request/multi-globals/current/current.html +++ /dev/null @@ -1,3 +0,0 @@ - -Current page used as a test helper - diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html b/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html deleted file mode 100644 index a885b8a0a73..00000000000 --- a/test/fixtures/wpt/fetch/api/request/multi-globals/incumbent/incumbent.html +++ /dev/null @@ -1,14 +0,0 @@ - -Incumbent page used as a test helper - - - - diff --git a/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html b/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html deleted file mode 100644 index df60e72507f..00000000000 --- a/test/fixtures/wpt/fetch/api/request/multi-globals/url-parsing.html +++ /dev/null @@ -1,27 +0,0 @@ - -Request constructor URL parsing, with multiple globals in play - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js b/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js deleted file mode 100644 index 915063bab56..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-bad-port.any.js +++ /dev/null @@ -1,94 +0,0 @@ -// META: global=window,worker - -// list of bad ports according to -// https://fetch.spec.whatwg.org/#port-blocking -var BLOCKED_PORTS_LIST = [ - 1, // tcpmux - 7, // echo - 9, // discard - 11, // systat - 13, // daytime - 15, // netstat - 17, // qotd - 19, // chargen - 20, // ftp-data - 21, // ftp - 22, // ssh - 23, // telnet - 25, // smtp - 37, // time - 42, // name - 43, // nicname - 53, // domain - 69, // tftp - 77, // priv-rjs - 79, // finger - 87, // ttylink - 95, // supdup - 101, // hostriame - 102, // iso-tsap - 103, // gppitnp - 104, // acr-nema - 109, // pop2 - 110, // pop3 - 111, // sunrpc - 113, // auth - 115, // sftp - 117, // uucp-path - 119, // nntp - 123, // ntp - 135, // loc-srv / epmap - 137, // netbios-ns - 139, // netbios-ssn - 143, // imap2 - 161, // snmp - 179, // bgp - 389, // ldap - 427, // afp (alternate) - 465, // smtp (alternate) - 512, // print / exec - 513, // login - 514, // shell - 515, // printer - 526, // tempo - 530, // courier - 531, // chat - 532, // netnews - 540, // uucp - 548, // afp - 554, // rtsp - 556, // remotefs - 563, // nntp+ssl - 587, // smtp (outgoing) - 601, // syslog-conn - 636, // ldap+ssl - 989, // ftps-data - 990, // ftps - 993, // ldap+ssl - 995, // pop3+ssl - 1719, // h323gatestat - 1720, // h323hostcall - 1723, // pptp - 2049, // nfs - 3659, // apple-sasl - 4045, // lockd - 4190, // sieve - 5060, // sip - 5061, // sips - 6000, // x11 - 6566, // sane-port - 6665, // irc (alternate) - 6666, // irc (alternate) - 6667, // irc (default) - 6668, // irc (alternate) - 6669, // irc (alternate) - 6679, // osaut - 6697, // irc+tls - 10080, // amanda -]; - -BLOCKED_PORTS_LIST.map(function(a){ - promise_test(function(t){ - return promise_rejects_js(t, TypeError, fetch(`${location.origin}:${a}`)) - }, 'Request on bad port ' + a + ' should throw TypeError.'); -}); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js deleted file mode 100644 index c5b2001cc8f..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-cache-default-conditional.any.js +++ /dev/null @@ -1,170 +0,0 @@ -// META: global=window,worker -// META: title=Request cache - default with conditional requests -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=request-cache.js - -var tests = [ - { - name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{}, {"If-Modified-Since": now.toGMTString()}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{}, {"If-Modified-Since": now.toGMTString()}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{"If-Modified-Since": now.toGMTString()}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{"If-Modified-Since": now.toGMTString()}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{}, {"If-None-Match": '"foo"'}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{}, {"If-None-Match": '"foo"'}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{"If-None-Match": '"foo"'}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{"If-None-Match": '"foo"'}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{}, {"If-Match": '"foo"'}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{}, {"If-Match": '"foo"'}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{"If-Match": '"foo"'}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{"If-Match": '"foo"'}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{}, {"If-Range": '"foo"'}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{}, {"If-Range": '"foo"'}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"', - state: "stale", - request_cache: ["default", "default"], - request_headers: [{"If-Range": '"foo"'}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"', - state: "fresh", - request_cache: ["default", "default"], - request_headers: [{"If-Range": '"foo"'}, {}], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js deleted file mode 100644 index dfa8369c9a3..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-cache-default.any.js +++ /dev/null @@ -1,39 +0,0 @@ -// META: global=window,worker -// META: title=Request cache - default -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=request-cache.js - -var tests = [ - { - name: 'RequestCache "default" mode checks the cache for previously cached content and goes to the network for stale responses', - state: "stale", - request_cache: ["default", "default"], - expected_validation_headers: [false, true], - expected_no_cache_headers: [false, false], - }, - { - name: 'RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists', - state: "fresh", - request_cache: ["default", "default"], - expected_validation_headers: [false], - expected_no_cache_headers: [false], - }, - { - name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache', - state: "stale", - cache_control: "no-store", - request_cache: ["default", "default"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, false], - }, - { - name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache', - state: "fresh", - cache_control: "no-store", - request_cache: ["default", "default"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, false], - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js deleted file mode 100644 index 00dce096c72..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-cache-force-cache.any.js +++ /dev/null @@ -1,67 +0,0 @@ -// META: global=window,worker -// META: title=Request cache - force-cache -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=request-cache.js - -var tests = [ - { - name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses', - state: "stale", - request_cache: ["default", "force-cache"], - expected_validation_headers: [false], - expected_no_cache_headers: [false], - }, - { - name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses', - state: "fresh", - request_cache: ["default", "force-cache"], - expected_validation_headers: [false], - expected_no_cache_headers: [false], - }, - { - name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found', - state: "stale", - request_cache: ["force-cache"], - expected_validation_headers: [false], - expected_no_cache_headers: [false], - }, - { - name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found', - state: "fresh", - request_cache: ["force-cache"], - expected_validation_headers: [false], - expected_no_cache_headers: [false], - }, - { - name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary', - state: "stale", - vary: "*", - request_cache: ["default", "force-cache"], - expected_validation_headers: [false, true], - expected_no_cache_headers: [false, false], - }, - { - name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary', - state: "fresh", - vary: "*", - request_cache: ["default", "force-cache"], - expected_validation_headers: [false, true], - expected_no_cache_headers: [false, false], - }, - { - name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network', - state: "stale", - request_cache: ["force-cache", "default"], - expected_validation_headers: [false, true], - expected_no_cache_headers: [false, false], - }, - { - name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network', - state: "fresh", - request_cache: ["force-cache", "default"], - expected_validation_headers: [false], - expected_no_cache_headers: [false], - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js deleted file mode 100644 index 41fc22baf23..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-cache-no-cache.any.js +++ /dev/null @@ -1,25 +0,0 @@ -// META: global=window,worker -// META: title=Request cache : no-cache -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=request-cache.js - -var tests = [ - { - name: 'RequestCache "no-cache" mode revalidates stale responses found in the cache', - state: "stale", - request_cache: ["default", "no-cache"], - expected_validation_headers: [false, true], - expected_no_cache_headers: [false, false], - expected_max_age_headers: [false, true], - }, - { - name: 'RequestCache "no-cache" mode revalidates fresh responses found in the cache', - state: "fresh", - request_cache: ["default", "no-cache"], - expected_validation_headers: [false, true], - expected_no_cache_headers: [false, false], - expected_max_age_headers: [false, true], - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js deleted file mode 100644 index 9a28718bf22..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-cache-no-store.any.js +++ /dev/null @@ -1,37 +0,0 @@ -// META: global=window,worker -// META: title=Request cache - no store -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=request-cache.js - -var tests = [ - { - name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless', - state: "stale", - request_cache: ["default", "no-store"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless', - state: "fresh", - request_cache: ["default", "no-store"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "no-store" mode does not store the response in the cache', - state: "stale", - request_cache: ["no-store", "default"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "no-store" mode does not store the response in the cache', - state: "fresh", - request_cache: ["no-store", "default"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [true, false], - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js deleted file mode 100644 index 1305787c7c1..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-cache-only-if-cached.any.js +++ /dev/null @@ -1,66 +0,0 @@ -// META: global=window,dedicatedworker,sharedworker -// META: title=Request cache - only-if-cached -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=request-cache.js - -// FIXME: avoid mixed content requests to enable service worker global -var tests = [ - { - name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses', - state: "stale", - request_cache: ["default", "only-if-cached"], - expected_validation_headers: [false], - expected_no_cache_headers: [false] - }, - { - name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses', - state: "fresh", - request_cache: ["default", "only-if-cached"], - expected_validation_headers: [false], - expected_no_cache_headers: [false] - }, - { - name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found', - state: "fresh", - request_cache: ["only-if-cached"], - response: ["error"], - expected_validation_headers: [], - expected_no_cache_headers: [] - }, - { - name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content', - state: "fresh", - request_cache: ["default", "only-if-cached"], - redirect: "same-origin", - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, false], - }, - { - name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content', - state: "stale", - request_cache: ["default", "only-if-cached"], - redirect: "same-origin", - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, false], - }, - { - name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects', - state: "fresh", - request_cache: ["default", "only-if-cached"], - redirect: "cross-origin", - response: [null, "error"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, false], - }, - { - name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects', - state: "stale", - request_cache: ["default", "only-if-cached"], - redirect: "cross-origin", - response: [null, "error"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, false], - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js b/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js deleted file mode 100644 index c7bfffb3988..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-cache-reload.any.js +++ /dev/null @@ -1,51 +0,0 @@ -// META: global=window,worker -// META: title=Request cache - reload -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=request-cache.js - -var tests = [ - { - name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless', - state: "stale", - request_cache: ["default", "reload"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless', - state: "fresh", - request_cache: ["default", "reload"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, - { - name: 'RequestCache "reload" mode does store the response in the cache', - state: "stale", - request_cache: ["reload", "default"], - expected_validation_headers: [false, true], - expected_no_cache_headers: [true, false], - }, - { - name: 'RequestCache "reload" mode does store the response in the cache', - state: "fresh", - request_cache: ["reload", "default"], - expected_validation_headers: [false], - expected_no_cache_headers: [true], - }, - { - name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored', - state: "stale", - request_cache: ["default", "reload", "default"], - expected_validation_headers: [false, false, true], - expected_no_cache_headers: [false, true, false], - }, - { - name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored', - state: "fresh", - request_cache: ["default", "reload", "default"], - expected_validation_headers: [false, false], - expected_no_cache_headers: [false, true], - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/api/request/request-cache.js b/test/fixtures/wpt/fetch/api/request/request-cache.js deleted file mode 100644 index f2fbecf4969..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-cache.js +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Each test is run twice: once using etag/If-None-Match and once with - * date/If-Modified-Since. Each test run gets its own URL and randomized - * content and operates independently. - * - * The test steps are run with request_cache.length fetch requests issued - * and their immediate results sanity-checked. The cache.py server script - * stashes an entry containing any If-None-Match, If-Modified-Since, Pragma, - * and Cache-Control observed headers for each request it receives. When - * the test fetches have run, this state is retrieved from cache.py and the - * expected_* lists are checked, including their length. - * - * This means that if a request_* fetch is expected to hit the cache and not - * touch the network, then there will be no entry for it in the expect_* - * lists. AKA (request_cache.length - expected_validation_headers.length) - * should equal the number of cache hits that didn't touch the network. - * - * Test dictionary keys: - * - state: required string that determines whether the Expires response for - * the fetched document should be set in the future ("fresh") or past - * ("stale"). - * - vary: optional string to be passed to the server for it to quote back - * in a Vary header on the response to us. - * - cache_control: optional string to be passed to the server for it to - * quote back in a Cache-Control header on the response to us. - * - redirect: optional string "same-origin" or "cross-origin". If - * provided, the server will issue an absolute redirect to the script on - * the same or a different origin, as appropriate. The redirected - * location is the script with the redirect parameter removed, so the - * content/state/etc. will be as if you hadn't specified a redirect. - * - request_cache: required array of cache modes to use (via `cache`). - * - request_headers: optional array of explicit fetch `headers` arguments. - * If provided, the server will log an empty dictionary for each request - * instead of the request headers it would normally log. - * - response: optional array of specialized response handling. Right now, - * "error" array entries indicate a network error response is expected - * which will reject with a TypeError. - * - expected_validation_headers: required boolean array indicating whether - * the server should have seen an If-None-Match/If-Modified-Since header - * in the request. - * - expected_no_cache_headers: required boolean array indicating whether - * the server should have seen Pragma/Cache-control:no-cache headers in - * the request. - * - expected_max_age_headers: optional boolean array indicating whether - * the server should have seen a Cache-Control:max-age=0 header in the - * request. - */ - -var now = new Date(); - -function base_path() { - return location.pathname.replace(/\/[^\/]*$/, '/'); -} -function make_url(uuid, id, value, content, info) { - var dates = { - fresh: new Date(now.getFullYear() + 1, now.getMonth(), now.getDay()).toGMTString(), - stale: new Date(now.getFullYear() - 1, now.getMonth(), now.getDay()).toGMTString(), - }; - var vary = ""; - if ("vary" in info) { - vary = "&vary=" + info.vary; - } - var cache_control = ""; - if ("cache_control" in info) { - cache_control = "&cache_control=" + info.cache_control; - } - var redirect = ""; - - var ignore_request_headers = ""; - if ("request_headers" in info) { - // Ignore the request headers that we send since they may be synthesized by the test. - ignore_request_headers = "&ignore"; - } - var url_sans_redirect = "resources/cache.py?token=" + uuid + - "&content=" + content + - "&" + id + "=" + value + - "&expires=" + dates[info.state] + - vary + cache_control + ignore_request_headers; - // If there's a redirect, the target is the script without any redirect at - // either the same domain or a different domain. - if ("redirect" in info) { - var host_info = get_host_info(); - var origin; - switch (info.redirect) { - case "same-origin": - origin = host_info['HTTP_ORIGIN']; - break; - case "cross-origin": - origin = host_info['HTTP_REMOTE_ORIGIN']; - break; - } - var redirected_url = origin + base_path() + url_sans_redirect; - return url_sans_redirect + "&redirect=" + encodeURIComponent(redirected_url); - } else { - return url_sans_redirect; - } -} -function expected_status(type, identifier, init) { - if (type == "date" && - init.headers && - init.headers["If-Modified-Since"] == identifier) { - // The server will respond with a 304 in this case. - return [304, "Not Modified"]; - } - return [200, "OK"]; -} -function expected_response_text(type, identifier, init, content) { - if (type == "date" && - init.headers && - init.headers["If-Modified-Since"] == identifier) { - // The server will respond with a 304 in this case. - return ""; - } - return content; -} -function server_state(uuid) { - return fetch("resources/cache.py?querystate&token=" + uuid) - .then(function(response) { - return response.text(); - }).then(function(text) { - // null will be returned if the server never received any requests - // for the given uuid. Normalize that to an empty list consistent - // with our representation. - return JSON.parse(text) || []; - }); -} -function make_test(type, info) { - return function(test) { - var uuid = token(); - var identifier = (type == "tag" ? Math.random() : now.toGMTString()); - var content = Math.random().toString(); - var url = make_url(uuid, type, identifier, content, info); - var fetch_functions = []; - for (var i = 0; i < info.request_cache.length; ++i) { - fetch_functions.push(function(idx) { - var init = {cache: info.request_cache[idx]}; - if ("request_headers" in info) { - init.headers = info.request_headers[idx]; - } - if (init.cache === "only-if-cached") { - // only-if-cached requires we use same-origin mode. - init.mode = "same-origin"; - } - return fetch(url, init) - .then(function(response) { - if ("response" in info && info.response[idx] === "error") { - assert_true(false, "fetch should have been an error"); - return; - } - assert_array_equals([response.status, response.statusText], - expected_status(type, identifier, init)); - return response.text(); - }).then(function(text) { - assert_equals(text, expected_response_text(type, identifier, init, content)); - }, function(reason) { - if ("response" in info && info.response[idx] === "error") { - assert_throws_js(TypeError, function() { throw reason; }); - } else { - throw reason; - } - }); - }); - } - var i = 0; - function run_next_step() { - if (fetch_functions.length) { - return fetch_functions.shift()(i++) - .then(run_next_step); - } else { - return Promise.resolve(); - } - } - return run_next_step() - .then(function() { - // Now, query the server state - return server_state(uuid); - }).then(function(state) { - var expectedState = []; - info.expected_validation_headers.forEach(function (validate) { - if (validate) { - if (type == "tag") { - expectedState.push({"If-None-Match": '"' + identifier + '"'}); - } else { - expectedState.push({"If-Modified-Since": identifier}); - } - } else { - expectedState.push({}); - } - }); - for (var i = 0; i < info.expected_no_cache_headers.length; ++i) { - if (info.expected_no_cache_headers[i]) { - expectedState[i]["Pragma"] = "no-cache"; - expectedState[i]["Cache-Control"] = "no-cache"; - } - } - if ("expected_max_age_headers" in info) { - for (var i = 0; i < info.expected_max_age_headers.length; ++i) { - if (info.expected_max_age_headers[i]) { - expectedState[i]["Cache-Control"] = "max-age=0"; - } - } - } - assert_equals(state.length, expectedState.length); - for (var i = 0; i < state.length; ++i) { - for (var header in state[i]) { - assert_equals(state[i][header], expectedState[i][header]); - delete expectedState[i][header]; - } - for (var header in expectedState[i]) { - assert_false(header in state[i]); - } - } - }); - }; -} - -function run_tests(tests) -{ - tests.forEach(function(info) { - promise_test(make_test("tag", info), info.name + " with Etag and " + info.state + " response"); - promise_test(make_test("date", info), info.name + " with Last-Modified and " + info.state + " response"); - }); -} diff --git a/test/fixtures/wpt/fetch/api/request/request-clone.sub.html b/test/fixtures/wpt/fetch/api/request/request-clone.sub.html deleted file mode 100644 index c690bb3dc03..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-clone.sub.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - Request clone - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/request-constructor-init-body-override.any.js b/test/fixtures/wpt/fetch/api/request/request-constructor-init-body-override.any.js deleted file mode 100644 index c2bbf86f304..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-constructor-init-body-override.any.js +++ /dev/null @@ -1,37 +0,0 @@ -promise_test(async function () { - const req1 = new Request("https://example.com/", { - body: "req1", - method: "POST", - }); - - const text1 = await req1.text(); - assert_equals( - text1, - "req1", - "The body of the first request should be 'req1'." - ); - - const req2 = new Request(req1, { body: "req2" }); - const bodyText = await req2.text(); - assert_equals( - bodyText, - "req2", - "The body of the second request should be overridden to 'req2'." - ); - -}, "Check that the body of a new request can be overridden when created from an existing Request object"); - -promise_test(async function () { - const req1 = new Request("https://example.com/", { - body: "req1", - method: "POST", - }); - - const req2 = new Request("https://example.com/", req1); - const bodyText = await req2.text(); - assert_equals( - bodyText, - "req1", - "The body of the second request should be the same as the first." - ); -}, "Check that the body of a new request can be duplicated from an existing Request object"); diff --git a/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js b/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js deleted file mode 100644 index 0bf9672a795..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-consume-empty.any.js +++ /dev/null @@ -1,89 +0,0 @@ -// META: global=window,worker -// META: title=Request consume empty bodies - -function checkBodyText(test, request) { - return request.text().then(function(bodyAsText) { - assert_equals(bodyAsText, "", "Resolved value should be empty"); - assert_false(request.bodyUsed); - }); -} - -async function checkBodyBlob(test, request) { - const bodyAsBlob = await request.blob(); - const body = await bodyAsBlob.text(); - assert_equals(body, "", "Resolved value should be empty"); - assert_false(request.bodyUsed); -} - -function checkBodyArrayBuffer(test, request) { - return request.arrayBuffer().then(function(bodyAsArrayBuffer) { - assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); - assert_false(request.bodyUsed); - }); -} - -function checkBodyJSON(test, request) { - return request.json().then( - function(bodyAsJSON) { - assert_unreached("JSON parsing should fail"); - }, - function() { - assert_false(request.bodyUsed); - }); -} - -function checkBodyFormData(test, request) { - return request.formData().then(function(bodyAsFormData) { - assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); - assert_false(request.bodyUsed); - }); -} - -function checkBodyFormDataError(test, request) { - return promise_rejects_js(test, TypeError, request.formData()).then(function() { - assert_false(request.bodyUsed); - }); -} - -function checkRequestWithNoBody(bodyType, checkFunction, headers = []) { - promise_test(function(test) { - var request = new Request("", {"method": "POST", "headers": headers}); - assert_false(request.bodyUsed); - return checkFunction(test, request); - }, "Consume request's body as " + bodyType); -} - -checkRequestWithNoBody("text", checkBodyText); -checkRequestWithNoBody("blob", checkBodyBlob); -checkRequestWithNoBody("arrayBuffer", checkBodyArrayBuffer); -checkRequestWithNoBody("json (error case)", checkBodyJSON); -checkRequestWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); -checkRequestWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); -checkRequestWithNoBody("formData without correct type (error case)", checkBodyFormDataError); - -function checkRequestWithEmptyBody(bodyType, body, asText) { - promise_test(function(test) { - var request = new Request("", {"method": "POST", "body": body}); - assert_false(request.bodyUsed, "bodyUsed is false at init"); - if (asText) { - return request.text().then(function(bodyAsString) { - assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); - assert_true(request.bodyUsed, "bodyUsed is true after being consumed"); - }); - } - return request.arrayBuffer().then(function(bodyAsArrayBuffer) { - assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); - assert_true(request.bodyUsed, "bodyUsed is true after being consumed"); - }); - }, "Consume empty " + bodyType + " request body as " + (asText ? "text" : "arrayBuffer")); -} - -// FIXME: Add BufferSource, FormData and URLSearchParams. -checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); -checkRequestWithEmptyBody("text", "", false); -checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); -checkRequestWithEmptyBody("text", "", true); -checkRequestWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); -// FIXME: This test assumes that the empty string be returned but it is not clear whether that is right. See https://github.com/web-platform-tests/wpt/pull/3950. -checkRequestWithEmptyBody("FormData", new FormData(), true); -checkRequestWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/test/fixtures/wpt/fetch/api/request/request-consume.any.js b/test/fixtures/wpt/fetch/api/request/request-consume.any.js deleted file mode 100644 index b4cbe7457d2..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-consume.any.js +++ /dev/null @@ -1,148 +0,0 @@ -// META: global=window,worker -// META: title=Request consume -// META: script=../resources/utils.js - -function checkBodyText(request, expectedBody) { - return request.text().then(function(bodyAsText) { - assert_equals(bodyAsText, expectedBody, "Retrieve and verify request's body"); - assert_true(request.bodyUsed, "body as text: bodyUsed turned true"); - }); -} - -async function checkBodyBlob(request, expectedBody, checkContentType) { - const bodyAsBlob = await request.blob(); - - if (checkContentType) - assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type"); - - const body = await bodyAsBlob.text(); - assert_equals(body, expectedBody, "Retrieve and verify request's body"); - assert_true(request.bodyUsed, "body as blob: bodyUsed turned true"); -} - -function checkBodyArrayBuffer(request, expectedBody) { - return request.arrayBuffer().then(function(bodyAsArrayBuffer) { - validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify request's body"); - assert_true(request.bodyUsed, "body as arrayBuffer: bodyUsed turned true"); - }); -} - -function checkBodyBytes(request, expectedBody) { - return request.bytes().then(function(bodyAsUint8Array) { - assert_true(bodyAsUint8Array instanceof Uint8Array); - validateBufferFromString(bodyAsUint8Array.buffer, expectedBody, "Retrieve and verify request's body"); - assert_true(request.bodyUsed, "body as bytes: bodyUsed turned true"); - }); -} - -function checkBodyJSON(request, expectedBody) { - return request.json().then(function(bodyAsJSON) { - var strBody = JSON.stringify(bodyAsJSON) - assert_equals(strBody, expectedBody, "Retrieve and verify request's body"); - assert_true(request.bodyUsed, "body as json: bodyUsed turned true"); - }); -} - -function checkBodyFormData(request, expectedBody) { - return request.formData().then(function(bodyAsFormData) { - assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); - assert_true(request.bodyUsed, "body as formData: bodyUsed turned true"); - }); -} - -function checkRequestBody(body, expected, bodyType) { - promise_test(function(test) { - var request = new Request("", {"method": "POST", "body": body, "headers": [["Content-Type", "text/PLAIN"]] }); - assert_false(request.bodyUsed, "bodyUsed is false at init"); - return checkBodyText(request, expected); - }, "Consume " + bodyType + " request's body as text"); - promise_test(function(test) { - var request = new Request("", {"method": "POST", "body": body }); - assert_false(request.bodyUsed, "bodyUsed is false at init"); - return checkBodyBlob(request, expected); - }, "Consume " + bodyType + " request's body as blob"); - promise_test(function(test) { - var request = new Request("", {"method": "POST", "body": body }); - assert_false(request.bodyUsed, "bodyUsed is false at init"); - return checkBodyArrayBuffer(request, expected); - }, "Consume " + bodyType + " request's body as arrayBuffer"); - promise_test(function(test) { - var request = new Request("", {"method": "POST", "body": body }); - assert_false(request.bodyUsed, "bodyUsed is false at init"); - return checkBodyBytes(request, expected); - }, "Consume " + bodyType + " request's body as bytes"); - promise_test(function(test) { - var request = new Request("", {"method": "POST", "body": body }); - assert_false(request.bodyUsed, "bodyUsed is false at init"); - return checkBodyJSON(request, expected); - }, "Consume " + bodyType + " request's body as JSON"); -} - -var textData = JSON.stringify("This is response's body"); -var blob = new Blob([textData], { "type" : "text/plain" }); - -checkRequestBody(textData, textData, "String"); - -var string = "\"123456\""; -function getArrayBuffer() { - var arrayBuffer = new ArrayBuffer(8); - var int8Array = new Int8Array(arrayBuffer); - for (var cptr = 0; cptr < 8; cptr++) - int8Array[cptr] = string.charCodeAt(cptr); - return arrayBuffer; -} - -function getArrayBufferWithZeros() { - var arrayBuffer = new ArrayBuffer(10); - var int8Array = new Int8Array(arrayBuffer); - for (var cptr = 0; cptr < 8; cptr++) - int8Array[cptr + 1] = string.charCodeAt(cptr); - return arrayBuffer; -} - -checkRequestBody(getArrayBuffer(), string, "ArrayBuffer"); -checkRequestBody(new Uint8Array(getArrayBuffer()), string, "Uint8Array"); -checkRequestBody(new Int8Array(getArrayBufferWithZeros(), 1, 8), string, "Int8Array"); -checkRequestBody(new Float32Array(getArrayBuffer()), string, "Float32Array"); -checkRequestBody(new DataView(getArrayBufferWithZeros(), 1, 8), string, "DataView"); - -promise_test(function(test) { - var formData = new FormData(); - formData.append("name", "value") - var request = new Request("", {"method": "POST", "body": formData }); - assert_false(request.bodyUsed, "bodyUsed is false at init"); - return checkBodyFormData(request, formData); -}, "Consume FormData request's body as FormData"); - -function checkBlobResponseBody(blobBody, blobData, bodyType, checkFunction) { - promise_test(function(test) { - var response = new Response(blobBody); - assert_false(response.bodyUsed, "bodyUsed is false at init"); - return checkFunction(response, blobData); - }, "Consume blob response's body as " + bodyType); -} - -checkBlobResponseBody(blob, textData, "blob", checkBodyBlob); -checkBlobResponseBody(blob, textData, "text", checkBodyText); -checkBlobResponseBody(blob, textData, "json", checkBodyJSON); -checkBlobResponseBody(blob, textData, "arrayBuffer", checkBodyArrayBuffer); -checkBlobResponseBody(blob, textData, "bytes", checkBodyBytes); -checkBlobResponseBody(new Blob([""]), "", "blob (empty blob as input)", checkBodyBlob); - -var goodJSONValues = ["null", "1", "true", "\"string\""]; -goodJSONValues.forEach(function(value) { - promise_test(function(test) { - var request = new Request("", {"method": "POST", "body": value}); - return request.json().then(function(v) { - assert_equals(v, JSON.parse(value)); - }); - }, "Consume JSON from text: '" + JSON.stringify(value) + "'"); -}); - -var badJSONValues = ["undefined", "{", "a", "["]; -badJSONValues.forEach(function(value) { - promise_test(function(test) { - var request = new Request("", {"method": "POST", "body": value}); - return promise_rejects_js(test, SyntaxError, request.json()); - }, "Trying to consume bad JSON text as JSON: '" + value + "'"); -}); diff --git a/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js b/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js deleted file mode 100644 index 8a11de78ff6..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-disturbed.any.js +++ /dev/null @@ -1,109 +0,0 @@ -// META: global=window,worker -// META: title=Request disturbed -// META: script=../resources/utils.js - -var initValuesDict = {"method" : "POST", - "body" : "Request's body" -}; - -var noBodyConsumed = new Request(""); -var bodyConsumed = new Request("", initValuesDict); - -test(() => { - assert_equals(noBodyConsumed.body, null, "body's default value is null"); - assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed"); - assert_not_equals(bodyConsumed.body, null, "non-null body"); - assert_true(bodyConsumed.body instanceof ReadableStream, "non-null body type"); - assert_false(noBodyConsumed.bodyUsed, "bodyUsed is false when request is not disturbed"); -}, "Request's body: initial state"); - -noBodyConsumed.blob(); -bodyConsumed.blob(); - -test(function() { - assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed"); - try { - noBodyConsumed.clone(); - } catch (e) { - assert_unreached("Can use request not disturbed for creating or cloning request"); - } -}, "Request without body cannot be disturbed"); - -test(function() { - assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); - assert_throws_js(TypeError, function() { bodyConsumed.clone(); }); -}, "Check cloning a disturbed request"); - -test(function() { - assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); - assert_throws_js(TypeError, function() { new Request(bodyConsumed); }); -}, "Check creating a new request from a disturbed request"); - -promise_test(function() { - assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); - const originalBody = bodyConsumed.body; - const bodyReplaced = new Request(bodyConsumed, { body: "Replaced body" }); - assert_not_equals(bodyReplaced.body, originalBody, "new request's body is new"); - assert_false(bodyReplaced.bodyUsed, "bodyUsed is false when request is not disturbed"); - return bodyReplaced.text().then(text => { - assert_equals(text, "Replaced body"); - }); -}, "Check creating a new request with a new body from a disturbed request"); - -promise_test(function() { - var bodyRequest = new Request("", initValuesDict); - const originalBody = bodyRequest.body; - assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed"); - var requestFromRequest = new Request(bodyRequest); - assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed"); - assert_equals(bodyRequest.body, originalBody, "body should not change"); - assert_not_equals(originalBody, undefined, "body should not be undefined"); - assert_not_equals(originalBody, null, "body should not be null"); - assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new"); - return requestFromRequest.text().then(text => { - assert_equals(text, "Request's body"); - }); -}, "Input request used for creating new request became disturbed"); - -promise_test(() => { - const bodyRequest = new Request("", initValuesDict); - const originalBody = bodyRequest.body; - assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed"); - const requestFromRequest = new Request(bodyRequest, { body : "init body" }); - assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed"); - assert_equals(bodyRequest.body, originalBody, "body should not change"); - assert_not_equals(originalBody, undefined, "body should not be undefined"); - assert_not_equals(originalBody, null, "body should not be null"); - assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new"); - - return requestFromRequest.text().then(text => { - assert_equals(text, "init body"); - }); -}, "Input request used for creating new request became disturbed even if body is not used"); - -promise_test(function(test) { - assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed"); - return promise_rejects_js(test, TypeError, bodyConsumed.blob()); -}, "Check consuming a disturbed request"); - -test(function() { - var req = new Request(URL, {method: 'POST', body: 'hello'}); - assert_false(req.bodyUsed, - 'Request should not be flagged as used if it has not been ' + - 'consumed.'); - assert_throws_js(TypeError, - function() { new Request(req, {method: 'GET'}); }, - 'A get request may not have body.'); - - assert_false(req.bodyUsed, 'After the GET case'); - - assert_throws_js(TypeError, - function() { new Request(req, {method: 'CONNECT'}); }, - 'Request() with a forbidden method must throw.'); - - assert_false(req.bodyUsed, 'After the forbidden method case'); - - var req2 = new Request(req); - assert_true(req.bodyUsed, - 'Request should be flagged as used if it has been consumed.'); -}, 'Request construction failure should not set "bodyUsed"'); diff --git a/test/fixtures/wpt/fetch/api/request/request-error.any.js b/test/fixtures/wpt/fetch/api/request/request-error.any.js deleted file mode 100644 index 9ec8015198d..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-error.any.js +++ /dev/null @@ -1,56 +0,0 @@ -// META: global=window,worker -// META: title=Request error -// META: script=request-error.js - -// badRequestArgTests is from response-error.js -for (const { args, testName } of badRequestArgTests) { - test(() => { - assert_throws_js( - TypeError, - () => new Request(...args), - "Expect TypeError exception" - ); - }, testName); -} - -test(function() { - assert_throws_js( - TypeError, - () => Request("about:blank"), - "Calling Request constructor without 'new' must throw" - ); -}); - -test(function() { - var initialHeaders = new Headers([["Content-Type", "potato"]]); - var initialRequest = new Request("", {"headers" : initialHeaders}); - var request = new Request(initialRequest); - assert_equals(request.headers.get("Content-Type"), "potato"); -}, "Request should get its content-type from the init request"); - -test(function() { - var initialHeaders = new Headers([["Content-Type", "potato"]]); - var initialRequest = new Request("", {"headers" : initialHeaders}); - var headers = new Headers([]); - var request = new Request(initialRequest, {"headers" : headers}); - assert_false(request.headers.has("Content-Type")); -}, "Request should not get its content-type from the init request if init headers are provided"); - -test(function() { - var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]); - var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); - var request = new Request(initialRequest); - assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8"); -}, "Request should get its content-type from the body if none is provided"); - -test(function() { - var initialHeaders = new Headers([["Content-Type", "potato"]]); - var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); - var request = new Request(initialRequest); - assert_equals(request.headers.get("Content-Type"), "potato"); -}, "Request should get its content-type from init headers if one is provided"); - -test(function() { - var options = {"cache": "only-if-cached", "mode": "same-origin"}; - new Request("test", options); -}, "Request with cache mode: only-if-cached and fetch mode: same-origin"); diff --git a/test/fixtures/wpt/fetch/api/request/request-error.js b/test/fixtures/wpt/fetch/api/request/request-error.js deleted file mode 100644 index cf77313f5bc..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-error.js +++ /dev/null @@ -1,57 +0,0 @@ -const badRequestArgTests = [ - { - args: ["", { "window": "http://test.url" }], - testName: "RequestInit's window is not null" - }, - { - args: ["http://:not a valid URL"], - testName: "Input URL is not valid" - }, - { - args: ["http://user:pass@test.url"], - testName: "Input URL has credentials" - }, - { - args: ["", { "mode": "navigate" }], - testName: "RequestInit's mode is navigate" - }, - { - args: ["", { "referrer": "http://:not a valid URL" }], - testName: "RequestInit's referrer is invalid" - }, - { - args: ["", { "method": "IN VALID" }], - testName: "RequestInit's method is invalid" - }, - { - args: ["", { "method": "TRACE" }], - testName: "RequestInit's method is forbidden" - }, - { - args: ["", { "mode": "no-cors", "method": "PUT" }], - testName: "RequestInit's mode is no-cors and method is not simple" - }, - { - args: ["", { "mode": "cors", "cache": "only-if-cached" }], - testName: "RequestInit's cache mode is only-if-cached and mode is not same-origin" - }, - { - args: ["test", { "cache": "only-if-cached", "mode": "cors" }], - testName: "Request with cache mode: only-if-cached and fetch mode cors" - }, - { - args: ["test", { "cache": "only-if-cached", "mode": "no-cors" }], - testName: "Request with cache mode: only-if-cached and fetch mode no-cors" - } -]; - -badRequestArgTests.push( - ...["referrerPolicy", "mode", "credentials", "cache", "redirect"].map(optionProp => { - const options = {}; - options[optionProp] = "BAD"; - return { - args: ["", options], - testName: `Bad ${optionProp} init parameter value` - }; - }) -); diff --git a/test/fixtures/wpt/fetch/api/request/request-headers.any.js b/test/fixtures/wpt/fetch/api/request/request-headers.any.js deleted file mode 100644 index a766bcb5fff..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-headers.any.js +++ /dev/null @@ -1,177 +0,0 @@ -// META: global=window,worker -// META: title=Request Headers - -var validRequestHeaders = [ - ["Content-Type", "OK"], - ["Potato", "OK"], - ["proxy", "OK"], - ["proxya", "OK"], - ["sec", "OK"], - ["secb", "OK"], - ["Set-Cookie2", "OK"], - ["User-Agent", "OK"], -]; -var invalidRequestHeaders = [ - ["Accept-Charset", "KO"], - ["accept-charset", "KO"], - ["ACCEPT-ENCODING", "KO"], - ["Accept-Encoding", "KO"], - ["Access-Control-Request-Headers", "KO"], - ["Access-Control-Request-Method", "KO"], - ["Connection", "KO"], - ["Content-Length", "KO"], - ["Cookie", "KO"], - ["Cookie2", "KO"], - ["Date", "KO"], - ["DNT", "KO"], - ["Expect", "KO"], - ["Host", "KO"], - ["Keep-Alive", "KO"], - ["Origin", "KO"], - ["Referer", "KO"], - ["Set-Cookie", "KO"], - ["TE", "KO"], - ["Trailer", "KO"], - ["Transfer-Encoding", "KO"], - ["Upgrade", "KO"], - ["Via", "KO"], - ["Proxy-", "KO"], - ["proxy-a", "KO"], - ["Sec-", "KO"], - ["sec-b", "KO"], -]; - -var validRequestNoCorsHeaders = [ - ["Accept", "OK"], - ["Accept-Language", "OK"], - ["content-language", "OK"], - ["content-type", "application/x-www-form-urlencoded"], - ["content-type", "application/x-www-form-urlencoded;charset=UTF-8"], - ["content-type", "multipart/form-data"], - ["content-type", "multipart/form-data;charset=UTF-8"], - ["content-TYPE", "text/plain"], - ["CONTENT-type", "text/plain;charset=UTF-8"], -]; -var invalidRequestNoCorsHeaders = [ - ["Content-Type", "KO"], - ["Potato", "KO"], - ["proxy", "KO"], - ["proxya", "KO"], - ["sec", "KO"], - ["secb", "KO"], - ["Empty-Value", ""], -]; - -validRequestHeaders.forEach(function(header) { - test(function() { - var request = new Request(""); - request.headers.set(header[0], header[1]); - assert_equals(request.headers.get(header[0]), header[1]); - }, "Adding valid request header \"" + header[0] + ": " + header[1] + "\""); -}); -invalidRequestHeaders.forEach(function(header) { - test(function() { - var request = new Request(""); - request.headers.set(header[0], header[1]); - assert_equals(request.headers.get(header[0]), null); - }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\""); -}); - -validRequestNoCorsHeaders.forEach(function(header) { - test(function() { - var requestNoCors = new Request("", {"mode": "no-cors"}); - requestNoCors.headers.set(header[0], header[1]); - assert_equals(requestNoCors.headers.get(header[0]), header[1]); - }, "Adding valid no-cors request header \"" + header[0] + ": " + header[1] + "\""); -}); -invalidRequestNoCorsHeaders.forEach(function(header) { - test(function() { - var requestNoCors = new Request("", {"mode": "no-cors"}); - requestNoCors.headers.set(header[0], header[1]); - assert_equals(requestNoCors.headers.get(header[0]), null); - }, "Adding invalid no-cors request header \"" + header[0] + ": " + header[1] + "\""); -}); - -test(function() { - var headers = new Headers([["Cookie2", "potato"]]); - var request = new Request("", {"headers": headers}); - assert_equals(request.headers.get("Cookie2"), null); -}, "Check that request constructor is filtering headers provided as init parameter"); - -test(function() { - var headers = new Headers([["Content-Type", "potato"]]); - var request = new Request("", {"headers": headers, "mode": "no-cors"}); - assert_equals(request.headers.get("Content-Type"), null); -}, "Check that no-cors request constructor is filtering headers provided as init parameter"); - -test(function() { - var headers = new Headers([["Content-Type", "potato"]]); - var initialRequest = new Request("", {"headers": headers}); - var request = new Request(initialRequest, {"mode": "no-cors"}); - assert_equals(request.headers.get("Content-Type"), null); -}, "Check that no-cors request constructor is filtering headers provided as part of request parameter"); - -test(function() { - var initialHeaders = new Headers([["Content-Type", "potato"]]); - var initialRequest = new Request("", {"headers" : initialHeaders}); - var request = new Request(initialRequest); - assert_equals(request.headers.get("Content-Type"), "potato"); -}, "Request should get its content-type from the init request"); - -test(function() { - var initialHeaders = new Headers([["Content-Type", "potato"]]); - var initialRequest = new Request("", {"headers" : initialHeaders}); - var headers = new Headers([]); - var request = new Request(initialRequest, {"headers" : headers}); - assert_false(request.headers.has("Content-Type")); -}, "Request should not get its content-type from the init request if init headers are provided"); - -test(function() { - var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]); - var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); - var request = new Request(initialRequest); - assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8"); -}, "Request should get its content-type from the body if none is provided"); - -test(function() { - var initialHeaders = new Headers([["Content-Type", "potato"]]); - var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"}); - var request = new Request(initialRequest); - assert_equals(request.headers.get("Content-Type"), "potato"); -}, "Request should get its content-type from init headers if one is provided"); - -test(function() { - var array = [["hello", "worldAHH"]]; - var object = {"hello": 'worldOOH'}; - var headers = new Headers(array); - - assert_equals(headers.get("hello"), "worldAHH"); - - var request1 = new Request("", {"headers": headers}); - var request2 = new Request("", {"headers": array}); - var request3 = new Request("", {"headers": object}); - - assert_equals(request1.headers.get("hello"), "worldAHH"); - assert_equals(request2.headers.get("hello"), "worldAHH"); - assert_equals(request3.headers.get("hello"), "worldOOH"); -}, "Testing request header creations with various objects"); - -promise_test(function(test) { - var request = new Request("", {"headers" : [["Content-Type", ""]], "body" : "this is my plate", "method" : "POST"}); - return request.blob().then(function(blob) { - assert_equals(blob.type, "", "Blob type should be the empty string"); - }); -}, "Testing empty Request Content-Type header"); - -test(function() { - const request1 = new Request(""); - assert_equals(request1.headers, request1.headers); - - const request2 = new Request("", {"headers": {"X-Foo": "bar"}}); - assert_equals(request2.headers, request2.headers); - const headers = request2.headers; - request2.headers.set("X-Foo", "quux"); - assert_equals(headers, request2.headers); - headers.set("X-Other-Header", "baz"); - assert_equals(headers, request2.headers); -}, "Test that Request.headers has the [SameObject] extended attribute"); diff --git a/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html b/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html deleted file mode 100644 index cc495a66527..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-init-001.sub.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - Request init: simple cases - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/request-init-002.any.js b/test/fixtures/wpt/fetch/api/request/request-init-002.any.js deleted file mode 100644 index abb6689f1e8..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-init-002.any.js +++ /dev/null @@ -1,60 +0,0 @@ -// META: global=window,worker -// META: title=Request init: headers and body - -test(function() { - var headerDict = {"name1": "value1", - "name2": "value2", - "name3": "value3" - }; - var headers = new Headers(headerDict); - var request = new Request("", { "headers" : headers }) - for (var name in headerDict) { - assert_equals(request.headers.get(name), headerDict[name], - "request's headers has " + name + " : " + headerDict[name]); - } -}, "Initialize Request with headers values"); - -function makeRequestInit(body, method) { - return {"method": method, "body": body}; -} - -function checkRequestInit(body, bodyType, expectedTextBody) { - promise_test(function(test) { - var request = new Request("", makeRequestInit(body, "POST")); - if (body) { - assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "GET")); }); - assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "HEAD")); }); - } else { - new Request("", makeRequestInit(body, "GET")); // should not throw - } - var reqHeaders = request.headers; - var mime = reqHeaders.get("Content-Type"); - assert_true(!body || (mime && mime.search(bodyType) > -1), "Content-Type header should be \"" + bodyType + "\", not \"" + mime + "\""); - return request.text().then(function(bodyAsText) { - //not equals: cannot guess formData exact value - assert_true( bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify request body"); - }); - }, `Initialize Request's body with "${body}", ${bodyType}`); -} - -var blob = new Blob(["This is a blob"], {type: "application/octet-binary"}); -var formaData = new FormData(); -formaData.append("name", "value"); -var usvString = "This is a USVString" - -checkRequestInit(undefined, undefined, ""); -checkRequestInit(null, null, ""); -checkRequestInit(blob, "application/octet-binary", "This is a blob"); -checkRequestInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue"); -checkRequestInit(usvString, "text/plain;charset=UTF-8", "This is a USVString"); -checkRequestInit({toString: () => "hi!"}, "text/plain;charset=UTF-8", "hi!"); - -// Ensure test does not time out in case of missing URLSearchParams support. -if (self.URLSearchParams) { - var urlSearchParams = new URLSearchParams("name=value"); - checkRequestInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value"); -} else { - promise_test(function(test) { - return Promise.reject("URLSearchParams not supported"); - }, "Initialize Request's body with application/x-www-form-urlencoded;charset=UTF-8"); -} diff --git a/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html b/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html deleted file mode 100644 index 79c91cdfe82..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-init-003.sub.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - Request: init with request or url - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/request-init-contenttype.any.js b/test/fixtures/wpt/fetch/api/request/request-init-contenttype.any.js deleted file mode 100644 index 18a6969d4f8..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-init-contenttype.any.js +++ /dev/null @@ -1,141 +0,0 @@ -function requestFromBody(body) { - return new Request( - "https://example.com", - { - method: "POST", - body, - duplex: "half", - }, - ); -} - -test(() => { - const request = requestFromBody(undefined); - assert_equals(request.headers.get("Content-Type"), null); -}, "Default Content-Type for Request with empty body"); - -test(() => { - const blob = new Blob([]); - const request = requestFromBody(blob); - assert_equals(request.headers.get("Content-Type"), null); -}, "Default Content-Type for Request with Blob body (no type set)"); - -test(() => { - const blob = new Blob([], { type: "" }); - const request = requestFromBody(blob); - assert_equals(request.headers.get("Content-Type"), null); -}, "Default Content-Type for Request with Blob body (empty type)"); - -test(() => { - const blob = new Blob([], { type: "a/b; c=d" }); - const request = requestFromBody(blob); - assert_equals(request.headers.get("Content-Type"), "a/b; c=d"); -}, "Default Content-Type for Request with Blob body (set type)"); - -test(() => { - const buffer = new Uint8Array(); - const request = requestFromBody(buffer); - assert_equals(request.headers.get("Content-Type"), null); -}, "Default Content-Type for Request with buffer source body"); - -promise_test(async () => { - const formData = new FormData(); - formData.append("a", "b"); - const request = requestFromBody(formData); - const boundary = (await request.text()).split("\r\n")[0].slice(2); - assert_equals( - request.headers.get("Content-Type"), - `multipart/form-data; boundary=${boundary}`, - ); -}, "Default Content-Type for Request with FormData body"); - -test(() => { - const usp = new URLSearchParams(); - const request = requestFromBody(usp); - assert_equals( - request.headers.get("Content-Type"), - "application/x-www-form-urlencoded;charset=UTF-8", - ); -}, "Default Content-Type for Request with URLSearchParams body"); - -test(() => { - const request = requestFromBody(""); - assert_equals( - request.headers.get("Content-Type"), - "text/plain;charset=UTF-8", - ); -}, "Default Content-Type for Request with string body"); - -test(() => { - const stream = new ReadableStream(); - const request = requestFromBody(stream); - assert_equals(request.headers.get("Content-Type"), null); -}, "Default Content-Type for Request with ReadableStream body"); - -// ----------------------------------------------------------------------------- - -const OVERRIDE_MIME = "test/only; mime=type"; - -function requestFromBodyWithOverrideMime(body) { - return new Request( - "https://example.com", - { - method: "POST", - body, - headers: { "Content-Type": OVERRIDE_MIME }, - duplex: "half", - }, - ); -} - -test(() => { - const request = requestFromBodyWithOverrideMime(undefined); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with empty body"); - -test(() => { - const blob = new Blob([]); - const request = requestFromBodyWithOverrideMime(blob); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with Blob body (no type set)"); - -test(() => { - const blob = new Blob([], { type: "" }); - const request = requestFromBodyWithOverrideMime(blob); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with Blob body (empty type)"); - -test(() => { - const blob = new Blob([], { type: "a/b; c=d" }); - const request = requestFromBodyWithOverrideMime(blob); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with Blob body (set type)"); - -test(() => { - const buffer = new Uint8Array(); - const request = requestFromBodyWithOverrideMime(buffer); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with buffer source body"); - -test(() => { - const formData = new FormData(); - const request = requestFromBodyWithOverrideMime(formData); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with FormData body"); - -test(() => { - const usp = new URLSearchParams(); - const request = requestFromBodyWithOverrideMime(usp); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with URLSearchParams body"); - -test(() => { - const request = requestFromBodyWithOverrideMime(""); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with string body"); - -test(() => { - const stream = new ReadableStream(); - const request = requestFromBodyWithOverrideMime(stream); - assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Request with ReadableStream body"); diff --git a/test/fixtures/wpt/fetch/api/request/request-init-priority.any.js b/test/fixtures/wpt/fetch/api/request/request-init-priority.any.js deleted file mode 100644 index eb5073c8578..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-init-priority.any.js +++ /dev/null @@ -1,26 +0,0 @@ -var priorities = ["high", - "low", - "auto" - ]; - -for (idx in priorities) { - test(() => { - new Request("", {priority: priorities[idx]}); - }, "new Request() with a '" + priorities[idx] + "' priority does not throw an error"); -} - -test(() => { - assert_throws_js(TypeError, () => { - new Request("", {priority: 'invalid'}); - }, "a new Request() must throw a TypeError if RequestInit's priority is an invalid value"); -}, "new Request() throws a TypeError if any of RequestInit's members' values are invalid"); - -for (idx in priorities) { - promise_test(function(t) { - return fetch('hello.txt', { priority: priorities[idx] }); - }, "fetch() with a '" + priorities[idx] + "' priority completes successfully"); -} - -promise_test(function(t) { - return promise_rejects_js(t, TypeError, fetch('hello.txt', { priority: 'invalid' })); -}, "fetch() with an invalid priority returns a rejected promise with a TypeError"); diff --git a/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js b/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js deleted file mode 100644 index f0ae441a002..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-init-stream.any.js +++ /dev/null @@ -1,147 +0,0 @@ -// META: global=window,worker - -"use strict"; - -const duplex = "half"; -const method = "POST"; - -test(() => { - const body = new ReadableStream(); - const request = new Request("...", { method, body, duplex }); - assert_equals(request.body, body); -}, "Constructing a Request with a stream holds the original object."); - -test((t) => { - const body = new ReadableStream(); - body.getReader(); - assert_throws_js(TypeError, - () => new Request("...", { method, body, duplex })); -}, "Constructing a Request with a stream on which getReader() is called"); - -test((t) => { - const body = new ReadableStream(); - body.getReader().read(); - assert_throws_js(TypeError, - () => new Request("...", { method, body, duplex })); -}, "Constructing a Request with a stream on which read() is called"); - -promise_test(async (t) => { - const body = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }); - const reader = body.getReader(); - await reader.read(); - reader.releaseLock(); - assert_throws_js(TypeError, - () => new Request("...", { method, body, duplex })); -}, "Constructing a Request with a stream on which read() and releaseLock() are called"); - -test((t) => { - const request = new Request("...", { method: "POST", body: "..." }); - request.body.getReader(); - assert_throws_js(TypeError, () => new Request(request)); - // This doesn't throw. - new Request(request, { body: "..." }); -}, "Constructing a Request with a Request on which body.getReader() is called"); - -test((t) => { - const request = new Request("...", { method: "POST", body: "..." }); - request.body.getReader().read(); - assert_throws_js(TypeError, () => new Request(request)); - // This doesn't throw. - new Request(request, { body: "..." }); -}, "Constructing a Request with a Request on which body.getReader().read() is called"); - -promise_test(async (t) => { - const request = new Request("...", { method: "POST", body: "..." }); - const reader = request.body.getReader(); - await reader.read(); - reader.releaseLock(); - assert_throws_js(TypeError, () => new Request(request)); - // This doesn't throw. - new Request(request, { body: "..." }); -}, "Constructing a Request with a Request on which read() and releaseLock() are called"); - -test((t) => { - new Request("...", { method, body: null }); -}, "It is OK to omit .duplex when the body is null."); - -test((t) => { - new Request("...", { method, body: "..." }); -}, "It is OK to omit .duplex when the body is a string."); - -test((t) => { - new Request("...", { method, body: new Uint8Array(3) }); -}, "It is OK to omit .duplex when the body is a Uint8Array."); - -test((t) => { - new Request("...", { method, body: new Blob([]) }); -}, "It is OK to omit .duplex when the body is a Blob."); - -test((t) => { - const body = new ReadableStream(); - assert_throws_js(TypeError, - () => new Request("...", { method, body })); -}, "It is error to omit .duplex when the body is a ReadableStream."); - -test((t) => { - new Request("...", { method, body: null, duplex: "half" }); -}, "It is OK to set .duplex = 'half' when the body is null."); - -test((t) => { - new Request("...", { method, body: "...", duplex: "half" }); -}, "It is OK to set .duplex = 'half' when the body is a string."); - -test((t) => { - new Request("...", { method, body: new Uint8Array(3), duplex: "half" }); -}, "It is OK to set .duplex = 'half' when the body is a Uint8Array."); - -test((t) => { - new Request("...", { method, body: new Blob([]), duplex: "half" }); -}, "It is OK to set .duplex = 'half' when the body is a Blob."); - -test((t) => { - const body = new ReadableStream(); - new Request("...", { method, body, duplex: "half" }); -}, "It is OK to set .duplex = 'half' when the body is a ReadableStream."); - -test((t) => { - const body = null; - const duplex = "full"; - assert_throws_js(TypeError, - () => new Request("...", { method, body, duplex })); -}, "It is error to set .duplex = 'full' when the body is null."); - -test((t) => { - const body = "..."; - const duplex = "full"; - assert_throws_js(TypeError, - () => new Request("...", { method, body, duplex })); -}, "It is error to set .duplex = 'full' when the body is a string."); - -test((t) => { - const body = new Uint8Array(3); - const duplex = "full"; - assert_throws_js(TypeError, - () => new Request("...", { method, body, duplex })); -}, "It is error to set .duplex = 'full' when the body is a Uint8Array."); - -test((t) => { - const body = new Blob([]); - const duplex = "full"; - assert_throws_js(TypeError, - () => new Request("...", { method, body, duplex })); -}, "It is error to set .duplex = 'full' when the body is a Blob."); - -test((t) => { - const body = new ReadableStream(); - const duplex = "full"; - assert_throws_js(TypeError, - () => new Request("...", { method, body, duplex })); -}, "It is error to set .duplex = 'full' when the body is a ReadableStream."); - -test((t) => { - const body = new ReadableStream(); - const duplex = "half"; - const req1 = new Request("...", { method, body, duplex }); - const req2 = new Request(req1); -}, "It is OK to omit duplex when init.body is not given and input.body is given."); - diff --git a/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html b/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html deleted file mode 100644 index 548ab38d7e1..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-keepalive-quota.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - Request Keepalive Quota Tests - - - - - - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js b/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js deleted file mode 100644 index cb4506db46c..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-keepalive.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// META: global=window,worker -// META: title=Request keepalive -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js - -test(() => { - assert_false(new Request('/').keepalive, 'default'); - assert_true(new Request('/', {keepalive: true}).keepalive, 'true'); - assert_false(new Request('/', {keepalive: false}).keepalive, 'false'); - assert_true(new Request('/', {keepalive: 1}).keepalive, 'truish'); - assert_false(new Request('/', {keepalive: 0}).keepalive, 'falsy'); -}, 'keepalive flag'); - -test(() => { - const init = {method: 'POST', keepalive: true, body: new ReadableStream()}; - assert_throws_js(TypeError, () => {new Request('/', init)}); -}, 'keepalive flag with stream body'); diff --git a/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html b/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html deleted file mode 100644 index 7be3608d737..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-reset-attributes.https.html +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/request/request-structure.any.js b/test/fixtures/wpt/fetch/api/request/request-structure.any.js deleted file mode 100644 index 5e785538555..00000000000 --- a/test/fixtures/wpt/fetch/api/request/request-structure.any.js +++ /dev/null @@ -1,143 +0,0 @@ -// META: global=window,worker -// META: title=Request structure - -var request = new Request(""); -var methods = ["clone", - //Request implements Body - "arrayBuffer", - "blob", - "formData", - "json", - "text" - ]; -var attributes = ["method", - "url", - "headers", - "destination", - "referrer", - "referrerPolicy", - "mode", - "credentials", - "cache", - "redirect", - "integrity", - "isReloadNavigation", - "isHistoryNavigation", - "duplex", - //Request implements Body - "bodyUsed" - ]; -var internalAttributes = ["priority", - "internalpriority", - "blocking" - ]; - -function isReadOnly(request, attributeToCheck) { - var defaultValue = undefined; - var newValue = undefined; - switch (attributeToCheck) { - case "method": - defaultValue = "GET"; - newValue = "POST"; - break; - - case "url": - //default value is base url - //i.e http://example.com/fetch/api/request-structure.html - newValue = "http://url.test"; - break; - - case "headers": - request.headers = new Headers ({"name":"value"}); - assert_false(request.headers.has("name"), "Headers attribute is read only"); - return; - - case "destination": - defaultValue = ""; - newValue = "worker"; - break; - - case "referrer": - defaultValue = "about:client"; - newValue = "http://url.test"; - break; - - case "referrerPolicy": - defaultValue = ""; - newValue = "unsafe-url"; - break; - - case "mode": - defaultValue = "cors"; - newValue = "navigate"; - break; - - case "credentials": - defaultValue = "same-origin"; - newValue = "cors"; - break; - - case "cache": - defaultValue = "default"; - newValue = "reload"; - break; - - case "redirect": - defaultValue = "follow"; - newValue = "manual"; - break; - - case "integrity": - newValue = "CannotWriteIntegrity"; - break; - - case "bodyUsed": - defaultValue = false; - newValue = true; - break; - - case "isReloadNavigation": - defaultValue = false; - newValue = true; - break; - - case "isHistoryNavigation": - defaultValue = false; - newValue = true; - break; - - case "duplex": - defaultValue = "half"; - newValue = "full"; - break; - - default: - return; - } - - request[attributeToCheck] = newValue; - if (defaultValue === undefined) - assert_not_equals(request[attributeToCheck], newValue, "Attribute " + attributeToCheck + " is read only"); - else - assert_equals(request[attributeToCheck], defaultValue, - "Attribute " + attributeToCheck + " is read only. Default value is " + defaultValue); -} - -for (var idx in methods) { - test(function() { - assert_true(methods[idx] in request, "request has " + methods[idx] + " method"); - }, "Request has " + methods[idx] + " method"); -} - -for (var idx in attributes) { - test(function() { - assert_true(attributes[idx] in request, "request has " + attributes[idx] + " attribute"); - isReadOnly(request, attributes[idx]); - }, "Check " + attributes[idx] + " attribute"); -} - -for (var idx in internalAttributes) { - test(function() { - assert_false(internalAttributes[idx] in request, "request does not expose " + internalAttributes[idx] + " attribute"); - }, "Request does not expose " + internalAttributes[idx] + " attribute"); -} \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/request/resources/cache.py b/test/fixtures/wpt/fetch/api/request/resources/cache.py deleted file mode 100644 index ca0bd644b4f..00000000000 --- a/test/fixtures/wpt/fetch/api/request/resources/cache.py +++ /dev/null @@ -1,67 +0,0 @@ -from wptserve.utils import isomorphic_decode - -def main(request, response): - token = request.GET.first(b"token", None) - if b"querystate" in request.GET: - from json import JSONEncoder - response.headers.set(b"Content-Type", b"text/plain") - return JSONEncoder().encode(request.server.stash.take(token)) - content = request.GET.first(b"content", None) - tag = request.GET.first(b"tag", None) - date = request.GET.first(b"date", None) - expires = request.GET.first(b"expires", None) - vary = request.GET.first(b"vary", None) - cc = request.GET.first(b"cache_control", None) - redirect = request.GET.first(b"redirect", None) - inm = request.headers.get(b"If-None-Match", None) - ims = request.headers.get(b"If-Modified-Since", None) - pragma = request.headers.get(b"Pragma", None) - cache_control = request.headers.get(b"Cache-Control", None) - ignore = b"ignore" in request.GET - - if tag: - tag = b'"%s"' % tag - - server_state = request.server.stash.take(token) - if not server_state: - server_state = [] - state = dict() - if not ignore: - if inm: - state[u"If-None-Match"] = isomorphic_decode(inm) - if ims: - state[u"If-Modified-Since"] = isomorphic_decode(ims) - if pragma: - state[u"Pragma"] = isomorphic_decode(pragma) - if cache_control: - state[u"Cache-Control"] = isomorphic_decode(cache_control) - server_state.append(state) - request.server.stash.put(token, server_state) - - if tag: - response.headers.set(b"ETag", b'%s' % tag) - elif date: - response.headers.set(b"Last-Modified", date) - if expires: - response.headers.set(b"Expires", expires) - if vary: - response.headers.set(b"Vary", vary) - if cc: - response.headers.set(b"Cache-Control", cc) - - # The only-if-cached redirect tests wants CORS to be okay, the other tests - # are all same-origin anyways and don't care. - response.headers.set(b"Access-Control-Allow-Origin", b"*") - - if redirect: - response.headers.set(b"Location", redirect) - response.status = (302, b"Redirect") - return b"" - elif ((inm is not None and inm == tag) or - (ims is not None and ims == date)): - response.status = (304, b"Not Modified") - return b"" - else: - response.status = (200, b"OK") - response.headers.set(b"Content-Type", b"text/plain") - return content diff --git a/test/fixtures/wpt/fetch/api/request/resources/hello.txt b/test/fixtures/wpt/fetch/api/request/resources/hello.txt deleted file mode 100644 index ce013625030..00000000000 --- a/test/fixtures/wpt/fetch/api/request/resources/hello.txt +++ /dev/null @@ -1 +0,0 @@ -hello diff --git a/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js b/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js deleted file mode 100644 index 4b264ca2fec..00000000000 --- a/test/fixtures/wpt/fetch/api/request/resources/request-reset-attributes-worker.js +++ /dev/null @@ -1,19 +0,0 @@ -self.addEventListener('fetch', (event) => { - const params = new URL(event.request.url).searchParams; - if (params.has('ignore')) { - return; - } - if (!params.has('name')) { - event.respondWith(Promise.reject(TypeError('No name is provided.'))); - return; - } - - const name = params.get('name'); - const old_attribute = event.request[name]; - // If any of |init|'s member is present... - const init = {cache: 'no-store'} - const new_attribute = (new Request(event.request, init))[name]; - - event.respondWith( - new Response(`old: ${old_attribute}, new: ${new_attribute}`)); - }); diff --git a/test/fixtures/wpt/fetch/api/request/url-encoding.html b/test/fixtures/wpt/fetch/api/request/url-encoding.html deleted file mode 100644 index 31c1ed3920b..00000000000 --- a/test/fixtures/wpt/fetch/api/request/url-encoding.html +++ /dev/null @@ -1,25 +0,0 @@ - - -Fetch: URL encoding - - - diff --git a/test/fixtures/wpt/fetch/api/resources/authentication.py b/test/fixtures/wpt/fetch/api/resources/authentication.py deleted file mode 100644 index 8b6b00b0873..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/authentication.py +++ /dev/null @@ -1,14 +0,0 @@ -def main(request, response): - user = request.auth.username - password = request.auth.password - - if user == b"user" and password == b"password": - return b"Authentication done" - - realm = b"test" - if b"realm" in request.GET: - realm = request.GET.first(b"realm") - - return ((401, b"Unauthorized"), - [(b"WWW-Authenticate", b'Basic realm="' + realm + b'"')], - b"Please login with credentials 'user' and 'password'") diff --git a/test/fixtures/wpt/fetch/api/resources/bad-chunk-encoding.py b/test/fixtures/wpt/fetch/api/resources/bad-chunk-encoding.py deleted file mode 100644 index 94a77adead8..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/bad-chunk-encoding.py +++ /dev/null @@ -1,13 +0,0 @@ -import time - -def main(request, response): - delay = float(request.GET.first(b"ms", 1000)) / 1E3 - count = int(request.GET.first(b"count", 50)) - time.sleep(delay) - response.headers.set(b"Transfer-Encoding", b"chunked") - response.write_status_headers() - time.sleep(delay) - for i in range(count): - response.writer.write_content(b"a\r\nTEST_CHUNK\r\n") - time.sleep(delay) - response.writer.write_content(b"garbage") diff --git a/test/fixtures/wpt/fetch/api/resources/basic.html b/test/fixtures/wpt/fetch/api/resources/basic.html deleted file mode 100644 index e23afd4bf6a..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/basic.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/test/fixtures/wpt/fetch/api/resources/cache.py b/test/fixtures/wpt/fetch/api/resources/cache.py deleted file mode 100644 index 4de751e30bf..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/cache.py +++ /dev/null @@ -1,18 +0,0 @@ -ETAG = b'"123abc"' -CONTENT_TYPE = b"text/plain" -CONTENT = b"lorem ipsum dolor sit amet" - - -def main(request, response): - # let caching kick in if possible (conditional GET) - etag = request.headers.get(b"If-None-Match", None) - if etag == ETAG: - response.headers.set(b"X-HTTP-STATUS", 304) - response.status = (304, b"Not Modified") - return b"" - - # cache miss, so respond with the actual content - response.status = (200, b"OK") - response.headers.set(b"ETag", ETAG) - response.headers.set(b"Content-Type", CONTENT_TYPE) - return CONTENT diff --git a/test/fixtures/wpt/fetch/api/resources/clean-stash.py b/test/fixtures/wpt/fetch/api/resources/clean-stash.py deleted file mode 100644 index ee8c69ac446..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/clean-stash.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(request, response): - token = request.GET.first(b"token") - if request.server.stash.take(token) is not None: - return b"1" - else: - return b"0" diff --git a/test/fixtures/wpt/fetch/api/resources/cors-top.txt b/test/fixtures/wpt/fetch/api/resources/cors-top.txt deleted file mode 100644 index 83a3157d14d..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/cors-top.txt +++ /dev/null @@ -1 +0,0 @@ -top \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers b/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers deleted file mode 100644 index cb762eff806..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/cors-top.txt.headers +++ /dev/null @@ -1 +0,0 @@ -Access-Control-Allow-Origin: * diff --git a/test/fixtures/wpt/fetch/api/resources/data.json b/test/fixtures/wpt/fetch/api/resources/data.json deleted file mode 100644 index 76519fa8cc2..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/data.json +++ /dev/null @@ -1 +0,0 @@ -{"key": "value"} diff --git a/test/fixtures/wpt/fetch/api/resources/dump-authorization-header.py b/test/fixtures/wpt/fetch/api/resources/dump-authorization-header.py deleted file mode 100644 index 0d82809f59d..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/dump-authorization-header.py +++ /dev/null @@ -1,19 +0,0 @@ -def main(request, response): - headers = [(b"Content-Type", "text/html"), - (b"Cache-Control", b"no-cache")] - - if (request.GET.first(b"strip_auth_header", False) and request.method == "OPTIONS" and - b"authorization" in request.headers.get(b"Access-Control-Request-Headers", b"").lower()): - # Auth header should not be sent for preflight after cross-origin redirect. - return 500, headers, "fail" - - if b"Origin" in request.headers: - headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) - headers.append((b"Access-Control-Allow-Credentials", b"true")) - else: - headers.append((b"Access-Control-Allow-Origin", b"*")) - headers.append((b"Access-Control-Allow-Headers", b'Authorization')) - - if b"authorization" in request.headers: - return 200, headers, request.headers.get(b"Authorization") - return 200, headers, "none" diff --git a/test/fixtures/wpt/fetch/api/resources/echo-content.h2.py b/test/fixtures/wpt/fetch/api/resources/echo-content.h2.py deleted file mode 100644 index 0be3ece4a5f..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/echo-content.h2.py +++ /dev/null @@ -1,7 +0,0 @@ -def handle_headers(frame, request, response): - response.status = 200 - response.headers.update([('Content-Type', 'text/plain')]) - response.write_status_headers() - -def handle_data(frame, request, response): - response.writer.write_data(frame.data) diff --git a/test/fixtures/wpt/fetch/api/resources/echo-content.py b/test/fixtures/wpt/fetch/api/resources/echo-content.py deleted file mode 100644 index 5e137e15d7d..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/echo-content.py +++ /dev/null @@ -1,12 +0,0 @@ -from wptserve.utils import isomorphic_encode - -def main(request, response): - - headers = [(b"X-Request-Method", isomorphic_encode(request.method)), - (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")), - (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")), - # Avoid any kind of content sniffing on the response. - (b"Content-Type", b"text/plain")] - content = request.body - - return headers, content diff --git a/test/fixtures/wpt/fetch/api/resources/empty.txt b/test/fixtures/wpt/fetch/api/resources/empty.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/fetch/api/resources/huge-response.py b/test/fixtures/wpt/fetch/api/resources/huge-response.py deleted file mode 100644 index 16a60078e53..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/huge-response.py +++ /dev/null @@ -1,22 +0,0 @@ -# A Python script that generates a huge response. Implemented as a script to -# avoid needing to add a huge file to the repository. - -TOTAL_SIZE = 8 * 1024 * 1024 * 1024 # 8 GB -CHUNK_SIZE = 1024 * 1024 # 1 MB - -assert TOTAL_SIZE % CHUNK_SIZE == 0 - - -def main(request, response): - response.headers.set(b"Content-type", b"text/plain") - response.headers.set(b"Content-Length", str(TOTAL_SIZE).encode()) - response.headers.set(b"Cache-Control", b"max-age=86400") - response.write_status_headers() - - chunk = bytes(CHUNK_SIZE) - total_sent = 0 - - while total_sent < TOTAL_SIZE: - if not response.writer.write(chunk): - break - total_sent += CHUNK_SIZE diff --git a/test/fixtures/wpt/fetch/api/resources/infinite-slow-response.py b/test/fixtures/wpt/fetch/api/resources/infinite-slow-response.py deleted file mode 100644 index a26cd8064c8..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/infinite-slow-response.py +++ /dev/null @@ -1,35 +0,0 @@ -import time - - -def url_dir(request): - return u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' - - -def stash_write(request, key, value): - """Write to the stash, overwriting any previous value""" - request.server.stash.take(key, url_dir(request)) - request.server.stash.put(key, value, url_dir(request)) - - -def main(request, response): - stateKey = request.GET.first(b"stateKey", b"") - abortKey = request.GET.first(b"abortKey", b"") - - if stateKey: - stash_write(request, stateKey, 'open') - - response.headers.set(b"Content-type", b"text/plain") - response.write_status_headers() - - # Writing an initial 2k so browsers realise it's there. *shrug* - response.writer.write(b"." * 2048) - - while True: - if not response.writer.write(b"."): - break - if abortKey and request.server.stash.take(abortKey, url_dir(request)): - break - time.sleep(0.01) - - if stateKey: - stash_write(request, stateKey, 'closed') diff --git a/test/fixtures/wpt/fetch/api/resources/inspect-headers.py b/test/fixtures/wpt/fetch/api/resources/inspect-headers.py deleted file mode 100644 index 9ed566e607b..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/inspect-headers.py +++ /dev/null @@ -1,24 +0,0 @@ -def main(request, response): - headers = [] - if b"headers" in request.GET: - checked_headers = request.GET.first(b"headers").split(b"|") - for header in checked_headers: - if header in request.headers: - headers.append((b"x-request-" + header, request.headers.get(header, b""))) - - if b"cors" in request.GET: - if b"Origin" in request.headers: - headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) - else: - headers.append((b"Access-Control-Allow-Origin", b"*")) - headers.append((b"Access-Control-Allow-Credentials", b"true")) - headers.append((b"Access-Control-Allow-Methods", b"GET, POST, HEAD")) - exposed_headers = [b"x-request-" + header for header in checked_headers] - headers.append((b"Access-Control-Expose-Headers", b", ".join(exposed_headers))) - if b"allow_headers" in request.GET: - headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) - else: - headers.append((b"Access-Control-Allow-Headers", b", ".join(request.headers))) - - headers.append((b"content-type", b"text/plain")) - return headers, b"" diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-helper.js b/test/fixtures/wpt/fetch/api/resources/keepalive-helper.js deleted file mode 100644 index ad0e9bfa06c..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/keepalive-helper.js +++ /dev/null @@ -1,199 +0,0 @@ -// Utility functions to help testing keepalive requests. - -// Returns a URL to an iframe that loads a keepalive URL on iframe loaded. -// -// The keepalive URL points to a target that stores `token`. The token will then -// be posted back on iframe loaded to the parent document. -// `method` defaults to GET. -// `frameOrigin` to specify the origin of the iframe to load. If not set, -// default to a different site origin. -// `requestOrigin` to specify the origin of the fetch request target. -// `sendOn` to specify the name of the event when the keepalive request should -// be sent instead of the default 'load'. -// `mode` to specify the fetch request's CORS mode. -// `disallowCrossOrigin` to ask the iframe to set up a server that disallows -// cross origin requests. -function getKeepAliveIframeUrl(token, method, { - frameOrigin = 'DEFAULT', - requestOrigin = '', - sendOn = 'load', - mode = 'cors', - disallowCrossOrigin = false -} = {}) { - const https = location.protocol.startsWith('https'); - frameOrigin = frameOrigin === 'DEFAULT' ? - get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'] : - frameOrigin; - return `${frameOrigin}/fetch/api/resources/keepalive-iframe.html?` + - `token=${token}&` + - `method=${method}&` + - `sendOn=${sendOn}&` + - `mode=${mode}&` + (disallowCrossOrigin ? `disallowCrossOrigin=1&` : ``) + - `origin=${requestOrigin}`; -} - -// Returns a different-site URL to an iframe that loads a keepalive URL. -// -// By default, the keepalive URL points to a target that redirects to another -// same-origin destination storing `token`. The token will then be posted back -// to parent document. -// -// The URL redirects can be customized from `origin1` to `origin2` if provided. -// Sets `withPreflight` to true to get URL enabling preflight. -function getKeepAliveAndRedirectIframeUrl( - token, origin1, origin2, withPreflight) { - const https = location.protocol.startsWith('https'); - const frameOrigin = - get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN']; - return `${frameOrigin}/fetch/api/resources/keepalive-redirect-iframe.html?` + - `token=${token}&` + - `origin1=${origin1}&` + - `origin2=${origin2}&` + (withPreflight ? `with-headers` : ``); -} - -async function iframeLoaded(iframe) { - return new Promise((resolve) => iframe.addEventListener('load', resolve)); -} - -// Obtains the token from the message posted by iframe after loading -// `getKeepAliveAndRedirectIframeUrl()`. -async function getTokenFromMessage() { - return new Promise((resolve) => { - window.addEventListener('message', (event) => { - resolve(event.data); - }, {once: true}); - }); -} - -// Tells if `token` has been stored in the server. -async function queryToken(token) { - const response = await fetch(`../resources/stash-take.py?key=${token}`); - const json = await response.json(); - return json; -} - -// A helper to assert the existence of `token` that should have been stored in -// the server by fetching ../resources/stash-put.py. -// -// This function simply wait for a custom amount of time before trying to -// retrieve `token` from the server. -// `expectTokenExist` tells if `token` should be present or not. -// -// NOTE: -// In order to parallelize the work, we are going to have an async_test -// for the rest of the work. Note that we want the serialized behavior -// for the steps so far, so we don't want to make the entire test case -// an async_test. -function assertStashedTokenAsync( - testName, token, {expectTokenExist = true} = {}) { - async_test(test => { - new Promise(resolve => test.step_timeout(resolve, 3000 /*ms*/)) - .then(test.step_func(() => { - return queryToken(token); - })) - .then(test.step_func(result => { - if (expectTokenExist) { - assert_equals(result, 'on', `token should be on (stashed).`); - test.done(); - } else { - assert_not_equals( - result, 'on', `token should not be on (stashed).`); - return Promise.reject(`Failed to retrieve token from server`); - } - })) - .catch(test.step_func(e => { - if (expectTokenExist) { - test.unreached_func(e); - } else { - test.done(); - } - })); - }, testName); -} - -/** - * In an iframe, and in `load` event handler, test to fetch a keepalive URL that - * involves in redirect to another URL. - * - * `unloadIframe` to unload the iframe before verifying stashed token to - * simulate the situation that unloads after fetching. Note that this test is - * different from `keepaliveRedirectInUnloadTest()` in that the latter - * performs fetch() call directly in `unload` event handler, while this test - * does it in `load`. - */ -function keepaliveRedirectTest(desc, { - origin1 = '', - origin2 = '', - withPreflight = false, - unloadIframe = false, - expectFetchSucceed = true, -} = {}) { - desc = `[keepalive][iframe][load] ${desc}` + - (unloadIframe ? ' [unload at end]' : ''); - promise_test(async (test) => { - const tokenToStash = token(); - const iframe = document.createElement('iframe'); - iframe.src = getKeepAliveAndRedirectIframeUrl( - tokenToStash, origin1, origin2, withPreflight); - document.body.appendChild(iframe); - await iframeLoaded(iframe); - assert_equals(await getTokenFromMessage(), tokenToStash); - if (unloadIframe) { - iframe.remove(); - } - - assertStashedTokenAsync( - desc, tokenToStash, {expectTokenExist: expectFetchSucceed}); - }, `${desc}; setting up`); -} - -/** - * Opens a different site window, and in `unload` event handler, test to fetch - * a keepalive URL that involves in redirect to another URL. - */ -function keepaliveRedirectInUnloadTest(desc, { - origin1 = '', - origin2 = '', - url2 = '', - withPreflight = false, - expectFetchSucceed = true -} = {}) { - desc = `[keepalive][new window][unload] ${desc}`; - - promise_test(async (test) => { - const targetUrl = - `${HTTP_NOTSAMESITE_ORIGIN}/fetch/api/resources/keepalive-redirect-window.html?` + - `origin1=${origin1}&` + - `origin2=${origin2}&` + - `url2=${url2}&` + (withPreflight ? `with-headers` : ``); - const w = window.open(targetUrl); - const token = await getTokenFromMessage(); - w.close(); - - assertStashedTokenAsync( - desc, token, {expectTokenExist: expectFetchSucceed}); - }, `${desc}; setting up`); -} - -/** -* utility to create pending keepalive fetch requests -* The pending request state is achieved by ensuring the server (trickle.py) does not -* immediately respond to the fetch requests. -* The response delay is set as a url parameter. -*/ - -function createPendingKeepAliveRequest(delay, remote = false) { - // trickle.py is a script that can make a delayed response to the client request - const trickleRemoteURL = get_host_info().HTTPS_REMOTE_ORIGIN + '/fetch/api/resources/trickle.py?count=1&ms='; - const trickleLocalURL = get_host_info().HTTP_ORIGIN + '/fetch/api/resources/trickle.py?count=1&ms='; - url = remote ? trickleRemoteURL : trickleLocalURL; - - const body = '*'.repeat(10); - return fetch(url + delay, { keepalive: true, body, method: 'POST' }).then(res => { - return res.text(); - }).then(() => { - return new Promise(resolve => step_timeout(resolve, 1)); - }).catch((error) => { - return Promise.reject(error);; - }) -} diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html b/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html deleted file mode 100644 index f9dae5a34ec..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/keepalive-iframe.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-iframe.html b/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-iframe.html deleted file mode 100644 index fdee00f3124..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-iframe.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-window.html b/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-window.html deleted file mode 100644 index c18650796cc..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/keepalive-redirect-window.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/resources/keepalive-worker.js b/test/fixtures/wpt/fetch/api/resources/keepalive-worker.js deleted file mode 100644 index 0808601d0d9..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/keepalive-worker.js +++ /dev/null @@ -1,15 +0,0 @@ -/** -* Script that sends keepalive -* fetch request and terminates immediately. -* The request URL is passed as a parameter to this worker -*/ -function sendFetchRequest() { - // Parse the query parameter from the worker's script URL - const urlString = self.location.search.replace("?param=", ""); - postMessage('started'); - fetch(`${urlString}`, { keepalive: true }); -} - -sendFetchRequest(); -// Terminate the worker -self.close(); diff --git a/test/fixtures/wpt/fetch/api/resources/method.py b/test/fixtures/wpt/fetch/api/resources/method.py deleted file mode 100644 index c1a111b4cdf..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/method.py +++ /dev/null @@ -1,18 +0,0 @@ -from wptserve.utils import isomorphic_encode - -def main(request, response): - headers = [] - if b"cors" in request.GET: - headers.append((b"Access-Control-Allow-Origin", b"*")) - headers.append((b"Access-Control-Allow-Credentials", b"true")) - headers.append((b"Access-Control-Allow-Methods", b"GET, POST, PUT, FOO")) - headers.append((b"Access-Control-Allow-Headers", b"x-test, x-foo")) - headers.append((b"Access-Control-Expose-Headers", b"x-request-method")) - - headers.append((b"x-request-method", isomorphic_encode(request.method))) - headers.append((b"x-request-content-type", request.headers.get(b"Content-Type", b"NO"))) - headers.append((b"x-request-content-length", request.headers.get(b"Content-Length", b"NO"))) - headers.append((b"x-request-content-encoding", request.headers.get(b"Content-Encoding", b"NO"))) - headers.append((b"x-request-content-language", request.headers.get(b"Content-Language", b"NO"))) - headers.append((b"x-request-content-location", request.headers.get(b"Content-Location", b"NO"))) - return headers, request.body diff --git a/test/fixtures/wpt/fetch/api/resources/preflight.py b/test/fixtures/wpt/fetch/api/resources/preflight.py deleted file mode 100644 index f983ef95227..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/preflight.py +++ /dev/null @@ -1,78 +0,0 @@ -def main(request, response): - headers = [(b"Content-Type", b"text/plain")] - stashed_data = {b'control_request_headers': b"", b'preflight': b"0", b'preflight_referrer': b""} - - token = None - if b"token" in request.GET: - token = request.GET.first(b"token") - - if b"origin" in request.GET: - for origin in request.GET[b'origin'].split(b", "): - headers.append((b"Access-Control-Allow-Origin", origin)) - else: - headers.append((b"Access-Control-Allow-Origin", b"*")) - - if b"clear-stash" in request.GET: - if request.server.stash.take(token) is not None: - return headers, b"1" - else: - return headers, b"0" - - if b"credentials" in request.GET: - headers.append((b"Access-Control-Allow-Credentials", b"true")) - - if request.method == u"OPTIONS": - if not b"Access-Control-Request-Method" in request.headers: - response.set_error(400, u"No Access-Control-Request-Method header") - return b"ERROR: No access-control-request-method in preflight!" - - if request.headers.get(b"Accept", b"") != b"*/*": - response.set_error(400, u"Request does not have 'Accept: */*' header") - return b"ERROR: Invalid access in preflight!" - - if b"control_request_headers" in request.GET: - stashed_data[b'control_request_headers'] = request.headers.get(b"Access-Control-Request-Headers", None) - - if b"max_age" in request.GET: - headers.append((b"Access-Control-Max-Age", request.GET[b'max_age'])) - - if b"allow_headers" in request.GET: - headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) - - if b"allow_methods" in request.GET: - headers.append((b"Access-Control-Allow-Methods", request.GET[b'allow_methods'])) - - preflight_status = 200 - if b"preflight_status" in request.GET: - preflight_status = int(request.GET.first(b"preflight_status")) - - stashed_data[b'preflight'] = b"1" - stashed_data[b'preflight_referrer'] = request.headers.get(b"Referer", b"") - stashed_data[b'preflight_user_agent'] = request.headers.get(b"User-Agent", b"") - if token: - request.server.stash.put(token, stashed_data) - - return preflight_status, headers, b"" - - - if token: - data = request.server.stash.take(token) - if data: - stashed_data = data - - if b"checkUserAgentHeaderInPreflight" in request.GET and request.headers.get(b"User-Agent") != stashed_data[b'preflight_user_agent']: - return 400, headers, b"ERROR: No user-agent header in preflight" - - #use x-* headers for returning value to bodyless responses - headers.append((b"Access-Control-Expose-Headers", b"x-did-preflight, x-control-request-headers, x-referrer, x-preflight-referrer, x-origin")) - headers.append((b"x-did-preflight", stashed_data[b'preflight'])) - if stashed_data[b'control_request_headers'] != None: - headers.append((b"x-control-request-headers", stashed_data[b'control_request_headers'])) - headers.append((b"x-preflight-referrer", stashed_data[b'preflight_referrer'])) - headers.append((b"x-referrer", request.headers.get(b"Referer", b""))) - headers.append((b"x-origin", request.headers.get(b"Origin", b""))) - - if token: - request.server.stash.put(token, stashed_data) - - return headers, b"" diff --git a/test/fixtures/wpt/fetch/api/resources/redirect-empty-location.py b/test/fixtures/wpt/fetch/api/resources/redirect-empty-location.py deleted file mode 100644 index 1a5f7feb2a4..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/redirect-empty-location.py +++ /dev/null @@ -1,3 +0,0 @@ -def main(request, response): - headers = [(b"Location", b"")] - return 302, headers, b"" diff --git a/test/fixtures/wpt/fetch/api/resources/redirect.h2.py b/test/fixtures/wpt/fetch/api/resources/redirect.h2.py deleted file mode 100644 index 69370145876..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/redirect.h2.py +++ /dev/null @@ -1,14 +0,0 @@ -from wptserve.utils import isomorphic_decode, isomorphic_encode - -def handle_headers(frame, request, response): - status = 302 - if b'redirect_status' in request.GET: - status = int(request.GET[b'redirect_status']) - response.status = status - - if b'location' in request.GET: - url = isomorphic_decode(request.GET[b'location']) - response.headers[b'Location'] = isomorphic_encode(url) - - response.headers.update([('Content-Type', 'text/plain')]) - response.write_status_headers() diff --git a/test/fixtures/wpt/fetch/api/resources/redirect.py b/test/fixtures/wpt/fetch/api/resources/redirect.py deleted file mode 100644 index d52ab5f3eee..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/redirect.py +++ /dev/null @@ -1,73 +0,0 @@ -import time - -from urllib.parse import urlencode, urlparse - -from wptserve.utils import isomorphic_decode, isomorphic_encode - -def main(request, response): - stashed_data = {b'count': 0, b'preflight': b"0"} - status = 302 - headers = [(b"Content-Type", b"text/plain"), - (b"Cache-Control", b"no-cache"), - (b"Pragma", b"no-cache")] - if b"Origin" in request.headers: - headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) - headers.append((b"Access-Control-Allow-Credentials", b"true")) - else: - headers.append((b"Access-Control-Allow-Origin", b"*")) - - token = None - if b"token" in request.GET: - token = request.GET.first(b"token") - data = request.server.stash.take(token) - if data: - stashed_data = data - - if request.method == u"OPTIONS": - if b"allow_headers" in request.GET: - headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) - stashed_data[b'preflight'] = b"1" - #Preflight is not redirected: return 200 - if not b"redirect_preflight" in request.GET: - if token: - request.server.stash.put(request.GET.first(b"token"), stashed_data) - return 200, headers, u"" - - if b"redirect_status" in request.GET: - status = int(request.GET[b'redirect_status']) - elif b"redirect_status" in request.POST: - status = int(request.POST[b'redirect_status']) - - stashed_data[b'count'] += 1 - - if b"location" in request.GET: - url = isomorphic_decode(request.GET[b'location']) - if b"simple" not in request.GET: - scheme = urlparse(url).scheme - if scheme == u"" or scheme == u"http" or scheme == u"https": - url += u"&" if u'?' in url else u"?" - #keep url parameters in location - url_parameters = {} - for item in request.GET.items(): - url_parameters[isomorphic_decode(item[0])] = isomorphic_decode(item[1][0]) - url += urlencode(url_parameters) - #make sure location changes during redirection loop - url += u"&count=" + str(stashed_data[b'count']) - headers.append((b"Location", isomorphic_encode(url))) - - if b"redirect_referrerpolicy" in request.GET: - headers.append((b"Referrer-Policy", request.GET[b'redirect_referrerpolicy'])) - - if b"delay" in request.GET: - time.sleep(float(request.GET.first(b"delay", 0)) / 1E3) - - if token: - request.server.stash.put(request.GET.first(b"token"), stashed_data) - if b"max_count" in request.GET: - max_count = int(request.GET[b'max_count']) - #stop redirecting and return count - if stashed_data[b'count'] > max_count: - # -1 because the last is not a redirection - return str(stashed_data[b'count'] - 1) - - return status, headers, u"" diff --git a/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html b/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html deleted file mode 100644 index 6e5d5065474..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/sandboxed-iframe.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/api/resources/script-with-header.py b/test/fixtures/wpt/fetch/api/resources/script-with-header.py deleted file mode 100644 index 9a9c70ef5cf..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/script-with-header.py +++ /dev/null @@ -1,7 +0,0 @@ -def main(request, response): - headers = [(b"Content-type", request.GET.first(b"mime"))] - if b"content" in request.GET and request.GET.first(b"content") == b"empty": - content = b'' - else: - content = b"console.log('Script loaded')" - return 200, headers, content diff --git a/test/fixtures/wpt/fetch/api/resources/stash-put.py b/test/fixtures/wpt/fetch/api/resources/stash-put.py deleted file mode 100644 index 91c198abb7d..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/stash-put.py +++ /dev/null @@ -1,41 +0,0 @@ -from wptserve.utils import isomorphic_decode - -def should_be_treated_as_same_origin_request(request): - """Tells whether request should be treated as same-origin request.""" - # In both of the following cases, allow to proceed with handling to simulate - # 'no-cors' mode: response is sent, but browser will make it opaque. - if request.GET.first(b'mode') == b'no-cors': - return True - - # We can't rely on the Origin header field of a fetch request, as it is only - # present for 'cors' mode or methods other than 'GET'/'HEAD' (i.e. present for - # 'POST'). See https://fetch.spec.whatwg.org/#http-origin - assert 'frame_origin ' in request.GET - frame_origin = request.GET.first(b'frame_origin').decode('utf-8') - host_origin = request.url_parts.scheme + '://' + request.url_parts.netloc - return frame_origin == host_origin - -def main(request, response): - if request.method == u'OPTIONS': - # CORS preflight - response.headers.set(b'Access-Control-Allow-Origin', b'*') - response.headers.set(b'Access-Control-Allow-Methods', b'*') - response.headers.set(b'Access-Control-Allow-Headers', b'*') - return 'done' - - if b'disallow_cross_origin' not in request.GET: - response.headers.set(b'Access-Control-Allow-Origin', b'*') - elif not should_be_treated_as_same_origin_request(request): - # As simple requests will not trigger preflight, we have to manually block - # cors requests before making any changes to storage. - # https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests - # https://fetch.spec.whatwg.org/#cors-preflight-fetch - return 'not stashing for cors request' - - url_dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' - key = request.GET.first(b'key') - value = request.GET.first(b'value') - # value here must be a text string. It will be json.dump()'ed in stash-take.py. - request.server.stash.put(key, isomorphic_decode(value), url_dir) - - return 'done' diff --git a/test/fixtures/wpt/fetch/api/resources/stash-take.py b/test/fixtures/wpt/fetch/api/resources/stash-take.py deleted file mode 100644 index e6db80dd86d..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/stash-take.py +++ /dev/null @@ -1,9 +0,0 @@ -from wptserve.handlers import json_handler - - -@json_handler -def main(request, response): - dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' - key = request.GET.first(b"key") - response.headers.set(b'Access-Control-Allow-Origin', b'*') - return request.server.stash.take(key, dir) diff --git a/test/fixtures/wpt/fetch/api/resources/status.py b/test/fixtures/wpt/fetch/api/resources/status.py deleted file mode 100644 index 05a59d5a637..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/status.py +++ /dev/null @@ -1,11 +0,0 @@ -from wptserve.utils import isomorphic_encode - -def main(request, response): - code = int(request.GET.first(b"code", 200)) - text = request.GET.first(b"text", b"OMG") - content = request.GET.first(b"content", b"") - type = request.GET.first(b"type", b"") - status = (code, text) - headers = [(b"Content-Type", type), - (b"X-Request-Method", isomorphic_encode(request.method))] - return status, headers, content diff --git a/test/fixtures/wpt/fetch/api/resources/sw-intercept-abort.js b/test/fixtures/wpt/fetch/api/resources/sw-intercept-abort.js deleted file mode 100644 index 19d4b189d85..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/sw-intercept-abort.js +++ /dev/null @@ -1,19 +0,0 @@ -async function messageClient(clientId, message) { - const client = await clients.get(clientId); - client.postMessage(message); -} - -addEventListener('fetch', event => { - let resolve; - const promise = new Promise(r => resolve = r); - - function onAborted() { - messageClient(event.clientId, event.request.signal.reason); - resolve(); - } - - messageClient(event.clientId, 'fetch event has arrived'); - - event.respondWith(promise.then(() => new Response('hello'))); - event.request.signal.addEventListener('abort', onAborted); -}); diff --git a/test/fixtures/wpt/fetch/api/resources/sw-intercept.js b/test/fixtures/wpt/fetch/api/resources/sw-intercept.js deleted file mode 100644 index b8166b62a5c..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/sw-intercept.js +++ /dev/null @@ -1,10 +0,0 @@ -async function broadcast(msg) { - for (const client of await clients.matchAll()) { - client.postMessage(msg); - } -} - -addEventListener('fetch', event => { - event.waitUntil(broadcast(event.request.url)); - event.respondWith(fetch(event.request)); -}); diff --git a/test/fixtures/wpt/fetch/api/resources/top.txt b/test/fixtures/wpt/fetch/api/resources/top.txt deleted file mode 100644 index 83a3157d14d..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/top.txt +++ /dev/null @@ -1 +0,0 @@ -top \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/api/resources/trickle.py b/test/fixtures/wpt/fetch/api/resources/trickle.py deleted file mode 100644 index 99833f1b38d..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/trickle.py +++ /dev/null @@ -1,15 +0,0 @@ -import time - -def main(request, response): - delay = float(request.GET.first(b"ms", 500)) / 1E3 - count = int(request.GET.first(b"count", 50)) - # Read request body - request.body - time.sleep(delay) - if not b"notype" in request.GET: - response.headers.set(b"Content-type", b"text/plain") - response.write_status_headers() - time.sleep(delay) - for i in range(count): - response.writer.write_content(b"TEST_TRICKLE\n") - time.sleep(delay) diff --git a/test/fixtures/wpt/fetch/api/resources/utils.js b/test/fixtures/wpt/fetch/api/resources/utils.js deleted file mode 100644 index 3721d9bf9cc..00000000000 --- a/test/fixtures/wpt/fetch/api/resources/utils.js +++ /dev/null @@ -1,120 +0,0 @@ -var RESOURCES_DIR = "../resources/"; - -function dirname(path) { - return path.replace(/\/[^\/]*$/, '/') -} - -function checkRequest(request, ExpectedValuesDict) { - for (var attribute in ExpectedValuesDict) { - switch(attribute) { - case "headers": - for (var key in ExpectedValuesDict["headers"].keys()) { - assert_equals(request["headers"].get(key), ExpectedValuesDict["headers"].get(key), - "Check headers attribute has " + key + ":" + ExpectedValuesDict["headers"].get(key)); - } - break; - - case "body": - //for checking body's content, a dedicated asyncronous/promise test should be used - assert_true(request["headers"].has("Content-Type") , "Check request has body using Content-Type header") - break; - - case "method": - case "referrer": - case "referrerPolicy": - case "credentials": - case "cache": - case "redirect": - case "integrity": - case "url": - case "destination": - assert_equals(request[attribute], ExpectedValuesDict[attribute], "Check " + attribute + " attribute") - break; - - default: - break; - } - } -} - -function stringToArray(str) { - var array = new Uint8Array(str.length); - for (var i=0, strLen = str.length; i < strLen; i++) - array[i] = str.charCodeAt(i); - return array; -} - -function encode_utf8(str) -{ - if (self.TextEncoder) - return (new TextEncoder).encode(str); - return stringToArray(unescape(encodeURIComponent(str))); -} - -function validateBufferFromString(buffer, expectedValue, message) -{ - return assert_array_equals(new Uint8Array(buffer !== undefined ? buffer : []), stringToArray(expectedValue), message); -} - -function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) { - // Passing Uint8Array for byte streams; non-byte streams will simply ignore it - return reader.read(new Uint8Array(64)).then(function(data) { - if (!data.done) { - assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); - var newBuffer; - if (retrievedArrayBuffer) { - newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length); - newBuffer.set(retrievedArrayBuffer, 0); - newBuffer.set(data.value, retrievedArrayBuffer.length); - } else { - newBuffer = data.value; - } - return validateStreamFromString(reader, expectedValue, newBuffer); - } - validateBufferFromString(retrievedArrayBuffer, expectedValue, "Retrieve and verify stream"); - }); -} - -function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) { - // Passing Uint8Array for byte streams; non-byte streams will simply ignore it - return reader.read(new Uint8Array(64)).then(function(data) { - if (!data.done) { - assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); - var newBuffer; - if (retrievedArrayBuffer) { - newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length); - newBuffer.set(retrievedArrayBuffer, 0); - newBuffer.set(data.value, retrievedArrayBuffer.length); - } else { - newBuffer = data.value; - } - return validateStreamFromPartialString(reader, expectedValue, newBuffer); - } - - var string = new TextDecoder("utf-8").decode(retrievedArrayBuffer); - return assert_true(string.search(expectedValue) != -1, "Retrieve and verify stream"); - }); -} - -// From streams tests -function delay(milliseconds) -{ - return new Promise(function(resolve) { - step_timeout(resolve, milliseconds); - }); -} - -function requestForbiddenHeaders(desc, forbiddenHeaders) { - var url = RESOURCES_DIR + "inspect-headers.py"; - var requestInit = {"headers": forbiddenHeaders} - var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|"); - - promise_test(function(test){ - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "HTTP status is 200"); - assert_equals(resp.type , "basic", "Response's type is basic"); - for (var header in forbiddenHeaders) - assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined"); - }); - }, desc); -} diff --git a/test/fixtures/wpt/fetch/api/response/json.any.js b/test/fixtures/wpt/fetch/api/response/json.any.js deleted file mode 100644 index 15f050e6324..00000000000 --- a/test/fixtures/wpt/fetch/api/response/json.any.js +++ /dev/null @@ -1,14 +0,0 @@ -// See also /xhr/json.any.js - -promise_test(async t => { - const response = await fetch(`data:,\uFEFF{ "b": 1, "a": 2, "b": 3 }`); - const json = await response.json(); - assert_array_equals(Object.keys(json), ["b", "a"]); - assert_equals(json.a, 2); - assert_equals(json.b, 3); -}, "Ensure the correct JSON parser is used"); - -promise_test(async t => { - const response = await fetch("/xhr/resources/utf16-bom.json"); - return promise_rejects_js(t, SyntaxError, response.json()); -}, "Ensure UTF-16 results in an error"); diff --git a/test/fixtures/wpt/fetch/api/response/many-empty-chunks-crash.html b/test/fixtures/wpt/fetch/api/response/many-empty-chunks-crash.html deleted file mode 100644 index fe5e7d4c075..00000000000 --- a/test/fixtures/wpt/fetch/api/response/many-empty-chunks-crash.html +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html b/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html deleted file mode 100644 index 9bb6e0bbf3f..00000000000 --- a/test/fixtures/wpt/fetch/api/response/multi-globals/current/current.html +++ /dev/null @@ -1,3 +0,0 @@ - -Current page used as a test helper - diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html b/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html deleted file mode 100644 index f63372e64c2..00000000000 --- a/test/fixtures/wpt/fetch/api/response/multi-globals/incumbent/incumbent.html +++ /dev/null @@ -1,16 +0,0 @@ - -Incumbent page used as a test helper - - - - - diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html b/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html deleted file mode 100644 index 44f42eda493..00000000000 --- a/test/fixtures/wpt/fetch/api/response/multi-globals/relevant/relevant.html +++ /dev/null @@ -1,2 +0,0 @@ - -Relevant page used as a test helper diff --git a/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html b/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html deleted file mode 100644 index 5f2f42a1cea..00000000000 --- a/test/fixtures/wpt/fetch/api/response/multi-globals/url-parsing.html +++ /dev/null @@ -1,27 +0,0 @@ - -Response.redirect URL parsing, with multiple globals in play - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/response/response-arraybuffer-realm.window.js b/test/fixtures/wpt/fetch/api/response/response-arraybuffer-realm.window.js deleted file mode 100644 index 19a5dfa5ff6..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-arraybuffer-realm.window.js +++ /dev/null @@ -1,23 +0,0 @@ -// META: title=realm of Response arrayBuffer() - -'use strict'; - -promise_test(async () => { - await new Promise(resolve => { - onload = resolve; - }); - - let iframe = document.createElement('iframe'); - document.body.appendChild(iframe); - iframe.srcdoc = ''; - await new Promise(resolve => { - iframe.onload = resolve; - }); - - let otherRealm = iframe.contentWindow; - - let ab = await window.Response.prototype.arrayBuffer.call(new otherRealm.Response('')); - - assert_true(ab instanceof otherRealm.ArrayBuffer, "ArrayBuffer should be created in receiver's realm"); - assert_false(ab instanceof ArrayBuffer, "ArrayBuffer should not be created in the arrayBuffer() methods's realm"); -}, 'realm of the ArrayBuffer from Response arrayBuffer()'); diff --git a/test/fixtures/wpt/fetch/api/response/response-blob-realm.any.js b/test/fixtures/wpt/fetch/api/response/response-blob-realm.any.js deleted file mode 100644 index 1cc51fc71b6..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-blob-realm.any.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: global=window -// META: title=realm of Response bytes() - -"use strict"; - -promise_test(async () => { - await new Promise(resolve => { - onload = resolve; - }); - - let iframe = document.createElement("iframe"); - document.body.appendChild(iframe); - iframe.srcdoc = ""; - await new Promise(resolve => { - iframe.onload = resolve; - }); - - let otherRealm = iframe.contentWindow; - - let ab = await window.Response.prototype.bytes.call(new otherRealm.Response("")); - - assert_true(ab instanceof otherRealm.Uint8Array, "Uint8Array should be created in receiver's realm"); - assert_false(ab instanceof Uint8Array, "Uint8Array should not be created in the bytes() methods's realm"); -}, "realm of the Uint8Array from Response bytes()"); diff --git a/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html b/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html deleted file mode 100644 index 64b07556661..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-body-read-task-handling.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js deleted file mode 100644 index 91140d1afd1..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-cancel-stream.any.js +++ /dev/null @@ -1,64 +0,0 @@ -// META: global=window,worker -// META: title=Response consume blob and http bodies -// META: script=../resources/utils.js - -promise_test(function(test) { - return new Response(new Blob([], { "type" : "text/plain" })).body.cancel(); -}, "Cancelling a starting blob Response stream"); - -promise_test(function(test) { - var response = new Response(new Blob(["This is data"], { "type" : "text/plain" })); - var reader = response.body.getReader(); - reader.read(); - return reader.cancel(); -}, "Cancelling a loading blob Response stream"); - -promise_test(function(test) { - var response = new Response(new Blob(["T"], { "type" : "text/plain" })); - var reader = response.body.getReader(); - - var closedPromise = reader.closed.then(function() { - return reader.cancel(); - }); - reader.read().then(function readMore({done, value}) { - if (!done) return reader.read().then(readMore); - }); - return closedPromise; -}, "Cancelling a closed blob Response stream"); - -promise_test(function(test) { - return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) { - return response.body.cancel(); - }); -}, "Cancelling a starting Response stream"); - -promise_test(function() { - return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) { - var reader = response.body.getReader(); - return reader.read().then(function() { - return reader.cancel(); - }); - }); -}, "Cancelling a loading Response stream"); - -promise_test(function() { - async function readAll(reader) { - while (true) { - const {value, done} = await reader.read(); - if (done) - return; - } - } - - return fetch(RESOURCES_DIR + "top.txt").then(function(response) { - var reader = response.body.getReader(); - return readAll(reader).then(() => reader.cancel()); - }); -}, "Cancelling a closed Response stream"); - -promise_test(async () => { - const response = await fetch(RESOURCES_DIR + "top.txt"); - const { body } = response; - await body.cancel(); - assert_equals(body, response.body, ".body should not change after cancellation"); -}, "Accessing .body after canceling it"); diff --git a/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js b/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js deleted file mode 100644 index da54616c376..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-clone-iframe.window.js +++ /dev/null @@ -1,32 +0,0 @@ -// Verify that calling Response clone() in a detached iframe doesn't crash. -// Regression test for https://crbug.com/1082688. - -'use strict'; - -promise_test(async () => { - // Wait for the document body to be available. - await new Promise(resolve => { - onload = resolve; - }); - - window.iframe = document.createElement('iframe'); - document.body.appendChild(iframe); - iframe.srcdoc = ` - -`; - - await new Promise(resolve => { - onmessage = evt => { - if (evt.data === 'okay') { - resolve(); - } - }; - }); - - // If it got here without crashing, the test passed. -}, 'clone within removed iframe should not crash'); diff --git a/test/fixtures/wpt/fetch/api/response/response-clone.any.js b/test/fixtures/wpt/fetch/api/response/response-clone.any.js deleted file mode 100644 index c0c844948db..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-clone.any.js +++ /dev/null @@ -1,141 +0,0 @@ -// META: global=window,worker -// META: title=Response clone -// META: script=../resources/utils.js - -var defaultValues = { "type" : "default", - "url" : "", - "ok" : true, - "status" : 200, - "statusText" : "" -}; - -var response = new Response(); -var clonedResponse = response.clone(); -test(function() { - for (var attributeName in defaultValues) { - var expectedValue = defaultValues[attributeName]; - assert_equals(clonedResponse[attributeName], expectedValue, - "Expect default response." + attributeName + " is " + expectedValue); - } -}, "Check Response's clone with default values, without body"); - -var body = "This is response body"; -var headersInit = { "name" : "value" }; -var responseInit = { "status" : 200, - "statusText" : "GOOD", - "headers" : headersInit -}; -var response = new Response(body, responseInit); -var clonedResponse = response.clone(); -test(function() { - assert_equals(clonedResponse.status, responseInit["status"], - "Expect response.status is " + responseInit["status"]); - assert_equals(clonedResponse.statusText, responseInit["statusText"], - "Expect response.statusText is " + responseInit["statusText"]); - assert_equals(clonedResponse.headers.get("name"), "value", - "Expect response.headers has name:value header"); -}, "Check Response's clone has the expected attribute values"); - -promise_test(function(test) { - return validateStreamFromString(response.body.getReader(), body); -}, "Check orginal response's body after cloning"); - -promise_test(function(test) { - return validateStreamFromString(clonedResponse.body.getReader(), body); -}, "Check cloned response's body"); - -promise_test(function(test) { - var disturbedResponse = new Response("data"); - return disturbedResponse.text().then(function() { - assert_true(disturbedResponse.bodyUsed, "response is disturbed"); - assert_throws_js(TypeError, function() { disturbedResponse.clone(); }, - "Expect TypeError exception"); - }); -}, "Cannot clone a disturbed response"); - -promise_test(function(t) { - var clone; - var result; - var response; - return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) { - clone = res.clone(); - response = res; - return clone.text(); - }).then(function(r) { - assert_equals(r.length, 26); - result = r; - return response.text(); - }).then(function(r) { - assert_equals(r, result, "cloned responses should provide the same data"); - }); - }, 'Cloned responses should provide the same data'); - -promise_test(function(t) { - var clone; - return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) { - clone = res.clone(); - res.body.cancel(); - assert_true(res.bodyUsed); - assert_false(clone.bodyUsed); - return clone.arrayBuffer(); - }).then(function(r) { - assert_equals(r.byteLength, 26); - assert_true(clone.bodyUsed); - }); -}, 'Cancelling stream should not affect cloned one'); - -function testReadableStreamClone(initialBuffer, bufferType) -{ - promise_test(function(test) { - var response = new Response(new ReadableStream({start : function(controller) { - controller.enqueue(initialBuffer); - controller.close(); - }})); - - var clone = response.clone(); - var stream1 = response.body; - var stream2 = clone.body; - - var buffer; - return stream1.getReader().read().then(function(data) { - assert_false(data.done); - assert_equals(data.value, initialBuffer, "Buffer of being-cloned response stream is the same as the original buffer"); - return stream2.getReader().read(); - }).then(function(data) { - assert_false(data.done); - if (initialBuffer instanceof ArrayBuffer) { - assert_true(data.value instanceof ArrayBuffer, "Cloned buffer is ArrayBufer"); - assert_equals(initialBuffer.byteLength, data.value.byteLength, "Length equal"); - assert_array_equals(new Uint8Array(data.value), new Uint8Array(initialBuffer), "Cloned buffer chunks have the same content"); - } else if (initialBuffer instanceof DataView) { - assert_true(data.value instanceof DataView, "Cloned buffer is DataView"); - assert_equals(initialBuffer.byteLength, data.value.byteLength, "Lengths equal"); - assert_equals(initialBuffer.byteOffset, data.value.byteOffset, "Offsets equal"); - for (let i = 0; i < initialBuffer.byteLength; ++i) { - assert_equals( - data.value.getUint8(i), initialBuffer.getUint8(i), "Mismatch at byte ${i}"); - } - } else { - assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content"); - } - assert_equals(Object.getPrototypeOf(data.value), Object.getPrototypeOf(initialBuffer), "Cloned buffers have the same type"); - assert_not_equals(data.value, initialBuffer, "Buffer of cloned response stream is a clone of the original buffer"); - }); - }, "Check response clone use structureClone for teed ReadableStreams (" + bufferType + "chunk)"); -} - -var arrayBuffer = new ArrayBuffer(16); -testReadableStreamClone(new Int8Array(arrayBuffer, 1), "Int8Array"); -testReadableStreamClone(new Int16Array(arrayBuffer, 2, 2), "Int16Array"); -testReadableStreamClone(new Int32Array(arrayBuffer), "Int32Array"); -testReadableStreamClone(arrayBuffer, "ArrayBuffer"); -testReadableStreamClone(new Uint8Array(arrayBuffer), "Uint8Array"); -testReadableStreamClone(new Uint8ClampedArray(arrayBuffer), "Uint8ClampedArray"); -testReadableStreamClone(new Uint16Array(arrayBuffer, 2), "Uint16Array"); -testReadableStreamClone(new Uint32Array(arrayBuffer), "Uint32Array"); -testReadableStreamClone(typeof BigInt64Array === "function" ? new BigInt64Array(arrayBuffer) : undefined, "BigInt64Array"); -testReadableStreamClone(typeof BigUint64Array === "function" ? new BigUint64Array(arrayBuffer) : undefined, "BigUint64Array"); -testReadableStreamClone(typeof Float16Array === "function" ? new Float16Array(arrayBuffer) : undefined, "Float16Array"); -testReadableStreamClone(new Float32Array(arrayBuffer), "Float32Array"); -testReadableStreamClone(new Float64Array(arrayBuffer), "Float64Array"); -testReadableStreamClone(new DataView(arrayBuffer, 2, 8), "DataView"); diff --git a/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js b/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js deleted file mode 100644 index a5df3562586..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-consume-empty.any.js +++ /dev/null @@ -1,88 +0,0 @@ -// META: global=window,worker -// META: title=Response consume empty bodies - -function checkBodyText(test, response) { - return response.text().then(function(bodyAsText) { - assert_equals(bodyAsText, "", "Resolved value should be empty"); - assert_false(response.bodyUsed); - }); -} - -async function checkBodyBlob(test, response) { - const bodyAsBlob = await response.blob(); - const body = await bodyAsBlob.text(); - - assert_equals(body, "", "Resolved value should be empty"); - assert_false(response.bodyUsed); -} - -function checkBodyArrayBuffer(test, response) { - return response.arrayBuffer().then(function(bodyAsArrayBuffer) { - assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); - assert_false(response.bodyUsed); - }); -} - -function checkBodyJSON(test, response) { - return response.json().then( - function(bodyAsJSON) { - assert_unreached("JSON parsing should fail"); - }, - function() { - assert_false(response.bodyUsed); - }); -} - -function checkBodyFormData(test, response) { - return response.formData().then(function(bodyAsFormData) { - assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); - assert_false(response.bodyUsed); - }); -} - -function checkBodyFormDataError(test, response) { - return promise_rejects_js(test, TypeError, response.formData()).then(function() { - assert_false(response.bodyUsed); - }); -} - -function checkResponseWithNoBody(bodyType, checkFunction, headers = []) { - promise_test(function(test) { - var response = new Response(undefined, { "headers": headers }); - assert_false(response.bodyUsed); - return checkFunction(test, response); - }, "Consume response's body as " + bodyType); -} - -checkResponseWithNoBody("text", checkBodyText); -checkResponseWithNoBody("blob", checkBodyBlob); -checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer); -checkResponseWithNoBody("json (error case)", checkBodyJSON); -checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); -checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); -checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError); - -function checkResponseWithEmptyBody(bodyType, body, asText) { - promise_test(function(test) { - var response = new Response(body); - assert_false(response.bodyUsed, "bodyUsed is false at init"); - if (asText) { - return response.text().then(function(bodyAsString) { - assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); - assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); - }); - } - return response.arrayBuffer().then(function(bodyAsArrayBuffer) { - assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); - assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); - }); - }, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer")); -} - -checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); -checkResponseWithEmptyBody("text", "", false); -checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); -checkResponseWithEmptyBody("text", "", true); -checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); -checkResponseWithEmptyBody("FormData", new FormData(), true); -checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js deleted file mode 100644 index f89d7341ac6..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-consume-stream.any.js +++ /dev/null @@ -1,80 +0,0 @@ -// META: global=window,worker -// META: title=Response consume -// META: script=../resources/utils.js - -promise_test(function(test) { - var body = ""; - var response = new Response(""); - return validateStreamFromString(response.body.getReader(), ""); -}, "Read empty text response's body as readableStream"); - -promise_test(function(test) { - var response = new Response(new Blob([], { "type" : "text/plain" })); - return validateStreamFromString(response.body.getReader(), ""); -}, "Read empty blob response's body as readableStream"); - -var formData = new FormData(); -formData.append("name", "value"); -var textData = JSON.stringify("This is response's body"); -var blob = new Blob([textData], { "type" : "text/plain" }); -var urlSearchParamsData = "name=value"; -var urlSearchParams = new URLSearchParams(urlSearchParamsData); - -for (const mode of [undefined, "byob"]) { - promise_test(function(test) { - var response = new Response(blob); - return validateStreamFromString(response.body.getReader({ mode }), textData); - }, `Read blob response's body as readableStream with mode=${mode}`); - - promise_test(function(test) { - var response = new Response(textData); - return validateStreamFromString(response.body.getReader({ mode }), textData); - }, `Read text response's body as readableStream with mode=${mode}`); - - promise_test(function(test) { - var response = new Response(urlSearchParams); - return validateStreamFromString(response.body.getReader({ mode }), urlSearchParamsData); - }, `Read URLSearchParams response's body as readableStream with mode=${mode}`); - - promise_test(function(test) { - var arrayBuffer = new ArrayBuffer(textData.length); - var int8Array = new Int8Array(arrayBuffer); - for (var cptr = 0; cptr < textData.length; cptr++) - int8Array[cptr] = textData.charCodeAt(cptr); - - return validateStreamFromString(new Response(arrayBuffer).body.getReader({ mode }), textData); - }, `Read array buffer response's body as readableStream with mode=${mode}`); - - promise_test(function(test) { - var response = new Response(formData); - return validateStreamFromPartialString(response.body.getReader({ mode }), - "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue"); - }, `Read form data response's body as readableStream with mode=${mode}`); -} - -test(function() { - assert_equals(Response.error().body, null); -}, "Getting an error Response stream"); - -test(function() { - assert_equals(Response.redirect("/").body, null); -}, "Getting a redirect Response stream"); - -promise_test(async function(test) { - var buffer = new ArrayBuffer(textData.length); - - var body = new Response(textData).body; - const reader = body.getReader( {mode: 'byob'} ); - - let offset = 3; - while (offset < textData.length) { - const {done, value} = await reader.read(new Uint8Array(buffer, offset)); - if (done) { - break; - } - buffer = value.buffer; - offset += value.byteLength; - } - - validateBufferFromString(buffer, `\0\0\0\"This is response's bo`, 'Buffer should be validated'); -}, `Reading with offset from Response stream`); diff --git a/test/fixtures/wpt/fetch/api/response/response-consume.html b/test/fixtures/wpt/fetch/api/response/response-consume.html deleted file mode 100644 index 89fc49fd3c2..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-consume.html +++ /dev/null @@ -1,317 +0,0 @@ - - - - - Response consume - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js deleted file mode 100644 index 33cad40e757..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-error-from-stream.any.js +++ /dev/null @@ -1,61 +0,0 @@ -// META: global=window,worker -// META: title=Response Receives Propagated Error from ReadableStream - -function newStreamWithStartError() { - var err = new Error("Start error"); - return [new ReadableStream({ - start(controller) { - controller.error(err); - } - }), - err] -} - -function newStreamWithPullError() { - var err = new Error("Pull error"); - return [new ReadableStream({ - pull(controller) { - controller.error(err); - } - }), - err] -} - -function runRequestPromiseTest([stream, err], responseReaderMethod, testDescription) { - promise_test(test => { - return promise_rejects_exactly( - test, - err, - new Response(stream)[responseReaderMethod](), - 'CustomTestError should propagate' - ) - }, testDescription) -} - - -promise_test(test => { - var [stream, err] = newStreamWithStartError(); - return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate') -}, "ReadableStreamDefaultReader Promise receives ReadableStream start() Error") - -promise_test(test => { - var [stream, err] = newStreamWithPullError(); - return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate') -}, "ReadableStreamDefaultReader Promise receives ReadableStream pull() Error") - - -// test start() errors for all Body reader methods -runRequestPromiseTest(newStreamWithStartError(), 'arrayBuffer', 'ReadableStream start() Error propagates to Response.arrayBuffer() Promise'); -runRequestPromiseTest(newStreamWithStartError(), 'blob', 'ReadableStream start() Error propagates to Response.blob() Promise'); -runRequestPromiseTest(newStreamWithStartError(), 'bytes', 'ReadableStream start() Error propagates to Response.bytes() Promise'); -runRequestPromiseTest(newStreamWithStartError(), 'formData', 'ReadableStream start() Error propagates to Response.formData() Promise'); -runRequestPromiseTest(newStreamWithStartError(), 'json', 'ReadableStream start() Error propagates to Response.json() Promise'); -runRequestPromiseTest(newStreamWithStartError(), 'text', 'ReadableStream start() Error propagates to Response.text() Promise'); - -// test pull() errors for all Body reader methods -runRequestPromiseTest(newStreamWithPullError(), 'arrayBuffer', 'ReadableStream pull() Error propagates to Response.arrayBuffer() Promise'); -runRequestPromiseTest(newStreamWithPullError(), 'blob', 'ReadableStream pull() Error propagates to Response.blob() Promise'); -runRequestPromiseTest(newStreamWithPullError(), 'bytes', 'ReadableStream pull() Error propagates to Response.bytes() Promise'); -runRequestPromiseTest(newStreamWithPullError(), 'formData', 'ReadableStream pull() Error propagates to Response.formData() Promise'); -runRequestPromiseTest(newStreamWithPullError(), 'json', 'ReadableStream pull() Error propagates to Response.json() Promise'); -runRequestPromiseTest(newStreamWithPullError(), 'text', 'ReadableStream pull() Error propagates to Response.text() Promise'); diff --git a/test/fixtures/wpt/fetch/api/response/response-error.any.js b/test/fixtures/wpt/fetch/api/response/response-error.any.js deleted file mode 100644 index a76bc438028..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-error.any.js +++ /dev/null @@ -1,27 +0,0 @@ -// META: global=window,worker -// META: title=Response error - -var invalidStatus = [0, 100, 199, 600, 1000]; -invalidStatus.forEach(function(status) { - test(function() { - assert_throws_js(RangeError, function() { new Response("", { "status" : status }); }, - "Expect RangeError exception when status is " + status); - },"Throws RangeError when responseInit's status is " + status); -}); - -var invalidStatusText = ["\n", "Ā"]; -invalidStatusText.forEach(function(statusText) { - test(function() { - assert_throws_js(TypeError, function() { new Response("", { "statusText" : statusText }); }, - "Expect TypeError exception " + statusText); - },"Throws TypeError when responseInit's statusText is " + statusText); -}); - -var nullBodyStatus = [204, 205, 304]; -nullBodyStatus.forEach(function(status) { - test(function() { - assert_throws_js(TypeError, - function() { new Response("body", {"status" : status }); }, - "Expect TypeError exception "); - },"Throws TypeError when building a response with body and a body status of " + status); -}); diff --git a/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js b/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js deleted file mode 100644 index ea5192bfb10..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-from-stream.any.js +++ /dev/null @@ -1,23 +0,0 @@ -// META: global=window,worker - -"use strict"; - -test(() => { - const stream = new ReadableStream(); - stream.getReader(); - assert_throws_js(TypeError, () => new Response(stream)); -}, "Constructing a Response with a stream on which getReader() is called"); - -test(() => { - const stream = new ReadableStream(); - stream.getReader().read(); - assert_throws_js(TypeError, () => new Response(stream)); -}, "Constructing a Response with a stream on which read() is called"); - -promise_test(async () => { - const stream = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }), - reader = stream.getReader(); - await reader.read(); - reader.releaseLock(); - assert_throws_js(TypeError, () => new Response(stream)); -}, "Constructing a Response with a stream on which read() and releaseLock() are called"); diff --git a/test/fixtures/wpt/fetch/api/response/response-headers-guard.any.js b/test/fixtures/wpt/fetch/api/response/response-headers-guard.any.js deleted file mode 100644 index 4a67d067a71..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-headers-guard.any.js +++ /dev/null @@ -1,8 +0,0 @@ -// META: global=window,worker -// META: title=Response: error static method - -promise_test (async () => { - const response = await fetch("../resources/data.json"); - assert_throws_js(TypeError, () => { response.headers.append("name", "value"); }); - assert_not_equals(response.headers.get("name"), "value", "response headers should be immutable"); -}, "Ensure response headers are immutable"); diff --git a/test/fixtures/wpt/fetch/api/response/response-init-001.any.js b/test/fixtures/wpt/fetch/api/response/response-init-001.any.js deleted file mode 100644 index 559e49ad11f..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-init-001.any.js +++ /dev/null @@ -1,64 +0,0 @@ -// META: global=window,worker -// META: title=Response init: simple cases - -var defaultValues = { "type" : "default", - "url" : "", - "ok" : true, - "status" : 200, - "statusText" : "", - "body" : null -}; - -var statusCodes = { "givenValues" : [200, 300, 400, 500, 599], - "expectedValues" : [200, 300, 400, 500, 599] -}; -var statusTexts = { "givenValues" : ["", "OK", "with space", String.fromCharCode(0x80)], - "expectedValues" : ["", "OK", "with space", String.fromCharCode(0x80)] -}; -var initValuesDict = { "status" : statusCodes, - "statusText" : statusTexts -}; - -function isOkStatus(status) { - return 200 <= status && 299 >= status; -} - -var response = new Response(); -for (var attributeName in defaultValues) { - test(function() { - var expectedValue = defaultValues[attributeName]; - assert_equals(response[attributeName], expectedValue, - "Expect default response." + attributeName + " is " + expectedValue); - }, "Check default value for " + attributeName + " attribute"); -} - -for (var attributeName in initValuesDict) { - test(function() { - var valuesToTest = initValuesDict[attributeName]; - for (var valueIdx in valuesToTest["givenValues"]) { - var givenValue = valuesToTest["givenValues"][valueIdx]; - var expectedValue = valuesToTest["expectedValues"][valueIdx]; - var responseInit = {}; - responseInit[attributeName] = givenValue; - var response = new Response("", responseInit); - assert_equals(response[attributeName], expectedValue, - "Expect response." + attributeName + " is " + expectedValue + - " when initialized with " + givenValue); - assert_equals(response.ok, isOkStatus(response.status), - "Expect response.ok is " + isOkStatus(response.status)); - } - }, "Check " + attributeName + " init values and associated getter"); -} - -test(function() { - const response1 = new Response(""); - assert_equals(response1.headers, response1.headers); - - const response2 = new Response("", {"headers": {"X-Foo": "bar"}}); - assert_equals(response2.headers, response2.headers); - const headers = response2.headers; - response2.headers.set("X-Foo", "quux"); - assert_equals(headers, response2.headers); - headers.set("X-Other-Header", "baz"); - assert_equals(headers, response2.headers); -}, "Test that Response.headers has the [SameObject] extended attribute"); diff --git a/test/fixtures/wpt/fetch/api/response/response-init-002.any.js b/test/fixtures/wpt/fetch/api/response/response-init-002.any.js deleted file mode 100644 index 6c0a46e4804..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-init-002.any.js +++ /dev/null @@ -1,61 +0,0 @@ -// META: global=window,worker -// META: title=Response init: body and headers -// META: script=../resources/utils.js - -test(function() { - var headerDict = {"name1": "value1", - "name2": "value2", - "name3": "value3" - }; - var headers = new Headers(headerDict); - var response = new Response("", { "headers" : headers }) - for (var name in headerDict) { - assert_equals(response.headers.get(name), headerDict[name], - "response's headers has " + name + " : " + headerDict[name]); - } -}, "Initialize Response with headers values"); - -function checkResponseInit(body, bodyType, expectedTextBody) { - promise_test(function(test) { - var response = new Response(body); - var resHeaders = response.headers; - var mime = resHeaders.get("Content-Type"); - assert_true(mime && mime.search(bodyType) > -1, "Content-Type header should be \"" + bodyType + "\" "); - return response.text().then(function(bodyAsText) { - //not equals: cannot guess formData exact value - assert_true(bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify response body"); - }); - }, "Initialize Response's body with " + bodyType); -} - -var blob = new Blob(["This is a blob"], {type: "application/octet-binary"}); -var formaData = new FormData(); -formaData.append("name", "value"); -var urlSearchParams = "URLSearchParams are not supported"; -//avoid test timeout if not implemented -if (self.URLSearchParams) - urlSearchParams = new URLSearchParams("name=value"); -var usvString = "This is a USVString" - -checkResponseInit(blob, "application/octet-binary", "This is a blob"); -checkResponseInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue"); -checkResponseInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value"); -checkResponseInit(usvString, "text/plain;charset=UTF-8", "This is a USVString"); - -promise_test(function(test) { - var body = "This is response body"; - var response = new Response(body); - return validateStreamFromString(response.body.getReader(), body); -}, "Read Response's body as readableStream"); - -promise_test(function(test) { - var response = new Response("This is my fork", {"headers" : [["Content-Type", ""]]}); - return response.blob().then(function(blob) { - assert_equals(blob.type, "", "Blob type should be the empty string"); - }); -}, "Testing empty Response Content-Type header"); - -test(function() { - var response = new Response(null, {status: 204}); - assert_equals(response.body, null); -}, "Testing null Response body"); diff --git a/test/fixtures/wpt/fetch/api/response/response-init-contenttype.any.js b/test/fixtures/wpt/fetch/api/response/response-init-contenttype.any.js deleted file mode 100644 index 3a7744c2878..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-init-contenttype.any.js +++ /dev/null @@ -1,125 +0,0 @@ -test(() => { - const response = new Response(); - assert_equals(response.headers.get("Content-Type"), null); -}, "Default Content-Type for Response with empty body"); - -test(() => { - const blob = new Blob([]); - const response = new Response(blob); - assert_equals(response.headers.get("Content-Type"), null); -}, "Default Content-Type for Response with Blob body (no type set)"); - -test(() => { - const blob = new Blob([], { type: "" }); - const response = new Response(blob); - assert_equals(response.headers.get("Content-Type"), null); -}, "Default Content-Type for Response with Blob body (empty type)"); - -test(() => { - const blob = new Blob([], { type: "a/b; c=d" }); - const response = new Response(blob); - assert_equals(response.headers.get("Content-Type"), "a/b; c=d"); -}, "Default Content-Type for Response with Blob body (set type)"); - -test(() => { - const buffer = new Uint8Array(); - const response = new Response(buffer); - assert_equals(response.headers.get("Content-Type"), null); -}, "Default Content-Type for Response with buffer source body"); - -promise_test(async () => { - const formData = new FormData(); - formData.append("a", "b"); - const response = new Response(formData); - const boundary = (await response.text()).split("\r\n")[0].slice(2); - assert_equals( - response.headers.get("Content-Type"), - `multipart/form-data; boundary=${boundary}`, - ); -}, "Default Content-Type for Response with FormData body"); - -test(() => { - const usp = new URLSearchParams(); - const response = new Response(usp); - assert_equals( - response.headers.get("Content-Type"), - "application/x-www-form-urlencoded;charset=UTF-8", - ); -}, "Default Content-Type for Response with URLSearchParams body"); - -test(() => { - const response = new Response(""); - assert_equals( - response.headers.get("Content-Type"), - "text/plain;charset=UTF-8", - ); -}, "Default Content-Type for Response with string body"); - -test(() => { - const stream = new ReadableStream(); - const response = new Response(stream); - assert_equals(response.headers.get("Content-Type"), null); -}, "Default Content-Type for Response with ReadableStream body"); - -// ----------------------------------------------------------------------------- - -const OVERRIDE_MIME = "test/only; mime=type"; - -function responseWithOverrideMime(body) { - return new Response( - body, - { headers: { "Content-Type": OVERRIDE_MIME } }, - ); -} - -test(() => { - const response = responseWithOverrideMime(undefined); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with empty body"); - -test(() => { - const blob = new Blob([]); - const response = responseWithOverrideMime(blob); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with Blob body (no type set)"); - -test(() => { - const blob = new Blob([], { type: "" }); - const response = responseWithOverrideMime(blob); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with Blob body (empty type)"); - -test(() => { - const blob = new Blob([], { type: "a/b; c=d" }); - const response = responseWithOverrideMime(blob); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with Blob body (set type)"); - -test(() => { - const buffer = new Uint8Array(); - const response = responseWithOverrideMime(buffer); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with buffer source body"); - -test(() => { - const formData = new FormData(); - const response = responseWithOverrideMime(formData); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with FormData body"); - -test(() => { - const usp = new URLSearchParams(); - const response = responseWithOverrideMime(usp); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with URLSearchParams body"); - -test(() => { - const response = responseWithOverrideMime(""); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with string body"); - -test(() => { - const stream = new ReadableStream(); - const response = responseWithOverrideMime(stream); - assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); -}, "Can override Content-Type for Response with ReadableStream body"); diff --git a/test/fixtures/wpt/fetch/api/response/response-static-error.any.js b/test/fixtures/wpt/fetch/api/response/response-static-error.any.js deleted file mode 100644 index 4097eab37b4..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-static-error.any.js +++ /dev/null @@ -1,22 +0,0 @@ -// META: global=window,worker -// META: title=Response: error static method - -test(function() { - var responseError = Response.error(); - assert_equals(responseError.type, "error", "Network error response's type is error"); - assert_equals(responseError.status, 0, "Network error response's status is 0"); - assert_equals(responseError.statusText, "", "Network error response's statusText is empty"); - assert_equals(responseError.body, null, "Network error response's body is null"); - - assert_true(responseError.headers.entries().next().done, "Headers should be empty"); -}, "Check response returned by static method error()"); - -test(function() { - const headers = Response.error().headers; - - // Avoid false positives if expected API is not available - assert_true(!!headers); - assert_equals(typeof headers.append, 'function'); - - assert_throws_js(TypeError, function () { headers.append('name', 'value'); }); -}, "the 'guard' of the Headers instance should be immutable"); diff --git a/test/fixtures/wpt/fetch/api/response/response-static-json.any.js b/test/fixtures/wpt/fetch/api/response/response-static-json.any.js deleted file mode 100644 index 5ec79e69aa3..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-static-json.any.js +++ /dev/null @@ -1,96 +0,0 @@ -// META: global=window,worker -// META: title=Response: json static method - -const APPLICATION_JSON = "application/json"; -const FOO_BAR = "foo/bar"; - -const INIT_TESTS = [ - [undefined, 200, "", APPLICATION_JSON, {}], - [{ status: 400 }, 400, "", APPLICATION_JSON, {}], - [{ statusText: "foo" }, 200, "foo", APPLICATION_JSON, {}], - [{ headers: {} }, 200, "", APPLICATION_JSON, {}], - [{ headers: { "content-type": FOO_BAR } }, 200, "", FOO_BAR, {}], - [{ headers: { "x-foo": "bar" } }, 200, "", APPLICATION_JSON, { "x-foo": "bar" }], -]; - -for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) { - promise_test(async function () { - const response = Response.json("hello world", init); - assert_equals(response.type, "default", "Response's type is default"); - assert_equals(response.status, expectedStatus, "Response's status is " + expectedStatus); - assert_equals(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText)); - assert_equals(response.headers.get("content-type"), expectedContentType, "Response's content-type is " + expectedContentType); - for (const key in expectedHeaders) { - assert_equals(response.headers.get(key), expectedHeaders[key], "Response's header " + key + " is " + JSON.stringify(expectedHeaders[key])); - } - - const data = await response.json(); - assert_equals(data, "hello world", "Response's body is 'hello world'"); - }, `Check response returned by static json() with init ${JSON.stringify(init)}`); -} - -const nullBodyStatus = [204, 205, 304]; -for (const status of nullBodyStatus) { - test(function () { - assert_throws_js( - TypeError, - function () { - Response.json("hello world", { status: status }); - }, - ); - }, `Throws TypeError when calling static json() with a status of ${status}`); -} - -promise_test(async function () { - const response = Response.json({ foo: "bar" }); - const data = await response.json(); - assert_equals(typeof data, "object", "Response's json body is an object"); - assert_equals(data.foo, "bar", "Response's json body is { foo: 'bar' }"); -}, "Check static json() encodes JSON objects correctly"); - -test(function () { - assert_throws_js( - TypeError, - function () { - Response.json(Symbol("foo")); - }, - ); -}, "Check static json() throws when data is not encodable"); - -test(function () { - const a = { b: 1 }; - a.a = a; - assert_throws_js( - TypeError, - function () { - Response.json(a); - }, - ); -}, "Check static json() throws when data is circular"); - -promise_test(async function () { - class CustomError extends Error { - name = "CustomError"; - } - assert_throws_js( - CustomError, - function () { - Response.json({ get foo() { throw new CustomError("bar") }}); - } - ) -}, "Check static json() propagates JSON serializer errors"); - -const encodingChecks = [ - ["𝌆", [34, 240, 157, 140, 134, 34]], - ["\uDF06\uD834", [34, 92, 117, 100, 102, 48, 54, 92, 117, 100, 56, 51, 52, 34]], - ["\uDEAD", [34, 92, 117, 100, 101, 97, 100, 34]], -]; - -for (const [input, expected] of encodingChecks) { - promise_test(async function () { - const response = Response.json(input); - const buffer = await response.arrayBuffer(); - const data = new Uint8Array(buffer); - assert_array_equals(data, expected); - }, `Check response returned by static json() with input ${input}`); -} diff --git a/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js b/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js deleted file mode 100644 index b16c56d8300..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-static-redirect.any.js +++ /dev/null @@ -1,40 +0,0 @@ -// META: global=window,worker -// META: title=Response: redirect static method - -var url = "http://test.url:1234/"; -test(function() { - const redirectResponse = Response.redirect(url); - assert_equals(redirectResponse.type, "default"); - assert_false(redirectResponse.redirected); - assert_false(redirectResponse.ok); - assert_equals(redirectResponse.status, 302, "Default redirect status is 302"); - assert_equals(redirectResponse.headers.get("Location"), url, - "redirected response has Location header with the correct url"); - assert_equals(redirectResponse.statusText, ""); -}, "Check default redirect response"); - -[301, 302, 303, 307, 308].forEach(function(status) { - test(function() { - const redirectResponse = Response.redirect(url, status); - assert_equals(redirectResponse.type, "default"); - assert_false(redirectResponse.redirected); - assert_false(redirectResponse.ok); - assert_equals(redirectResponse.status, status, "Redirect status is " + status); - assert_equals(redirectResponse.headers.get("Location"), url); - assert_equals(redirectResponse.statusText, ""); - }, "Check response returned by static method redirect(), status = " + status); -}); - -test(function() { - var invalidUrl = "http://:This is not an url"; - assert_throws_js(TypeError, function() { Response.redirect(invalidUrl); }, - "Expect TypeError exception"); -}, "Check error returned when giving invalid url to redirect()"); - -var invalidRedirectStatus = [200, 309, 400, 500]; -invalidRedirectStatus.forEach(function(invalidStatus) { - test(function() { - assert_throws_js(RangeError, function() { Response.redirect(url, invalidStatus); }, - "Expect RangeError exception"); - }, "Check error returned when giving invalid status to redirect(), status = " + invalidStatus); -}); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-bad-chunk.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-bad-chunk.any.js deleted file mode 100644 index 8e83cd19087..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-bad-chunk.any.js +++ /dev/null @@ -1,25 +0,0 @@ -// META: global=window,worker -// META: title=Response causes TypeError from bad chunk type - -function runChunkTest(responseReaderMethod, testDescription) { - promise_test(test => { - let stream = new ReadableStream({ - start(controller) { - controller.enqueue("not Uint8Array"); - controller.close(); - } - }); - - return promise_rejects_js(test, TypeError, - new Response(stream)[responseReaderMethod](), - 'TypeError should propagate' - ) - }, testDescription) -} - -runChunkTest('arrayBuffer', 'ReadableStream with non-Uint8Array chunk passed to Response.arrayBuffer() causes TypeError'); -runChunkTest('blob', 'ReadableStream with non-Uint8Array chunk passed to Response.blob() causes TypeError'); -runChunkTest('bytes', 'ReadableStream with non-Uint8Array chunk passed to Response.bytes() causes TypeError'); -runChunkTest('formData', 'ReadableStream with non-Uint8Array chunk passed to Response.formData() causes TypeError'); -runChunkTest('json', 'ReadableStream with non-Uint8Array chunk passed to Response.json() causes TypeError'); -runChunkTest('text', 'ReadableStream with non-Uint8Array chunk passed to Response.text() causes TypeError'); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js deleted file mode 100644 index 64f65f16f23..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-1.any.js +++ /dev/null @@ -1,44 +0,0 @@ -// META: global=window,worker -// META: title=Consuming Response body after getting a ReadableStream -// META: script=./response-stream-disturbed-util.js - -async function createResponseWithReadableStream(bodySource, callback) { - const response = await responseFromBodySource(bodySource); - const reader = response.body.getReader(); - reader.releaseLock(); - return callback(response); -} - -for (const bodySource of ["fetch", "stream", "string"]) { - promise_test(function() { - return createResponseWithReadableStream(bodySource, function(response) { - return response.blob().then(function(blob) { - assert_true(blob instanceof Blob); - }); - }); - }, `Getting blob after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); - - promise_test(function() { - return createResponseWithReadableStream(bodySource, function(response) { - return response.text().then(function(text) { - assert_true(text.length > 0); - }); - }); - }, `Getting text after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); - - promise_test(function() { - return createResponseWithReadableStream(bodySource, function(response) { - return response.json().then(function(json) { - assert_equals(typeof json, "object"); - }); - }); - }, `Getting json after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); - - promise_test(function() { - return createResponseWithReadableStream(bodySource, function(response) { - return response.arrayBuffer().then(function(arrayBuffer) { - assert_true(arrayBuffer.byteLength > 0); - }); - }); - }, `Getting arrayBuffer after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); -} diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js deleted file mode 100644 index c46a180a18d..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-2.any.js +++ /dev/null @@ -1,35 +0,0 @@ -// META: global=window,worker -// META: title=Consuming Response body after getting a ReadableStream -// META: script=./response-stream-disturbed-util.js - -async function createResponseWithLockedReadableStream(bodySource, callback) { - const response = await responseFromBodySource(bodySource); - response.body.getReader(); - return callback(response); -} - -for (const bodySource of ["fetch", "stream", "string"]) { - promise_test(function(test) { - return createResponseWithLockedReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.blob()); - }); - }, `Getting blob after getting a locked Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithLockedReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.text()); - }); - }, `Getting text after getting a locked Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithLockedReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.json()); - }); - }, `Getting json after getting a locked Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithLockedReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.arrayBuffer()); - }); - }, `Getting arrayBuffer after getting a locked Response body (body source: ${bodySource})`); -} diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js deleted file mode 100644 index 35fb086469b..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-3.any.js +++ /dev/null @@ -1,36 +0,0 @@ -// META: global=window,worker -// META: title=Consuming Response body after getting a ReadableStream -// META: script=./response-stream-disturbed-util.js - -async function createResponseWithDisturbedReadableStream(bodySource, callback) { - const response = await responseFromBodySource(bodySource); - const reader = response.body.getReader(); - reader.read(); - return callback(response); -} - -for (const bodySource of ["fetch", "stream", "string"]) { - promise_test(function(test) { - return createResponseWithDisturbedReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.blob()); - }); - }, `Getting blob after reading the Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithDisturbedReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.text()); - }); - }, `Getting text after reading the Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithDisturbedReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.json()); - }); - }, `Getting json after reading the Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithDisturbedReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.arrayBuffer()); - }); - }, `Getting arrayBuffer after reading the Response body (body source: ${bodySource})`); -} diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js deleted file mode 100644 index 490672febd0..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-4.any.js +++ /dev/null @@ -1,35 +0,0 @@ -// META: global=window,worker -// META: title=Consuming Response body after getting a ReadableStream -// META: script=./response-stream-disturbed-util.js - -async function createResponseWithCancelledReadableStream(bodySource, callback) { - const response = await responseFromBodySource(bodySource); - response.body.cancel(); - return callback(response); -} - -for (const bodySource of ["fetch", "stream", "string"]) { - promise_test(function(test) { - return createResponseWithCancelledReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.blob()); - }); - }, `Getting blob after cancelling the Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithCancelledReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.text()); - }); - }, `Getting text after cancelling the Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithCancelledReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.json()); - }); - }, `Getting json after cancelling the Response body (body source: ${bodySource})`); - - promise_test(function(test) { - return createResponseWithCancelledReadableStream(bodySource, function(response) { - return promise_rejects_js(test, TypeError, response.arrayBuffer()); - }); - }, `Getting arrayBuffer after cancelling the Response body (body source: ${bodySource})`); -} diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js deleted file mode 100644 index 348fc393831..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-5.any.js +++ /dev/null @@ -1,19 +0,0 @@ -// META: global=window,worker -// META: title=Consuming Response body after getting a ReadableStream -// META: script=./response-stream-disturbed-util.js - -for (const bodySource of ["fetch", "stream", "string"]) { - for (const consumeAs of ["blob", "text", "json", "arrayBuffer"]) { - promise_test( - async () => { - const response = await responseFromBodySource(bodySource); - response[consumeAs](); - assert_not_equals(response.body, null); - assert_throws_js(TypeError, function () { - response.body.getReader(); - }); - }, - `Getting a body reader after consuming as ${consumeAs} (body source: ${bodySource})`, - ); - } -} diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js deleted file mode 100644 index 61d8544f078..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-6.any.js +++ /dev/null @@ -1,76 +0,0 @@ -// META: global=window,worker -// META: title=ReadableStream disturbed tests, via Response's bodyUsed property - -"use strict"; - -test(() => { - const stream = new ReadableStream(); - const response = new Response(stream); - assert_false(response.bodyUsed, "On construction"); - - const reader = stream.getReader(); - assert_false(response.bodyUsed, "After getting a reader"); - - reader.read(); - assert_true(response.bodyUsed, "After calling stream.read()"); -}, "A non-closed stream on which read() has been called"); - -test(() => { - const stream = new ReadableStream(); - const response = new Response(stream); - assert_false(response.bodyUsed, "On construction"); - - const reader = stream.getReader(); - assert_false(response.bodyUsed, "After getting a reader"); - - reader.cancel(); - assert_true(response.bodyUsed, "After calling stream.cancel()"); -}, "A non-closed stream on which cancel() has been called"); - -test(() => { - const stream = new ReadableStream({ - start(c) { - c.close(); - } - }); - const response = new Response(stream); - assert_false(response.bodyUsed, "On construction"); - - const reader = stream.getReader(); - assert_false(response.bodyUsed, "After getting a reader"); - - reader.read(); - assert_true(response.bodyUsed, "After calling stream.read()"); -}, "A closed stream on which read() has been called"); - -test(() => { - const stream = new ReadableStream({ - start(c) { - c.error(new Error("some error")); - } - }); - const response = new Response(stream); - assert_false(response.bodyUsed, "On construction"); - - const reader = stream.getReader(); - assert_false(response.bodyUsed, "After getting a reader"); - - reader.read().then(() => { }, () => { }); - assert_true(response.bodyUsed, "After calling stream.read()"); -}, "An errored stream on which read() has been called"); - -test(() => { - const stream = new ReadableStream({ - start(c) { - c.error(new Error("some error")); - } - }); - const response = new Response(stream); - assert_false(response.bodyUsed, "On construction"); - - const reader = stream.getReader(); - assert_false(response.bodyUsed, "After getting a reader"); - - reader.cancel().then(() => { }, () => { }); - assert_true(response.bodyUsed, "After calling stream.cancel()"); -}, "An errored stream on which cancel() has been called"); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js deleted file mode 100644 index 5341b75271e..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-by-pipe.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// META: global=window,worker - -test(() => { - const r = new Response(new ReadableStream()); - // highWaterMark: 0 means that nothing will actually be read from the body. - r.body.pipeTo(new WritableStream({}, {highWaterMark: 0})); - assert_true(r.bodyUsed, 'bodyUsed should be true'); -}, 'using pipeTo on Response body should disturb it synchronously'); - -test(() => { - const r = new Response(new ReadableStream()); - r.body.pipeThrough({ - writable: new WritableStream({}, {highWaterMark: 0}), - readable: new ReadableStream() - }); - assert_true(r.bodyUsed, 'bodyUsed should be true'); -}, 'using pipeThrough on Response body should disturb it synchronously'); diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-util.js b/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-util.js deleted file mode 100644 index 50bb586aa07..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-disturbed-util.js +++ /dev/null @@ -1,17 +0,0 @@ -const BODY = '{"key": "value"}'; - -function responseFromBodySource(bodySource) { - if (bodySource === "fetch") { - return fetch("../resources/data.json"); - } else if (bodySource === "stream") { - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(BODY)); - controller.close(); - }, - }); - return new Response(stream); - } else { - return new Response(BODY); - } -} diff --git a/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js b/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js deleted file mode 100644 index 8fef66c8a28..00000000000 --- a/test/fixtures/wpt/fetch/api/response/response-stream-with-broken-then.any.js +++ /dev/null @@ -1,117 +0,0 @@ -// META: global=window,worker -// META: script=../resources/utils.js - -promise_test(async () => { - // t.add_cleanup doesn't work when Object.prototype.then is overwritten, so - // these tests use add_completion_callback for cleanup instead. - add_completion_callback(() => delete Object.prototype.then); - const hello = new TextEncoder().encode('hello'); - const bye = new TextEncoder().encode('bye'); - const rs = new ReadableStream({ - start(controller) { - controller.enqueue(hello); - controller.close(); - } - }); - const resp = new Response(rs); - Object.prototype.then = (onFulfilled) => { - delete Object.prototype.then; - onFulfilled({done: false, value: bye}); - }; - const text = await resp.text(); - delete Object.prototype.then; - assert_equals(text, 'hello', 'The value should be "hello".'); -}, 'Attempt to inject {done: false, value: bye} via Object.prototype.then.'); - -promise_test(async (t) => { - add_completion_callback(() => delete Object.prototype.then); - const hello = new TextEncoder().encode('hello'); - const rs = new ReadableStream({ - start(controller) { - controller.enqueue(hello); - controller.close(); - } - }); - const resp = new Response(rs); - Object.prototype.then = (onFulfilled) => { - delete Object.prototype.then; - onFulfilled({done: false, value: undefined}); - }; - const text = await resp.text(); - delete Object.prototype.then; - assert_equals(text, 'hello', 'The value should be "hello".'); -}, 'Attempt to inject value: undefined via Object.prototype.then.'); - -promise_test(async (t) => { - add_completion_callback(() => delete Object.prototype.then); - const hello = new TextEncoder().encode('hello'); - const rs = new ReadableStream({ - start(controller) { - controller.enqueue(hello); - controller.close(); - } - }); - const resp = new Response(rs); - Object.prototype.then = (onFulfilled) => { - delete Object.prototype.then; - onFulfilled(undefined); - }; - const text = await resp.text(); - delete Object.prototype.then; - assert_equals(text, 'hello', 'The value should be "hello".'); -}, 'Attempt to inject undefined via Object.prototype.then.'); - -promise_test(async (t) => { - add_completion_callback(() => delete Object.prototype.then); - const hello = new TextEncoder().encode('hello'); - const rs = new ReadableStream({ - start(controller) { - controller.enqueue(hello); - controller.close(); - } - }); - const resp = new Response(rs); - Object.prototype.then = (onFulfilled) => { - delete Object.prototype.then; - onFulfilled(8.2); - }; - const text = await resp.text(); - delete Object.prototype.then; - assert_equals(text, 'hello', 'The value should be "hello".'); -}, 'Attempt to inject 8.2 via Object.prototype.then.'); - -promise_test(async () => { - add_completion_callback(() => delete Object.prototype.then); - const hello = new TextEncoder().encode('hello'); - const bye = new TextEncoder().encode('bye'); - const resp = new Response(hello); - Object.prototype.then = (onFulfilled) => { - delete Object.prototype.then; - onFulfilled({done: false, value: bye}); - }; - const text = await resp.text(); - delete Object.prototype.then; - assert_equals(text, 'hello', 'The value should be "hello".'); -}, 'intercepting arraybuffer to text conversion via Object.prototype.then ' + - 'should not be possible'); - -promise_test(async () => { - add_completion_callback(() => delete Object.prototype.then); - const u8a123 = new Uint8Array([1, 2, 3]); - const u8a456 = new Uint8Array([4, 5, 6]); - const resp = new Response(u8a123); - const writtenBytes = []; - const ws = new WritableStream({ - write(chunk) { - writtenBytes.push(...Array.from(chunk)); - } - }); - Object.prototype.then = (onFulfilled) => { - delete Object.prototype.then; - onFulfilled({done: false, value: u8a456}); - }; - await resp.body.pipeTo(ws); - delete Object.prototype.then; - assert_array_equals(writtenBytes, u8a123, 'The value should be [1, 2, 3]'); -}, 'intercepting arraybuffer to body readable stream conversion via ' + - 'Object.prototype.then should not be possible'); diff --git a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-cache.tentative.https.html b/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-cache.tentative.https.html deleted file mode 100644 index c8bcf7fdf12..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-cache.tentative.https.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-cookies.tentative.https.html b/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-cookies.tentative.https.html deleted file mode 100644 index aa1673e88c8..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-cookies.tentative.https.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-storage.tentative.https.html b/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-storage.tentative.https.html deleted file mode 100644 index 22747eb6565..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-clear-site-data-storage.tentative.https.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-decompression.tentative.https.html b/test/fixtures/wpt/fetch/compression-dictionary/dictionary-decompression.tentative.https.html deleted file mode 100644 index 33aeb4466ba..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-decompression.tentative.https.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-fetch-with-link-element.tentative.https.html b/test/fixtures/wpt/fetch/compression-dictionary/dictionary-fetch-with-link-element.tentative.https.html deleted file mode 100644 index d465ceb3d85..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-fetch-with-link-element.tentative.https.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html b/test/fixtures/wpt/fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html deleted file mode 100644 index 007067d71ad..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-fetch-with-link-header.tentative.https.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-registration.tentative.https.html b/test/fixtures/wpt/fetch/compression-dictionary/dictionary-registration.tentative.https.html deleted file mode 100644 index f0782aff3bd..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/dictionary-registration.tentative.https.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/compression-dictionary/resources/clear-site-data.py b/test/fixtures/wpt/fetch/compression-dictionary/resources/clear-site-data.py deleted file mode 100644 index 0db51bf797f..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/resources/clear-site-data.py +++ /dev/null @@ -1,4 +0,0 @@ -def main(request, response): - directive = request.GET.first(b"directive") - response.headers.set(b"Clear-Site-Data", b"\"" + directive + b"\"") - return b"OK" diff --git a/test/fixtures/wpt/fetch/compression-dictionary/resources/compressed-data.py b/test/fixtures/wpt/fetch/compression-dictionary/resources/compressed-data.py deleted file mode 100644 index bb9d7fe8466..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/resources/compressed-data.py +++ /dev/null @@ -1,31 +0,0 @@ -def main(request, response): - response.headers.set(b"Access-Control-Allow-Origin", b"*") - response.headers.set(b"Content-Type", b"text/plain") - - # `dcb_data` and `dcz_data` are generated using the following commands: - # - # $ echo "This is a test dictionary." > /tmp/dict - # $ echo -n "This is compressed test data using a test dictionary" \ - # > /tmp/data - # - # $ echo -en '\xffDCB' > /tmp/out.dcb - # $ openssl dgst -sha256 -binary /tmp/dict >> /tmp/out.dcb - # $ brotli --stdout -D /tmp/dict /tmp/data >> /tmp/out.dcb - # $ xxd -p /tmp/out.dcb | tr -d '\n' | sed 's/\(..\)/\\x\1/g' - dcb_data = b"\xff\x44\x43\x42\x53\x96\x9b\xcf\x5e\x96\x0e\x0e\xdb\xf0\xa4\xbd\xde\x6b\x0b\x3e\x93\x81\xe1\x56\xde\x7f\x5b\x91\xce\x83\x91\x62\x42\x70\xf4\x16\xa1\x98\x01\x80\x62\xa4\x4c\x1d\xdf\x12\x84\x8c\xae\xc2\xca\x60\x22\x07\x6e\x81\x05\x14\xc9\xb7\xc3\x44\x8e\xbc\x16\xe0\x15\x0e\xec\xc1\xee\x34\x33\x3e\x0d" - # $ echo -en '\x5e\x2a\x4d\x18\x20\x00\x00\x00' > /tmp/out.dcz - # $ openssl dgst -sha256 -binary /tmp/dict >> /tmp/out.dcz - # $ zstd -D /tmp/dict -f -o /tmp/tmp.zstd /tmp/data - # $ cat /tmp/tmp.zstd >> /tmp/out.dcz - # $ xxd -p /tmp/out.dcz | tr -d '\n' | sed 's/\(..\)/\\x\1/g' - dcz_data = b"\x5e\x2a\x4d\x18\x20\x00\x00\x00\x53\x96\x9b\xcf\x5e\x96\x0e\x0e\xdb\xf0\xa4\xbd\xde\x6b\x0b\x3e\x93\x81\xe1\x56\xde\x7f\x5b\x91\xce\x83\x91\x62\x42\x70\xf4\x16\x28\xb5\x2f\xfd\x24\x34\xf5\x00\x00\x98\x63\x6f\x6d\x70\x72\x65\x73\x73\x65\x64\x61\x74\x61\x20\x75\x73\x69\x6e\x67\x03\x00\x59\xf9\x73\x54\x46\x27\x26\x10\x9e\x99\xf2\xbc" - - if b'content_encoding' in request.GET: - content_encoding = request.GET.first(b"content_encoding") - response.headers.set(b"Content-Encoding", content_encoding) - if content_encoding == b"dcb": - # Send the pre compressed file - response.content = dcb_data - if content_encoding == b"dcz": - # Send the pre compressed file - response.content = dcz_data diff --git a/test/fixtures/wpt/fetch/compression-dictionary/resources/compression-dictionary-util.js b/test/fixtures/wpt/fetch/compression-dictionary/resources/compression-dictionary-util.js deleted file mode 100644 index 7d86f594a80..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/resources/compression-dictionary-util.js +++ /dev/null @@ -1,120 +0,0 @@ - -const kDefaultDictionaryContent = 'This is a test dictionary.\n'; -const kDefaultDictionaryHashBase64 = - ':U5abz16WDg7b8KS93msLPpOB4Vbef1uRzoORYkJw9BY=:'; -const kRegisterDictionaryPath = './resources/register-dictionary.py'; -const kCompressedDataPath = './resources/compressed-data.py'; -const kExpectedCompressedData = - `This is compressed test data using a test dictionary`; -const kCheckAvailableDictionaryHeaderMaxRetry = 10; -const kCheckAvailableDictionaryHeaderRetryTimeout = 200; -const kCheckPreviousRequestHeadersMaxRetry = 5; -const kCheckPreviousRequestHeadersRetryTimeout = 250; - -// Gets the remote URL corresponding to `relative_path`. -function getRemoteHostUrl(relative_path) { - const remote_origin = new URL(get_host_info().HTTPS_REMOTE_ORIGIN); - let result = new URL(relative_path, location.href); - result.protocol = remote_origin.protocol; - result.hostname = remote_origin.hostname; - result.port = remote_origin.port; - return result.href; -} - -// Calculates the Structured Field Byte Sequence containing the SHA-256 hash of -// the contents of the dictionary text. -async function calculateDictionaryHash(dictionary_text) { - const encoded = (new TextEncoder()).encode(dictionary_text); - const digest = await crypto.subtle.digest('SHA-256', encoded) - return ':' + btoa(String.fromCharCode(...new Uint8Array(digest))) + ':'; -} - -// Checks the HTTP request headers which is sent to the server. -async function checkHeaders(check_remote = false) { - let url = './resources/echo-headers.py'; - if (check_remote) { - url = getRemoteHostUrl(url); - } - return await (await fetch(url)).json(); -} - -// Checks the "available-dictionary" header in the HTTP request headers. -async function checkAvailableDictionaryHeader(check_remote = false) { - return (await checkHeaders(check_remote))['available-dictionary']; -} - -// Waits until the "available-dictionary" header is available in the HTTP -// request headers, and returns the header. If the header is not available after -// the specified number of retries, returns an error message. If the -// `expected_header` is specified, this method waits until the header is -// available and matches the `expected_header`. -async function waitUntilAvailableDictionaryHeader(test, { - max_retry = kCheckAvailableDictionaryHeaderMaxRetry, - expected_header = undefined, - check_remote = false -}) { - for (let retry_count = 0; retry_count <= max_retry; retry_count++) { - const header = await checkAvailableDictionaryHeader(check_remote); - if (header) { - if (expected_header === undefined || header == expected_header) { - return header; - } - } - await new Promise( - (resolve) => test.step_timeout( - resolve, kCheckAvailableDictionaryHeaderRetryTimeout)); - } - return '"available-dictionary" header is not available'; -} - -// Checks the HTTP request headers which was sent to the server with `token` -// to register a dictionary. -async function checkPreviousRequestHeaders(token, check_remote = false) { - let url = `./resources/register-dictionary.py?get_previous_header=${token}`; - if (check_remote) { - url = getRemoteHostUrl(url); - } - return await (await fetch(url)).json(); -} - -// Waits until the HTTP request headers which was sent to the server with -// `token` to register a dictionary is available, and returns the header. If the -// header is not available after the specified number of retries, returns -// `undefined`. -async function waitUntilPreviousRequestHeaders( - test, token, check_remote = false) { - for (let retry_count = 0; retry_count <= kCheckPreviousRequestHeadersMaxRetry; - retry_count++) { - const header = - (await checkPreviousRequestHeaders(token, check_remote))['headers']; - if (header) { - return header; - } - await new Promise( - (resolve) => test.step_timeout( - resolve, kCheckPreviousRequestHeadersRetryTimeout)); - } - return undefined; -} - -// Clears the site data for the specified directive by sending a request to -// `./resources/clear-site-data.py` which returns `Clear-Site-Data` response -// header. -// Note: When `directive` is 'cache' or 'cookies' is specified, registered -// compression dictionaries should be also cleared. -async function clearSiteData(directive = 'cache') { - return await (await fetch( - `./resources/clear-site-data.py?directive=${directive}`)) - .text(); -} - -// A utility test method that adds the `clearSiteData()` method to the -// testharness cleanup function. This is intended to ensure that registered -// dictionaries are cleared in tests and that registered dictionaries do not -// interfere with subsequent tests. -function compression_dictionary_promise_test(func, name, properties) { - promise_test(async (test) => { - test.add_cleanup(clearSiteData); - await func(test); - }, name, properties); -} diff --git a/test/fixtures/wpt/fetch/compression-dictionary/resources/echo-headers.py b/test/fixtures/wpt/fetch/compression-dictionary/resources/echo-headers.py deleted file mode 100644 index aabd99eb101..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/resources/echo-headers.py +++ /dev/null @@ -1,10 +0,0 @@ -import json - -def main(request, response): - response.headers.set(b"Access-Control-Allow-Origin", b"*") - headers = {} - for header in request.headers: - key = header.decode('utf-8') - value = request.headers.get(header).decode('utf-8') - headers[key] = value - return json.dumps(headers) diff --git a/test/fixtures/wpt/fetch/compression-dictionary/resources/empty.html b/test/fixtures/wpt/fetch/compression-dictionary/resources/empty.html deleted file mode 100644 index 0e76edd65b7..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/resources/empty.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/fixtures/wpt/fetch/compression-dictionary/resources/register-dictionary.py b/test/fixtures/wpt/fetch/compression-dictionary/resources/register-dictionary.py deleted file mode 100644 index 0bd57225ef1..00000000000 --- a/test/fixtures/wpt/fetch/compression-dictionary/resources/register-dictionary.py +++ /dev/null @@ -1,37 +0,0 @@ -import json - -def main(request, response): - response.headers.set(b"Access-Control-Allow-Origin", b"*") - match = b"/fetch/compression-dictionary/resources/*" - content = b"This is a test dictionary.\n" - if b"match" in request.GET: - match = request.GET.first(b"match") - if b"content" in request.GET: - content = request.GET.first(b"content") - - token = request.GET.first(b"save_header", None) - if token is not None: - headers = {} - for header in request.headers: - key = header.decode('utf-8') - value = request.headers.get(header).decode('utf-8') - headers[key] = value - with request.server.stash.lock: - request.server.stash.put(token, json.dumps(headers)) - - previous_token = request.GET.first(b"get_previous_header", None) - if previous_token is not None: - result = {} - with request.server.stash.lock: - store = request.server.stash.take(previous_token) - if store is not None: - headers = json.loads(store) - result["headers"] = headers - return json.dumps(result) - - options = b"match=\"" + match + b"\"" - if b"id" in request.GET: - options += b", id=\"" + request.GET.first(b"id") + b"\"" - response.headers.set(b"Use-As-Dictionary", options) - response.headers.set(b"Cache-Control", b"max-age=3600") - return content diff --git a/test/fixtures/wpt/fetch/connection-pool/network-partition-key.html b/test/fixtures/wpt/fetch/connection-pool/network-partition-key.html deleted file mode 100644 index 60a784cd84e..00000000000 --- a/test/fixtures/wpt/fetch/connection-pool/network-partition-key.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - - Connection partitioning by site - - - - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-about-blank-checker.html b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-about-blank-checker.html deleted file mode 100644 index 7a8b6132375..00000000000 --- a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-about-blank-checker.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - about:blank Network Partition Checker - - - - - - - diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-checker.html b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-checker.html deleted file mode 100644 index b058f611242..00000000000 --- a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-checker.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - Network Partition Checker - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-iframe-checker.html b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-iframe-checker.html deleted file mode 100644 index f76ed184471..00000000000 --- a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-iframe-checker.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Iframe Network Partition Checker - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.js b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.js deleted file mode 100644 index bd66109380f..00000000000 --- a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.js +++ /dev/null @@ -1,47 +0,0 @@ -// Runs multiple fetches that validate connections see only a single partition_id. -// Requests are run in parallel so that they use multiple connections to maximize the -// chance of exercising all matching connections in the connection pool. Only returns -// once all requests have completed to make cleaning up server state non-racy. -function check_partition_ids(location) { - const NUM_FETCHES = 20; - - var base_url = 'SUBRESOURCE_PREFIX:&dispatch=check_partition'; - - // Not a perfect parse of the query string, but good enough for this test. - var include_credentials = base_url.search('include_credentials=true') != -1; - var exclude_credentials = base_url.search('include_credentials=false') != -1; - if (include_credentials != !exclude_credentials) - throw new Exception('Credentials mode not specified'); - - - // Run NUM_FETCHES in parallel. - var fetches = []; - for (i = 0; i < NUM_FETCHES; ++i) { - var fetch_params = { - credentials: 'omit', - mode: 'cors', - headers: { - 'Header-To-Force-CORS': 'cors' - }, - }; - - // Use a unique URL for each request, in case the caching layer serializes multiple - // requests for the same URL. - var url = `${base_url}&${token()}`; - - fetches.push(fetch(url, fetch_params).then( - function (response) { - return response.text().then(function(text) { - assert_equals(text, 'ok', `Socket unexpectedly reused`); - }); - })); - } - - // Wait for all promises to complete. - return Promise.allSettled(fetches).then(function (results) { - results.forEach(function (result) { - if (result.status != 'fulfilled') - throw result.reason; - }); - }); -} diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.py b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.py deleted file mode 100644 index 32fe4999b7d..00000000000 --- a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-key.py +++ /dev/null @@ -1,130 +0,0 @@ -import mimetypes -import os - -from wptserve.utils import isomorphic_decode, isomorphic_encode - -# Test server that tracks the last partition_id was used with each connection for each uuid, and -# lets consumers query if multiple different partition_ids have been been used for any socket. -# -# Server assumes that ports aren't reused, so a client address and a server port uniquely identify -# a connection. If that constraint is ever violated, the test will be flaky. No sockets being -# closed for the duration of the test is sufficient to ensure that, though even if sockets are -# closed, the OS should generally prefer to use new ports for new connections, if any are -# available. -def main(request, response): - response.headers.set(b"Cache-Control", b"no-store") - dispatch = request.GET.first(b"dispatch", None) - uuid = request.GET.first(b"uuid", None) - partition_id = request.GET.first(b"partition_id", None) - - if not uuid or not dispatch or not partition_id: - return simple_response(request, response, 404, b"Not found", b"Invalid query parameters") - - # Unless nocheck_partition is true, check partition_id against server_state, and update server_state. - stash = request.server.stash - test_failed = False - request_count = 0; - connection_count = 0; - if request.GET.first(b"nocheck_partition", None) != b"True": - # Need to grab the lock to access the Stash, since requests are made in parallel. - with stash.lock: - # Don't use server hostname here, since H2 allows multiple hosts to reuse a connection. - # Server IP is not currently available, unfortunately. - address_key = isomorphic_encode(str(request.client_address) + u"|" + str(request.url_parts.port)) - server_state = stash.take(uuid) or {b"test_failed": False, - b"request_count": 0, b"connection_count": 0} - request_count = server_state[b"request_count"] - request_count += 1 - server_state[b"request_count"] = request_count - if address_key in server_state: - if server_state[address_key] != partition_id: - server_state[b"test_failed"] = True - else: - connection_count = server_state[b"connection_count"] - connection_count += 1 - server_state[b"connection_count"] = connection_count - server_state[address_key] = partition_id - test_failed = server_state[b"test_failed"] - stash.put(uuid, server_state) - - origin = request.headers.get(b"Origin") - if origin: - response.headers.set(b"Access-Control-Allow-Origin", origin) - response.headers.set(b"Access-Control-Allow-Credentials", b"true") - - if request.method == u"OPTIONS": - return handle_preflight(request, response) - - if dispatch == b"fetch_file": - return handle_fetch_file(request, response, partition_id, uuid) - - if dispatch == b"check_partition": - status = request.GET.first(b"status", 200) - if test_failed: - return simple_response(request, response, status, b"OK", b"Multiple partition IDs used on a socket") - body = b"ok" - if request.GET.first(b"addcounter", False): - body += (". Request was sent " + str(request_count) + " times. " + - str(connection_count) + " connections were created.").encode('utf-8') - return simple_response(request, response, status, b"OK", body) - - if dispatch == b"clean_up": - stash.take(uuid) - if test_failed: - return simple_response(request, response, 200, b"OK", b"Test failed, but cleanup completed.") - return simple_response(request, response, 200, b"OK", b"cleanup complete") - - return simple_response(request, response, 404, b"Not Found", b"Unrecognized dispatch parameter: " + dispatch) - -def handle_preflight(request, response): - response.status = (200, b"OK") - response.headers.set(b"Access-Control-Allow-Methods", b"GET") - response.headers.set(b"Access-Control-Allow-Headers", b"header-to-force-cors") - response.headers.set(b"Access-Control-Max-Age", b"86400") - return b"Preflight request" - -def simple_response(request, response, status_code, status_message, body, content_type=b"text/plain"): - response.status = (status_code, status_message) - response.headers.set(b"Content-Type", content_type) - return body - -def handle_fetch_file(request, response, partition_id, uuid): - subresource_origin = request.GET.first(b"subresource_origin", None) - rel_path = request.GET.first(b"path", None) - - # This needs to be passed on to subresources so they all have access to it. - include_credentials = request.GET.first(b"include_credentials", None) - if not subresource_origin or not rel_path or not include_credentials: - return simple_response(request, response, 404, b"Not found", b"Invalid query parameters") - - cur_path = os.path.realpath(isomorphic_decode(__file__)) - base_path = os.path.abspath(os.path.join(os.path.dirname(cur_path), os.pardir, os.pardir, os.pardir)) - path = os.path.abspath(os.path.join(base_path, isomorphic_decode(rel_path))) - - # Basic security check. - if not path.startswith(base_path): - return simple_response(request, response, 404, b"Not found", b"Invalid path") - - sandbox = request.GET.first(b"sandbox", None) - if sandbox == b"true": - response.headers.set(b"Content-Security-Policy", b"sandbox allow-scripts") - - file = open(path, mode="rb") - body = file.read() - file.close() - - subresource_path = b"/" + isomorphic_encode(os.path.relpath(isomorphic_decode(__file__), base_path)).replace(b'\\', b'/') - subresource_params = b"?partition_id=" + partition_id + b"&uuid=" + uuid + b"&subresource_origin=" + subresource_origin + b"&include_credentials=" + include_credentials - body = body.replace(b"SUBRESOURCE_PREFIX:", subresource_origin + subresource_path + subresource_params) - - other_origin = request.GET.first(b"other_origin", None) - if other_origin: - body = body.replace(b"OTHER_PREFIX:", other_origin + subresource_path + subresource_params) - - mimetypes.init() - mimetype_pair = mimetypes.guess_type(path) - mimetype = mimetype_pair[0] - - if mimetype == None or mimetype_pair[1] != None: - return simple_response(request, response, 500, b"Server Error", b"Unknown MIME type") - return simple_response(request, response, 200, b"OK", body, mimetype) diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker-checker.html b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker-checker.html deleted file mode 100644 index e6b7ea7673f..00000000000 --- a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker-checker.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - Worker Network Partition Checker - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker.js b/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker.js deleted file mode 100644 index 1745edfacb1..00000000000 --- a/test/fixtures/wpt/fetch/connection-pool/resources/network-partition-worker.js +++ /dev/null @@ -1,15 +0,0 @@ -// This tests the partition key of fetches to subresouce_origin made by the worker and -// imported scripts from subresource_origin. -importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js'); -importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js'); -importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js'); - -async function fetch_and_reply() { - try { - await check_partition_ids(); - self.postMessage({result: 'success'}); - } catch (e) { - self.postMessage({result: 'error', details: e.message}); - } -} -fetch_and_reply(); diff --git a/test/fixtures/wpt/fetch/content-encoding/br/bad-br-body.https.any.js b/test/fixtures/wpt/fetch/content-encoding/br/bad-br-body.https.any.js deleted file mode 100644 index af5df674da6..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/bad-br-body.https.any.js +++ /dev/null @@ -1,17 +0,0 @@ -// META: global=window - -[ - "arrayBuffer", - "blob", - "bytes", - "formData", - "json", - "text" -].forEach(method => { - promise_test(t => { - return fetch("resources/bad-br-body.py").then(res => { - assert_equals(res.status, 200); - return promise_rejects_js(t, TypeError, res[method]()); - }); - }, "Consuming the body of a resource with bad br content with " + method + "() should reject"); -}); diff --git a/test/fixtures/wpt/fetch/content-encoding/br/big-br-body.https.any.js b/test/fixtures/wpt/fetch/content-encoding/br/big-br-body.https.any.js deleted file mode 100644 index 1427dd73026..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/big-br-body.https.any.js +++ /dev/null @@ -1,55 +0,0 @@ -// META: global=window,worker - -const EXPECTED_SIZE = 27000000; -const EXPECTED_SHA256 = [ - 74, 100, 37, 243, 147, 61, 116, 60, 241, 221, 126, - 18, 24, 71, 204, 28, 50, 62, 201, 130, 152, 225, - 217, 183, 10, 201, 143, 214, 102, 155, 212, 248, - ]; - -promise_test(async () => { - const response = await fetch('resources/big.text.br'); - assert_true(response.ok); - const arrayBuffer = await response.arrayBuffer(); - assert_equals(arrayBuffer.byteLength, EXPECTED_SIZE, - 'uncompressed size should match'); - const sha256 = await crypto.subtle.digest('SHA-256', arrayBuffer); - assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, - 'digest should match'); -}, 'large br data should be decompressed successfully'); - -promise_test(async () => { - const response = await fetch('resources/big.text.br'); - assert_true(response.ok); - const reader = response.body.getReader({mode: 'byob'}); - let offset = 0; - // Pre-allocate space for the output. The response body will be read - // chunk-by-chunk into this array. - let ab = new ArrayBuffer(EXPECTED_SIZE); - while (offset < EXPECTED_SIZE) { - // To stress the data pipe, we want to use a different size read each - // time. Unfortunately, JavaScript doesn't have a seeded random number - // generator, so this creates the possibility of making this test flaky if - // it doesn't work for some edge cases. - let size = Math.floor(Math.random() * 65535 + 1); - if (size + offset > EXPECTED_SIZE) { - size = EXPECTED_SIZE - offset; - } - const u8 = new Uint8Array(ab, offset, size); - const { value, done } = await reader.read(u8); - ab = value.buffer; - // Check that we got our original array back. - assert_equals(ab.byteLength, EXPECTED_SIZE, - 'backing array should be the same size'); - assert_equals(offset, value.byteOffset, 'offset should match'); - assert_less_than_equal(value.byteLength, size, - 'we should not have got more than we asked for'); - offset = value.byteOffset + value.byteLength; - if (done) break; - } - assert_equals(offset, EXPECTED_SIZE, - 'we should have read the whole thing'); - const sha256 = await crypto.subtle.digest('SHA-256', new Uint8Array(ab)); - assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, - 'digest should match'); -}, 'large br data should be decompressed successfully with byte stream'); diff --git a/test/fixtures/wpt/fetch/content-encoding/br/br-body.https.any.js b/test/fixtures/wpt/fetch/content-encoding/br/br-body.https.any.js deleted file mode 100644 index 2c2dbb5d293..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/br-body.https.any.js +++ /dev/null @@ -1,15 +0,0 @@ -// META: global=window,worker - -const expectedDecompressedSize = 10500; -[ - "text", - "octetstream" -].forEach(contentType => { - promise_test(async t => { - let response = await fetch(`resources/foo.${contentType}.br`); - assert_true(response.ok); - let arrayBuffer = await response.arrayBuffer() - let u8 = new Uint8Array(arrayBuffer); - assert_equals(u8.length, expectedDecompressedSize); - }, `fetched br data with content type ${contentType} should be decompressed.`); -}); diff --git a/test/fixtures/wpt/fetch/content-encoding/br/br-navigation.https.window.js b/test/fixtures/wpt/fetch/content-encoding/br/br-navigation.https.window.js deleted file mode 100644 index 6fbb42ad3ab..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/br-navigation.https.window.js +++ /dev/null @@ -1,6 +0,0 @@ -// META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js - -promise_test(async t => { - const frame = await with_iframe('./resources/hello.html.br'); - assert_equals(frame.contentDocument.body.textContent, 'Hello world'); -}, `Naigation to br encoded page`); diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/bad-br-body.py b/test/fixtures/wpt/fetch/content-encoding/br/resources/bad-br-body.py deleted file mode 100644 index 0710e7ffdee..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/resources/bad-br-body.py +++ /dev/null @@ -1,3 +0,0 @@ -def main(request, response): - headers = [(b"Content-Encoding", b"br")] - return headers, b"not actually br" diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/big.text.br b/test/fixtures/wpt/fetch/content-encoding/br/resources/big.text.br deleted file mode 100644 index b3a530d757d..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/br/resources/big.text.br and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/big.text.br.headers b/test/fixtures/wpt/fetch/content-encoding/br/resources/big.text.br.headers deleted file mode 100644 index aba00bd5d4a..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/resources/big.text.br.headers +++ /dev/null @@ -1,3 +0,0 @@ -Content-type: text/plain -Content-Encoding: br -Cache-Control: no-store diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.octetstream.br b/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.octetstream.br deleted file mode 100644 index 30cb2f7095e..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.octetstream.br and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.octetstream.br.headers b/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.octetstream.br.headers deleted file mode 100644 index c0c19bc82af..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.octetstream.br.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: application/octet-stream -Content-Encoding: br diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.text.br b/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.text.br deleted file mode 100644 index 30cb2f7095e..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.text.br and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.text.br.headers b/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.text.br.headers deleted file mode 100644 index 8c03b823e09..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/resources/foo.text.br.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: text/plain -Content-Encoding: br diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/hello.html.br b/test/fixtures/wpt/fetch/content-encoding/br/resources/hello.html.br deleted file mode 100644 index eff82bcb41e..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/br/resources/hello.html.br and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/br/resources/hello.html.br.headers b/test/fixtures/wpt/fetch/content-encoding/br/resources/hello.html.br.headers deleted file mode 100644 index c001132850f..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/br/resources/hello.html.br.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: text/html -Content-Encoding: br diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/bad-gzip-body.any.js b/test/fixtures/wpt/fetch/content-encoding/gzip/bad-gzip-body.any.js deleted file mode 100644 index 77a183f408b..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/bad-gzip-body.any.js +++ /dev/null @@ -1,23 +0,0 @@ -// META: global=window,worker - -promise_test((test) => { - return fetch("resources/bad-gzip-body.py").then(res => { - assert_equals(res.status, 200); - }); -}, "Fetching a resource with bad gzip content should still resolve"); - -[ - "arrayBuffer", - "blob", - "bytes", - "formData", - "json", - "text" -].forEach(method => { - promise_test(t => { - return fetch("resources/bad-gzip-body.py").then(res => { - assert_equals(res.status, 200); - return promise_rejects_js(t, TypeError, res[method]()); - }); - }, "Consuming the body of a resource with bad gzip content with " + method + "() should reject"); -}); diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/big-gzip-body.https.any.js b/test/fixtures/wpt/fetch/content-encoding/gzip/big-gzip-body.https.any.js deleted file mode 100644 index b5d62c98045..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/big-gzip-body.https.any.js +++ /dev/null @@ -1,55 +0,0 @@ -// META: global=window,worker - -const EXPECTED_SIZE = 27000000; -const EXPECTED_SHA256 = [ - 74, 100, 37, 243, 147, 61, 116, 60, 241, 221, 126, - 18, 24, 71, 204, 28, 50, 62, 201, 130, 152, 225, - 217, 183, 10, 201, 143, 214, 102, 155, 212, 248, - ]; - -promise_test(async () => { - const response = await fetch('resources/big.text.gz'); - assert_true(response.ok); - const arrayBuffer = await response.arrayBuffer(); - assert_equals(arrayBuffer.byteLength, EXPECTED_SIZE, - 'uncompressed size should match'); - const sha256 = await crypto.subtle.digest('SHA-256', arrayBuffer); - assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, - 'digest should match'); -}, 'large gzip data should be decompressed successfully'); - -promise_test(async () => { - const response = await fetch('resources/big.text.gz'); - assert_true(response.ok); - const reader = response.body.getReader({mode: 'byob'}); - let offset = 0; - // Pre-allocate space for the output. The response body will be read - // chunk-by-chunk into this array. - let ab = new ArrayBuffer(EXPECTED_SIZE); - while (offset < EXPECTED_SIZE) { - // To stress the data pipe, we want to use a different size read each - // time. Unfortunately, JavaScript doesn't have a seeded random number - // generator, so this creates the possibility of making this test flaky if - // it doesn't work for some edge cases. - let size = Math.floor(Math.random() * 65535 + 1); - if (size + offset > EXPECTED_SIZE) { - size = EXPECTED_SIZE - offset; - } - const u8 = new Uint8Array(ab, offset, size); - const { value, done } = await reader.read(u8); - ab = value.buffer; - // Check that we got our original array back. - assert_equals(ab.byteLength, EXPECTED_SIZE, - 'backing array should be the same size'); - assert_equals(offset, value.byteOffset, 'offset should match'); - assert_less_than_equal(value.byteLength, size, - 'we should not have got more than we asked for'); - offset = value.byteOffset + value.byteLength; - if (done) break; - } - assert_equals(offset, EXPECTED_SIZE, - 'we should have read the whole thing'); - const sha256 = await crypto.subtle.digest('SHA-256', new Uint8Array(ab)); - assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, - 'digest should match'); -}, 'large gzip data should be decompressed successfully with byte stream'); diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/gzip-body.any.js b/test/fixtures/wpt/fetch/content-encoding/gzip/gzip-body.any.js deleted file mode 100644 index 37758b7d917..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/gzip-body.any.js +++ /dev/null @@ -1,16 +0,0 @@ -// META: global=window,worker - -const expectedDecompressedSize = 10500; -[ - "text", - "octetstream" -].forEach(contentType => { - promise_test(async t => { - let response = await fetch(`resources/foo.${contentType}.gz`); - assert_true(response.ok); - let arrayBuffer = await response.arrayBuffer() - let u8 = new Uint8Array(arrayBuffer); - assert_equals(u8.length, expectedDecompressedSize); - }, `fetched gzip data with content type ${contentType} should be decompressed.`); -}); - diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/gzip-navigation.https.window.js b/test/fixtures/wpt/fetch/content-encoding/gzip/gzip-navigation.https.window.js deleted file mode 100644 index 0ef68766098..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/gzip-navigation.https.window.js +++ /dev/null @@ -1,6 +0,0 @@ -// META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js - -promise_test(async t => { - const frame = await with_iframe('./resources/hello.html.gz'); - assert_equals(frame.contentDocument.body.textContent, 'Hello world'); -}, `Naigation to gzip encoded page`); diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/bad-gzip-body.py b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/bad-gzip-body.py deleted file mode 100644 index a79b94ed041..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/bad-gzip-body.py +++ /dev/null @@ -1,3 +0,0 @@ -def main(request, response): - headers = [(b"Content-Encoding", b"gzip")] - return headers, b"not actually gzip" diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/big.text.gz b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/big.text.gz deleted file mode 100644 index 13441bc3998..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/big.text.gz and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/big.text.gz.headers b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/big.text.gz.headers deleted file mode 100644 index 55d2345c239..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/big.text.gz.headers +++ /dev/null @@ -1,3 +0,0 @@ -Content-type: text/plain -Content-Encoding: gzip -Cache-Control: no-store diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.octetstream.gz b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.octetstream.gz deleted file mode 100644 index f3df4cb89b5..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.octetstream.gz and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.octetstream.gz.headers b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.octetstream.gz.headers deleted file mode 100644 index 27d4f401f1b..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.octetstream.gz.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: application/octet-stream -Content-Encoding: gzip diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.text.gz b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.text.gz deleted file mode 100644 index 05a5cce07b5..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.text.gz and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.text.gz.headers b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.text.gz.headers deleted file mode 100644 index 7def3ddc148..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/foo.text.gz.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: text/plain -Content-Encoding: gzip diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/hello.html.gz b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/hello.html.gz deleted file mode 100644 index 87140901396..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/hello.html.gz and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/hello.html.gz.headers b/test/fixtures/wpt/fetch/content-encoding/gzip/resources/hello.html.gz.headers deleted file mode 100644 index 937eff5d7d8..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/gzip/resources/hello.html.gz.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: text/html -Content-Encoding: gzip diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/content-encoding/zstd/WEB_FEATURES.yml deleted file mode 100644 index b5d970fdb14..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/WEB_FEATURES.yml +++ /dev/null @@ -1,3 +0,0 @@ -features: -- name: zstd - files: "**" diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/bad-zstd-body.https.any.js b/test/fixtures/wpt/fetch/content-encoding/zstd/bad-zstd-body.https.any.js deleted file mode 100644 index c59980c2c6f..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/bad-zstd-body.https.any.js +++ /dev/null @@ -1,23 +0,0 @@ -// META: global=window,worker - -promise_test((test) => { - return fetch("resources/bad-zstd-body.py").then(res => { - assert_equals(res.status, 200); - }); -}, "Fetching a resource with bad zstd content should still resolve"); - -[ - "arrayBuffer", - "blob", - "bytes", - "formData", - "json", - "text" -].forEach(method => { - promise_test(t => { - return fetch("resources/bad-zstd-body.py").then(res => { - assert_equals(res.status, 200); - return promise_rejects_js(t, TypeError, res[method]()); - }); - }, "Consuming the body of a resource with bad zstd content with " + method + "() should reject"); -}); diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/big-window-zstd-body.tentative.https.any.js b/test/fixtures/wpt/fetch/content-encoding/zstd/big-window-zstd-body.tentative.https.any.js deleted file mode 100644 index c1dc9449567..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/big-window-zstd-body.tentative.https.any.js +++ /dev/null @@ -1,9 +0,0 @@ -// META: global=window,worker -// See https://github.com/facebook/zstd/issues/2713 for discussion about -// standardizing window size limits. - -promise_test(async t => { - const response = await fetch('resources/big.window.zst'); - assert_true(response.ok); - await promise_rejects_js(t, TypeError, response.text()); -}, 'Consuming the body of a resource with too large of a zstd window size should reject'); diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/big-zstd-body.https.any.js b/test/fixtures/wpt/fetch/content-encoding/zstd/big-zstd-body.https.any.js deleted file mode 100644 index 6835f6e4255..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/big-zstd-body.https.any.js +++ /dev/null @@ -1,55 +0,0 @@ -// META: global=window,worker - -const EXPECTED_SIZE = 27000000; -const EXPECTED_SHA256 = [ - 74, 100, 37, 243, 147, 61, 116, 60, 241, 221, 126, - 18, 24, 71, 204, 28, 50, 62, 201, 130, 152, 225, - 217, 183, 10, 201, 143, 214, 102, 155, 212, 248, - ]; - -promise_test(async () => { - const response = await fetch('resources/big.text.zst'); - assert_true(response.ok); - const arrayBuffer = await response.arrayBuffer(); - assert_equals(arrayBuffer.byteLength, EXPECTED_SIZE, - 'uncompressed size should match'); - const sha256 = await crypto.subtle.digest('SHA-256', arrayBuffer); - assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, - 'digest should match'); -}, 'large zstd data should be decompressed successfully'); - -promise_test(async () => { - const response = await fetch('resources/big.text.zst'); - assert_true(response.ok); - const reader = response.body.getReader({mode: 'byob'}); - let offset = 0; - // Pre-allocate space for the output. The response body will be read - // chunk-by-chunk into this array. - let ab = new ArrayBuffer(EXPECTED_SIZE); - while (offset < EXPECTED_SIZE) { - // To stress the data pipe, we want to use a different size read each - // time. Unfortunately, JavaScript doesn't have a seeded random number - // generator, so this creates the possibility of making this test flaky if - // it doesn't work for some edge cases. - let size = Math.floor(Math.random() * 65535 + 1); - if (size + offset > EXPECTED_SIZE) { - size = EXPECTED_SIZE - offset; - } - const u8 = new Uint8Array(ab, offset, size); - const { value, done } = await reader.read(u8); - ab = value.buffer; - // Check that we got our original array back. - assert_equals(ab.byteLength, EXPECTED_SIZE, - 'backing array should be the same size'); - assert_equals(offset, value.byteOffset, 'offset should match'); - assert_less_than_equal(value.byteLength, size, - 'we should not have got more than we asked for'); - offset = value.byteOffset + value.byteLength; - if (done) break; - } - assert_equals(offset, EXPECTED_SIZE, - 'we should have read the whole thing'); - const sha256 = await crypto.subtle.digest('SHA-256', new Uint8Array(ab)); - assert_array_equals(new Uint8Array(sha256), EXPECTED_SHA256, - 'digest should match'); -}, 'large zstd data should be decompressed successfully with byte stream'); diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/bad-zstd-body.py b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/bad-zstd-body.py deleted file mode 100644 index 496f26881dc..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/bad-zstd-body.py +++ /dev/null @@ -1,3 +0,0 @@ -def main(request, response): - headers = [(b"Content-Encoding", b"zstd")] - return headers, b"not actually zstd" diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.text.zst b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.text.zst deleted file mode 100644 index 30eda2443f3..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.text.zst and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.text.zst.headers b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.text.zst.headers deleted file mode 100644 index ea5b05cf2c2..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.text.zst.headers +++ /dev/null @@ -1,3 +0,0 @@ -Content-type: text/plain -Content-Encoding: zstd -Cache-Control: no-store diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.window.zst b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.window.zst deleted file mode 100644 index a1bca732283..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.window.zst and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.window.zst.headers b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.window.zst.headers deleted file mode 100644 index c5974e126a3..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/big.window.zst.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: text/plain -Content-Encoding: zstd diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.octetstream.zst b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.octetstream.zst deleted file mode 100644 index a73bbdd2245..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.octetstream.zst and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.octetstream.zst.headers b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.octetstream.zst.headers deleted file mode 100644 index e397816f545..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.octetstream.zst.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: application/octet-stream -Content-Encoding: zstd diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.text.zst b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.text.zst deleted file mode 100644 index a73bbdd2245..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.text.zst and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.text.zst.headers b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.text.zst.headers deleted file mode 100644 index c5974e126a3..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/foo.text.zst.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: text/plain -Content-Encoding: zstd diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/hello.html.zst b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/hello.html.zst deleted file mode 100644 index 1e3598106fc..00000000000 Binary files a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/hello.html.zst and /dev/null differ diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/hello.html.zst.headers b/test/fixtures/wpt/fetch/content-encoding/zstd/resources/hello.html.zst.headers deleted file mode 100644 index 45763963093..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/resources/hello.html.zst.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-type: text/html -Content-Encoding: zstd diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/zstd-body.https.any.js b/test/fixtures/wpt/fetch/content-encoding/zstd/zstd-body.https.any.js deleted file mode 100644 index 86923857432..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/zstd-body.https.any.js +++ /dev/null @@ -1,15 +0,0 @@ -// META: global=window,worker - -const expectedDecompressedSize = 10500; -[ - "text", - "octetstream" -].forEach(contentType => { - promise_test(async t => { - let response = await fetch(`resources/foo.${contentType}.zst`); - assert_true(response.ok); - let arrayBuffer = await response.arrayBuffer() - let u8 = new Uint8Array(arrayBuffer); - assert_equals(u8.length, expectedDecompressedSize); - }, `fetched zstd data with content type ${contentType} should be decompressed.`); -}); diff --git a/test/fixtures/wpt/fetch/content-encoding/zstd/zstd-navigation.https.window.js b/test/fixtures/wpt/fetch/content-encoding/zstd/zstd-navigation.https.window.js deleted file mode 100644 index 27cf25ae782..00000000000 --- a/test/fixtures/wpt/fetch/content-encoding/zstd/zstd-navigation.https.window.js +++ /dev/null @@ -1,6 +0,0 @@ -// META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js - -promise_test(async t => { - const frame = await with_iframe('./resources/hello.html.zst'); - assert_equals(frame.contentDocument.body.textContent, 'Hello world'); -}, `Naigation to zstd encoded page`); diff --git a/test/fixtures/wpt/fetch/content-length/api-and-duplicate-headers.any.js b/test/fixtures/wpt/fetch/content-length/api-and-duplicate-headers.any.js deleted file mode 100644 index 8015289f8d6..00000000000 --- a/test/fixtures/wpt/fetch/content-length/api-and-duplicate-headers.any.js +++ /dev/null @@ -1,23 +0,0 @@ -promise_test(async t => { - const response = await fetch("resources/identical-duplicates.asis"); - assert_equals(response.statusText, "BLAH"); - assert_equals(response.headers.get("test"), "x, x"); - assert_equals(response.headers.get("content-type"), "text/plain, text/plain"); - assert_equals(response.headers.get("content-length"), "6, 6"); - const text = await response.text(); - assert_equals(text, "Test.\n"); -}, "fetch() and duplicate Content-Length/Content-Type headers"); - -async_test(t => { - const xhr = new XMLHttpRequest(); - xhr.open("GET", "resources/identical-duplicates.asis"); - xhr.send(); - xhr.onload = t.step_func_done(() => { - assert_equals(xhr.statusText, "BLAH"); - assert_equals(xhr.getResponseHeader("test"), "x, x"); - assert_equals(xhr.getResponseHeader("content-type"), "text/plain, text/plain"); - assert_equals(xhr.getResponseHeader("content-length"), "6, 6"); - assert_equals(xhr.getAllResponseHeaders(), "content-length: 6, 6\r\ncontent-type: text/plain, text/plain\r\ntest: x, x\r\n"); - assert_equals(xhr.responseText, "Test.\n"); - }); -}, "XMLHttpRequest and duplicate Content-Length/Content-Type headers"); diff --git a/test/fixtures/wpt/fetch/content-length/content-length.html b/test/fixtures/wpt/fetch/content-length/content-length.html deleted file mode 100644 index cda9b5b5237..00000000000 --- a/test/fixtures/wpt/fetch/content-length/content-length.html +++ /dev/null @@ -1,14 +0,0 @@ - - -Content-Length Test - - - -PASS -but FAIL if this is in the body. \ No newline at end of file diff --git a/test/fixtures/wpt/fetch/content-length/content-length.html.headers b/test/fixtures/wpt/fetch/content-length/content-length.html.headers deleted file mode 100644 index 25389b7c0fa..00000000000 --- a/test/fixtures/wpt/fetch/content-length/content-length.html.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Length: 403 diff --git a/test/fixtures/wpt/fetch/content-length/parsing.window.js b/test/fixtures/wpt/fetch/content-length/parsing.window.js deleted file mode 100644 index 5028ad943de..00000000000 --- a/test/fixtures/wpt/fetch/content-length/parsing.window.js +++ /dev/null @@ -1,18 +0,0 @@ -promise_test(() => { - return fetch("resources/content-lengths.json").then(res => res.json()).then(runTests); -}, "Loading JSON…"); - -function runTests(testUnits) { - testUnits.forEach(({ input, output }) => { - promise_test(t => { - const result = fetch(`resources/content-length.py?length=${encodeURIComponent(input)}`); - if (output === null) { - return promise_rejects_js(t, TypeError, result); - } else { - return result.then(res => res.text()).then(text => { - assert_equals(text.length, output); - }); - } - }, `Input: ${format_value(input)}. Expected: ${output === null ? "network error" : output}.`); - }); -} diff --git a/test/fixtures/wpt/fetch/content-length/resources/content-length.py b/test/fixtures/wpt/fetch/content-length/resources/content-length.py deleted file mode 100644 index 92cfadeb061..00000000000 --- a/test/fixtures/wpt/fetch/content-length/resources/content-length.py +++ /dev/null @@ -1,10 +0,0 @@ -def main(request, response): - response.add_required_headers = False - output = b"HTTP/1.1 200 OK\r\n" - output += b"Content-Type: text/plain;charset=UTF-8\r\n" - output += b"Connection: close\r\n" - output += request.GET.first(b"length") + b"\r\n" - output += b"\r\n" - output += b"Fact: this is really forty-two bytes long." - response.writer.write(output) - response.close_connection = True diff --git a/test/fixtures/wpt/fetch/content-length/resources/content-lengths.json b/test/fixtures/wpt/fetch/content-length/resources/content-lengths.json deleted file mode 100644 index ac6f1a24680..00000000000 --- a/test/fixtures/wpt/fetch/content-length/resources/content-lengths.json +++ /dev/null @@ -1,142 +0,0 @@ -[ - { - "input": "Content-Length: 42", - "output": 42 - }, - { - "input": "Content-Length: 42,42", - "output": 42 - }, - { - "input": "Content-Length: 42\r\nContent-Length: 42", - "output": 42 - }, - { - "input": "Content-Length: 42\r\nContent-Length: 42,42", - "output": 42 - }, - { - "input": "Content-Length: 30", - "output": 30 - }, - { - "input": "Content-Length: 30,30", - "output": 30 - }, - { - "input": "Content-Length: 30\r\nContent-Length: 30", - "output": 30 - }, - { - "input": "Content-Length: 30\r\nContent-Length: 30,30", - "output": 30 - }, - { - "input": "Content-Length: 30,30\r\nContent-Length: 30,30", - "output": 30 - }, - { - "input": "Content-Length: 30,30, 30 \r\nContent-Length: 30 ", - "output": 30 - }, - { - "input": "Content-Length: 30,42\r\nContent-Length: 30", - "output": null - }, - { - "input": "Content-Length: 30,42\r\nContent-Length: 30,42", - "output": null - }, - { - "input": "Content-Length: 42,30", - "output": null - }, - { - "input": "Content-Length: 30,42", - "output": null - }, - { - "input": "Content-Length: 42\r\nContent-Length: 30", - "output": null - }, - { - "input": "Content-Length: 30\r\nContent-Length: 42", - "output": null - }, - { - "input": "Content-Length: 30,", - "output": null - }, - { - "input": "Content-Length: ,30", - "output": null - }, - { - "input": "Content-Length: 30\r\nContent-Length: \t", - "output": null - }, - { - "input": "Content-Length: \r\nContent-Length: 30", - "output": null - }, - { - "input": "Content-Length: aaaah\r\nContent-Length: nah", - "output": null - }, - { - "input": "Content-Length: aaaah, nah", - "output": null - }, - { - "input": "Content-Length: aaaah\r\nContent-Length: aaaah", - "output": 42 - }, - { - "input": "Content-Length: aaaah, aaaah", - "output": 42 - }, - { - "input": "Content-Length: aaaah", - "output": 42 - }, - { - "input": "Content-Length: 42s", - "output": 42 - }, - { - "input": "Content-Length: 30s", - "output": 42 - }, - { - "input": "Content-Length: -1", - "output": 42 - }, - { - "input": "Content-Length: 0x20", - "output": 42 - }, - { - "input": "Content-Length: 030", - "output": 30 - }, - { - "input": "Content-Length: 030\r\nContent-Length: 30", - "output": null - }, - { - "input": "Content-Length: 030, 30", - "output": null - }, - { - "input": "Content-Length: \"30\"", - "output": 42 - }, - { - "input": "Content-Length:30\r\nContent-Length:,\r\nContent-Length:30", - "output": null - }, - { - "input": "Content-Length: ", - "output": 42 - } -] diff --git a/test/fixtures/wpt/fetch/content-length/resources/identical-duplicates.asis b/test/fixtures/wpt/fetch/content-length/resources/identical-duplicates.asis deleted file mode 100644 index f38c9a4b8a0..00000000000 --- a/test/fixtures/wpt/fetch/content-length/resources/identical-duplicates.asis +++ /dev/null @@ -1,9 +0,0 @@ -HTTP/1.1 200 BLAH -Test: x -Test: x -Content-Type: text/plain -Content-Type: text/plain -Content-Length: 6 -Content-Length: 6 - -Test. diff --git a/test/fixtures/wpt/fetch/content-length/too-long.window.js b/test/fixtures/wpt/fetch/content-length/too-long.window.js deleted file mode 100644 index f8cefaa9c23..00000000000 --- a/test/fixtures/wpt/fetch/content-length/too-long.window.js +++ /dev/null @@ -1,4 +0,0 @@ -promise_test(async t => { - const result = await fetch(`resources/content-length.py?length=${encodeURIComponent("Content-Length: 50")}`); - await promise_rejects_js(t, TypeError, result.text()); -}, "Content-Length header value of network response exceeds response body"); diff --git a/test/fixtures/wpt/fetch/content-type/README.md b/test/fixtures/wpt/fetch/content-type/README.md deleted file mode 100644 index f553b7ee8e6..00000000000 --- a/test/fixtures/wpt/fetch/content-type/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# `resources/content-types.json` - -An array of tests. Each test has these fields: - -* `contentType`: an array of values for the `Content-Type` header. A harness needs to run the test twice if there are multiple values. One time with the values concatenated with `,` followed by a space and one time with multiple `Content-Type` declarations, each on their own line with one of the values, in order. -* `encoding`: the expected encoding, null for the default. -* `mimeType`: the result of extracting a MIME type and serializing it. -* `documentContentType`: the MIME type expected to be exposed in DOM documents. - -(These tests are currently somewhat geared towards browser use, but could be generalized easily enough if someone wanted to contribute tests for MIME types that would cause downloads in the browser or some such.) - -# `resources/script-content-types.json` - -An array of tests, surprise. Each test has these fields: - -* `contentType`: see above. -* `executes`: whether the script is expected to execute. -* `encoding`: how the script is expected to be decoded. - -These tests are expected to be loaded through ` - -
- diff --git a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html deleted file mode 100644 index a771ed6a653..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html deleted file mode 100644 index 82adc47b0cf..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub-ref.html b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub-ref.html deleted file mode 100644 index ebb337dba80..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub-ref.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub.html b/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub.html deleted file mode 100644 index 1ae4cfcaa7c..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-png-mislabeled-as-html.sub.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html b/test/fixtures/wpt/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html deleted file mode 100644 index 3219feda171..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html b/test/fixtures/wpt/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html deleted file mode 100644 index efcfaa27374..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-svg-invalid.sub-ref.html b/test/fixtures/wpt/fetch/corb/img-svg-invalid.sub-ref.html deleted file mode 100644 index 484cd0a4fde..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-svg-invalid.sub-ref.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-svg-labeled-as-dash.sub.html b/test/fixtures/wpt/fetch/corb/img-svg-labeled-as-dash.sub.html deleted file mode 100644 index 0578b835fe5..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-svg-labeled-as-dash.sub.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-svg-labeled-as-svg-xml.sub.html b/test/fixtures/wpt/fetch/corb/img-svg-labeled-as-svg-xml.sub.html deleted file mode 100644 index 30a2eb3246c..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-svg-labeled-as-svg-xml.sub.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-svg-xml-decl.sub.html b/test/fixtures/wpt/fetch/corb/img-svg-xml-decl.sub.html deleted file mode 100644 index 0d3aeafb258..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-svg-xml-decl.sub.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/fetch/corb/img-svg.sub-ref.html b/test/fixtures/wpt/fetch/corb/img-svg.sub-ref.html deleted file mode 100644 index 5462f685a0e..00000000000 --- a/test/fixtures/wpt/fetch/corb/img-svg.sub-ref.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html b/test/fixtures/wpt/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html deleted file mode 100644 index cea80f2f89f..00000000000 --- a/test/fixtures/wpt/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - -
- - - - - diff --git a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css deleted file mode 100644 index afd2b92975d..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css +++ /dev/null @@ -1 +0,0 @@ -#header { color: red; } diff --git a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers deleted file mode 100644 index 0f228f94ecb..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-Type: text/html -X-Content-Type-Options: nosniff diff --git a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css deleted file mode 100644 index afd2b92975d..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css +++ /dev/null @@ -1 +0,0 @@ -#header { color: red; } diff --git a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css.headers b/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css.headers deleted file mode 100644 index 156209f9c81..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/css-mislabeled-as-html.css.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/css-with-json-parser-breaker.css b/test/fixtures/wpt/fetch/corb/resources/css-with-json-parser-breaker.css deleted file mode 100644 index 7db6f5c6d36..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/css-with-json-parser-breaker.css +++ /dev/null @@ -1,3 +0,0 @@ -)]}' -{} -#header { color: red; } diff --git a/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png b/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png.headers b/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png.headers deleted file mode 100644 index e7be84a714b..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/empty-labeled-as-png.png.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: image/png diff --git a/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html b/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html deleted file mode 100644 index 7bad71bfbd8..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Page Title - - -

Page body

- - diff --git a/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html.headers b/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html.headers deleted file mode 100644 index 156209f9c81..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/html-correctly-labeled.html.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js deleted file mode 100644 index db45bb4acc9..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js.headers b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js.headers deleted file mode 100644 index 156209f9c81..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js deleted file mode 100644 index faae1b7682b..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js.headers b/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js.headers deleted file mode 100644 index 156209f9c81..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/html-js-polyglot2.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js deleted file mode 100644 index a880a5bc724..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js +++ /dev/null @@ -1 +0,0 @@ -window.has_executed_script = true; diff --git a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers deleted file mode 100644 index 0f228f94ecb..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-Type: text/html -X-Content-Type-Options: nosniff diff --git a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js deleted file mode 100644 index a880a5bc724..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js +++ /dev/null @@ -1 +0,0 @@ -window.has_executed_script = true; diff --git a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js.headers b/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js.headers deleted file mode 100644 index 156209f9c81..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/js-mislabeled-as-html.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png b/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png deleted file mode 100644 index 820f8cace21..00000000000 Binary files a/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png and /dev/null differ diff --git a/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png.headers b/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png.headers deleted file mode 100644 index e7be84a714b..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/png-correctly-labeled.png.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: image/png diff --git a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png deleted file mode 100644 index 820f8cace21..00000000000 Binary files a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png and /dev/null differ diff --git a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers deleted file mode 100644 index 0f228f94ecb..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers +++ /dev/null @@ -1,2 +0,0 @@ -Content-Type: text/html -X-Content-Type-Options: nosniff diff --git a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png deleted file mode 100644 index 820f8cace21..00000000000 Binary files a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png and /dev/null differ diff --git a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png.headers b/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png.headers deleted file mode 100644 index 156209f9c81..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/png-mislabeled-as-html.png.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/corb/resources/response_block_probe.js b/test/fixtures/wpt/fetch/corb/resources/response_block_probe.js deleted file mode 100644 index 9c3b87bcbd3..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/response_block_probe.js +++ /dev/null @@ -1 +0,0 @@ -alert(1); // Arbitrary JavaScript. Details don't matter for the test. diff --git a/test/fixtures/wpt/fetch/corb/resources/response_block_probe.js.headers b/test/fixtures/wpt/fetch/corb/resources/response_block_probe.js.headers deleted file mode 100644 index 0d848b02c2e..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/response_block_probe.js.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/csv diff --git a/test/fixtures/wpt/fetch/corb/resources/sniffable-resource.py b/test/fixtures/wpt/fetch/corb/resources/sniffable-resource.py deleted file mode 100644 index f8150936acb..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/sniffable-resource.py +++ /dev/null @@ -1,11 +0,0 @@ -def main(request, response): - body = request.GET.first(b"body", None) - type = request.GET.first(b"type", None) - - response.add_required_headers = False - response.writer.write_status(200) - response.writer.write_header(b"content-length", len(body)) - response.writer.write_header(b"content-type", type) - response.writer.end_headers() - - response.writer.write(body) diff --git a/test/fixtures/wpt/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html b/test/fixtures/wpt/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html deleted file mode 100644 index 67b3ad5a600..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg b/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg deleted file mode 100644 index fa2d29b3b0f..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers b/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers deleted file mode 100644 index 29515ee7d4a..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg b/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg deleted file mode 100644 index fa2d29b3b0f..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers b/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers deleted file mode 100644 index 070de35fbe9..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: image/svg+xml diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-dash.svg b/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-dash.svg deleted file mode 100644 index 2b7d1016b1e..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-dash.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-dash.svg.headers b/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-dash.svg.headers deleted file mode 100644 index 43ce612c9fd..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-dash.svg.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: application/dash+xml diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-svg-xml.svg b/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-svg-xml.svg deleted file mode 100644 index 2b7d1016b1e..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-svg-xml.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers b/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers deleted file mode 100644 index 070de35fbe9..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: image/svg+xml diff --git a/test/fixtures/wpt/fetch/corb/resources/svg-xml-decl.svg b/test/fixtures/wpt/fetch/corb/resources/svg-xml-decl.svg deleted file mode 100644 index 3b39aff8e5c..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg-xml-decl.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/corb/resources/svg.svg b/test/fixtures/wpt/fetch/corb/resources/svg.svg deleted file mode 100644 index 2b7d1016b1e..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/test/fixtures/wpt/fetch/corb/resources/svg.svg.headers b/test/fixtures/wpt/fetch/corb/resources/svg.svg.headers deleted file mode 100644 index 070de35fbe9..00000000000 --- a/test/fixtures/wpt/fetch/corb/resources/svg.svg.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: image/svg+xml diff --git a/test/fixtures/wpt/fetch/corb/response_block.tentative.https.html b/test/fixtures/wpt/fetch/corb/response_block.tentative.https.html deleted file mode 100644 index 6b116000d45..00000000000 --- a/test/fixtures/wpt/fetch/corb/response_block.tentative.https.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - diff --git a/test/fixtures/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html b/test/fixtures/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html deleted file mode 100644 index 6d1947cea7d..00000000000 --- a/test/fixtures/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - -
- diff --git a/test/fixtures/wpt/fetch/corb/script-html-js-polyglot.sub.html b/test/fixtures/wpt/fetch/corb/script-html-js-polyglot.sub.html deleted file mode 100644 index 9a272d63ffc..00000000000 --- a/test/fixtures/wpt/fetch/corb/script-html-js-polyglot.sub.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - -
- diff --git a/test/fixtures/wpt/fetch/corb/script-html-via-cross-origin-blob-url.sub.html b/test/fixtures/wpt/fetch/corb/script-html-via-cross-origin-blob-url.sub.html deleted file mode 100644 index c8a90c79b3f..00000000000 --- a/test/fixtures/wpt/fetch/corb/script-html-via-cross-origin-blob-url.sub.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - -
- diff --git a/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html b/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html deleted file mode 100644 index b6bc90964de..00000000000 --- a/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - -
- - - - - - - diff --git a/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html.sub.html b/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html.sub.html deleted file mode 100644 index 44cb1f8659d..00000000000 --- a/test/fixtures/wpt/fetch/corb/script-js-mislabeled-as-html.sub.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - -
- - - - - - - diff --git a/test/fixtures/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html b/test/fixtures/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html deleted file mode 100644 index f0eb1f0ab1f..00000000000 --- a/test/fixtures/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - -
- diff --git a/test/fixtures/wpt/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html b/test/fixtures/wpt/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html deleted file mode 100644 index 6d490d55bce..00000000000 --- a/test/fixtures/wpt/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - -
- diff --git a/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html b/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html deleted file mode 100644 index 8fef0dc59e4..00000000000 --- a/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html +++ /dev/null @@ -1,42 +0,0 @@ - - - -CSS is not applied (because of nosniff + non-text/css headers) - - - - - - - - - - - -

Header example

-

Paragraph body

- - - diff --git a/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html.sub.html b/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html.sub.html deleted file mode 100644 index 4f0b4c22f56..00000000000 --- a/test/fixtures/wpt/fetch/corb/style-css-mislabeled-as-html.sub.html +++ /dev/null @@ -1,36 +0,0 @@ - - - -CSS is not applied (because of strict content-type enforcement for cross-origin stylesheets) - - - - - - - - - - - -

Header example

-

Paragraph body

- - - diff --git a/test/fixtures/wpt/fetch/corb/style-css-with-json-parser-breaker.sub.html b/test/fixtures/wpt/fetch/corb/style-css-with-json-parser-breaker.sub.html deleted file mode 100644 index 29ed586a4f0..00000000000 --- a/test/fixtures/wpt/fetch/corb/style-css-with-json-parser-breaker.sub.html +++ /dev/null @@ -1,38 +0,0 @@ - - - -CORB doesn't block a stylesheet that has a proper Content-Type and begins with a JSON parser breaker - - - - - - - - - - - -

Header example

-

Paragraph body

- - - diff --git a/test/fixtures/wpt/fetch/corb/style-html-correctly-labeled.sub.html b/test/fixtures/wpt/fetch/corb/style-html-correctly-labeled.sub.html deleted file mode 100644 index cdefcd2d2c9..00000000000 --- a/test/fixtures/wpt/fetch/corb/style-html-correctly-labeled.sub.html +++ /dev/null @@ -1,41 +0,0 @@ - - - -CSS is not applied (because of mismatched Content-Type header) - - - - - - - - - - - -

Header example

-

Paragraph body

- - - diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch-in-iframe.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch-in-iframe.html deleted file mode 100644 index cc6a3a81bcf..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch-in-iframe.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.any.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.any.js deleted file mode 100644 index 64a7bfeb864..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.any.js +++ /dev/null @@ -1,76 +0,0 @@ -// META: timeout=long -// META: global=window,dedicatedworker,sharedworker -// META: script=/common/get-host-info.sub.js - -const host = get_host_info(); -const path = "/fetch/cross-origin-resource-policy/"; -const localBaseURL = host.HTTP_ORIGIN + path; -const sameSiteBaseURL = "http://" + host.ORIGINAL_HOST + ":" + host.HTTP_PORT2 + path; -const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + path; -const httpsBaseURL = host.HTTPS_ORIGIN + path; - -promise_test(async () => { - const response = await fetch("./resources/hello.py?corp=same-origin"); - assert_equals(await response.text(), "hello"); -}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); - -promise_test(async () => { - const response = await fetch("./resources/hello.py?corp=same-site"); - assert_equals(await response.text(), "hello"); -}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); - -promise_test(async (test) => { - const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin"); - assert_equals(await response.text(), "hello"); -}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); - -promise_test(async (test) => { - const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site"); - assert_equals(await response.text(), "hello"); -}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); - -promise_test((test) => { - const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; - return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); - -promise_test((test) => { - const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site"; - return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); - -promise_test((test) => { - const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-site"; - return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); -}, "Cross-scheme (HTTP to HTTPS) no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-site' response header."); - -promise_test((test) => { - const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-origin"; - return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); -}, "Cross-origin no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-origin' response header."); - -promise_test(async (test) => { - const remoteSameSiteURL = sameSiteBaseURL + "resources/hello.py?corp=same-site"; - - await fetch(remoteSameSiteURL, { mode: "no-cors" }); - - return promise_rejects_js(test, TypeError, fetch(sameSiteBaseURL + "resources/hello.py?corp=same-origin", { mode: "no-cors" })); -}, "Valid cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); - -promise_test((test) => { - const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; - return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection."); - -promise_test((test) => { - const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; - return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection."); - -promise_test(async (test) => { - const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; - - await fetch(finalURL, { mode: "no-cors" }); - - return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header."); diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.https.any.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.https.any.js deleted file mode 100644 index c9b5b7502f4..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/fetch.https.any.js +++ /dev/null @@ -1,56 +0,0 @@ -// META: timeout=long -// META: global=window,worker -// META: script=/common/get-host-info.sub.js - -const host = get_host_info(); -const path = "/fetch/cross-origin-resource-policy/"; -const localBaseURL = host.HTTPS_ORIGIN + path; -const notSameSiteBaseURL = host.HTTPS_NOTSAMESITE_ORIGIN + path; - -promise_test(async () => { - const response = await fetch("./resources/hello.py?corp=same-origin"); - assert_equals(await response.text(), "hello"); -}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); - -promise_test(async () => { - const response = await fetch("./resources/hello.py?corp=same-site"); - assert_equals(await response.text(), "hello"); -}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); - -promise_test(async (test) => { - const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin"); - assert_equals(await response.text(), "hello"); -}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); - -promise_test(async (test) => { - const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site"); - assert_equals(await response.text(), "hello"); -}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); - -promise_test((test) => { - const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; - return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); - -promise_test((test) => { - const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site"; - return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); - -promise_test((test) => { - const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; - return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection."); - -promise_test((test) => { - const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; - return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection."); - -promise_test(async (test) => { - const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; - - await fetch(finalURL, { mode: "no-cors" }); - - return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); -}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header."); diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/iframe-loads.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/iframe-loads.html deleted file mode 100644 index 63902c302b7..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/iframe-loads.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/image-loads.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/image-loads.html deleted file mode 100644 index 060b7551ea5..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/image-loads.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - -
- - - diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/green.png b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/green.png deleted file mode 100644 index 28a1faab377..00000000000 Binary files a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/green.png and /dev/null differ diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/hello.py b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/hello.py deleted file mode 100644 index 2b1cb84bad2..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/hello.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(request, response): - headers = [(b"Cross-Origin-Resource-Policy", request.GET[b'corp'])] - if b'origin' in request.headers: - headers.append((b'Access-Control-Allow-Origin', request.headers[b'origin'])) - - return 200, headers, b"hello" diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframe.py b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframe.py deleted file mode 100644 index 815ecf5927c..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframe.py +++ /dev/null @@ -1,5 +0,0 @@ -def main(request, response): - headers = [(b"Content-Type", b"text/html"), - (b"Cross-Origin-Resource-Policy", request.GET[b'corp'])] - return 200, headers, b"

The iframe

" - diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframeFetch.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframeFetch.html deleted file mode 100644 index 257185805d9..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/iframeFetch.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - -

The iframe making a same origin fetch call.

- - diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/image.py b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/image.py deleted file mode 100644 index 2a779cf11bf..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/image.py +++ /dev/null @@ -1,22 +0,0 @@ -import os.path - -from wptserve.utils import isomorphic_decode - -def main(request, response): - type = request.GET.first(b"type", None) - - body = open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"green.png"), u"rb").read() - - response.add_required_headers = False - response.writer.write_status(200) - - if b'corp' in request.GET: - response.writer.write_header(b"cross-origin-resource-policy", request.GET[b'corp']) - if b'acao' in request.GET: - response.writer.write_header(b"access-control-allow-origin", request.GET[b'acao']) - response.writer.write_header(b"content-length", len(body)) - if(type != None): - response.writer.write_header(b"content-type", type) - response.writer.end_headers() - - response.writer.write(body) diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/redirect.py b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/redirect.py deleted file mode 100644 index 0dad4dd923b..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/redirect.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(request, response): - headers = [(b"Location", request.GET[b'redirectTo'])] - if b'corp' in request.GET: - headers.append((b'Cross-Origin-Resource-Policy', request.GET[b'corp'])) - - return 302, headers, b"" diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/script.py b/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/script.py deleted file mode 100644 index 58f8d341547..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/resources/script.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(request, response): - headers = [(b"Cross-Origin-Resource-Policy", request.GET[b'corp'])] - if b'origin' in request.headers: - headers.append((b'Access-Control-Allow-Origin', request.headers[b'origin'])) - - return 200, headers, b"" diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.any.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.any.js deleted file mode 100644 index 8f6338176a3..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.any.js +++ /dev/null @@ -1,7 +0,0 @@ -// META: script=/common/get-host-info.sub.js - -promise_test(t => { - return promise_rejects_js(t, - TypeError, - fetch(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp=same-site", { mode: "no-cors" })); -}, "Cross-Origin-Resource-Policy: same-site blocks retrieving HTTPS from HTTP"); diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js deleted file mode 100644 index 4c745718741..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js +++ /dev/null @@ -1,13 +0,0 @@ -// META: script=/common/get-host-info.sub.js - -promise_test(t => { - const img = new Image(); - img.src = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/image.py?corp=same-site"; - return new Promise((resolve, reject) => { - img.onload = resolve; - img.onerror = reject; - document.body.appendChild(img); - }).finally(() => { - img.remove(); - }); -}, "Cross-Origin-Resource-Policy does not block Mixed Content "); diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/script-loads.html b/test/fixtures/wpt/fetch/cross-origin-resource-policy/script-loads.html deleted file mode 100644 index a9690fc70be..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/script-loads.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - -
- - - diff --git a/test/fixtures/wpt/fetch/cross-origin-resource-policy/syntax.any.js b/test/fixtures/wpt/fetch/cross-origin-resource-policy/syntax.any.js deleted file mode 100644 index dc874977a63..00000000000 --- a/test/fixtures/wpt/fetch/cross-origin-resource-policy/syntax.any.js +++ /dev/null @@ -1,19 +0,0 @@ -// META: script=/common/get-host-info.sub.js - -const crossOriginURL = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp="; - -[ - "same", - "same, same-origin", - "SAME-ORIGIN", - "Same-Origin", - "same-origin, <>", - "same-origin, same-origin", - "https://www.example.com", // See https://github.com/whatwg/fetch/issues/760 -].forEach(incorrectHeaderValue => { - // Note: an incorrect value results in a successful load, so this test is only meaningful in - // implementations with support for the header. - promise_test(t => { - return fetch(crossOriginURL + encodeURIComponent(incorrectHeaderValue), { mode: "no-cors" }); - }, "Parsing Cross-Origin-Resource-Policy: " + incorrectHeaderValue); -}); diff --git a/test/fixtures/wpt/fetch/data-urls/README.md b/test/fixtures/wpt/fetch/data-urls/README.md deleted file mode 100644 index 1ce5b18b538..00000000000 --- a/test/fixtures/wpt/fetch/data-urls/README.md +++ /dev/null @@ -1,11 +0,0 @@ -## data: URLs - -`resources/data-urls.json` contains `data:` URL tests. The tests are encoded as a JSON array. Each value in the array is an array of two or three values. The first value describes the input, the second value describes the expected MIME type, null if the input is expected to fail somehow, or the empty string if the expected value is `text/plain;charset=US-ASCII`. The third value, if present, describes the expected body as an array of integers representing bytes. - -These tests are used for `data:` URLs in this directory (see `processing.any.js`). - -## Forgiving-base64 decode - -`resources/base64.json` contains [forgiving-base64 decode](https://infra.spec.whatwg.org/#forgiving-base64-decode) tests. The tests are encoded as a JSON array. Each value in the array is an array of two values. The first value describes the input, the second value describes the output as an array of integers representing bytes or null if the input cannot be decoded. - -These tests are used for `data:` URLs in this directory (see `base64.any.js`) and `window.atob()` in `../../html/webappapis/atob/base64.html`. diff --git a/test/fixtures/wpt/fetch/data-urls/base64.any.js b/test/fixtures/wpt/fetch/data-urls/base64.any.js deleted file mode 100644 index 83f34db1777..00000000000 --- a/test/fixtures/wpt/fetch/data-urls/base64.any.js +++ /dev/null @@ -1,18 +0,0 @@ -// META: global=window,worker - -promise_test(() => fetch("resources/base64.json").then(res => res.json()).then(runBase64Tests), "Setup."); -function runBase64Tests(tests) { - for(let i = 0; i < tests.length; i++) { - const input = tests[i][0], - output = tests[i][1], - dataURL = "data:;base64," + input; - promise_test(t => { - if(output === null) { - return promise_rejects_js(t, TypeError, fetch(dataURL)); - } - return fetch(dataURL).then(res => res.arrayBuffer()).then(body => { - assert_array_equals(new Uint8Array(body), output); - }); - }, "data: URL base64 handling: " + format_value(input)); - } -} diff --git a/test/fixtures/wpt/fetch/data-urls/navigate.window.js b/test/fixtures/wpt/fetch/data-urls/navigate.window.js deleted file mode 100644 index b532a006830..00000000000 --- a/test/fixtures/wpt/fetch/data-urls/navigate.window.js +++ /dev/null @@ -1,75 +0,0 @@ -// META: timeout=long -// -// Test some edge cases around navigation to data: URLs to ensure they use the same code path - -[ - { - input: "data:text/html,", - result: 1, - name: "Nothing fancy", - }, - { - input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoMiwgJyonKTwvc2NyaXB0Pg==", - result: 2, - name: "base64", - }, - { - input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNCwgJyonKTwvc2NyaXB0Pr+/", - result: 4, - name: "base64 with code points that differ from base64url" - }, - { - input: "data:text/html;base64,PHNjcml%09%20%20%0A%0C%0DwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNiwgJyonKTwvc2NyaXB0Pg==", - result: 6, - name: "ASCII whitespace in the input is removed" - } -].forEach(({ input, result, name }) => { - // Use promise_test so they go sequentially - promise_test(async t => { - const event = await new Promise((resolve, reject) => { - self.addEventListener("message", t.step_func(resolve), { once: true }); - const frame = document.body.appendChild(document.createElement("iframe")); - t.add_cleanup(() => frame.remove()); - - // The assumption is that postMessage() is quicker - t.step_timeout(reject, 500); - frame.src = input; - }); - assert_equals(event.data, result); - }, name); -}); - -// Failure cases -[ - { - input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoMywgJyonKTwvc2NyaXB0Pg=", - name: "base64 with incorrect padding", - }, - { - input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNSwgJyonKTwvc2NyaXB0Pr-_", - name: "base64url is not supported" - }, - { - input: "data:text/html;base64,%0BPHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNywgJyonKTwvc2NyaXB0Pg==", - name: "Vertical tab in the input leads to an error" - } -].forEach(({ input, name }) => { - // Continue to use promise_test so they go sequentially - promise_test(async t => { - const event = await new Promise((resolve, reject) => { - self.addEventListener("message", t.step_func(reject), { once: true }); - const frame = document.body.appendChild(document.createElement("iframe")); - t.add_cleanup(() => frame.remove()); - - // The assumption is that postMessage() is quicker - t.step_timeout(resolve, 500); - frame.src = input; - }); - }, name); -}); - -// I found some of the interesting code point cases above through brute force: -// -// for (i = 0; i < 256; i++) { -// w(btoa(" - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html b/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html deleted file mode 100644 index 75e9ece7ba5..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html +++ /dev/null @@ -1,19 +0,0 @@ - - - -FetchLater Referrer Header: No Referrer Policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html b/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html deleted file mode 100644 index b9f14171ba3..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html +++ /dev/null @@ -1,25 +0,0 @@ - - - -FetchLater Referrer Header: Origin When Cross Origin Policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html b/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html deleted file mode 100644 index ce7abf92039..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html +++ /dev/null @@ -1,23 +0,0 @@ - - - -FetchLater Referrer Header: Origin Policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html b/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html deleted file mode 100644 index 264beddc03a..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -FetchLater Referrer Header: Same Origin Policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html b/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html deleted file mode 100644 index 9133f2496fe..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -FetchLater Referrer Header: Strict Origin Policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html b/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html deleted file mode 100644 index 943d70bbc58..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -FetchLater Referrer Header: Strict Origin Policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html b/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html deleted file mode 100644 index a602e0003a4..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -FetchLater Referrer Header: Unsafe Url Policy - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/iframe.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/iframe.tentative.https.window.js deleted file mode 100644 index b30156b41b2..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/iframe.tentative.https.window.js +++ /dev/null @@ -1,31 +0,0 @@ -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js - -'use strict'; - -const { - HTTPS_ORIGIN, - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -async function loadElement(el) { - const loaded = new Promise(resolve => el.onload = resolve); - document.body.appendChild(el); - await loaded; -} - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - - // Loads a blank iframe that fires a fetchLater request. - const iframe = document.createElement('iframe'); - iframe.addEventListener('load', () => { - fetchLater(url, {activateAfter: 0}); - }); - await loadElement(iframe); - - // The iframe should have sent the request. - await expectBeacon(uuid, {count: 1}); -}, 'A blank iframe can trigger fetchLater.'); diff --git a/test/fixtures/wpt/fetch/fetch-later/new-window.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/new-window.tentative.https.window.js deleted file mode 100644 index 5a64eb596e1..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/new-window.tentative.https.window.js +++ /dev/null @@ -1,93 +0,0 @@ -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js - -'use strict'; - -const { - HTTPS_ORIGIN, - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -function fetchLaterPopupUrl(host, targetUrl) { - return `${host}/fetch/fetch-later/resources/fetch-later.html?url=${ - encodeURIComponent(targetUrl)}&activateAfter=0`; -} - -async function receiveMessageFromPopup(url) { - const expect = - new FetchLaterIframeExpectation(FetchLaterExpectationType.DONE); - const messageType = await new Promise((resolve, reject) => { - window.addEventListener('message', function handler(e) { - try { - if (expect.run(e, url)) { - window.removeEventListener('message', handler); - resolve(e.data.type); - } - } catch (err) { - reject(err); - } - }); - }); - - assert_equals(messageType, FetchLaterIframeMessageType.DONE); -} - -for (const target of ['', '_blank']) { - // NOTE: noopener popup window cannot communicate back. It will be too - // unreliable to only use `expectBeacon()` to test such window. - for (const features of ['', 'popup']) { - parallelPromiseTest( - async t => { - const uuid = token(); - const url = - generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); - - // Opens a blank popup window that fires a fetchLater request. - const w = window.open( - `javascript: fetchLater("${url}", {activateAfter: 0})`, target, - features); - await new Promise(resolve => w.addEventListener('load', resolve)); - - // The popup should have sent the request. - await expectBeacon(uuid, {count: 1}); - w.close(); - }, - `A blank window[target='${target}'][features='${ - features}'] can trigger fetchLater.`); - - parallelPromiseTest( - async t => { - const uuid = token(); - const popupUrl = - fetchLaterPopupUrl(HTTPS_ORIGIN, generateSetBeaconURL(uuid)); - - // Opens a same-origin popup that fires a fetchLater request. - const w = window.open(popupUrl, target, features); - await receiveMessageFromPopup(popupUrl); - - // The popup should have sent the request. - await expectBeacon(uuid, {count: 1}); - w.close(); - }, - `A same-origin window[target='${target}'][features='${ - features}'] can trigger fetchLater.`); - - parallelPromiseTest( - async t => { - const uuid = token(); - const popupUrl = fetchLaterPopupUrl( - HTTPS_NOTSAMESITE_ORIGIN, generateSetBeaconURL(uuid)); - - // Opens a cross-origin popup that fires a fetchLater request. - const w = window.open(popupUrl, target, features); - await receiveMessageFromPopup(popupUrl); - - // The popup should have sent the request. - await expectBeacon(uuid, {count: 1}); - w.close(); - }, - `A cross-origin window[target='${target}'][features='${ - features}'] can trigger fetchLater.`); - } -} diff --git a/test/fixtures/wpt/fetch/fetch-later/non-secure.window.js b/test/fixtures/wpt/fetch/fetch-later/non-secure.window.js deleted file mode 100644 index c13932e353c..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/non-secure.window.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -test(() => { - assert_false(window.hasOwnProperty('fetchLater')); -}, `fetchLater() is not supported in non-secure context.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/README.md b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/README.md deleted file mode 100644 index a0a724a25e0..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Permissions Policy: "deferred-fetch" Tests - -This folder contains tests to cover the permissions policy "deferred-fetch", -which is used to gate the `fetchLater()` API. - -The tests follow the patterns from -permissions-policy/README.md to cover all use cases of permissions policy for a -new feature. diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy-attribute-redirect.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy-attribute-redirect.tentative.https.window.js deleted file mode 100644 index 13870bc94b0..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy-attribute-redirect.tentative.https.window.js +++ /dev/null @@ -1,34 +0,0 @@ -// META: title=Permissions Policy "deferred-fetch" is allowed to redirect by allow attribute -// META: script=/permissions-policy/resources/permissions-policy.js -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/permissions-policy/resources/helper.js -// META: timeout=long -'use strict'; - -const { - HTTPS_ORIGIN, - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -const baseUrl = '/permissions-policy/resources/redirect-on-load.html#'; -// https://whatpr.org/fetch/1647.html#dom-permissionspolicy-deferred-fetch-minimal -const description = 'Permissions policy allow="deferred-fetch"'; - -async_test(t => { - test_feature_availability( - 'fetchLater()', t, - getDeferredFetchPolicyInIframeHelperUrl(`${baseUrl}${HTTPS_ORIGIN}`), - expect_feature_available_default, /*feature_name=*/ 'deferred-fetch'); -}, `${description} allows fetchLater() from a redirected same-origin iframe.`); - -// By default, "deferred-fetch" is off for cross-origin iframe. -// It requires a Permissions-Policy header in addition to the allow attribute. -async_test(t => { - test_feature_availability( - 'fetchLater()', t, - getDeferredFetchPolicyInIframeHelperUrl( - `${baseUrl}${HTTPS_NOTSAMESITE_ORIGIN}`), - expect_feature_unavailable_default, /*feature_name=*/ 'deferred-fetch'); -}, `${description} disallows fetchLater() from a redirected cross-origin iframe.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy-attribute.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy-attribute.tentative.https.window.js deleted file mode 100644 index c881609b4f1..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy-attribute.tentative.https.window.js +++ /dev/null @@ -1,36 +0,0 @@ -// META: title=Permissions Policy "deferred-fetch" is allowed by allow attribute -// META: script=/permissions-policy/resources/permissions-policy.js -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/permissions-policy/resources/helper.js -// META: timeout=long -'use strict'; - -const { - HTTPS_ORIGIN, - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -const description = 'Permissions policy "deferred-fetch"'; -const attribute = 'allow="deferred-fetch" attribute'; - -async_test( - t => { - test_feature_availability( - 'fetchLater()', t, - getDeferredFetchPolicyInIframeHelperUrl(HTTPS_ORIGIN), - expect_feature_available_default, /*feature_name=*/ 'deferred-fetch'); - }, - `${description} can be enabled in the same-origin iframe using ${ - attribute}.`); - -async_test( - t => { - test_feature_availability( - 'fetchLater()', t, - getDeferredFetchPolicyInIframeHelperUrl(HTTPS_NOTSAMESITE_ORIGIN), - expect_feature_available_default, /*feature_name=*/ 'deferred-fetch'); - }, - `${description} can be enabled in the cross-origin iframe using ${ - attribute}.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy.tentative.https.window.js deleted file mode 100644 index 24628f0103a..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy.tentative.https.window.js +++ /dev/null @@ -1,45 +0,0 @@ -// META: title=Permissions Policy "deferred-fetch" is allowed -// META: script=/permissions-policy/resources/permissions-policy.js -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/permissions-policy/resources/helper.js -// META: timeout=long -'use strict'; - -const { - HTTPS_ORIGIN, - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -const description = 'Permissions policy header: "deferred-fetch=*"'; - -parallelPromiseTest(async _ => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - - // Request the browser to fetchLater() immediately. - fetchLater(url, {activateAfter: 0}); - - await expectBeacon(uuid, {count: 1}); -}, `${description} allows fetchLater() in the top-level document.`); - -async_test(t => { - test_feature_availability( - 'fetchLater()', t, getDeferredFetchPolicyInIframeHelperUrl(HTTPS_ORIGIN), - expect_feature_available_default); -}, `${description} allows fetchLater() in the same-origin iframe.`); - -async_test(t => { - test_feature_availability( - 'fetchLater()', t, - getDeferredFetchPolicyInIframeHelperUrl(HTTPS_NOTSAMESITE_ORIGIN), - expect_feature_available_default); -}, `${description} allows fetchLater() in the cross-origin iframe.`); - -async_test(t => { - test_feature_availability( - 'fetchLater()', t, - getDeferredFetchPolicyInIframeHelperUrl(HTTPS_NOTSAMESITE_ORIGIN), - expect_feature_available_default, /*feature_name=*/ 'deferred-fetch'); -}, `${description} allow="deferred-fetch" allows fetchLater() in the cross-origin iframe.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy.tentative.https.window.js.headers b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy.tentative.https.window.js.headers deleted file mode 100644 index cd356bd1a39..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy.tentative.https.window.js.headers +++ /dev/null @@ -1 +0,0 @@ -Permissions-Policy: deferred-fetch=* diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-default-permissions-policy.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-default-permissions-policy.tentative.https.window.js deleted file mode 100644 index 4982221952f..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-default-permissions-policy.tentative.https.window.js +++ /dev/null @@ -1,43 +0,0 @@ -// META: title=Permissions Policy "deferred-fetch" default behavior -// META: script=/permissions-policy/resources/permissions-policy.js -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/permissions-policy/resources/helper.js -// META: timeout=long -'use strict'; - -const { - HTTPS_ORIGIN, - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -// https://whatpr.org/fetch/1647.html#dom-permissionspolicy-deferred-fetch -const deferredFetchPolicy = - 'Default "deferred-fetch" permissions policy ["self"]'; -// https://whatpr.org/fetch/1647.html#dom-permissionspolicy-deferred-fetch-minimal -const deferredFetchMinimalPolicy = - 'Default "deferred-fetch-minimal" permissions policy ["*"]'; - -parallelPromiseTest(async _ => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - - // Request the browser to fetchLater() immediately. - fetchLater(url, {activateAfter: 0}); - - await expectBeacon(uuid, {count: 1}); -}, `${deferredFetchPolicy} allows fetchLater() in the top-level document.`); - -async_test(t => { - test_feature_availability( - 'fetchLater()', t, getDeferredFetchPolicyInIframeHelperUrl(HTTPS_ORIGIN), - expect_feature_available_default); -}, `${deferredFetchPolicy} allows fetchLater() in the same-origin iframe.`); - -async_test(t => { - test_feature_availability( - 'fetchLater()', t, - getDeferredFetchPolicyInIframeHelperUrl(HTTPS_NOTSAMESITE_ORIGIN), - expect_feature_available_default); -}, `${deferredFetchMinimalPolicy} allows fetchLater() in the cross-origin iframe.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-supported-by-permissions-policy.tentative.window.js b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-supported-by-permissions-policy.tentative.window.js deleted file mode 100644 index 385b0e054ed..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/deferred-fetch-supported-by-permissions-policy.tentative.window.js +++ /dev/null @@ -1,9 +0,0 @@ -// META: title=The feature list should advertise deferred-fetch -'use strict'; - -// https://w3c.github.io/webappsec-permissions-policy/#dom-permissions-policy-features -// https://wicg.github.io/local-fonts/#permissions-policy -test(() => { - assert_in_array('deferred-fetch', document.featurePolicy.features()); - assert_in_array('deferred-fetch-minimal', document.featurePolicy.features()); -}, 'document.featurePolicy.features should advertise deferred-fetch.'); diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/resources/helper.js b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/resources/helper.js deleted file mode 100644 index 502e07aa4b5..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/resources/helper.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -/** - * Returns a URL to a document that can be used to initialize an iframe to test - * whether the Permissions Policy "deferred-fetch" or "deferred-fetch-minimal" - * is enabled. - */ -function getDeferredFetchPolicyInIframeHelperUrl(iframeOrigin) { - if (!iframeOrigin.endsWith('/')) { - iframeOrigin += '/'; - } - return `${ - iframeOrigin}fetch/fetch-later/permissions-policy/resources/permissions-policy-deferred-fetch.html`; -} diff --git a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/resources/permissions-policy-deferred-fetch.html b/test/fixtures/wpt/fetch/fetch-later/permissions-policy/resources/permissions-policy-deferred-fetch.html deleted file mode 100644 index f5ce3178db4..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/permissions-policy/resources/permissions-policy-deferred-fetch.html +++ /dev/null @@ -1,14 +0,0 @@ - - diff --git a/test/fixtures/wpt/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js deleted file mode 100644 index 32a3e106a0c..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js +++ /dev/null @@ -1,26 +0,0 @@ -// META: title=FetchLater: allowed by CSP -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -'use strict'; - -const { - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -// FetchLater requests allowed by Content Security Policy. -// https://w3c.github.io/webappsec-csp/#should-block-request - -const meta = document.createElement('meta'); -meta.setAttribute('http-equiv', 'Content-Security-Policy'); -meta.setAttribute('content', `connect-src 'self' ${HTTPS_NOTSAMESITE_ORIGIN}`); -document.head.appendChild(meta); - -promise_test(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); - fetchLater(url, {activateAfter: 0}); - - await expectBeacon(uuid, {count: 1}); - t.done(); -}, 'FetchLater allowed by CSP should succeed'); diff --git a/test/fixtures/wpt/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js deleted file mode 100644 index ca9d881e8cd..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js +++ /dev/null @@ -1,31 +0,0 @@ -// META: title=FetchLater: blocked by CSP -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -'use strict'; - -const { - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -// FetchLater requests blocked by Content Security Policy are rejected. -// https://w3c.github.io/webappsec-csp/#should-block-request - -const meta = document.createElement('meta'); -meta.setAttribute('http-equiv', 'Content-Security-Policy'); -meta.setAttribute('content', 'connect-src \'self\''); -document.head.appendChild(meta); - -promise_test(async t => { - const uuid = token(); - const cspViolationUrl = - generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); - fetchLater(cspViolationUrl, {activateAfter: 0}); - - await new Promise( - resolve => window.addEventListener('securitypolicyviolation', e => { - assert_equals(e.violatedDirective, 'connect-src'); - resolve(); - })); - t.done(); -}, 'FetchLater blocked by CSP should reject'); diff --git a/test/fixtures/wpt/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js deleted file mode 100644 index 584f476b456..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js +++ /dev/null @@ -1,33 +0,0 @@ -// META: title=FetchLater: redirect blocked by CSP -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: timeout=long - -'use strict'; - -const { - HTTPS_NOTSAMESITE_ORIGIN, -} = get_host_info(); - -// FetchLater requests redirect to URL blocked by Content Security Policy. -// https://w3c.github.io/webappsec-csp/#should-block-request - -const meta = document.createElement('meta'); -meta.setAttribute('http-equiv', 'Content-Security-Policy'); -meta.setAttribute('content', 'connect-src \'self\''); -document.head.appendChild(meta); - -promise_test(async t => { - const uuid = token(); - const cspViolationUrl = - generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); - const url = - `/common/redirect.py?location=${encodeURIComponent(cspViolationUrl)}`; - fetchLater(url, {activateAfter: 0}); - - // TODO(crbug.com/1465781): redirect csp check is handled in browser, of which - // result cannot be populated to renderer at this moment. - await expectBeacon(uuid, {count: 0}); - t.done(); -}, 'FetchLater redirect blocked by CSP should reject'); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/README.md b/test/fixtures/wpt/fetch/fetch-later/quota/README.md deleted file mode 100644 index fc59ea73f4e..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Quota Tests - -This folder contains tests to cover the `fetchLater()` API's -per-reporting-origin quota. - -See the following links: - -- Initial Proposal: https://github.com/WICG/pending-beacon/issues/87#issuecomment-1985358609. -- fetch PR: https://github.com/whatwg/fetch/pull/1647 -- html PR: https://github.com/whatwg/html/pull/10903 diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/accumulated-oversized-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/accumulated-oversized-payload.tentative.https.window.js deleted file mode 100644 index 6439b392e2a..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/accumulated-oversized-payload.tentative.https.window.js +++ /dev/null @@ -1,63 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// Skips FormData & URLSearchParams, as browser adds extra bytes to them -// in addition to the user-provided content. It is difficult to test a -// request right at the quota limit. -// Skips File & Blob as it's difficult to estimate what additional data are -// added into them. -const dataType = BeaconDataType.String; - -// Request headers are counted into total request size. -const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'}); - -const requestUrl = `${HTTPS_ORIGIN}/`; -const quota = getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers); -const halfQuota = Math.ceil(quota / 2); - - -// Tests that a reporting origin only allow queuing requests within its quota. -test( - () => { - const controller = new AbortController(); - - // Queues with the 1st call (POST) that sends max/2 quota. - fetchLater(requestUrl, { - method: 'POST', - signal: controller.signal, - body: makeBeaconData(generatePayload(halfQuota), dataType), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // Makes the 2nd call (POST) to the same reporting origin that sends - // max bytes, which should be rejected. - assert_throws_dom('QuotaExceededError', () => { - fetchLater(requestUrl, { - method: 'POST', - signal: controller.signal, - body: makeBeaconData(generatePayload(quota), dataType), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - }); - - // Makes the 3rd call (GET) to the same reporting origin, where its - // request size is len(requestUrl) + headers, which should be accepted. - fetchLater(requestUrl, { - method: 'GET', - signal: controller.signal, - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // Release quota taken by the pending requests for subsequent tests. - controller.abort(); - }, - `The 2nd fetchLater(same-origin) call in the top-level document is not allowed to exceed per-origin quota for its POST body of ${ - dataType}.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/README.md b/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/README.md deleted file mode 100644 index bd802f0b2d2..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Quota Tests for Cross-Origin iframes - -This folder contains tests to cover fetchLater() requests from cross-origin -iframe. - -According to [spec], up to 16 cross-origin iframes can get a minimal quota (8kb) -to make fetchLater() calls. - -[spec]: https://whatpr.org/fetch/1647.html#available-deferred-fetch-quota diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/accumulated-oversized-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/accumulated-oversized-payload.tentative.https.window.js deleted file mode 100644 index 071a9c600b7..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/accumulated-oversized-payload.tentative.https.window.js +++ /dev/null @@ -1,56 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// Skips FormData & URLSearchParams, as browser adds extra bytes to them -// in addition to the user-provided content. It is difficult to test a -// request right at the quota limit. -// Skips File & Blob as it's difficult to estimate what additional data are -// added into them. -const dataType = BeaconDataType.String; - -// Request headers are counted into total request size. -const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'}); - -const requestUrl = `${HTTPS_ORIGIN}/`; -const quota = getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers); -const SMALL_REQUEST_BODY_SIZE = 20; - -// Tests that a reporting origin's quota is not shared with another cross-origin -// iframe. -promise_test( - async _ => { - const controller = new AbortController(); - - // Queues with the 1st call (POST) that sends max quota. - fetchLater(requestUrl, { - method: 'POST', - signal: controller.signal, - body: makeBeaconData(generatePayload(quota), dataType), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // In a default cross-origin iframe, makes the 2nd call (POST) to the - // same reporting origin that sends some bytes, which should also be - // accepted, as the iframe has its own QUOTA_PER_CROSS_ORIGIN for - // fetchLater. - await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - targetUrl: requestUrl, - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: SMALL_REQUEST_BODY_SIZE, - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // Release quota taken by the pending requests for subsequent tests. - controller.abort(); - }, - `The 2nd fetchLater(same-origin) call in a default cross-origin child iframe has its owned per-origin quota for a request POST body of ${ - dataType}.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/empty-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/empty-payload.tentative.https.window.js deleted file mode 100644 index 6c7becfacaf..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/empty-payload.tentative.https.window.js +++ /dev/null @@ -1,37 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// In a cross-origin iframe, test making a POST request with empty -// payload, which is not accepted by fetchLater API. -for (const dataType in BeaconDataType) { - if (dataType === BeaconDataType.FormData) { - // An empty FormData object serializes to non-empty String. Hence, there - // will be no error thrown from fetchLater. - parallelPromiseTest( - async _ => await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: 0, - }), - `fetchLater() accepts a non-empty POST request body of ${ - dataType} in a default cross-origin iframe.`); - continue; - } - - parallelPromiseTest( - async _ => await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: 0, - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_JS, TypeError), - }), - `fetchLater() does not accept empty POST request body of ${ - dataType} in a default cross-origin iframe.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/max-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/max-payload.tentative.https.window.js deleted file mode 100644 index 480f90988bf..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/max-payload.tentative.https.window.js +++ /dev/null @@ -1,113 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// Skips FormData & URLSearchParams, as browser adds extra bytes to them -// in addition to the user-provided content. It is difficult to test a -// request right at the quota limit. -// Skips File & Blob as it's difficult to estimate what additional data are -// added into them. -const dataType = BeaconDataType.String; - -// Request headers are counted into total request size. -const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'}); - - -// In a cross-origin iframe, test making a POST request (same origin as parent -// frame) with max possible payload. -promise_test( - async _ => { - const uuid = token(); - const requestUrl = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN}); - - await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - targetUrl: requestUrl, - uuid: uuid, - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: - getRemainingQuota(QUOTA_PER_CROSS_ORIGIN, requestUrl, headers), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - }, - `fetchLater() accepts max payload in a parent-frame-origin POST request body of ${ - dataType} in a default cross-origin iframe.`); - -// In a cross-origin iframe, test making a POST request (same origin as parent -// frame) with max possible payload + 1 byte. -promise_test( - async _ => { - const uuid = token(); - const requestUrl = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN}); - - await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - targetUrl: requestUrl, - uuid: uuid, - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: - getRemainingQuota(QUOTA_PER_CROSS_ORIGIN, requestUrl, headers) + 1, - // Required, as the size of referrer also take up quota. - referrer: '', - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_DOM, 'QuotaExceededError'), - }); - }, - `fetchLater() rejects max+1 payload in a parent-frame-origin POST request body of ${ - dataType} in a default cross-origin iframe.`); - -// In a cross-origin iframe, test making a POST request (same origin as iframe) -// with max possible payload. -promise_test( - async _ => { - const uuid = token(); - const crossOriginRequestUrl = - generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); - - await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - targetUrl: crossOriginRequestUrl, - uuid: uuid, - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: getRemainingQuota( - QUOTA_PER_CROSS_ORIGIN, crossOriginRequestUrl, headers), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - }, - `fetchLater() accepts max payload in a self-frame-origin POST request body of ${ - dataType} in a default cross-origin iframe.`); - -// In a cross-origin iframe, test making a POST request (same origin as iframe) -// with max possible payload + 1 byte. -promise_test( - async _ => { - const uuid = token(); - const crossOriginRequestUrl = - generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN}); - - await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - targetUrl: crossOriginRequestUrl, - uuid: uuid, - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: getRemainingQuota( - QUOTA_PER_CROSS_ORIGIN, crossOriginRequestUrl, headers) + - 1, - // Required, as the size of referrer also take up quota. - referrer: '', - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_DOM, 'QuotaExceededError'), - }); - }, - `fetchLater() rejects max+1 payload in a self-frame-origin POST request body of ${ - dataType} in a default cross-origin iframe.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/multiple-iframes.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/multiple-iframes.tentative.https.window.js deleted file mode 100644 index 3538c73a396..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/multiple-iframes.tentative.https.window.js +++ /dev/null @@ -1,62 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// Skips FormData & URLSearchParams, as browser adds extra bytes to them -// in addition to the user-provided content. It is difficult to test a -// request right at the quota limit. -// Skips File & Blob as it's difficult to estimate what additional data are -// added into them. -const dataType = BeaconDataType.String; - -// Request headers are counted into total request size. -const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'}); - -// Test 2 direct cross-origin iframes does not share the same quota, even if -// they are same origin. -promise_test(async _ => { - const controller = new AbortController(); - const uuid = token(); - const requestUrl = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN}); - - // Queues a max bytes request in the 1st cross-origin iframe. - await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - targetUrl: requestUrl, - method: 'POST', - bodyType: dataType, - bodySize: getRemainingQuota(QUOTA_PER_CROSS_ORIGIN, requestUrl, headers), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // Queues a max bytes request in the 2nd cross-origin iframe, which should - // still be accepted. - await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - targetUrl: requestUrl, - method: 'POST', - bodyType: dataType, - bodySize: getRemainingQuota(QUOTA_PER_CROSS_ORIGIN, requestUrl, headers), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // Queues a max bytes request in the root document, which should be - // accepted. - fetchLater(requestUrl, { - method: 'POST', - body: generatePayload(QUOTA_PER_ORIGIN, dataType), - signal: controller.signal, - body: generatePayload( - getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers), dataType), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // Release quota taken by the pending requests for subsequent tests. - document.body.innerHTML = ''; - controller.abort(); -}, `fetchLater() request quota are delegated to cross-origin iframes and not shared, even if they are same origin.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/oversized-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/oversized-payload.tentative.https.window.js deleted file mode 100644 index 43a60515134..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/oversized-payload.tentative.https.window.js +++ /dev/null @@ -1,25 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); -const OVERSIZED_REQUEST_BODY_SIZE = QUOTA_PER_CROSS_ORIGIN + 1; - -for (const dataType in BeaconDataType) { - // In a cross-origin iframe, test making a POST request with - // oversized payload, which should be rejected by fetchLater API. - parallelPromiseTest( - async _ => await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: OVERSIZED_REQUEST_BODY_SIZE, - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_DOM, 'QuotaExceededError'), - }), - `fetchLater() does not accept payload[size=${ - OVERSIZED_REQUEST_BODY_SIZE}] exceeding per-origin quota in a POST request body of ${ - dataType} in a default cross-origin iframe.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/small-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/small-payload.tentative.https.window.js deleted file mode 100644 index ad73db5516e..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/cross-origin-iframe/small-payload.tentative.https.window.js +++ /dev/null @@ -1,22 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -'use strict'; - -const {HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); -const SMALL_REQUEST_BODY_SIZE = 20; - -for (const dataType in BeaconDataType) { - // In a cross-origin iframe, test making a POST request with small - // payload. - parallelPromiseTest( - async _ => await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, { - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: SMALL_REQUEST_BODY_SIZE, - }), - `fetchLater() accepts payload[size=${ - SMALL_REQUEST_BODY_SIZE}] in a POST request body of ${ - dataType} in a default cross-origin iframe.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/empty-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/empty-payload.tentative.https.window.js deleted file mode 100644 index c2c4dd50326..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/empty-payload.tentative.https.window.js +++ /dev/null @@ -1,37 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// Test making a HTTP POST request with empty payload, which is not accepted by -// fetchLater API. -for (const dataType in BeaconDataType) { - const requestInit = { - activateAfter: 0, - method: 'POST', - body: makeBeaconData('', dataType) - }; - - if (dataType === BeaconDataType.FormData) { - // An empty FormData object serializes to non-empty String. Hence, there - // will be no error thrown from fetchLater. - parallelPromiseTest(async _ => { - expectFetchLater(requestInit); - }, `fetchLater() accepts a non-empty POST request body of ${dataType}.`); - continue; - } - test( - () => assert_throws_js(TypeError, () => fetchLater('/', requestInit)), - `fetchLater() does not accept an empty POST request body of ${ - dataType}.`); -} - -// Test making HTTP non-POST requests, which has no payload and should be -// accepted by fetchLater API. -for (const method of ['GET', 'DELETE', 'PUT']) { - parallelPromiseTest( - async _ => expectFetchLater({activateAfter: 0, method: method}), - `fetchLater() accept a ${method} request.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/max-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/max-payload.tentative.https.window.js deleted file mode 100644 index 8457060797a..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/max-payload.tentative.https.window.js +++ /dev/null @@ -1,48 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN} = get_host_info(); - -// Skips FormData & URLSearchParams, as browser adds extra bytes to them -// in addition to the user-provided content. It is difficult to test a -// request right at the quota limit. -// Skips File & Blob as it's difficult to estimate what additional data are -// added into them. -const dataType = BeaconDataType.String; - -// Request headers are counted into total request size. -const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'}); - -// Test making a POST request with max possible payload. -promise_test(async _ => { - const uuid = token(); - const requestUrl = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN}); - await expectFetchLater( - { - activateAfter: 0, - method: 'POST', - bodySize: getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers), - }, - { - targetUrl: requestUrl, - uuid: uuid, - }); -}, `fetchLater() accepts max payload in a POST request body of ${dataType}.`); - -// Test making a POST request with max+1 possible payload. -test(_ => { - const uuid = token(); - const requestUrl = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN}); - assert_throws_dom( - 'QuotaExceededError', - () => fetchLater(requestUrl, { - activateAfter: 0, - method: 'POST', - body: generatePayload( - getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers) + 1, - dataType), - })); -}, `fetchLater() rejects max+1 payload in a POST request body of ${dataType}.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/multiple-origins.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/multiple-origins.tentative.https.window.js deleted file mode 100644 index 5849068e4d5..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/multiple-origins.tentative.https.window.js +++ /dev/null @@ -1,43 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -for (const dataType in BeaconDataType) { - // Tests multiple request reporting origins within same document. - test( - () => { - const controller = new AbortController(); - - // Makes the 1st call (POST) that sends max/2+1 quota to `HTTPS_ORIGIN`. - fetchLater(`${HTTPS_ORIGIN}/`, { - method: 'POST', - signal: controller.signal, - body: makeBeaconData( - generatePayload(QUOTA_PER_REPORTING_ORIGIN / 2 + 1), dataType) - }); - - // Makes the 2nd call (POST) that sends max/2+1 of allowed quota, but to - // a different reporting origin `HTTPS_NOTSAMESITE_ORIGIN`. - fetchLater(`${HTTPS_NOTSAMESITE_ORIGIN}/`, { - method: 'POST', - signal: controller.signal, - body: makeBeaconData( - generatePayload(QUOTA_PER_REPORTING_ORIGIN / 2 + 1), dataType) - }); - - // Makes the 3rd call (GET) to `HTTPS_NOTSAMESITE_ORIGIN`. - fetchLater(`${HTTPS_NOTSAMESITE_ORIGIN}/`, { - method: 'GET', - signal: controller.signal, - }); - - // Release quota taken by the pending requests for subsequent tests. - controller.abort(); - }, - `fetchLater() has per-request-origin quota for its POST body of ${ - dataType}.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/oversized-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/oversized-payload.tentative.https.window.js deleted file mode 100644 index 0036d040d8e..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/oversized-payload.tentative.https.window.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const OVERSIZED_REQUEST_BODY_SIZE = QUOTA_PER_ORIGIN + 1; - -for (const dataType in BeaconDataType) { - // Test making a POST request with oversized payload, which should be rejected - // by fetchLater API. - test( - () => assert_throws_dom( - 'QuotaExceededError', - () => fetchLater('/', { - activateAfter: 0, - method: 'POST', - body: makeBeaconData( - generatePayload(OVERSIZED_REQUEST_BODY_SIZE), dataType), - })), - `fetchLater() does not accept payload[size=${ - OVERSIZED_REQUEST_BODY_SIZE}] exceeding per-origin quota in a POST request body of ${ - dataType}.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/resources/helper.js b/test/fixtures/wpt/fetch/fetch-later/quota/resources/helper.js deleted file mode 100644 index bd9571c82f5..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/resources/helper.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -// See the max returned value from -// https://whatpr.org/fetch/1647.html#available-deferred-fetch-quota -const QUOTA_PER_ORIGIN = 64 * 1024; // 64 kibibytes. -// See the "minimal quota" from -// https://whatpr.org/fetch/1647.html#available-deferred-fetch-quota -const QUOTA_PER_CROSS_ORIGIN = 8 * 1024; // 8 kibibytes. -// See the "quotaForRequestOrigin" from -// https://whatpr.org/fetch/1647.html#available-deferred-fetch-quota -const QUOTA_PER_REPORTING_ORIGIN = 64 * 1024; // 64 kibibytes. - - -/** - * Calculates max possible remaining quota for a fetchLater request. - * - * Note that this function does NOT know how many fetchLater calls has been made - * before calling, nor does it know anything about requests within the URL - * origin, and both of them take up quota for the origin. - * - * @param {number} maxQuota The max quota to deduct from. Caller must ensure - * this is actually used by its owned context. - * @param {String} url The URL for a fetchLater call. - * @param {Headers=} headers The headers for a fetchLater call. - * @return {number} The max possible per-origin quota for fetchLater calls. - */ -function getRemainingQuota(maxQuota, url, headers = {}) { - let quota = maxQuota - url.length; - if (headers instanceof Headers) { - for (const kv of headers.entries()) { - quota -= kv[0].length + kv[1].length; - } - } - return quota > 0 ? quota : 0; -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/README.md b/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/README.md deleted file mode 100644 index 025422314ed..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Quota Tests for Same-Origin iframes - -This folder contains tests to cover fetchLater() requests from same-origin -iframe. - -According to [spec], all same-origin iframes share the same quota with their -same-origin parent document. - -[spec]: https://whatpr.org/fetch/1647.html#available-deferred-fetch-quota diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/accumulated-oversized-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/accumulated-oversized-payload.tentative.https.window.js deleted file mode 100644 index 8b894c011ad..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/accumulated-oversized-payload.tentative.https.window.js +++ /dev/null @@ -1,55 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// Skips FormData & URLSearchParams, as browser adds extra bytes to them -// in addition to the user-provided content. It is difficult to test a -// request right at the quota limit. -// Skips File & Blob as it's difficult to estimate what additional data are -// added into them. -const dataType = BeaconDataType.String; - -// Request headers are counted into total request size. -const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'}); - -const requestUrl = `${HTTPS_ORIGIN}/`; -const quota = getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers); -const SMALL_REQUEST_BODY_SIZE = 20; - -// Tests that a reporting origin's quota is shared across same-origin frames. -promise_test( - async _ => { - const controller = new AbortController(); - - // Queues with the 1st call (POST) that sends max quota. - fetchLater(requestUrl, { - method: 'POST', - signal: controller.signal, - body: makeBeaconData(generatePayload(quota), dataType), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // In a same-origin iframe, makes the 2nd call (POST) to the same - // reporting origin that sends some bytes, which should be rejected. - await loadFetchLaterIframe(HTTPS_ORIGIN, { - targetUrl: requestUrl, - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: SMALL_REQUEST_BODY_SIZE, - // Required, as the size of referrer also take up quota. - referrer: '', - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_DOM, 'QuotaExceededError'), - }); - - // Release quota taken by the pending requests for subsequent tests. - controller.abort(); - }, - `The 2nd fetchLater(same-origin) call in a same-origin child iframe is not allowed to exceed per-origin quota for its POST body of ${ - dataType}.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/empty-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/empty-payload.tentative.https.window.js deleted file mode 100644 index 53cae60047e..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/empty-payload.tentative.https.window.js +++ /dev/null @@ -1,37 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// In a same-origin iframe, test making a POST request with empty payload, -// which is not accepted by fetchLater API. -for (const dataType in BeaconDataType) { - if (dataType === BeaconDataType.FormData) { - // An empty FormData object serializes to non-empty String. Hence, there - // will be no error thrown from fetchLater. - parallelPromiseTest( - async _ => await loadFetchLaterIframe(HTTPS_ORIGIN, { - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: 0, - }), - `fetchLater() accepts a non-empty POST request body of ${ - dataType} in same-origin iframe.`); - continue; - } - - parallelPromiseTest( - async _ => await loadFetchLaterIframe(HTTPS_ORIGIN, { - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: 0, - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_JS, TypeError), - }), - `fetchLater() does not accept empty POST request body of ${ - dataType} in same-origin iframe.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/max-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/max-payload.tentative.https.window.js deleted file mode 100644 index 9d865eeb5ee..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/max-payload.tentative.https.window.js +++ /dev/null @@ -1,61 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// Skips FormData & URLSearchParams, as browser adds extra bytes to them -// in addition to the user-provided content. It is difficult to test a -// request right at the quota limit. -// Skips File & Blob as it's difficult to estimate what additional data are -// added into them. -const dataType = BeaconDataType.String; - -// Request headers are counted into total request size. -const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'}); - -// In a same-origin iframe, test making a POST request with max possible -// payload. -promise_test( - async _ => { - const uuid = token(); - const requestUrl = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN}); - - await loadFetchLaterIframe(HTTPS_ORIGIN, { - targetUrl: requestUrl, - uuid: uuid, - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - }, - `fetchLater() accepts max payload in a POST request body of ${ - dataType} in same-origin iframe.`); - -// In a same-origin iframe, test making a POST request with max possible -// payload + 1 byte. -promise_test( - async _ => { - const uuid = token(); - const requestUrl = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN}); - - await loadFetchLaterIframe(HTTPS_ORIGIN, { - targetUrl: requestUrl, - uuid: uuid, - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers) + 1, - // Required, as the size of referrer also take up quota. - referrer: '', - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_DOM, 'QuotaExceededError'), - }); - }, - `fetchLater() rejects max+1 payload in a POST request body of ${ - dataType} in same-origin iframe.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/multiple-iframes.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/multiple-iframes.tentative.https.window.js deleted file mode 100644 index c6968903f94..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/multiple-iframes.tentative.https.window.js +++ /dev/null @@ -1,65 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info(); - -// Skips FormData & URLSearchParams, as browser adds extra bytes to them -// in addition to the user-provided content. It is difficult to test a -// request right at the quota limit. -// Skips File & Blob as it's difficult to estimate what additional data are -// added into them. -const dataType = BeaconDataType.String; - -// Request headers are counted into total request size. -const headers = new Headers({'Content-Type': 'text/plain;charset=UTF-8'}); - -// Test 2 direct same-origin iframes use the same quota with top-level document. -promise_test(async _ => { - const uuid = token(); - const requestUrl = generateSetBeaconURL(uuid, {host: HTTPS_ORIGIN}); - - // Queues a max bytes request in the 1st same-origin iframe. - await loadFetchLaterIframe(HTTPS_ORIGIN, { - targetUrl: requestUrl, - method: 'POST', - bodyType: dataType, - bodySize: getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers), - // Required, as the size of referrer also take up quota. - referrer: '', - }); - - // Queues a max bytes request to `HTTPS_ORIGIN` in the 2nd same-origin iframe, - // which should be rejected because the target URL's origin `HTTPS_ORIGIN` - // already has 64kb pending from 1st request. - await loadFetchLaterIframe(HTTPS_ORIGIN, { - targetUrl: requestUrl, - method: 'POST', - bodyType: dataType, - bodySize: getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers), - // Required, as the size of referrer also take up quota. - referrer: '', - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_DOM, 'QuotaExceededError'), - }); - - // Queues a max bytes request in the root document, which should also be - // rejected, because the target URL's origin `HTTPS_ORIGIN` already has 64kb - // pending from 1st request. - assert_throws_dom( - 'QuotaExceededError', - () => fetchLater(requestUrl, { - method: 'POST', - body: generatePayload( - getRemainingQuota(QUOTA_PER_ORIGIN, requestUrl, headers), dataType), - // Required, as the size of referrer also take up quota. - referrer: '', - })); - - // Release quota taken by the pending requests for subsequent tests. - for (const element of document.querySelectorAll('iframe')) { - element.remove(); - } -}, `fetchLater() request quota are shared by same-origin iframes and root.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/oversized-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/oversized-payload.tentative.https.window.js deleted file mode 100644 index d71b87190ce..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/oversized-payload.tentative.https.window.js +++ /dev/null @@ -1,25 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: script=/fetch/fetch-later/quota/resources/helper.js -'use strict'; - -const {HTTPS_ORIGIN} = get_host_info(); -const OVERSIZED_REQUEST_BODY_SIZE = QUOTA_PER_ORIGIN + 1; - -for (const dataType in BeaconDataType) { - // In a same-origin iframe, test making a POST request with oversized - // payload, which should be rejected by fetchLater API. - parallelPromiseTest( - async _ => await loadFetchLaterIframe(HTTPS_ORIGIN, { - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: OVERSIZED_REQUEST_BODY_SIZE, - expect: new FetchLaterIframeExpectation( - FetchLaterExpectationType.ERROR_DOM, 'QuotaExceededError'), - }), - `fetchLater() does not accept payload[size=${ - OVERSIZED_REQUEST_BODY_SIZE}] exceeding per-origin quota in a POST request body of ${ - dataType} in same-origin iframe.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/small-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/small-payload.tentative.https.window.js deleted file mode 100644 index 2b20a6fc629..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/same-origin-iframe/small-payload.tentative.https.window.js +++ /dev/null @@ -1,21 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -'use strict'; - -const {HTTPS_ORIGIN} = get_host_info(); -const SMALL_REQUEST_BODY_SIZE = 20; - -for (const dataType in BeaconDataType) { - // In a same-origin iframe, test making a POST request with small payload. - parallelPromiseTest( - async _ => await loadFetchLaterIframe(HTTPS_ORIGIN, { - activateAfter: 0, - method: 'POST', - bodyType: dataType, - bodySize: SMALL_REQUEST_BODY_SIZE, - }), - `fetchLater() accepts payload[size=${ - SMALL_REQUEST_BODY_SIZE}] in a POST request body of ${ - dataType} in same-origin iframe.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/quota/small-payload.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/quota/small-payload.tentative.https.window.js deleted file mode 100644 index 862ca09affd..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/quota/small-payload.tentative.https.window.js +++ /dev/null @@ -1,19 +0,0 @@ -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -'use strict'; - -const SMALL_REQUEST_BODY_SIZE = 20; - -for (const dataType in BeaconDataType) { - // Test making a POST request with small payload. - parallelPromiseTest( - async _ => expectFetchLater({ - activateAfter: 0, - method: 'POST', - body: - makeBeaconData(generatePayload(SMALL_REQUEST_BODY_SIZE), dataType), - }), - `fetchLater() accepts small payload in a POST request body of ${ - dataType}.`); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/resources/fetch-later-helper.js b/test/fixtures/wpt/fetch/fetch-later/resources/fetch-later-helper.js deleted file mode 100644 index ef1d7090a8f..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/resources/fetch-later-helper.js +++ /dev/null @@ -1,514 +0,0 @@ -/** - * IMPORTANT: Before using this file, you must also import the following files: - * - /common/utils.js - */ -'use strict'; - -const ROOT_NAME = 'fetch/fetch-later'; - -function parallelPromiseTest(func, description) { - async_test((t) => { - Promise.resolve(func(t)).then(() => t.done()).catch(t.step_func((e) => { - throw e; - })); - }, description); -} - -/** @enum {string} */ -const BeaconDataType = { - String: 'String', - ArrayBuffer: 'ArrayBuffer', - FormData: 'FormData', - URLSearchParams: 'URLSearchParams', - Blob: 'Blob', - File: 'File', -}; - -/** @enum {string} */ -const BeaconDataTypeToSkipCharset = { - String: '', - ArrayBuffer: '', - FormData: '\n\r', // CRLF characters will be normalized by FormData - URLSearchParams: ';,/?:@&=+$', // reserved URI characters - Blob: '', - File: '', -}; - -const BEACON_PAYLOAD_KEY = 'payload'; - -// Creates beacon data of the given `dataType` from `data`. -// @param {string} data - A string representation of the beacon data. Note that -// it cannot contain UTF-16 surrogates for all `BeaconDataType` except BLOB. -// @param {BeaconDataType} dataType - must be one of `BeaconDataType`. -// @param {string} contentType - Request Content-Type. -function makeBeaconData(data, dataType, contentType) { - switch (dataType) { - case BeaconDataType.String: - return data; - case BeaconDataType.ArrayBuffer: - return new TextEncoder().encode(data).buffer; - case BeaconDataType.FormData: - const formData = new FormData(); - if (data.length > 0) { - formData.append(BEACON_PAYLOAD_KEY, data); - } - return formData; - case BeaconDataType.URLSearchParams: - if (data.length > 0) { - return new URLSearchParams(`${BEACON_PAYLOAD_KEY}=${data}`); - } - return new URLSearchParams(); - case BeaconDataType.Blob: { - const options = {type: contentType || undefined}; - return new Blob([data], options); - } - case BeaconDataType.File: { - const options = {type: contentType || 'text/plain'}; - return new File([data], 'file.txt', options); - } - default: - throw Error(`Unsupported beacon dataType: ${dataType}`); - } -} - -// Create a string of `end`-`begin` characters, with characters starting from -// UTF-16 code unit `begin` to `end`-1. -function generateSequentialData(begin, end, skip) { - const codeUnits = Array(end - begin).fill().map((el, i) => i + begin); - if (skip) { - return String.fromCharCode( - ...codeUnits.filter(c => !skip.includes(String.fromCharCode(c)))); - } - return String.fromCharCode(...codeUnits); -} - -function generatePayload(size) { - if (size == 0) { - return ''; - } - const prefix = String(size) + ':'; - if (size < prefix.length) { - return Array(size).fill('*').join(''); - } - if (size == prefix.length) { - return prefix; - } - - return prefix + Array(size - prefix.length).fill('*').join(''); -} - -function generateSetBeaconURL(uuid, options) { - const host = (options && options.host) || ''; - let url = `${host}/${ROOT_NAME}/resources/set_beacon.py?uuid=${uuid}`; - if (options) { - if (options.expectOrigin !== undefined) { - url = `${url}&expectOrigin=${options.expectOrigin}`; - } - if (options.expectPreflight !== undefined) { - url = `${url}&expectPreflight=${options.expectPreflight}`; - } - if (options.expectCredentials !== undefined) { - url = `${url}&expectCredentials=${options.expectCredentials}`; - } - - if (options.useRedirectHandler) { - const redirect = `${host}/common/redirect.py` + - `?location=${encodeURIComponent(url)}`; - url = redirect; - } - } - return url; -} - -async function poll(asyncFunc, expected) { - const maxRetries = 30; - const waitInterval = 100; // milliseconds. - const delay = ms => new Promise(res => setTimeout(res, ms)); - - let result = {data: []}; - for (let i = 0; i < maxRetries; i++) { - result = await asyncFunc(); - if (!expected(result)) { - await delay(waitInterval); - continue; - } - return result; - } - return result; -} - -// Waits until the `options.count` number of beacon data available from the -// server. Defaults to 1. -// If `options.data` is set, it will be used to compare with the data from the -// response. -async function expectBeacon(uuid, options) { - const expectedCount = - (options && options.count !== undefined) ? options.count : 1; - - const res = await poll( - async () => { - const res = await fetch( - `/${ROOT_NAME}/resources/get_beacon.py?uuid=${uuid}`, - {cache: 'no-store'}); - return await res.json(); - }, - (res) => { - if (expectedCount == 0) { - // If expecting no beacon, we should try to wait as long as possible. - // So always returning false here until `poll()` decides to terminate - // itself. - return false; - } - return res.data.length == expectedCount; - }); - if (!options || !options.data) { - assert_equals( - res.data.length, expectedCount, - 'Number of sent beacons does not match expected count:'); - return; - } - - if (expectedCount == 0) { - assert_equals( - res.data.length, 0, - 'Number of sent beacons does not match expected count:'); - return; - } - - const decoder = options && options.percentDecoded ? (s) => { - // application/x-www-form-urlencoded serializer encodes space as '+' - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent - s = s.replace(/\+/g, '%20'); - return decodeURIComponent(s); - } : (s) => s; - - assert_equals( - res.data.length, options.data.length, - `The size of beacon data ${ - res.data.length} from server does not match expected value ${ - options.data.length}.`); - for (let i = 0; i < options.data.length; i++) { - assert_equals( - decoder(res.data[i]), options.data[i], - 'The beacon data does not match expected value.'); - } -} - -function generateHTML(script) { - return ``; -} - -// Loads `script` into an iframe and appends it to the current document. -// Returns the loaded iframe element. -async function loadScriptAsIframe(script) { - const iframe = document.createElement('iframe'); - iframe.srcdoc = generateHTML(script); - const iframeLoaded = new Promise(resolve => iframe.onload = resolve); - document.body.appendChild(iframe); - await iframeLoaded; - return iframe; -} - -/** - * A helper to make a fetchLater request and wait for it being received. - * - * This function can also be used when the caller does not care about where a - * fetchLater() makes request to. - * - * @param {!RequestInit} init The request config to pass into fetchLater() call. - */ -async function expectFetchLater( - init, {targetUrl = undefined, uuid = undefined} = {}) { - if ((targetUrl && !uuid) || (!targetUrl && uuid)) { - throw new Error('uuid and targetUrl must be provided together.'); - } - if (uuid && targetUrl && !targetUrl.includes(uuid)) { - throw new Error(`Conflicting uuid=${ - uuid} is provided: must also be included in the targetUrl ${ - targetUrl}`); - } - if (!uuid) { - uuid = token(); - } - if (!targetUrl) { - targetUrl = generateSetBeaconURL(uuid); - } - - fetchLater(targetUrl, init); - - await expectBeacon(uuid, {count: 1}); -} - -/** - * A helper to append `el` into document and wait for it being loaded. - * @param {!Element} el - */ -async function loadElement(el) { - const loaded = new Promise(resolve => el.onload = resolve); - document.body.appendChild(el); - await loaded; -} - -/** - * The options to configure a fetchLater() call in an iframe. - * @record - */ -class FetchLaterIframeOptions { - constructor() { - /** - * @type {string=} The url to pass to the fetchLater() call. - */ - this.targetUrl; - - /** - * @type {string=} The uuid to wait for. Must also be part of `targetUrl`. - */ - this.uuid; - - /** - * @type {number=} The activateAfter field of DeferredRequestInit to pass - * to the fetchLater() call. - * https://whatpr.org/fetch/1647.html#dictdef-deferredrequestinit - */ - this.activateAfter; - - /** - * @type {string=} The method field of DeferredRequestInit to pass to the - * fetchLater() call. - * https://whatpr.org/fetch/1647.html#dictdef-deferredrequestinit - */ - this.method; - - /** - * @type {string=} The referrer field of DeferredRequestInit to pass to the - * fetchLater() call. - * https://whatpr.org/fetch/1647.html#requestinit - */ - this.referrer; - - /** - * @type {string=} One of the `BeaconDataType` to tell the iframe how to - * generate the body for its fetchLater() call. - */ - this.bodyType; - - /** - * @type {number=} The size to tell the iframe how to generate the body of - * its fetchLater() call. - */ - this.bodySize; - - /** - * @type {bool} Whether to set allow="deferred-fetch" attribute for the - * iframe. Combing with a Permissions-Policy header, this will enable - * fetchLater() being used in a cross-origin iframe. - */ - this.allowDeferredFetch; - - /** - * @type {FetchLaterIframeExpectation=} The expectation on the iframe's - * behavior. - */ - this.expect; - } -} - -/** - * The enum to classify the messages posted from an iframe that has called - * fetchLater() API. - * @enum {string} - */ -const FetchLaterIframeMessageType = { - // Tells that a fetchLater() call has been executed without any error thrown. - DONE: 'fetchLater.done', - // Tells that there are some error thrown from a fetchLater() call. - ERROR: 'fetchLater.error', -}; - -/** - * The enum to indicate what type of iframe behavior the caller is expecting. - * @enum {number} - */ -const FetchLaterExpectationType = { - // A fetchLater() call should have been made without any errors. - DONE: 0, - // A fetchLater() call is made and an JS error is thrown. - ERROR_JS: 1, - // A fetchLater() call is made and an DOMException is thrown. - ERROR_DOM: 2, -}; - -class FetchLaterExpectationError extends Error { - constructor(src, actual, expected) { - const message = `iframe[src=${src}] threw ${actual}, expected ${expected}`; - super(message); - } -} - -class FetchLaterIframeExpectation { - constructor(expectationType, expectedError) { - this.expectationType = expectationType; - if (expectationType == FetchLaterExpectationType.DONE && !expectedError) { - this.expectedErrorType = undefined; - } else if ( - expectationType == FetchLaterExpectationType.ERROR_JS && - typeof expectedError == 'function') { - this.expectedErrorType = expectedError; - } else if ( - expectationType == FetchLaterExpectationType.ERROR_DOM && - typeof expectedError == 'string') { - this.expectedDomErrorName = expectedError; - } else { - throw Error(`Expectation type "${expectationType}" and expected error "${ - expectedError}" do not match`); - } - } - - /** - * Verifies the message from `e` against the configured expectation. - * - * @param {MessageEvent} e - * @param {string} url The source URL of the iframe where `e` is dispatched - * from. - * @return {bool} - * - Returns true if the expected message event is passed into the function - * and the expectation is fulfilled. The caller should be able to safely - * remove the message event listener afterwards. - * - Returns false if the passed in event is not of the expected type. The - * caller should continue waiting for another message event and call this - * function again. - * @throws {Error} Throws an error if the expected message event is passed but - * the expectation fails. The caller should remove the message event - * listener and perform test failure handling. - */ - run(e, url) { - if (this.expectationType === FetchLaterExpectationType.DONE) { - if (e.data.type === FetchLaterIframeMessageType.DONE) { - return true; - } - if (e.data.type === FetchLaterIframeMessageType.ERROR && - e.data.error !== undefined) { - throw new FetchLaterExpectationError( - url, e.data.error.name, 'no error'); - } - } - - if (this.expectationType === FetchLaterExpectationType.ERROR_JS) { - if (e.data.type === FetchLaterIframeMessageType.DONE) { - throw new FetchLaterExpectationError( - url, 'nothing', this.expectedErrorType.name); - } - if (e.data.type === FetchLaterIframeMessageType.ERROR) { - if (e.data.error.constructor === this.expectedErrorType && - e.data.error.name === this.expectedErrorType.name) { - return true; - } - throw new FetchLaterExpectationError( - url, e.data.error, this.expectedErrorType.name); - } - } - - if (this.expectationType === FetchLaterExpectationType.ERROR_DOM) { - if (e.data.type === FetchLaterIframeMessageType.DONE) { - throw new FetchLaterExpectationError( - url, 'nothing', this.expectedDomErrorName); - } - if (e.data.type === FetchLaterIframeMessageType.ERROR) { - const actual = e.data.error.name || e.data.error.type; - if (e.data.error.constructor.name === 'DOMException' && - actual == this.expectedDomErrorName) { - return true; - } - throw new FetchLaterExpectationError( - url, actual, this.expectedDomErrorName); - } - } - - return false; - } -} - -/** - * A helper to load an iframe of the specified `origin` that makes a fetchLater - * request to `targetUrl`. - * - * If `targetUrl` is not provided, this function generates a target URL by - * itself. - * - * If `expect` is not provided: - * - If `targetUrl` is not provided, this function will wait for the fetchLater - * request being received by the test server before returning. - * - If `targetUrl` is provided and `uuid` is missing, it will NOT wait for the - * request. - * - If both `targetUrl` and `uuid` are provided, it will wait for the request. - * - * Note that the iframe posts various messages back to its parent document. - * - * @param {!string} origin The origin URL of the iframe to load. - * @param {FetchLaterIframeOptions=} nameIgnored - * @return {!HTMLIFrameElement} the loaded iframe. - */ -async function loadFetchLaterIframe(origin, { - targetUrl = undefined, - uuid = undefined, - activateAfter = undefined, - referrer = undefined, - method = undefined, - bodyType = undefined, - bodySize = undefined, - allowDeferredFetch = false, - expect = undefined -} = {}) { - if (uuid && targetUrl && !targetUrl.includes(uuid)) { - throw new Error(`Conflicted uuid=${ - uuid} is provided: must also be included in the targetUrl ${ - targetUrl}`); - } - if (!uuid) { - uuid = targetUrl ? undefined : token(); - } - targetUrl = targetUrl || generateSetBeaconURL(uuid); - const params = new URLSearchParams(Object.assign( - {}, - {url: encodeURIComponent(targetUrl)}, - activateAfter !== undefined ? {activateAfter} : null, - referrer !== undefined ? {referrer} : null, - method !== undefined ? {method} : null, - bodyType !== undefined ? {bodyType} : null, - bodySize !== undefined ? {bodySize} : null, - )); - const url = - `${origin}/fetch/fetch-later/resources/fetch-later.html?${params}`; - expect = - expect || new FetchLaterIframeExpectation(FetchLaterExpectationType.DONE); - - const iframe = document.createElement('iframe'); - if (allowDeferredFetch) { - iframe.allow = 'deferred-fetch'; - } - iframe.src = url; - - const messageReceived = new Promise((resolve, reject) => { - addEventListener('message', function handler(e) { - if (e.source !== iframe.contentWindow) { - return; - } - try { - if (expect.run(e, url)) { - removeEventListener('message', handler); - resolve(e.data.type); - } - } catch (err) { - reject(err); - } - }); - }); - - await loadElement(iframe); - const messageType = await messageReceived; - if (messageType === FetchLaterIframeMessageType.DONE && uuid) { - await expectBeacon(uuid, {count: 1}); - } - - return iframe; -} diff --git a/test/fixtures/wpt/fetch/fetch-later/resources/fetch-later.html b/test/fixtures/wpt/fetch/fetch-later/resources/fetch-later.html deleted file mode 100644 index b295be116c7..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/resources/fetch-later.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/fetch-later/resources/get_beacon.py b/test/fixtures/wpt/fetch/fetch-later/resources/get_beacon.py deleted file mode 100644 index 32cb9a9ba30..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/resources/get_beacon.py +++ /dev/null @@ -1,30 +0,0 @@ -"""An HTTP request handler for WPT that handles /get_beacon.py requests.""" - -import json - -_BEACON_ID_KEY = b"uuid" -_BEACON_DATA_PATH = "beacon_data" - - -def main(request, response): - """Retrieves the beacon data keyed by the given uuid from server storage. - - The response content is a JSON string in one of the following formats: - - "{'data': ['abc', null, '123',...]}" - - "{'data': []}" indicates that no data has been set for this uuid. - """ - if _BEACON_ID_KEY not in request.GET: - response.status = 400 - return "Must provide a UUID to store beacon data" - uuid = request.GET.first(_BEACON_ID_KEY) - - with request.server.stash.lock: - body = {'data': []} - data = request.server.stash.take(key=uuid, path=_BEACON_DATA_PATH) - if data: - body['data'] = data - # The stash is read-once/write-once, so it has to be put back after - # reading if `data` is not None. - request.server.stash.put( - key=uuid, value=data, path=_BEACON_DATA_PATH) - return [(b'Content-Type', b'text/plain')], json.dumps(body) diff --git a/test/fixtures/wpt/fetch/fetch-later/resources/header-referrer-helper.js b/test/fixtures/wpt/fetch/fetch-later/resources/header-referrer-helper.js deleted file mode 100644 index 374097614ae..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/resources/header-referrer-helper.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer -const REFERRER_ORIGIN = self.location.origin + '/'; -const REFERRER_URL = self.location.href; - -function testReferrerHeader(id, host, expectedReferrer) { - const url = `${ - host}/beacon/resources/inspect-header.py?header=referer&cmd=put&id=${id}`; - - promise_test(t => { - fetchLater(url, {activateAfter: 0}); - return pollResult(expectedReferrer, id).then(result => { - assert_equals(result, expectedReferrer, 'Correct referrer header result'); - }); - }, `Test referer header ${host}`); -} - -function pollResult(expectedReferrer, id) { - const checkUrl = - `/beacon/resources/inspect-header.py?header=referer&cmd=get&id=${id}`; - - return new Promise(resolve => { - function checkResult() { - fetch(checkUrl).then(response => { - assert_equals( - response.status, 200, 'Inspect header response\'s status is 200'); - let result = response.headers.get('x-request-referer'); - - if (result != undefined) { - resolve(result); - } else { - step_timeout(checkResult.bind(this), 100); - } - }); - } - checkResult(); - }); -} diff --git a/test/fixtures/wpt/fetch/fetch-later/resources/set_beacon.py b/test/fixtures/wpt/fetch/fetch-later/resources/set_beacon.py deleted file mode 100644 index 1c71f23e578..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/resources/set_beacon.py +++ /dev/null @@ -1,83 +0,0 @@ -"""An HTTP request handler for WPT that handles /set_beacon.py requests.""" - -_BEACON_ID_KEY = b"uuid" -_BEACON_DATA_PATH = "beacon_data" -_BEACON_FORM_PAYLOAD_KEY = b"payload" -_BEACON_BODY_PAYLOAD_KEY = "payload=" -_BEACON_EXPECT_ORIGIN_KEY = b"expectOrigin" -_BEACON_EXPECT_PREFLIGHT_KEY = b"expectPreflight" -_BEACON_EXPECT_CREDS_KEY = b"expectCredentials" - - -def main(request, response): - """Stores the given beacon's data keyed by uuid in the server. - - For GET request, this handler assumes no data. - For POST request, this handler extracts data from request body: - - Content-Type=multipart/form-data: data keyed by 'payload'. - - the entire request body. - - Multiple data can be added for the same uuid. - - The data is stored as UTF-8 format. - """ - if _BEACON_ID_KEY not in request.GET: - response.status = 400 - return "Must provide a UUID to store beacon data" - uuid = request.GET.first(_BEACON_ID_KEY) - - expected_origin = request.GET.get(_BEACON_EXPECT_ORIGIN_KEY) - if b"origin" in request.headers: - origin = request.headers.get(b"origin") - if expected_origin: - assert origin == expected_origin, f"expected {expected_origin}, got {origin}" - response.headers.set(b"Access-Control-Allow-Origin", origin) - else: - assert expected_origin is None, f"expected None, got {expected_origin}" - - # Handles preflight request first. - if request.method == u"OPTIONS": - assert request.GET.get( - _BEACON_EXPECT_PREFLIGHT_KEY) == b"true", "Preflight not expected." - - # preflight must not have cookies. - assert b"Cookie" not in request.headers - - requested_headers = request.headers.get( - b"Access-Control-Request-Headers") - assert b"content-type" in requested_headers, f"expected content-type, got {requested_headers}" - response.headers.set(b"Access-Control-Allow-Headers", b"content-type") - - requested_method = request.headers.get(b"Access-Control-Request-Method") - assert requested_method == b"POST", f"expected POST, got {requested_method}" - response.headers.set(b"Access-Control-Allow-Methods", b"POST") - - return response - - expect_creds = request.GET.get(_BEACON_EXPECT_CREDS_KEY) == b"true" - if expect_creds: - assert b"Cookie" in request.headers - else: - assert b"Cookie" not in request.headers - - data = None - if request.method == u"POST": - if b"multipart/form-data" in request.headers.get(b"Content-Type", b""): - if _BEACON_FORM_PAYLOAD_KEY in request.POST: - data = request.POST.first(_BEACON_FORM_PAYLOAD_KEY).decode( - 'utf-8') - elif request.body: - data = request.body.decode('utf-8') - if data.startswith(_BEACON_BODY_PAYLOAD_KEY): - data = data.split(_BEACON_BODY_PAYLOAD_KEY)[1] - - with request.server.stash.lock: - saved_data = request.server.stash.take(key=uuid, path=_BEACON_DATA_PATH) - if not saved_data: - saved_data = [data] - else: - saved_data.append(data) - request.server.stash.put( - key=uuid, value=saved_data, path=_BEACON_DATA_PATH) - - response.status = 200 diff --git a/test/fixtures/wpt/fetch/fetch-later/send-on-deactivate-with-background-sync.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/send-on-deactivate-with-background-sync.tentative.https.window.js deleted file mode 100644 index 881bdd23f93..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/send-on-deactivate-with-background-sync.tentative.https.window.js +++ /dev/null @@ -1,128 +0,0 @@ -// META: script=/resources/testdriver.js -// META: script=/resources/testdriver-vendor.js -// META: script=/common/dispatcher/dispatcher.js -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js -// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: timeout=long - -'use strict'; - -async function setBackgroundSyncEnabled(enabled) { - const status = enabled ? 'granted' : 'denied'; - await test_driver.set_permission({name: 'background-sync'}, status); -} - -parallelPromiseTest(async t => { - // Enables BackgroundSync permission such that deferred request won't be - // immediately sent out on entering BFCache. - await setBackgroundSyncEnabled(true); - - const uuid = token(); - const url = generateSetBeaconURL(uuid); - // Sets no option to test the default behavior when a document enters BFCache. - const helper = new RemoteContextHelper(); - // Opens a window with noopener so that BFCache will work. - const rc1 = await helper.addWindow( - /*config=*/ null, /*options=*/ {features: 'noopener'}); - - // Creates a fetchLater request with default config in remote, which should - // only be sent on page discarded (not on entering BFCache). - await rc1.executeScript(url => { - fetchLater(url); - // Add a pageshow listener to stash the BFCache event. - window.addEventListener('pageshow', e => { - window.pageshowEvent = e; - }); - }, [url]); - // Navigates away to let page enter BFCache. - const rc2 = await rc1.navigateToNew(); - // Navigates back. - await rc2.historyBack(); - // Verifies the page was BFCached. - assert_true(await rc1.executeScript(() => { - return window.pageshowEvent.persisted; - })); - - // By default, pending requests are all flushed on BFCache no matter - // BackgroundSync is on or not. See http://b/310541607#comment28. - await expectBeacon(uuid, {count: 1}); -}, `fetchLater() does send on page entering BFCache even if BackgroundSync is on.`); - -parallelPromiseTest(async t => { - // Enables BackgroundSync permission such that deferred request won't be - // immediately sent out on entering BFCache. - await setBackgroundSyncEnabled(true); - - const uuid = token(); - const url = generateSetBeaconURL(uuid); - // activateAfter = 0s means the request should be sent out right on - // document becoming deactivated (BFCached or frozen) after navigating away. - const options = {activateAfter: 0}; - const helper = new RemoteContextHelper(); - // Opens a window with noopener so that BFCache will work. - const rc1 = await helper.addWindow( - /*config=*/ null, /*options=*/ {features: 'noopener'}); - - // Creates a fetchLater request in remote which should only be sent on - // navigating away. - await rc1.executeScript((url, options) => { - fetchLater(url, options); - - // Add a pageshow listener to stash the BFCache event. - window.addEventListener('pageshow', e => { - window.pageshowEvent = e; - }); - }, [url, options]); - // Navigates away to trigger request sending. - const rc2 = await rc1.navigateToNew(); - // Navigates back. - await rc2.historyBack(); - // Verifies the page was BFCached. - assert_true(await rc1.executeScript(() => { - return window.pageshowEvent.persisted; - })); - - await expectBeacon(uuid, {count: 1}); -}, `fetchLater() with activateAfter=0 sends on page entering BFCache if BackgroundSync is on.`); - -parallelPromiseTest(async t => { - // Enables BackgroundSync permission such that deferred request won't be - // immediately sent out on entering BFCache. - await setBackgroundSyncEnabled(true); - - const uuid = token(); - const url = generateSetBeaconURL(uuid); - // activateAfter = 1m means the request should NOT be sent out on - // document becoming deactivated (BFCached or frozen) until after 1 minute. - const options = {activateAfter: 60000}; - const helper = new RemoteContextHelper(); - // Opens a window with noopener so that BFCache will work. - const rc1 = await helper.addWindow( - /*config=*/ null, /*options=*/ {features: 'noopener'}); - - // Creates a fetchLater request in remote which should only be sent on - // navigating away. - await rc1.executeScript((url, options) => { - fetchLater(url, options); - - // Adds a pageshow listener to stash the BFCache event. - window.addEventListener('pageshow', e => { - window.pageshowEvent = e; - }); - }, [url, options]); - // Navigates away to trigger request sending. - const rc2 = await rc1.navigateToNew(); - // Navigates back. - await rc2.historyBack(); - // Verifies the page was BFCached. - assert_true(await rc1.executeScript(() => { - return window.pageshowEvent.persisted; - })); - - // By default, pending requests are all flushed on BFCache no matter - // BackgroundSync is on or not. See http://b/310541607#comment28. - await expectBeacon(uuid, {count: 1}); -}, `fetchLater() with activateAfter=1m does send on page entering BFCache even if BackgroundSync is on.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/send-on-deactivate.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/send-on-deactivate.tentative.https.window.js deleted file mode 100644 index 3bcf07483ad..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/send-on-deactivate.tentative.https.window.js +++ /dev/null @@ -1,183 +0,0 @@ -// META: script=/common/dispatcher/dispatcher.js -// META: script=/common/get-host-info.sub.js -// META: script=/common/utils.js -// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js -// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js - -'use strict'; - -// NOTE: Due to the restriction of WPT runner, the following tests are all run -// with BackgroundSync off, which is different from some browsers, -// e.g. Chrome, default behavior, as the testing infra does not support enabling -// it. - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - // Sets no option to test the default behavior when a document enters BFCache. - const helper = new RemoteContextHelper(); - // Opens a window with noopener so that BFCache will work. - const rc1 = await helper.addWindow( - /*config=*/ null, /*options=*/ {features: 'noopener'}); - - // Creates a fetchLater request with default config in remote, which should - // only be sent on page discarded (not on entering BFCache). - await rc1.executeScript(url => { - fetchLater(url); - // Add a pageshow listener to stash the BFCache event. - window.addEventListener('pageshow', e => { - window.pageshowEvent = e; - }); - }, [url]); - // Navigates away to let page enter BFCache. - const rc2 = await rc1.navigateToNew(); - // Navigates back. - await rc2.historyBack(); - // Verifies the page was BFCached. - assert_true(await rc1.executeScript(() => { - return window.pageshowEvent.persisted; - })); - - // Theoretically, the request should still be pending thus 0 request received. - // However, 1 request is sent, as by default the WPT test runner, e.g. - // content_shell in Chromium, does not enable BackgroundSync permission, - // resulting in forcing request sending on every navigation. - await expectBeacon(uuid, {count: 1}); -}, `fetchLater() sends on page entering BFCache if BackgroundSync is off.`); - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - const helper = new RemoteContextHelper(); - // Opens a window with noopener so that BFCache will work. - const rc1 = await helper.addWindow( - /*config=*/ null, /*options=*/ {features: 'noopener'}); - - // When the remote is put into BFCached, creates a fetchLater request w/ - // activateAfter = 0s. It should be sent out immediately. - await rc1.executeScript(url => { - window.addEventListener('pagehide', e => { - if (e.persisted) { - fetchLater(url, {activateAfter: 0}); - } - }); - // Add a pageshow listener to stash the BFCache event. - window.addEventListener('pageshow', e => { - window.pageshowEvent = e; - }); - }, [url]); - // Navigates away to trigger request sending. - const rc2 = await rc1.navigateToNew(); - // Navigates back. - await rc2.historyBack(); - // Verifies the page was BFCached. - assert_true(await rc1.executeScript(() => { - return window.pageshowEvent.persisted; - })); - - // NOTE: In this case, it does not matter if BackgroundSync is on or off. - await expectBeacon(uuid, {count: 1}); -}, `Call fetchLater() when BFCached with activateAfter=0 sends immediately.`); - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - // Sets no option to test the default behavior when a document gets discarded - // on navigated away. - const helper = new RemoteContextHelper(); - // Opens a window without BFCache. - const rc1 = await helper.addWindow(); - - // Creates a fetchLater request in remote which should only be sent on - // navigating away. - await rc1.executeScript(url => { - fetchLater(url); - // Add a pageshow listener to stash the BFCache event. - window.addEventListener('pageshow', e => { - window.pageshowEvent = e; - }); - }, [url]); - // Navigates away to trigger request sending. - const rc2 = await rc1.navigateToNew(); - // Navigates back. - await rc2.historyBack(); - // Verifies the page was NOT BFCached. - assert_equals(undefined, await rc1.executeScript(() => { - return window.pageshowEvent; - })); - - // NOTE: In this case, it does not matter if BackgroundSync is on or off. - await expectBeacon(uuid, {count: 1}); -}, `fetchLater() sends on navigating away a page w/o BFCache.`); - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - // Sets no option to test the default behavior when a document gets discarded - // on navigated away. - const helper = new RemoteContextHelper(); - // Opens a window without BFCache. - const rc1 = await helper.addWindow(); - - // Creates 2 fetchLater requests in remote, and one of them is aborted - // immediately. The other one should only be sent right on navigating away. - await rc1.executeScript(url => { - const controller = new AbortController(); - fetchLater(url, {signal: controller.signal}); - fetchLater(url); - controller.abort(); - // Add a pageshow listener to stash the BFCache event. - window.addEventListener('pageshow', e => { - window.pageshowEvent = e; - }); - }, [url]); - // Navigates away to trigger request sending. - const rc2 = await rc1.navigateToNew(); - // Navigates back. - await rc2.historyBack(); - // Verifies the page was NOT BFCached. - assert_equals(undefined, await rc1.executeScript(() => { - return window.pageshowEvent; - })); - - // NOTE: In this case, it does not matter if BackgroundSync is on or off. - await expectBeacon(uuid, {count: 1}); -}, `fetchLater() does not send aborted request on navigating away a page w/o BFCache.`); - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - const options = {activateAfter: 60000}; - const helper = new RemoteContextHelper(); - // Opens a window with noopener so that BFCache will work. - const rc1 = await helper.addWindow( - /*config=*/ null, /*options=*/ {features: 'noopener'}); - - // Creates a fetchLater request in remote which should only be sent on - // navigating away. - await rc1.executeScript((url) => { - // Sets activateAfter = 1m to indicate the request should NOT be sent out - // immediately. - fetchLater(url, {activateAfter: 60000}); - // Adds a pageshow listener to stash the BFCache event. - window.addEventListener('pageshow', e => { - window.pageshowEvent = e; - }); - }, [url]); - // Navigates away to trigger request sending. - const rc2 = await rc1.navigateToNew(); - // Navigates back. - await rc2.historyBack(); - // Verifies the page was BFCached. - assert_true(await rc1.executeScript(() => { - return window.pageshowEvent.persisted; - })); - - // Theoretically, the request should still be pending thus 0 request received. - // However, 1 request is sent, as by default the WPT test runner, e.g. - // content_shell in Chromium, does not enable BackgroundSync permission, - // resulting in forcing request sending on every navigation, even if page is - // put into BFCache. - await expectBeacon(uuid, {count: 1}); -}, `fetchLater() with activateAfter=1m sends on page entering BFCache if BackgroundSync is off.`); diff --git a/test/fixtures/wpt/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js deleted file mode 100644 index 6ddafd78131..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js +++ /dev/null @@ -1,23 +0,0 @@ -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js - -'use strict'; - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - - // Loads an iframe that creates 2 fetchLater requests. One of them is aborted. - const iframe = await loadScriptAsIframe(` - const url = '${url}'; - const controller = new AbortController(); - fetchLater(url, {signal: controller.signal}); - fetchLater(url, {method: 'POST'}); - controller.abort(); - `); - // Delete the iframe to trigger deferred request sending. - document.body.removeChild(iframe); - - // The iframe should not send the aborted request. - await expectBeacon(uuid, {count: 1}); -}, 'A discarded document does not send an already aborted fetchLater request.'); diff --git a/test/fixtures/wpt/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js deleted file mode 100644 index 0bbe94c39f0..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js +++ /dev/null @@ -1,30 +0,0 @@ -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: timeout=long - -'use strict'; - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - const numPerMethod = 20; - const total = numPerMethod * 2; - - // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests. - const iframe = await loadScriptAsIframe(` - const url = '${url}'; - for (let i = 0; i < ${numPerMethod}; i++) { - // Changing the URL of each request to avoid HTTP Cache issue. - // See crbug.com/1498203#c17. - fetchLater(url + "&method=GET&i=" + i, - {method: 'GET', activateAfter: 10000}); // 10s - fetchLater(url + "&method=POST&i=" + i, - {method: 'POST', activateAfter: 8000}); // 8s - } - `); - // Delete the iframe to trigger deferred request sending. - document.body.removeChild(iframe); - - // The iframe should have sent all requests. - await expectBeacon(uuid, {count: total}); -}, 'A discarded document sends all its fetchLater requests, no matter how much their activateAfter timeout remain.'); diff --git a/test/fixtures/wpt/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js b/test/fixtures/wpt/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js deleted file mode 100644 index 05bb2dc1149..00000000000 --- a/test/fixtures/wpt/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js +++ /dev/null @@ -1,28 +0,0 @@ -// META: script=/common/utils.js -// META: script=/fetch/fetch-later/resources/fetch-later-helper.js -// META: timeout=long - -'use strict'; - -parallelPromiseTest(async t => { - const uuid = token(); - const url = generateSetBeaconURL(uuid); - const numPerMethod = 20; - const total = numPerMethod * 2; - - // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests. - const iframe = await loadScriptAsIframe(` - const url = '${url}'; - for (let i = 0; i < ${numPerMethod}; i++) { - // Changing the URL of each request to avoid HTTP Cache issue. - // See crbug.com/1498203#c17. - fetchLater(url + "&method=GET&i=" + i); - fetchLater(url + "&method=POST&i=" + i, {method: 'POST'}); - } - `); - // Delete the iframe to trigger deferred request sending. - document.body.removeChild(iframe); - - // The iframe should have sent all requests. - await expectBeacon(uuid, {count: total}); -}, 'A discarded document sends all its fetchLater requests.'); diff --git a/test/fixtures/wpt/fetch/h1-parsing/README.md b/test/fixtures/wpt/fetch/h1-parsing/README.md deleted file mode 100644 index 487a892dcff..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/README.md +++ /dev/null @@ -1,5 +0,0 @@ -This directory tries to document "rough consensus" on where HTTP/1 parsing should end up between browsers. - -Any tests that browsers currently fail should have associated bug reports. - -[whatwg/fetch issue #1156](https://github.com/whatwg/fetch/issues/1156) provides context for this effort and pointers to the various issues, pull requests, and bug reports that are associated with it. diff --git a/test/fixtures/wpt/fetch/h1-parsing/lone-cr.window.js b/test/fixtures/wpt/fetch/h1-parsing/lone-cr.window.js deleted file mode 100644 index 6b46ed632f4..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/lone-cr.window.js +++ /dev/null @@ -1,23 +0,0 @@ -// These tests expect that a network error is returned if there's a CR that is not immediately -// followed by LF before reaching message-body. -// -// No browser does this currently, but Firefox does treat it equivalently to a space which gives -// hope. - -[ - "HTTP/1.1\r200 OK\n\nBODY", - "HTTP/1.1 200\rOK\n\nBODY", - "HTTP/1.1 200 OK\n\rHeader: Value\n\nBODY", - "HTTP/1.1 200 OK\nHeader\r: Value\n\nBODY", - "HTTP/1.1 200 OK\nHeader:\r Value\n\nBODY", - "HTTP/1.1 200 OK\nHeader: Value\r\n\nBody", - "HTTP/1.1 200 OK\nHeader: Value\r\r\nBODY", - "HTTP/1.1 200 OK\nHeader: Value\rHeader2: Value2\n\nBODY", - "HTTP/1.1 200 OK\nHeader: Value\n\rBODY", - "HTTP/1.1 200 OK\nHeader: Value\n\r" -].forEach(input => { - promise_test(t => { - const message = encodeURIComponent(input); - return promise_rejects_js(t, TypeError, fetch(`resources/message.py?message=${message}`)); - }, `Parsing response with a lone CR before message-body (${input})`); -}); diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources-with-0x00-in-header.window.js b/test/fixtures/wpt/fetch/h1-parsing/resources-with-0x00-in-header.window.js deleted file mode 100644 index b617911105a..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/resources-with-0x00-in-header.window.js +++ /dev/null @@ -1,37 +0,0 @@ -// META: script=/common/get-host-info.sub.js - -async_test(t => { - const script = document.createElement("script"); - t.add_cleanup(() => script.remove()); - script.src = "resources/script-with-0x00-in-header.py"; - script.onerror = t.step_func_done(); - script.onload = t.unreached_func(); - document.body.append(script); -}, "Expect network error for script with 0x00 in a header"); - -async_test(t => { - const frame = document.createElement("iframe"); - t.add_cleanup(() => frame.remove()); - frame.src = "resources/document-with-0x00-in-header.py"; - // If network errors result in load events for frames per - // https://github.com/whatwg/html/issues/125 and https://github.com/whatwg/html/issues/1230 this - // should be changed to use the load event instead. - t.step_timeout(() => { - assert_equals(window.frameLoaded, undefined); - t.done(); - }, 1000); - document.body.append(frame); -}, "Expect network error for frame navigation to resource with 0x00 in a header"); - -async_test(t => { - const img = document.createElement("img"); - t.add_cleanup(() => img.remove()); - img.src = "resources/blue-with-0x00-in-a-header.asis"; - img.onerror = t.step_func_done(); - img.onload = t.unreached_func(); - document.body.append(img); -}, "Expect network error for image with 0x00 in a header"); - -promise_test(async t => { - return promise_rejects_js(t, TypeError, fetch(get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis", {mode:"no-cors"})); -}, "Expect network error for fetch with 0x00 in a header"); diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources/README.md b/test/fixtures/wpt/fetch/h1-parsing/resources/README.md deleted file mode 100644 index 2175d274088..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/resources/README.md +++ /dev/null @@ -1,6 +0,0 @@ -`blue-with-0x00-in-a-header.asis` is a copy from `../../images/blue.png` with the following prepended using Control Pictures to signify actual newlines and 0x00: -``` -HTTP/1.1 200 AN IMAGE␍␊ -Content-Type: image/png␍␊ -Custom: ␀␍␊␍␊ -``` diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis b/test/fixtures/wpt/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis deleted file mode 100644 index 102340a6313..00000000000 Binary files a/test/fixtures/wpt/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis and /dev/null differ diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources/document-with-0x00-in-header.py b/test/fixtures/wpt/fetch/h1-parsing/resources/document-with-0x00-in-header.py deleted file mode 100644 index d91998b998d..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/resources/document-with-0x00-in-header.py +++ /dev/null @@ -1,4 +0,0 @@ -def main(request, response): - response.headers.set(b"Content-Type", b"text/html") - response.headers.set(b"Custom", b"\0") - return b"This is a document." diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources/message.py b/test/fixtures/wpt/fetch/h1-parsing/resources/message.py deleted file mode 100644 index 640080c18bc..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/resources/message.py +++ /dev/null @@ -1,3 +0,0 @@ -def main(request, response): - response.writer.write(request.GET.first(b"message")) - response.close_connection = True diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources/script-with-0x00-in-header.py b/test/fixtures/wpt/fetch/h1-parsing/resources/script-with-0x00-in-header.py deleted file mode 100644 index 39f58d8270e..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/resources/script-with-0x00-in-header.py +++ /dev/null @@ -1,4 +0,0 @@ -def main(request, response): - response.headers.set(b"Content-Type", b"text/javascript") - response.headers.set(b"Custom", b"\0") - return b"var thisIsJavaScript = 0" diff --git a/test/fixtures/wpt/fetch/h1-parsing/resources/status-code.py b/test/fixtures/wpt/fetch/h1-parsing/resources/status-code.py deleted file mode 100644 index 5421893b267..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/resources/status-code.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(request, response): - output = b"HTTP/1.1 " - output += request.GET.first(b"input") - output += b"\nheader-parsing: is sad\n" - response.writer.write(output) - response.close_connection = True diff --git a/test/fixtures/wpt/fetch/h1-parsing/status-code.window.js b/test/fixtures/wpt/fetch/h1-parsing/status-code.window.js deleted file mode 100644 index 5776cf4050f..00000000000 --- a/test/fixtures/wpt/fetch/h1-parsing/status-code.window.js +++ /dev/null @@ -1,98 +0,0 @@ -[ - { - input: "", - expected: null - }, - { - input: "BLAH", - expected: null - }, - { - input: "0 OK", - expected: { - status: 0, - statusText: "OK" - } - }, - { - input: "1 OK", - expected: { - status: 1, - statusText: "OK" - } - }, - { - input: "99 NOT OK", - expected: { - status: 99, - statusText: "NOT OK" - } - }, - { - input: "077 77", - expected: { - status: 77, - statusText: "77" - } - }, - { - input: "099 HELLO", - expected: { - status: 99, - statusText: "HELLO" - } - }, - { - input: "200", - expected: { - status: 200, - statusText: "" - } - }, - { - input: "999 DOES IT MATTER", - expected: { - status: 999, - statusText: "DOES IT MATTER" - } - }, - { - input: "1000 BOO", - expected: null - }, - { - input: "0200 BOO", - expected: null - }, - { - input: "65736 NOT 200 OR SOME SUCH", - expected: null - }, - { - input: "131072 HI", - expected: null - }, - { - input: "-200 TEST", - expected: null - }, - { - input: "0xA", - expected: null - }, - { - input: "C8", - expected: null - } -].forEach(({ description, input, expected }) => { - promise_test(async t => { - if (expected !== null) { - const response = await fetch("resources/status-code.py?input=" + input); - assert_equals(response.status, expected.status); - assert_equals(response.statusText, expected.statusText); - assert_equals(response.headers.get("header-parsing"), "is sad"); - } else { - await promise_rejects_js(t, TypeError, fetch("resources/status-code.py?input=" + input)); - } - }, `HTTP/1.1 ${input} ${expected === null ? "(network error)" : ""}`); -}); diff --git a/test/fixtures/wpt/fetch/http-cache/304-update.any.js b/test/fixtures/wpt/fetch/http-cache/304-update.any.js deleted file mode 100644 index 15484f01eb3..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/304-update.any.js +++ /dev/null @@ -1,146 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - 304 Updates -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - { - name: "HTTP cache updates returned headers from a Last-Modified 304", - requests: [ - { - response_headers: [ - ["Expires", -5000], - ["Last-Modified", -3000], - ["Test-Header", "A"] - ] - }, - { - response_headers: [ - ["Expires", -3000], - ["Last-Modified", -3000], - ["Test-Header", "B"] - ], - expected_type: "lm_validated", - expected_response_headers: [ - ["Test-Header", "B"] - ] - } - ] - }, - { - name: "HTTP cache updates stored headers from a Last-Modified 304", - requests: [ - { - response_headers: [ - ["Expires", -5000], - ["Last-Modified", -3000], - ["Test-Header", "A"] - ] - }, - { - response_headers: [ - ["Expires", 3000], - ["Last-Modified", -3000], - ["Test-Header", "B"] - ], - expected_type: "lm_validated", - expected_response_headers: [ - ["Test-Header", "B"] - ], - pause_after: true - }, - { - expected_type: "cached", - expected_response_headers: [ - ["Test-Header", "B"] - ] - } - ] - }, - { - name: "HTTP cache updates returned headers from a ETag 304", - requests: [ - { - response_headers: [ - ["Expires", -5000], - ["ETag", "ABC"], - ["Test-Header", "A"] - ] - }, - { - response_headers: [ - ["Expires", -3000], - ["ETag", "ABC"], - ["Test-Header", "B"] - ], - expected_type: "etag_validated", - expected_response_headers: [ - ["Test-Header", "B"] - ] - } - ] - }, - { - name: "HTTP cache updates stored headers from a ETag 304", - requests: [ - { - response_headers: [ - ["Expires", -5000], - ["ETag", "DEF"], - ["Test-Header", "A"] - ] - }, - { - response_headers: [ - ["Expires", 3000], - ["ETag", "DEF"], - ["Test-Header", "B"] - ], - expected_type: "etag_validated", - expected_response_headers: [ - ["Test-Header", "B"] - ], - pause_after: true - }, - { - expected_type: "cached", - expected_response_headers: [ - ["Test-Header", "B"] - ] - } - ] - }, - { - name: "Content-* header", - requests: [ - { - response_headers: [ - ["Expires", -5000], - ["ETag", "GHI"], - ["Content-Test-Header", "A"] - ] - }, - { - response_headers: [ - ["Expires", 3000], - ["ETag", "GHI"], - ["Content-Test-Header", "B"] - ], - expected_type: "etag_validated", - expected_response_headers: [ - ["Content-Test-Header", "B"] - ], - pause_after: true - }, - { - expected_type: "cached", - expected_response_headers: [ - ["Content-Test-Header", "B"] - ] - } - ] - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/README.md b/test/fixtures/wpt/fetch/http-cache/README.md deleted file mode 100644 index f3815aabfd9..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/README.md +++ /dev/null @@ -1,72 +0,0 @@ -## HTTP Caching Tests - -These tests cover HTTP-specified behaviours for caches, primarily from -[RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html), but as seen through the -lens of Fetch. - -A few notes: - -* By its nature, [caching is entirely optional]( - https://www.rfc-editor.org/rfc/rfc9111.html#section-2-2); - some tests expecting a response to be - cached might fail because the client chose not to cache it, or chose to - race the cache with a network request. - -* Likewise, some tests might fail because there is a separate document-level - cache that's not well defined; see [this - issue](https://github.com/whatwg/fetch/issues/354). - -* [Partial content tests](partial.any.js) (a.k.a. Range requests) are not specified - in Fetch; tests are included here for interest only. - -* Some browser caches will behave differently when reloading / - shift-reloading, despite the `cache mode` staying the same. - -* [cache-tests.fyi](https://cache-tests.fyi/) is another test suite of HTTP caching - which also caters to server/CDN implementations. - -## Test Format - -Each test run gets its own URL and randomized content and operates independently. - -Each test is an an array of objects, with the following members: - -- `name` - The name of the test. -- `requests` - a list of request objects (see below). - -Possible members of a request object: - -- template - A template object for the request, by name. -- request_method - A string containing the HTTP method to be used. -- request_headers - An array of `[header_name_string, header_value_string]` arrays to - emit in the request. -- request_body - A string to use as the request body. -- mode - The mode string to pass to `fetch()`. -- credentials - The credentials string to pass to `fetch()`. -- cache - The cache string to pass to `fetch()`. -- pause_after - Boolean controlling a 3-second pause after the request completes. -- response_status - A `[number, string]` array containing the HTTP status code - and phrase to return. -- response_headers - An array of `[header_name_string, header_value_string]` arrays to - emit in the response. These values will also be checked like - expected_response_headers, unless there is a third value that is - `false`. See below for special handling considerations. -- response_body - String to send as the response body. If not set, it will contain - the test identifier. -- expected_type - One of `["cached", "not_cached", "lm_validate", "etag_validate", "error"]` -- expected_status - A number representing a HTTP status code to check the response for. - If not set, the value of `response_status[0]` will be used; if that - is not set, 200 will be used. -- expected_request_headers - An array of `[header_name_string, header_value_string]` representing - headers to check the request for. -- expected_response_headers - An array of `[header_name_string, header_value_string]` representing - headers to check the response for. See also response_headers. -- expected_response_text - A string to check the response body against. If not present, `response_body` will be checked if present and non-null; otherwise the response body will be checked for the test uuid (unless the status code disallows a body). Set to `null` to disable all response body checking. -- url_params - A string of url parameters that will be appended to the end of the url, separated by "&" and without leading "&". - -Some headers in `response_headers` are treated specially: - -* For date-carrying headers, if the value is a number, it will be interpreted as a delta to the time of the first request at the server. -* For URL-carrying headers, the value will be appended as a query parameter for `target`. - -See the source for exact details. diff --git a/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test-ref.html b/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test-ref.html deleted file mode 100644 index 905facdc888..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test-ref.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test.html b/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test.html deleted file mode 100644 index a8979baf548..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/basic-auth-cache-test.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/http-cache/cache-mode.any.js b/test/fixtures/wpt/fetch/http-cache/cache-mode.any.js deleted file mode 100644 index 8f406d5a6a5..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/cache-mode.any.js +++ /dev/null @@ -1,61 +0,0 @@ -// META: global=window,worker -// META: title=Fetch - Cache Mode -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - { - name: "Fetch sends Cache-Control: max-age=0 when cache mode is no-cache", - requests: [ - { - cache: "no-cache", - expected_request_headers: [['cache-control', 'max-age=0']] - } - ] - }, - { - name: "Fetch doesn't touch Cache-Control when cache mode is no-cache and Cache-Control is already present", - requests: [ - { - cache: "no-cache", - request_headers: [['cache-control', 'foo']], - expected_request_headers: [['cache-control', 'foo']] - } - ] - }, - { - name: "Fetch sends Cache-Control: no-cache and Pragma: no-cache when cache mode is no-store", - requests: [ - { - cache: "no-store", - expected_request_headers: [ - ['cache-control', 'no-cache'], - ['pragma', 'no-cache'] - ] - } - ] - }, - { - name: "Fetch doesn't touch Cache-Control when cache mode is no-store and Cache-Control is already present", - requests: [ - { - cache: "no-store", - request_headers: [['cache-control', 'foo']], - expected_request_headers: [['cache-control', 'foo']] - } - ] - }, - { - name: "Fetch doesn't touch Pragma when cache mode is no-store and Pragma is already present", - requests: [ - { - cache: "no-store", - request_headers: [['pragma', 'foo']], - expected_request_headers: [['pragma', 'foo']] - } - ] - } -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/cc-request.any.js b/test/fixtures/wpt/fetch/http-cache/cc-request.any.js deleted file mode 100644 index d5565668414..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/cc-request.any.js +++ /dev/null @@ -1,202 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Cache-Control Request Directives -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - { - name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=0", - requests: [ - { - template: "fresh", - pause_after: true - }, - { - request_headers: [ - ["Cache-Control", "max-age=0"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=1", - requests: [ - { - template: "fresh", - pause_after: true - }, - { - request_headers: [ - ["Cache-Control", "max-age=1"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache doesn't use fresh response with Age header when request contains Cache-Control: max-age that is greater than remaining freshness", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Age", "1800"] - ] - }, - { - request_headers: [ - ["Cache-Control", "max-age=600"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does use aged stale response when request contains Cache-Control: max-stale that permits its use", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=1"] - ], - pause_after: true - }, - { - request_headers: [ - ["Cache-Control", "max-stale=1000"] - ], - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache does reuse stale response with Age header when request contains Cache-Control: max-stale that permits its use", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=1500"], - ["Age", "2000"] - ] - }, - { - request_headers: [ - ["Cache-Control", "max-stale=1000"] - ], - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: min-fresh that wants it fresher", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=1500"] - ] - }, - { - request_headers: [ - ["Cache-Control", "min-fresh=2000"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache doesn't reuse fresh response with Age header when request contains Cache-Control: min-fresh that wants it fresher", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=1500"], - ["Age", "1000"] - ] - }, - { - request_headers: [ - ["Cache-Control", "min-fresh=1000"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-cache", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"] - ] - }, - { - request_headers: [ - ["Cache-Control", "no-cache"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache validates fresh response with Last-Modified when request contains Cache-Control: no-cache", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Last-Modified", -10000] - ] - }, - { - request_headers: [ - ["Cache-Control", "no-cache"] - ], - expected_type: "lm_validate" - } - ] - }, - { - name: "HTTP cache validates fresh response with ETag when request contains Cache-Control: no-cache", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["ETag", http_content("abc")] - ] - }, - { - request_headers: [ - ["Cache-Control", "no-cache"] - ], - expected_type: "etag_validate" - } - ] - }, - { - name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-store", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"] - ] - }, - { - request_headers: [ - ["Cache-Control", "no-store"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache generates 504 status code when nothing is in cache and request contains Cache-Control: only-if-cached", - requests: [ - { - request_headers: [ - ["Cache-Control", "only-if-cached"] - ], - expected_status: 504, - expected_response_text: null - } - ] - } -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/credentials.tentative.any.js b/test/fixtures/wpt/fetch/http-cache/credentials.tentative.any.js deleted file mode 100644 index 31770925cde..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/credentials.tentative.any.js +++ /dev/null @@ -1,62 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Content -// META: timeout=long -// META: script=/common/utils.js -// META: script=http-cache.js - -// This is a tentative test. -// Firefox behavior is used as expectations. -// -// whatwg/fetch issue: -// https://github.com/whatwg/fetch/issues/1253 -// -// Chrome design doc: -// https://docs.google.com/document/d/1lvbiy4n-GM5I56Ncw304sgvY5Td32R6KHitjRXvkZ6U/edit# - -const request_cacheable = { - request_headers: [], - response_headers: [ - ['Cache-Control', 'max-age=3600'], - ], - // TODO(arthursonzogni): The behavior is tested only for same-origin requests. - // It must behave similarly for cross-site and cross-origin requests. The - // problems is the http-cache.js infrastructure returns the - // "Server-Request-Count" as HTTP response headers, which aren't readable for - // CORS requests. - base_url: location.href.replace(/\/[^\/]*$/, '/'), -}; - -const request_credentialled = { ...request_cacheable, credentials: 'include', }; -const request_anonymous = { ...request_cacheable, credentials: 'omit', }; - -const responseIndex = count => { - return { - expected_response_headers: [ - ['Server-Request-Count', count.toString()], - ], - } -}; - -var tests = [ - { - name: 'same-origin: 2xAnonymous, 2xCredentialled, 1xAnonymous', - requests: [ - { ...request_anonymous , ...responseIndex(1)} , - { ...request_anonymous , ...responseIndex(1)} , - { ...request_credentialled , ...responseIndex(2)} , - { ...request_credentialled , ...responseIndex(2)} , - { ...request_anonymous , ...responseIndex(1)} , - ] - }, - { - name: 'same-origin: 2xCredentialled, 2xAnonymous, 1xCredentialled', - requests: [ - { ...request_credentialled , ...responseIndex(1)} , - { ...request_credentialled , ...responseIndex(1)} , - { ...request_anonymous , ...responseIndex(2)} , - { ...request_anonymous , ...responseIndex(2)} , - { ...request_credentialled , ...responseIndex(1)} , - ] - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/freshness.any.js b/test/fixtures/wpt/fetch/http-cache/freshness.any.js deleted file mode 100644 index 86c2620aa66..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/freshness.any.js +++ /dev/null @@ -1,243 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Freshness -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - // response directives - { - name: "HTTP cache reuses a response with a future Expires", - requests: [ - { - response_headers: [ - ["Expires", (30 * 24 * 60 * 60)] - ] - }, - { - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache does not reuse a response with a past Expires", - requests: [ - { - response_headers: [ - ["Expires", (-30 * 24 * 60 * 60)] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does not reuse a response with a present Expires", - requests: [ - { - response_headers: [ - ["Expires", 0] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does not reuse a response with an invalid Expires", - requests: [ - { - response_headers: [ - ["Expires", "0"] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does not reuse a response with an invalid Expires with Last-Modified now", - requests: [ - { - response_headers: [ - ["Expires", "0"], - ['Last-Modified', 0] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does not reuse a response with an invalid Expires with past Last-Modified", - requests: [ - { - response_headers: [ - ["Expires", "0"], - ['Last-Modified', -100000] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache reuses a response with positive Cache-Control: max-age", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"] - ] - }, - { - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache does not reuse a response with Cache-Control: max-age=0", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=0"] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache reuses a response with positive Cache-Control: max-age and a past Expires", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Expires", -10000] - ] - }, - { - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache reuses a response with positive Cache-Control: max-age and an invalid Expires", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Expires", "0"] - ] - }, - { - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache does not reuse a response with Cache-Control: max-age=0 and a future Expires", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=0"], - ["Expires", 10000] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does not prefer Cache-Control: s-maxage over Cache-Control: max-age", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=1, s-maxage=3600"] - ], - pause_after: true, - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does not reuse a response when the Age header is greater than its freshness lifetime", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Age", "12000"] - ], - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does not store a response with Cache-Control: no-store", - requests: [ - { - response_headers: [ - ["Cache-Control", "no-store"] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache does not store a response with Cache-Control: no-store, even with max-age and Expires", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=10000, no-store"], - ["Expires", 10000] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use", - requests: [ - { - response_headers: [ - ["Cache-Control", "no-cache"], - ["ETag", "abcd"] - ] - }, - { - expected_type: "etag_validated" - } - ] - }, - { - name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use, even with max-age and Expires", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=10000, no-cache"], - ["Expires", 10000], - ["ETag", "abcd"] - ] - }, - { - expected_type: "etag_validated" - } - ] - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/heuristic.any.js b/test/fixtures/wpt/fetch/http-cache/heuristic.any.js deleted file mode 100644 index 2bec1aa4c64..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/heuristic.any.js +++ /dev/null @@ -1,116 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Heuristic Freshness -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - { - name: "HTTP cache reuses an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is present", - requests: [ - { - response_status: [299, "Whatever"], - response_headers: [ - ["Last-Modified", (-3 * 100)], - ["Cache-Control", "public"] - ], - }, - { - expected_type: "cached", - response_status: [299, "Whatever"] - } - ] - }, - { - name: "HTTP cache does not reuse an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is not present", - requests: [ - { - response_status: [299, "Whatever"], - response_headers: [ - ["Last-Modified", (-3 * 100)] - ], - }, - { - expected_type: "not_cached", - } - ] - }, - { - name: "HTTP cache does not reuse a redirected response with no max-age and no Last-Modified header", - requests: [ - { - response_status: [301, "Moved Permanently"], - response_headers: [ - ["Cache-Control", "private"], - ["Location", "redirect_target"] - ], - }, - { skip: true}, // Response to first redirect - { - response_status: [301, "Moved Permanently"], - response_headers: [ - ["Cache-Control", "private"], - ["Location", "redirect_target"] - ], - expected_type: "not_cached", - }, - { skip: true}, // response to second redirect - ], - check_count: true, - }, -]; - -function check_status(status) { - var succeed = status[0]; - var code = status[1]; - var phrase = status[2]; - var body = status[3]; - if (body === undefined) { - body = http_content(code); - } - var expected_type = "not_cached"; - var desired = "does not use" - if (succeed === true) { - expected_type = "cached"; - desired = "reuses"; - } - tests.push( - { - name: "HTTP cache " + desired + " a " + code + " " + phrase + " response with Last-Modified based upon heuristic freshness", - requests: [ - { - response_status: [code, phrase], - response_headers: [ - ["Last-Modified", (-3 * 100)] - ], - response_body: body - }, - { - expected_type: expected_type, - response_status: [code, phrase], - response_body: body - } - ] - } - ) -} -[ - [true, 200, "OK"], - [true, 203, "Non-Authoritative Information"], - [true, 204, "No Content", ""], - [true, 404, "Not Found"], - [true, 405, "Method Not Allowed"], - [true, 410, "Gone"], - [true, 414, "URI Too Long"], - [true, 501, "Not Implemented"] -].forEach(check_status); -[ - [false, 201, "Created"], - [false, 202, "Accepted"], - [false, 403, "Forbidden"], - [false, 502, "Bad Gateway"], - [false, 503, "Service Unavailable"], - [false, 504, "Gateway Timeout"], -].forEach(check_status); -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/http-cache.js b/test/fixtures/wpt/fetch/http-cache/http-cache.js deleted file mode 100644 index e2234736910..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/http-cache.js +++ /dev/null @@ -1,288 +0,0 @@ -/* global btoa fetch token promise_test step_timeout */ -/* global assert_equals assert_true assert_own_property assert_throws_js assert_less_than */ - -const templates = { - 'fresh': { - 'response_headers': [ - ['Expires', 100000], - ['Last-Modified', 0] - ] - }, - 'stale': { - 'response_headers': [ - ['Expires', -5000], - ['Last-Modified', -100000] - ] - }, - 'lcl_response': { - 'response_headers': [ - ['Location', 'location_target'], - ['Content-Location', 'content_location_target'] - ] - }, - 'location': { - 'query_arg': 'location_target', - 'response_headers': [ - ['Expires', 100000], - ['Last-Modified', 0] - ] - }, - 'content_location': { - 'query_arg': 'content_location_target', - 'response_headers': [ - ['Expires', 100000], - ['Last-Modified', 0] - ] - } -} - -const noBodyStatus = new Set([204, 304]) - -function makeTest (test) { - return function () { - var uuid = token() - var requests = expandTemplates(test) - var fetchFunctions = makeFetchFunctions(requests, uuid) - return runTest(fetchFunctions, test, requests, uuid) - } -} - -function makeFetchFunctions(requests, uuid) { - var fetchFunctions = [] - for (let i = 0; i < requests.length; ++i) { - var config = requests[i]; - if (config.skip) { - // Skip request are ones that we expect the browser to make in - // response to a redirect. We don't fetch them again, but - // the server needs them in the config to be able to respond to - // them. - continue; - } - fetchFunctions.push({ - code: function (idx) { - var config = requests[idx] - var url = makeTestUrl(uuid, config); - var init = fetchInit(requests, config) - return fetch(url, init) - .then(makeCheckResponse(idx, config)) - .then(makeCheckResponseBody(config, uuid), function (reason) { - if ('expected_type' in config && config.expected_type === 'error') { - assert_throws_js(TypeError, function () { throw reason }) - } else { - throw reason - } - }) - }, - pauseAfter: 'pause_after' in requests[i] - }) - } - return fetchFunctions -} - -function runTest(fetchFunctions, test, requests, uuid) { - var idx = 0 - function runNextStep () { - if (fetchFunctions.length) { - var nextFetchFunction = fetchFunctions.shift() - if (nextFetchFunction.pauseAfter === true) { - return nextFetchFunction.code(idx++) - .then(pause) - .then(runNextStep) - } else { - return nextFetchFunction.code(idx++) - .then(runNextStep) - } - } else { - return Promise.resolve() - } - } - - return runNextStep() - .then(function () { - return getServerState(uuid) - }).then(function (testState) { - checkRequests(test, requests, testState) - return Promise.resolve() - }) -} - -function expandTemplates (test) { - var rawRequests = test.requests - var requests = [] - for (let i = 0; i < rawRequests.length; i++) { - var request = rawRequests[i] - request.name = test.name - if ('template' in request) { - var template = templates[request['template']] - for (let member in template) { - if (!request.hasOwnProperty(member)) { - request[member] = template[member] - } - } - } - requests.push(request) - } - return requests -} - -function fetchInit (requests, config) { - var init = { - 'headers': [] - } - if ('request_method' in config) init.method = config['request_method'] - // Note: init.headers must be a copy of config['request_headers'] array, - // because new elements are added later. - if ('request_headers' in config) init.headers = [...config['request_headers']]; - if ('name' in config) init.headers.push(['Test-Name', config.name]) - if ('request_body' in config) init.body = config['request_body'] - if ('mode' in config) init.mode = config['mode'] - if ('credentials' in config) init.credentials = config['credentials'] - if ('cache' in config) init.cache = config['cache'] - init.headers.push(['Test-Requests', btoa(JSON.stringify(requests))]) - return init -} - -function makeCheckResponse (idx, config) { - return function checkResponse (response) { - var reqNum = idx + 1 - var resNum = parseInt(response.headers.get('Server-Request-Count')) - if ('expected_type' in config) { - if (config.expected_type === 'error') { - assert_true(false, `Request ${reqNum} doesn't throw an error`) - return response.text() - } - if (config.expected_type === 'cached') { - assert_less_than(resNum, reqNum, `Response ${reqNum} does not come from cache`) - } - if (config.expected_type === 'not_cached') { - assert_equals(resNum, reqNum, `Response ${reqNum} comes from cache`) - } - } - if ('expected_status' in config) { - assert_equals(response.status, config.expected_status, - `Response ${reqNum} status is ${response.status}, not ${config.expected_status}`) - } else if ('response_status' in config && config.response_status[0] != 301) { - assert_equals(response.status, config.response_status[0], - `Response ${reqNum} status is ${response.status}, not ${config.response_status[0]}`) - } else { - assert_equals(response.status, 200, `Response ${reqNum} status is ${response.status}, not 200`) - } - if ('response_headers' in config) { - config.response_headers.forEach(function (header) { - if (header.len < 3 || header[2] === true) { - assert_equals(response.headers.get(header[0]), header[1], - `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`) - } - }) - } - if ('expected_response_headers' in config) { - config.expected_response_headers.forEach(function (header) { - assert_equals(response.headers.get(header[0]), header[1], - `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`) - }) - } - return response.text() - } -} - -function makeCheckResponseBody (config, uuid) { - return function checkResponseBody (resBody) { - var statusCode = 200 - if ('response_status' in config) { - statusCode = config.response_status[0] - } - if ('expected_response_text' in config) { - if (config.expected_response_text !== null) { - assert_equals(resBody, config.expected_response_text, - `Response body is "${resBody}", not expected "${config.expected_response_text}"`) - } - } else if ('response_body' in config && config.response_body !== null) { - assert_equals(resBody, config.response_body, - `Response body is "${resBody}", not sent "${config.response_body}"`) - } else if (!noBodyStatus.has(statusCode)) { - assert_equals(resBody, uuid, `Response body is "${resBody}", not default "${uuid}"`) - } - } -} - -function checkRequests (test, requests, testState) { - var testIdx = 0 - for (let i = 0; i < requests.length; ++i) { - var expectedValidatingHeaders = [] - var config = requests[i] - var serverRequest = testState[testIdx] - var reqNum = i + 1 - if ('expected_type' in config) { - if (config.expected_type === 'cached') continue // the server will not see the request - if (config.expected_type === 'etag_validated') { - expectedValidatingHeaders.push('if-none-match') - } - if (config.expected_type === 'lm_validated') { - expectedValidatingHeaders.push('if-modified-since') - } - } - testIdx++ - expectedValidatingHeaders.forEach(vhdr => { - assert_own_property(serverRequest.request_headers, vhdr, - `request ${reqNum} doesn't have ${vhdr} header`) - }) - if ('expected_request_headers' in config) { - config.expected_request_headers.forEach(expectedHdr => { - assert_equals(serverRequest.request_headers[expectedHdr[0].toLowerCase()], expectedHdr[1], - `request ${reqNum} header ${expectedHdr[0]} value is "${serverRequest.request_headers[expectedHdr[0].toLowerCase()]}", not "${expectedHdr[1]}"`) - }) - } - } - if (test?.check_count && testState) { - assert_equals(requests.length, testState.length); - } -} - -function pause () { - return new Promise(function (resolve, reject) { - step_timeout(function () { - return resolve() - }, 3000) - }) -} - -function makeTestUrl (uuid, config) { - var arg = '' - var base_url = '' - if ('base_url' in config) { - base_url = config.base_url - } - if ('query_arg' in config) { - arg = `&target=${config.query_arg}` - } - if ('url_params' in config) { - arg = `${arg}&${config.url_params}` - } - return `${base_url}resources/http-cache.py?dispatch=test&uuid=${uuid}${arg}` -} - -function getServerState (uuid) { - return fetch(`resources/http-cache.py?dispatch=state&uuid=${uuid}`) - .then(function (response) { - return response.text() - }).then(function (text) { - return JSON.parse(text) || [] - }) -} - -function run_tests (tests) { - tests.forEach(function (test) { - promise_test(makeTest(test), test.name) - }) -} - -var contentStore = {} -function http_content (csKey) { - if (csKey in contentStore) { - return contentStore[csKey] - } else { - var content = btoa(Math.random() * Date.now()) - contentStore[csKey] = content - return content - } -} diff --git a/test/fixtures/wpt/fetch/http-cache/invalidate.any.js b/test/fixtures/wpt/fetch/http-cache/invalidate.any.js deleted file mode 100644 index 9f8090ace65..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/invalidate.any.js +++ /dev/null @@ -1,235 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Invalidation -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - { - name: 'HTTP cache invalidates after a successful response from a POST', - requests: [ - { - template: "fresh" - }, { - request_method: "POST", - request_body: "abc" - }, { - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache does not invalidate after a failed response from an unsafe request', - requests: [ - { - template: "fresh" - }, { - request_method: "POST", - request_body: "abc", - response_status: [500, "Internal Server Error"] - }, { - expected_type: "cached" - } - ] - }, - { - name: 'HTTP cache invalidates after a successful response from a PUT', - requests: [ - { - template: "fresh" - }, { - template: "fresh", - request_method: "PUT", - request_body: "abc" - }, { - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache invalidates after a successful response from a DELETE', - requests: [ - { - template: "fresh" - }, { - request_method: "DELETE", - request_body: "abc" - }, { - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache invalidates after a successful response from an unknown method', - requests: [ - { - template: "fresh" - }, { - request_method: "FOO", - request_body: "abc" - }, { - expected_type: "not_cached" - } - ] - }, - - - { - name: 'HTTP cache invalidates Location URL after a successful response from a POST', - requests: [ - { - template: "location" - }, { - request_method: "POST", - request_body: "abc", - template: "lcl_response" - }, { - template: "location", - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache does not invalidate Location URL after a failed response from an unsafe request', - requests: [ - { - template: "location" - }, { - template: "lcl_response", - request_method: "POST", - request_body: "abc", - response_status: [500, "Internal Server Error"] - }, { - template: "location", - expected_type: "cached" - } - ] - }, - { - name: 'HTTP cache invalidates Location URL after a successful response from a PUT', - requests: [ - { - template: "location" - }, { - template: "lcl_response", - request_method: "PUT", - request_body: "abc" - }, { - template: "location", - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache invalidates Location URL after a successful response from a DELETE', - requests: [ - { - template: "location" - }, { - template: "lcl_response", - request_method: "DELETE", - request_body: "abc" - }, { - template: "location", - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache invalidates Location URL after a successful response from an unknown method', - requests: [ - { - template: "location" - }, { - template: "lcl_response", - request_method: "FOO", - request_body: "abc" - }, { - template: "location", - expected_type: "not_cached" - } - ] - }, - - - - { - name: 'HTTP cache invalidates Content-Location URL after a successful response from a POST', - requests: [ - { - template: "content_location" - }, { - request_method: "POST", - request_body: "abc", - template: "lcl_response" - }, { - template: "content_location", - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache does not invalidate Content-Location URL after a failed response from an unsafe request', - requests: [ - { - template: "content_location" - }, { - template: "lcl_response", - request_method: "POST", - request_body: "abc", - response_status: [500, "Internal Server Error"] - }, { - template: "content_location", - expected_type: "cached" - } - ] - }, - { - name: 'HTTP cache invalidates Content-Location URL after a successful response from a PUT', - requests: [ - { - template: "content_location" - }, { - template: "lcl_response", - request_method: "PUT", - request_body: "abc" - }, { - template: "content_location", - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache invalidates Content-Location URL after a successful response from a DELETE', - requests: [ - { - template: "content_location" - }, { - template: "lcl_response", - request_method: "DELETE", - request_body: "abc" - }, { - template: "content_location", - expected_type: "not_cached" - } - ] - }, - { - name: 'HTTP cache invalidates Content-Location URL after a successful response from an unknown method', - requests: [ - { - template: "content_location" - }, { - template: "lcl_response", - request_method: "FOO", - request_body: "abc" - }, { - template: "content_location", - expected_type: "not_cached" - } - ] - } - -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/no-vary-search.tentative.any.js b/test/fixtures/wpt/fetch/http-cache/no-vary-search.tentative.any.js deleted file mode 100644 index 6c5a714e287..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/no-vary-search.tentative.any.js +++ /dev/null @@ -1,91 +0,0 @@ -// META: global=window,worker -// META: title=NoVarySearch HTTP Cache -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js -/* -NOTE for testing No-Vary-Search-Header: -- If `params` is set to true, `expect=("dispatch" "uuid")` must be specified. - Otherwise: - - The same HTTP Cache will be used by other tests, which are supposed - to be distinguished by uuid. - - The test utility cannot get the server's states because UA will use the HTTP - Cache instead of sending a new request to server to ask for the latest state. -- Do not test not_cached cases and cached cases within one test. Test infra - checks the number of requests and responses without considering if the - previous responses should be served from cache or not. -*/ -var tests = [ - { - name: "When params is set to true, URL differs only by their parameters (other than `dispatch` and `uuid`) should not be cached as different entries.", - requests: [ - { - url_params: "a=1&b=2", - response_headers: [ - ["Cache-Control", "max-age=10000"], - ["No-Vary-Search", "params, except=(\"dispatch\" \"uuid\")"], - ], - }, - { - expected_type: "cached" - } - ] - }, - { - name: "Ground truth: When key-order is not set, URLs should be compared in an order-sensitive way.", - requests: [ - { - url_params: "a=1&b=2", - response_headers: [ - ["Cache-Control", "max-age=10000"], - ], - }, - { - url_params: "b=2&a=1", - expected_type: "not_cached" - } - ] - }, - { - name: "When key-order is set , URLs should be compared in an order-insensitive way. Matched cases:", - requests: [ - { - url_params: "a=1&b=2", - response_headers: [ - ["Cache-Control", "max-age=10000"], - ["No-Vary-Search", "key-order"], - ], - }, - { - url_params: "b=2&a=1", - expected_type: "cached" - } - ] - }, - { - name: "When key-order is set , URLs should be compared in an order-insensitive way. Not matched cases", - requests: [ - { - url_params: "a=1&b=2", - response_headers: [ - ["Cache-Control", "max-age=10000"], - ["No-Vary-Search", "key-order"], - ], - }, - { - url_params: "b=2", - expected_type: "not_cached" - }, - { - url_params: "a=2&b=2", - expected_type: "not_cached" - }, - { - url_params: "a=1&b=2&c=3", - expected_type: "not_cached" - } - ] - } -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/partial.any.js b/test/fixtures/wpt/fetch/http-cache/partial.any.js deleted file mode 100644 index 3f23b5930f7..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/partial.any.js +++ /dev/null @@ -1,208 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Partial Content -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - { - name: "HTTP cache stores partial content and reuses it", - requests: [ - { - request_headers: [ - ['Range', "bytes=-5"] - ], - response_status: [206, "Partial Content"], - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Content-Range", "bytes 4-9/10"] - ], - response_body: "01234", - expected_request_headers: [ - ["Range", "bytes=-5"] - ] - }, - { - request_headers: [ - ["Range", "bytes=-5"] - ], - expected_type: "cached", - expected_status: 206, - expected_response_text: "01234" - } - ] - }, - { - name: "HTTP cache stores complete response and serves smaller ranges from it (byte-range-spec)", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"] - ], - response_body: "01234567890" - }, - { - request_headers: [ - ['Range', "bytes=0-1"] - ], - expected_type: "cached", - expected_status: 206, - expected_response_text: "01" - }, - ] - }, - { - name: "HTTP cache stores complete response and serves smaller ranges from it (absent last-byte-pos)", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"], - ], - response_body: "01234567890" - }, - { - request_headers: [ - ['Range', "bytes=1-"] - ], - expected_type: "cached", - expected_status: 206, - expected_response_text: "1234567890" - } - ] - }, - { - name: "HTTP cache stores complete response and serves smaller ranges from it (suffix-byte-range-spec)", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"], - ], - response_body: "0123456789A" - }, - { - request_headers: [ - ['Range', "bytes=-1"] - ], - expected_type: "cached", - expected_status: 206, - expected_response_text: "A" - } - ] - }, - { - name: "HTTP cache stores complete response and serves smaller ranges from it with only-if-cached", - requests: [ - { - response_headers: [ - ["Cache-Control", "max-age=3600"] - ], - response_body: "01234567890" - }, - { - request_headers: [ - ['Range', "bytes=0-1"] - ], - mode: "same-origin", - cache: "only-if-cached", - expected_type: "cached", - expected_status: 206, - expected_response_text: "01" - }, - ] - }, - { - name: "HTTP cache stores partial response and serves smaller ranges from it (byte-range-spec)", - requests: [ - { - request_headers: [ - ['Range', "bytes=-5"] - ], - response_status: [206, "Partial Content"], - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Content-Range", "bytes 4-9/10"] - ], - response_body: "01234" - }, - { - request_headers: [ - ['Range', "bytes=6-8"] - ], - expected_type: "cached", - expected_status: 206, - expected_response_text: "234" - } - ] - }, - { - name: "HTTP cache stores partial response and serves smaller ranges from it (absent last-byte-pos)", - requests: [ - { - request_headers: [ - ['Range', "bytes=-5"] - ], - response_status: [206, "Partial Content"], - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Content-Range", "bytes 4-9/10"] - ], - response_body: "01234" - }, - { - request_headers: [ - ["Range", "bytes=6-"] - ], - expected_type: "cached", - expected_status: 206, - expected_response_text: "234" - } - ] - }, - { - name: "HTTP cache stores partial response and serves smaller ranges from it (suffix-byte-range-spec)", - requests: [ - { - request_headers: [ - ['Range', "bytes=-5"] - ], - response_status: [206, "Partial Content"], - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Content-Range", "bytes 4-9/10"] - ], - response_body: "01234" - }, - { - request_headers: [ - ['Range', "bytes=-1"] - ], - expected_type: "cached", - expected_status: 206, - expected_response_text: "4" - } - ] - }, - { - name: "HTTP cache stores partial content and completes it", - requests: [ - { - request_headers: [ - ['Range', "bytes=-5"] - ], - response_status: [206, "Partial Content"], - response_headers: [ - ["Cache-Control", "max-age=3600"], - ["Content-Range", "bytes 0-4/10"] - ], - response_body: "01234" - }, - { - expected_request_headers: [ - ["range", "bytes=5-"] - ] - } - ] - }, -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/post-patch.any.js b/test/fixtures/wpt/fetch/http-cache/post-patch.any.js deleted file mode 100644 index 0a69baa5c66..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/post-patch.any.js +++ /dev/null @@ -1,46 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Caching POST and PATCH responses -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - { - name: "HTTP cache uses content after PATCH request with response containing Content-Location and cache-allowing header", - requests: [ - { - request_method: "PATCH", - request_body: "abc", - response_status: [200, "OK"], - response_headers: [ - ['Cache-Control', "private, max-age=1000"], - ['Content-Location', ""] - ], - response_body: "abc" - }, - { - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache uses content after POST request with response containing Content-Location and cache-allowing header", - requests: [ - { - request_method: "POST", - request_body: "abc", - response_status: [200, "OK"], - response_headers: [ - ['Cache-Control', "private, max-age=1000"], - ['Content-Location', ""] - ], - response_body: "abc" - }, - { - expected_type: "cached" - } - ] - } -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/pragma-no-cache-with-cache-control.html b/test/fixtures/wpt/fetch/http-cache/pragma-no-cache-with-cache-control.html deleted file mode 100644 index 19a80fe5edc..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/pragma-no-cache-with-cache-control.html +++ /dev/null @@ -1,28 +0,0 @@ - - -HTTP Cache: Cache-Control with Pragma: no-cache - - - - - diff --git a/test/fixtures/wpt/fetch/http-cache/resources/cached_pragma_rand.py b/test/fixtures/wpt/fetch/http-cache/resources/cached_pragma_rand.py deleted file mode 100644 index 18c7d25159f..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/resources/cached_pragma_rand.py +++ /dev/null @@ -1,14 +0,0 @@ -def main(request, response): - # Disable non-standard XSS protection - response.headers.set(b"X-XSS-Protection", b"0") - response.headers.set(b"Content-Type", b"text/html") - - # Set caching headers - # According to rfc9111 Pragma: no-cache is deprecated, so we expect - # Cache-Control to take precedence when there's a mismatch. - response.headers.set(b"Cache-Control", b"max-age=2592000, public") - response.headers.set(b"Pragma", b"no-cache") - - # Include a timestamp to verify caching behavior - import time - response.content = f"Timestamp: {time.time()}".encode('utf-8') diff --git a/test/fixtures/wpt/fetch/http-cache/resources/http-cache.py b/test/fixtures/wpt/fetch/http-cache/resources/http-cache.py deleted file mode 100644 index 3ab610dd142..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/resources/http-cache.py +++ /dev/null @@ -1,124 +0,0 @@ -import datetime -import json -import time -from base64 import b64decode - -from wptserve.utils import isomorphic_decode, isomorphic_encode - -NOTEHDRS = set([u'content-type', u'access-control-allow-origin', u'last-modified', u'etag']) -NOBODYSTATUS = set([204, 304]) -LOCATIONHDRS = set([u'location', u'content-location']) -DATEHDRS = set([u'date', u'expires', u'last-modified']) - -def main(request, response): - dispatch = request.GET.first(b"dispatch", None) - uuid = request.GET.first(b"uuid", None) - response.headers.set(b"Access-Control-Allow-Credentials", b"true") - - if request.method == u"OPTIONS": - return handle_preflight(uuid, request, response) - if not uuid: - response.status = (404, b"Not Found") - response.headers.set(b"Content-Type", b"text/plain") - return b"UUID not found" - if dispatch == b'test': - return handle_test(uuid, request, response) - elif dispatch == b'state': - return handle_state(uuid, request, response) - response.status = (404, b"Not Found") - response.headers.set(b"Content-Type", b"text/plain") - return b"Fallthrough" - -def handle_preflight(uuid, request, response): - response.status = (200, b"OK") - response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin") or '*') - response.headers.set(b"Access-Control-Allow-Methods", b"GET") - response.headers.set(b"Access-Control-Allow-Headers", request.headers.get(b"Access-Control-Request-Headers") or "*") - response.headers.set(b"Access-Control-Max-Age", b"86400") - return b"Preflight request" - -def handle_state(uuid, request, response): - response.headers.set(b"Content-Type", b"text/plain") - return json.dumps(request.server.stash.take(uuid)) - -def handle_test(uuid, request, response): - server_state = request.server.stash.take(uuid) or [] - try: - requests = json.loads(b64decode(request.headers.get(b'Test-Requests', b""))) - except: - response.status = (400, b"Bad Request") - response.headers.set(b"Content-Type", b"text/plain") - return b"No or bad Test-Requests request header" - config = requests[len(server_state)] - if not config: - response.status = (404, b"Not Found") - response.headers.set(b"Content-Type", b"text/plain") - return b"Config not found" - noted_headers = {} - now = time.time() - for header in config.get(u'response_headers', []): - if header[0].lower() in LOCATIONHDRS: # magic locations - if (len(header[1]) > 0): - header[1] = u"%s&target=%s" % (request.url, header[1]) - else: - header[1] = request.url - if header[0].lower() in DATEHDRS and isinstance(header[1], int): # magic dates - header[1] = http_date(now, header[1]) - response.headers.set(isomorphic_encode(header[0]), isomorphic_encode(header[1])) - if header[0].lower() in NOTEHDRS: - noted_headers[header[0].lower()] = header[1] - state = { - u'now': now, - u'request_method': request.method, - u'request_headers': dict([[isomorphic_decode(h.lower()), isomorphic_decode(request.headers[h])] for h in request.headers]), - u'response_headers': noted_headers - } - server_state.append(state) - request.server.stash.put(uuid, server_state) - - if u"access-control-allow-origin" not in noted_headers: - response.headers.set(b"Access-Control-Allow-Origin", b"*") - if u"content-type" not in noted_headers: - response.headers.set(b"Content-Type", b"text/plain") - response.headers.set(b"Server-Request-Count", len(server_state)) - - code, phrase = config.get(u"response_status", [200, b"OK"]) - if config.get(u"expected_type", u"").endswith(u'validated'): - ref_hdrs = server_state[0][u'response_headers'] - previous_lm = ref_hdrs.get(u'last-modified', False) - if previous_lm and request.headers.get(b"If-Modified-Since", False) == isomorphic_encode(previous_lm): - code, phrase = [304, b"Not Modified"] - previous_etag = ref_hdrs.get(u'etag', False) - if previous_etag and request.headers.get(b"If-None-Match", False) == isomorphic_encode(previous_etag): - code, phrase = [304, b"Not Modified"] - if code != 304: - code, phrase = [999, b'304 Not Generated'] - response.status = (code, phrase) - - content = config.get(u"response_body", uuid) - if code in NOBODYSTATUS: - return b"" - return content - - -def get_header(headers, header_name): - result = None - for header in headers: - if header[0].lower() == header_name.lower(): - result = header[1] - return result - -WEEKDAYS = [u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat', u'Sun'] -MONTHS = [None, u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul', - u'Aug', u'Sep', u'Oct', u'Nov', u'Dec'] - -def http_date(now, delta_secs=0): - date = datetime.datetime.utcfromtimestamp(now + delta_secs) - return u"%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT" % ( - WEEKDAYS[date.weekday()], - date.day, - MONTHS[date.month], - date.year, - date.hour, - date.minute, - date.second) diff --git a/test/fixtures/wpt/fetch/http-cache/resources/securedimage.py b/test/fixtures/wpt/fetch/http-cache/resources/securedimage.py deleted file mode 100644 index cac9cfedd27..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/resources/securedimage.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 - - -from wptserve.utils import isomorphic_decode, isomorphic_encode - -def main(request, response): - image_url = str.replace(request.url, u"fetch/http-cache/resources/securedimage.py", u"images/green.png") - - if b"authorization" not in request.headers: - response.status = 401 - response.headers.set(b"WWW-Authenticate", b"Basic") - return - else: - auth = request.headers.get(b"Authorization") - if auth != b"Basic dGVzdHVzZXI6dGVzdHBhc3M=": - response.set_error(403, u"Invalid username or password - " + isomorphic_decode(auth)) - return - - response.status = 301 - response.headers.set(b"Location", isomorphic_encode(image_url)) diff --git a/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup-with-iframe.html b/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup-with-iframe.html deleted file mode 100644 index 48b16180cfd..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup-with-iframe.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - HTTP Cache - helper - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup.html b/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup.html deleted file mode 100644 index edb57947941..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/resources/split-cache-popup.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - HTTP Cache - helper - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/http-cache/split-cache.html b/test/fixtures/wpt/fetch/http-cache/split-cache.html deleted file mode 100644 index c822abba3ac..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/split-cache.html +++ /dev/null @@ -1,158 +0,0 @@ - - - - - HTTP Cache - Partioning by site - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/http-cache/status.any.js b/test/fixtures/wpt/fetch/http-cache/status.any.js deleted file mode 100644 index 10c83a25a26..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/status.any.js +++ /dev/null @@ -1,60 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Status Codes -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = []; -function check_status(status) { - var code = status[0]; - var phrase = status[1]; - var body = status[2]; - if (body === undefined) { - body = http_content(code); - } - tests.push({ - name: "HTTP cache goes to the network if it has a stale " + code + " response", - requests: [ - { - template: "stale", - response_status: [code, phrase], - response_body: body - }, { - expected_type: "not_cached", - response_status: [code, phrase], - response_body: body - } - ] - }) - tests.push({ - name: "HTTP cache avoids going to the network if it has a fresh " + code + " response", - requests: [ - { - template: "fresh", - response_status: [code, phrase], - response_body: body - }, { - expected_type: "cached", - response_status: [code, phrase], - response_body: body - } - ] - }) -} -[ - [200, "OK"], - [203, "Non-Authoritative Information"], - [204, "No Content", null], - [299, "Whatever"], - [400, "Bad Request"], - [404, "Not Found"], - [410, "Gone"], - [499, "Whatever"], - [500, "Internal Server Error"], - [502, "Bad Gateway"], - [503, "Service Unavailable"], - [504, "Gateway Timeout"], - [599, "Whatever"] -].forEach(check_status); -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/http-cache/vary.any.js b/test/fixtures/wpt/fetch/http-cache/vary.any.js deleted file mode 100644 index 2cfd226af81..00000000000 --- a/test/fixtures/wpt/fetch/http-cache/vary.any.js +++ /dev/null @@ -1,313 +0,0 @@ -// META: global=window,worker -// META: title=HTTP Cache - Vary -// META: timeout=long -// META: script=/common/utils.js -// META: script=/common/get-host-info.sub.js -// META: script=http-cache.js - -var tests = [ - { - name: "HTTP cache reuses Vary response when request matches", - requests: [ - { - request_headers: [ - ["Foo", "1"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo"] - ] - }, - { - request_headers: [ - ["Foo", "1"] - ], - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache doesn't use Vary response when request doesn't match", - requests: [ - { - request_headers: [ - ["Foo", "1"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo"] - ] - }, - { - request_headers: [ - ["Foo", "2"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache doesn't use Vary response when request omits variant header", - requests: [ - { - request_headers: [ - ["Foo", "1"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo"] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache doesn't invalidate existing Vary response", - requests: [ - { - request_headers: [ - ["Foo", "1"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo"] - ], - response_body: http_content('foo_1') - }, - { - request_headers: [ - ["Foo", "2"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo"] - ], - expected_type: "not_cached", - response_body: http_content('foo_2'), - }, - { - request_headers: [ - ["Foo", "1"] - ], - response_body: http_content('foo_1'), - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache doesn't pay attention to headers not listed in Vary", - requests: [ - { - request_headers: [ - ["Foo", "1"], - ["Other", "2"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo"] - ], - }, - { - request_headers: [ - ["Foo", "1"], - ["Other", "3"] - ], - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache reuses two-way Vary response when request matches", - requests: [ - { - request_headers: [ - ["Foo", "1"], - ["Bar", "abc"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo, Bar"] - ] - }, - { - request_headers: [ - ["Foo", "1"], - ["Bar", "abc"] - ], - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache doesn't use two-way Vary response when request doesn't match", - requests: [ - { - request_headers: [ - ["Foo", "1"], - ["Bar", "abc"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo, Bar"] - ] - }, - { - request_headers: [ - ["Foo", "2"], - ["Bar", "abc"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache doesn't use two-way Vary response when request omits variant header", - requests: [ - { - request_headers: [ - ["Foo", "1"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo, Bar"] - ] - }, - { - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache reuses three-way Vary response when request matches", - requests: [ - { - request_headers: [ - ["Foo", "1"], - ["Bar", "abc"], - ["Baz", "789"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo, Bar, Baz"] - ] - }, - { - request_headers: [ - ["Foo", "1"], - ["Bar", "abc"], - ["Baz", "789"] - ], - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache doesn't use three-way Vary response when request doesn't match", - requests: [ - { - request_headers: [ - ["Foo", "1"], - ["Bar", "abc"], - ["Baz", "789"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo, Bar, Baz"] - ] - }, - { - request_headers: [ - ["Foo", "2"], - ["Bar", "abc"], - ["Baz", "789"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache doesn't use three-way Vary response when request doesn't match, regardless of header order", - requests: [ - { - request_headers: [ - ["Foo", "1"], - ["Bar", "abc4"], - ["Baz", "789"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo, Bar, Baz"] - ] - }, - { - request_headers: [ - ["Foo", "1"], - ["Bar", "abc"], - ["Baz", "789"] - ], - expected_type: "not_cached" - } - ] - }, - { - name: "HTTP cache uses three-way Vary response when both request and the original request omited a variant header", - requests: [ - { - request_headers: [ - ["Foo", "1"], - ["Baz", "789"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "Foo, Bar, Baz"] - ] - }, - { - request_headers: [ - ["Foo", "1"], - ["Baz", "789"] - ], - expected_type: "cached" - } - ] - }, - { - name: "HTTP cache doesn't use Vary response with a field value of '*'", - requests: [ - { - request_headers: [ - ["Foo", "1"], - ["Baz", "789"] - ], - response_headers: [ - ["Expires", 5000], - ["Last-Modified", -3000], - ["Vary", "*"] - ] - }, - { - request_headers: [ - ["*", "1"], - ["Baz", "789"] - ], - expected_type: "not_cached" - } - ] - } -]; -run_tests(tests); diff --git a/test/fixtures/wpt/fetch/images/canvas-remote-read-remote-image-redirect.html b/test/fixtures/wpt/fetch/images/canvas-remote-read-remote-image-redirect.html deleted file mode 100644 index 4a887f3d331..00000000000 --- a/test/fixtures/wpt/fetch/images/canvas-remote-read-remote-image-redirect.html +++ /dev/null @@ -1,28 +0,0 @@ - - -Load a no-cors image from a same-origin URL that redirects to a cross-origin URL that redirects to the initial origin - - - - diff --git a/test/fixtures/wpt/fetch/local-network-access/META.yml b/test/fixtures/wpt/fetch/local-network-access/META.yml deleted file mode 100644 index 4c5c6983ed0..00000000000 --- a/test/fixtures/wpt/fetch/local-network-access/META.yml +++ /dev/null @@ -1,5 +0,0 @@ -spec: https://wicg.github.io/local-network-access/ -suggested_reviewers: - - cthomp - - camillelamy - - hchao diff --git a/test/fixtures/wpt/fetch/local-network-access/README.md b/test/fixtures/wpt/fetch/local-network-access/README.md deleted file mode 100644 index 95066cdcd0b..00000000000 --- a/test/fixtures/wpt/fetch/local-network-access/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Local Network Access tests - -This directory contains tests for Local Network Access' integration with -the Fetch specification. - -See also: - -* [Explainer](https://github.com/explainers-by-googlers/local-network-access) - -Local Network Access replaced [Private Network -Access](https://wicg.github.io/local-network-access/). diff --git a/test/fixtures/wpt/fetch/local-network-access/fetch.tentative.https.html b/test/fixtures/wpt/fetch/local-network-access/fetch.tentative.https.html deleted file mode 100644 index b783269207b..00000000000 --- a/test/fixtures/wpt/fetch/local-network-access/fetch.tentative.https.html +++ /dev/null @@ -1,87 +0,0 @@ - - -LNA Fetch tests: HTTPS - - - - - - - - diff --git a/test/fixtures/wpt/fetch/local-network-access/resources/fetch-local-http.html b/test/fixtures/wpt/fetch/local-network-access/resources/fetch-local-http.html deleted file mode 100644 index dcad775bfec..00000000000 --- a/test/fixtures/wpt/fetch/local-network-access/resources/fetch-local-http.html +++ /dev/null @@ -1,40 +0,0 @@ - - -Fetch HTTP local with targetAddressSpace local - - - - - diff --git a/test/fixtures/wpt/fetch/local-network-access/resources/fetch-local.html b/test/fixtures/wpt/fetch/local-network-access/resources/fetch-local.html deleted file mode 100644 index bc12e2660bc..00000000000 --- a/test/fixtures/wpt/fetch/local-network-access/resources/fetch-local.html +++ /dev/null @@ -1,52 +0,0 @@ - - -Fetch Local resource - - - - - diff --git a/test/fixtures/wpt/fetch/local-network-access/resources/fetch-public-http-wrong-address-space.html b/test/fixtures/wpt/fetch/local-network-access/resources/fetch-public-http-wrong-address-space.html deleted file mode 100644 index fb306f99d3c..00000000000 --- a/test/fixtures/wpt/fetch/local-network-access/resources/fetch-public-http-wrong-address-space.html +++ /dev/null @@ -1,42 +0,0 @@ - - -Fetch HTTP public targetAddress local resource - - - - - diff --git a/test/fixtures/wpt/fetch/local-network-access/resources/support.sub.js b/test/fixtures/wpt/fetch/local-network-access/resources/support.sub.js deleted file mode 100644 index 09e234c5f24..00000000000 --- a/test/fixtures/wpt/fetch/local-network-access/resources/support.sub.js +++ /dev/null @@ -1,189 +0,0 @@ -// Maps protocol (without the trailing colon) and address space to port. -// -// TODO(crbug.com/418737577): change keys to be consistent with new address -// space names. -const SERVER_PORTS = { - "http": { - "loopback": {{ports[http][0]}}, - "local": {{ports[http-local][0]}}, - "public": {{ports[http-public][0]}}, - }, - "https": { - "loopback": {{ports[https][0]}}, - "other-loopback": {{ports[https][1]}}, - "local": {{ports[https-local][0]}}, - "public": {{ports[https-public][0]}}, - }, - "ws": { - "loopback": {{ports[ws][0]}}, - }, - "wss": { - "loopback": {{ports[wss][0]}}, - }, -}; - -// A `Server` is a web server accessible by tests. It has the following shape: -// -// { -// addressSpace: the IP address space of the server ("loopback", "local" or -// "public"), -// name: a human-readable name for the server, -// port: the port on which the server listens for connections, -// protocol: the protocol (including trailing colon) spoken by the server, -// } -// -// Constants below define the available servers, which can also be accessed -// programmatically with `get()`. -class Server { - // Maps the given `protocol` (without a trailing colon) and `addressSpace` to - // a server. Returns null if no such server exists. - static get(protocol, addressSpace) { - const ports = SERVER_PORTS[protocol]; - if (ports === undefined) { - return null; - } - - const port = ports[addressSpace]; - if (port === undefined) { - return null; - } - - return { - addressSpace, - name: `${protocol}-${addressSpace}`, - port, - protocol: protocol + ':', - }; - } - - static HTTP_LOOPBACK = Server.get("http", "loopback"); - static HTTP_LOCAL = Server.get("http", "local"); - static HTTP_PUBLIC = Server.get("http", "public"); - static HTTPS_LOOPBACK = Server.get("https", "loopback"); - static OTHER_HTTPS_LOOPBACK = Server.get("https", "other-loopback"); - static HTTPS_LOCAL = Server.get("https", "local"); - static HTTPS_PUBLIC = Server.get("https", "public"); - static WS_LOOPBACK = Server.get("ws", "loopback"); - static WSS_LOOPBACK = Server.get("wss", "loopback"); -}; - -// Resolves a URL relative to the current location, returning an absolute URL. -// -// `url` specifies the relative URL, e.g. "foo.html" or "http://foo.example". -// `options`, if defined, should have the following shape: -// -// { -// // Optional. Overrides the protocol of the returned URL. -// protocol, -// -// // Optional. Overrides the port of the returned URL. -// port, -// -// // Extra headers. -// headers, -// -// // Extra search params. -// searchParams, -// } -// -function resolveUrl(url, options) { - const result = new URL(url, window.location); - if (options === undefined) { - return result; - } - - const { port, protocol, headers, searchParams } = options; - if (port !== undefined) { - result.port = port; - } - if (protocol !== undefined) { - result.protocol = protocol; - } - if (headers !== undefined) { - const pipes = []; - for (key in headers) { - pipes.push(`header(${key},${headers[key]})`); - } - result.searchParams.append("pipe", pipes.join("|")); - } - if (searchParams !== undefined) { - for (key in searchParams) { - result.searchParams.append(key, searchParams[key]); - } - } - - return result; -} - -// Computes options to pass to `resolveUrl()` for a source document's URL. -// -// `server` identifies the server from which to load the document. -// `treatAsPublic`, if set to true, specifies that the source document should -// be artificially placed in the `public` address space using CSP. -function sourceResolveOptions({ server, treatAsPublic }) { - const options = {...server}; - if (treatAsPublic) { - options.headers = { "Content-Security-Policy": "treat-as-public-address" }; - } - return options; -} - -// Computes the URL of a target handler configured with the given options. -// -// `server` identifies the server from which to load the resource. -// `behavior` specifies the behavior of the target server. It may contain: -// - `response`: The result of calling one of `ResponseBehavior`'s methods. -// - `redirect`: A URL to which the target should redirect GET requests. -function resolveTargetUrl({ server, behavior }) { - if (server === undefined) { - throw new Error("no server specified."); - } - const options = {...server}; - if (behavior) { - const { response, redirect } = behavior; - options.searchParams = { - ...response, - }; - if (redirect !== undefined) { - options.searchParams.redirect = redirect; - } - } - - return resolveUrl("target.py", options); -} - -// Methods generate behavior specifications for how `resources/target.py` -// should behave upon receiving a regular (non-preflight) request. -const ResponseBehavior = { - // The response should succeed without CORS headers. - default: () => ({}), - - // The response should succeed with CORS headers. - allowCrossOrigin: () => ({ "final-headers": "cors" }), -}; - -const FetchTestResult = { - SUCCESS: { - ok: true, - body: "success", - }, - OPAQUE: { - ok: false, - type: "opaque", - body: "", - }, - FAILURE: { - error: "TypeError: Failed to fetch", - }, -}; - -// Helper function for checking results from fetch tests. -function checkTestResult(actual, expected) { - assert_equals(actual.error, expected.error, "error mismatch"); - assert_equals(actual.ok, expected.ok, "response ok mismatch"); - assert_equals(actual.body, expected.body, "response body mismatch"); - - if (expected.type !== undefined) { - assert_equals(type, expected.type, "response type mismatch"); - } -} diff --git a/test/fixtures/wpt/fetch/local-network-access/resources/target.py b/test/fixtures/wpt/fetch/local-network-access/resources/target.py deleted file mode 100644 index eabcdd47517..00000000000 --- a/test/fixtures/wpt/fetch/local-network-access/resources/target.py +++ /dev/null @@ -1,105 +0,0 @@ -# This endpoint responds to requests for a target of a (possible) LNA request. -# -# Its behavior can be configured with various search/GET parameters, all of -# which are optional: -# -# - final-headers: Valid values are: -# - cors: this endpoint responds with valid CORS headers to CORS-enabled -# non-preflight requests. These should be sufficient for non-preflighted -# CORS-enabled requests to succeed. -# - sw: this endpoint responds with a valid Service-Worker header to allow -# for the request to serve as a Service worker script resource. This is -# only valid in conjunction with the cors value above. -# - unspecified: this endpoint responds with no CORS headers to non-preflight -# requests. This should fail CORS-enabled requests, but be sufficient for -# no-CORS requests. -# -# The following parameters only affect non-preflight responses: -# -# - redirect: If set, the response code is set to 301 and the `Location` -# response header is set to this value. -# - mime-type: If set, the `Content-Type` response header is set to this value. -# - file: Specifies a path (relative to this file's directory) to a file. If -# set, the response body is copied from this file. -# - random-js-prefix: If set to any value, the response body is prefixed with -# a Javascript comment line containing a random value. This is useful in -# service worker tests, since service workers are only updated if the new -# script is not byte-for-byte identical with the old script. -# - body: If set and `file` is not, the response body is set to this value. -# - -import os -import random - -from wptserve.utils import isomorphic_encode - -_ACAO = ("Access-Control-Allow-Origin", "*") -_ACAH = ("Access-Control-Allow-Headers", "Service-Worker") - -def _get_response_headers(method, mode, origin): - acam = ("Access-Control-Allow-Methods", method) - - if mode == b"cors": - return [acam, _ACAO] - - if mode == b"cors+sw": - return [acam, _ACAO, _ACAH] - - if mode == b"navigation": - return [ - acam, - ("Access-Control-Allow-Origin", origin), - ("Access-Control-Allow-Credentials", "true"), - ] - - return [] - - -def _is_loaded_in_fenced_frame(request): - return request.GET.get(b"is-loaded-in-fenced-frame") - -def _final_response_body(request): - file_name = None - if file_name is None: - file_name = request.GET.get(b"file") - if file_name is None: - return request.GET.get(b"body") or "success" - - prefix = b"" - if request.GET.get(b"random-js-prefix"): - value = random.randint(0, 1000000000) - prefix = isomorphic_encode("// Random value: {}\n\n".format(value)) - - path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), file_name) - with open(path, 'rb') as f: - contents = f.read() - - return prefix + contents - -def _handle_final_request(request, response): - mode = request.GET.get(b"final-headers") - origin = request.headers.get("Origin") - headers = _get_response_headers(request.method, mode, origin) - - redirect = request.GET.get(b"redirect") - if redirect is not None: - headers.append(("Location", redirect)) - return (301, headers, b"") - - mime_type = request.GET.get(b"mime-type") - if mime_type is not None: - headers.append(("Content-Type", mime_type),) - - if _is_loaded_in_fenced_frame(request): - headers.append(("Supports-Loading-Mode", "fenced-frame")) - - body = _final_response_body(request) - return (headers, body) - - -def main(request, response): - try: - return _handle_final_request(request, response) - except BaseException as e: - # Surface exceptions to the client, where they show up as assertion errors. - return (500, [("X-exception", str(e))], "exception: {}".format(e)) diff --git a/test/fixtures/wpt/fetch/metadata/META.yml b/test/fixtures/wpt/fetch/metadata/META.yml deleted file mode 100644 index 85f0a7d2ee1..00000000000 --- a/test/fixtures/wpt/fetch/metadata/META.yml +++ /dev/null @@ -1,4 +0,0 @@ -spec: https://w3c.github.io/webappsec-fetch-metadata/ -suggested_reviewers: - - mikewest - - iVanlIsh diff --git a/test/fixtures/wpt/fetch/metadata/README.md b/test/fixtures/wpt/fetch/metadata/README.md deleted file mode 100644 index 34864d4a4b6..00000000000 --- a/test/fixtures/wpt/fetch/metadata/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Fetch Metadata Tests -==================== - -This directory contains tests related to the Fetch Metadata proposal: - -: Explainer -:: -: "Spec" -:: diff --git a/test/fixtures/wpt/fetch/metadata/WEB_FEATURES.yml b/test/fixtures/wpt/fetch/metadata/WEB_FEATURES.yml deleted file mode 100644 index fb48eaa3112..00000000000 --- a/test/fixtures/wpt/fetch/metadata/WEB_FEATURES.yml +++ /dev/null @@ -1,3 +0,0 @@ -features: -- name: fetch-metadata - files: "**" diff --git a/test/fixtures/wpt/fetch/metadata/audio-worklet.https.html b/test/fixtures/wpt/fetch/metadata/audio-worklet.https.html deleted file mode 100644 index 3b768ef0b5d..00000000000 --- a/test/fixtures/wpt/fetch/metadata/audio-worklet.https.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/embed.https.sub.tentative.html b/test/fixtures/wpt/fetch/metadata/embed.https.sub.tentative.html deleted file mode 100644 index 1900dbdf081..00000000000 --- a/test/fixtures/wpt/fetch/metadata/embed.https.sub.tentative.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - -

Relevant issue: -<embed> should support loading random HTML documents, like <object> -

- - diff --git a/test/fixtures/wpt/fetch/metadata/fetch-preflight.https.sub.any.js b/test/fixtures/wpt/fetch/metadata/fetch-preflight.https.sub.any.js deleted file mode 100644 index d52474353ba..00000000000 --- a/test/fixtures/wpt/fetch/metadata/fetch-preflight.https.sub.any.js +++ /dev/null @@ -1,29 +0,0 @@ -// META: global=window,worker -// META: script=/fetch/metadata/resources/helper.js - -// Site -promise_test(t => { - return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", - { - mode: "cors", - headers: { 'x-test': 'testing' } - }, { - "site": "same-site", - "user": "", - "mode": "cors", - "dest": "empty" - }, "Same-site fetch with preflight"); -}, "Same-site fetch with preflight"); - -promise_test(t => { - return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", - { - mode: "cors", - headers: { 'x-test': 'testing' } - }, { - "site": "cross-site", - "user": "", - "mode": "cors", - "dest": "empty" - }, "Cross-site fetch with preflight"); -}, "Cross-site fetch with preflight"); diff --git a/test/fixtures/wpt/fetch/metadata/fetch.https.sub.any.js b/test/fixtures/wpt/fetch/metadata/fetch.https.sub.any.js deleted file mode 100644 index aeec5cdf2dc..00000000000 --- a/test/fixtures/wpt/fetch/metadata/fetch.https.sub.any.js +++ /dev/null @@ -1,58 +0,0 @@ -// META: global=window,worker -// META: script=/fetch/metadata/resources/helper.js - -// Site -promise_test(t => { - return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { - "site": "same-origin", - "user": "", - "mode": "cors", - "dest": "empty" - }, "Same-origin fetch"); -}, "Same-origin fetch"); - -promise_test(t => { - return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { - "site": "same-site", - "user": "", - "mode": "cors", - "dest": "empty" - }, "Same-site fetch"); -}, "Same-site fetch"); - -promise_test(t => { - return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { - "site": "cross-site", - "user": "", - "mode": "cors", - "dest": "empty" - }, "Cross-site fetch"); -}, "Cross-site fetch"); - -// Mode -promise_test(t => { - return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "same-origin"}, { - "site": "same-origin", - "user": "", - "mode": "same-origin", - "dest": "empty" - }, "Same-origin mode"); -}, "Same-origin mode"); - -promise_test(t => { - return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "cors"}, { - "site": "same-origin", - "user": "", - "mode": "cors", - "dest": "empty" - }, "CORS mode"); -}, "CORS mode"); - -promise_test(t => { - return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "no-cors"}, { - "site": "same-origin", - "user": "", - "mode": "no-cors", - "dest": "empty" - }, "no-CORS mode"); -}, "no-CORS mode"); diff --git a/test/fixtures/wpt/fetch/metadata/generated/audioworklet.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/audioworklet.https.sub.html deleted file mode 100644 index 5ea1cdc8bc9..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/audioworklet.https.sub.html +++ /dev/null @@ -1,297 +0,0 @@ - - - - - HTTP headers on request for AudioWorklet module - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/css-font-face.https.sub.tentative.html b/test/fixtures/wpt/fetch/metadata/generated/css-font-face.https.sub.tentative.html deleted file mode 100644 index 481355c0a6c..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/css-font-face.https.sub.tentative.html +++ /dev/null @@ -1,250 +0,0 @@ - - - - - HTTP headers on request for CSS font-face - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/css-font-face.sub.tentative.html b/test/fixtures/wpt/fetch/metadata/generated/css-font-face.sub.tentative.html deleted file mode 100644 index a0d2a06fed3..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/css-font-face.sub.tentative.html +++ /dev/null @@ -1,226 +0,0 @@ - - - - - HTTP headers on request for CSS font-face - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/css-images.https.sub.tentative.html b/test/fixtures/wpt/fetch/metadata/generated/css-images.https.sub.tentative.html deleted file mode 100644 index 63dcb4bf027..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/css-images.https.sub.tentative.html +++ /dev/null @@ -1,1529 +0,0 @@ - - - - - - HTTP headers on request for CSS image-accepting properties - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/css-images.sub.tentative.html b/test/fixtures/wpt/fetch/metadata/generated/css-images.sub.tentative.html deleted file mode 100644 index 2f6f2f75f1b..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/css-images.sub.tentative.html +++ /dev/null @@ -1,1309 +0,0 @@ - - - - - - HTTP headers on request for CSS image-accepting properties - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-a.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-a.https.sub.html deleted file mode 100644 index 6bc50692761..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-a.https.sub.html +++ /dev/null @@ -1,522 +0,0 @@ - - - - - - HTTP headers on request for HTML "a" element navigation - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-a.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-a.sub.html deleted file mode 100644 index 389d3393b64..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-a.sub.html +++ /dev/null @@ -1,402 +0,0 @@ - - - - - - HTTP headers on request for HTML "a" element navigation - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-area.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-area.https.sub.html deleted file mode 100644 index 2355cceda09..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-area.https.sub.html +++ /dev/null @@ -1,522 +0,0 @@ - - - - - - HTTP headers on request for HTML "area" element navigation - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-area.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-area.sub.html deleted file mode 100644 index 1932a66cc09..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-area.sub.html +++ /dev/null @@ -1,402 +0,0 @@ - - - - - - HTTP headers on request for HTML "area" element navigation - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-audio.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-audio.https.sub.html deleted file mode 100644 index f0ce53cbd15..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-audio.https.sub.html +++ /dev/null @@ -1,352 +0,0 @@ - - - - - HTTP headers on request for HTML "audio" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-audio.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-audio.sub.html deleted file mode 100644 index efe9164c6d2..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-audio.sub.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - HTTP headers on request for HTML "audio" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-embed.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-embed.https.sub.html deleted file mode 100644 index 2aea58d0b4d..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-embed.https.sub.html +++ /dev/null @@ -1,245 +0,0 @@ - - - - - HTTP headers on request for HTML "embed" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-embed.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-embed.sub.html deleted file mode 100644 index e394d4d7f9a..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-embed.sub.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - HTTP headers on request for HTML "embed" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-frame.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-frame.https.sub.html deleted file mode 100644 index 7df86fb7b4b..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-frame.https.sub.html +++ /dev/null @@ -1,338 +0,0 @@ - - - - - HTTP headers on request for HTML "frame" element source - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-frame.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-frame.sub.html deleted file mode 100644 index 6a10ca2b34d..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-frame.sub.html +++ /dev/null @@ -1,292 +0,0 @@ - - - - - HTTP headers on request for HTML "frame" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-iframe.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-iframe.https.sub.html deleted file mode 100644 index 10de0061480..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-iframe.https.sub.html +++ /dev/null @@ -1,338 +0,0 @@ - - - - - HTTP headers on request for HTML "frame" element source - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-iframe.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-iframe.sub.html deleted file mode 100644 index 5d2f096a3fa..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-iframe.sub.html +++ /dev/null @@ -1,292 +0,0 @@ - - - - - HTTP headers on request for HTML "frame" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-img-environment-change.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-img-environment-change.https.sub.html deleted file mode 100644 index 9e9c3723e9a..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-img-environment-change.https.sub.html +++ /dev/null @@ -1,386 +0,0 @@ - - - - - HTTP headers on image request triggered by change to environment - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-img-environment-change.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-img-environment-change.sub.html deleted file mode 100644 index 0d99cb3171b..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-img-environment-change.sub.html +++ /dev/null @@ -1,312 +0,0 @@ - - - - - HTTP headers on image request triggered by change to environment - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-img.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-img.https.sub.html deleted file mode 100644 index 5a0f2e4e200..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-img.https.sub.html +++ /dev/null @@ -1,703 +0,0 @@ - - - - - HTTP headers on request for HTML "img" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-img.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-img.sub.html deleted file mode 100644 index 23008791d7f..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-img.sub.html +++ /dev/null @@ -1,540 +0,0 @@ - - - - - HTTP headers on request for HTML "img" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-input-image.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-input-image.https.sub.html deleted file mode 100644 index b5537eec60e..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-input-image.https.sub.html +++ /dev/null @@ -1,250 +0,0 @@ - - - - - HTTP headers on request for HTML "input" element with type="button" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-input-image.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-input-image.sub.html deleted file mode 100644 index 6f8743633fa..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-input-image.sub.html +++ /dev/null @@ -1,214 +0,0 @@ - - - - - HTTP headers on request for HTML "input" element with type="button" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-link-icon.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-link-icon.https.sub.html deleted file mode 100644 index b41f0afe4a8..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-link-icon.https.sub.html +++ /dev/null @@ -1,402 +0,0 @@ - - - - - - HTTP headers on request for HTML "link" element with rel="icon" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-link-icon.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-link-icon.sub.html deleted file mode 100644 index ea9ecb4ffe0..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-link-icon.sub.html +++ /dev/null @@ -1,324 +0,0 @@ - - - - - - HTTP headers on request for HTML "link" element with rel="icon" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html deleted file mode 100644 index 16347abc825..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html +++ /dev/null @@ -1,590 +0,0 @@ - - - - - - HTTP headers on request for HTML "link" element with rel="prefetch" - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-link-prefetch.optional.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-link-prefetch.optional.sub.html deleted file mode 100644 index c9386717602..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-link-prefetch.optional.sub.html +++ /dev/null @@ -1,320 +0,0 @@ - - - - - - HTTP headers on request for HTML "link" element with rel="prefetch" - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html deleted file mode 100644 index 7f763d630a3..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html +++ /dev/null @@ -1,300 +0,0 @@ - - - - - HTTP headers on request for HTML "meta" element with http-equiv="refresh" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-meta-refresh.optional.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-meta-refresh.optional.sub.html deleted file mode 100644 index c1f3ecbbf86..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-meta-refresh.optional.sub.html +++ /dev/null @@ -1,261 +0,0 @@ - - - - - HTTP headers on request for HTML "meta" element with http-equiv="refresh" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-picture.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-picture.https.sub.html deleted file mode 100644 index 2de087218ac..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-picture.https.sub.html +++ /dev/null @@ -1,1090 +0,0 @@ - - - - - HTTP headers on request for HTML "picture" element source - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-picture.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-picture.sub.html deleted file mode 100644 index f0e087483d5..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-picture.sub.html +++ /dev/null @@ -1,856 +0,0 @@ - - - - - HTTP headers on request for HTML "picture" element source - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-script.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-script.https.sub.html deleted file mode 100644 index e52b165b747..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-script.https.sub.html +++ /dev/null @@ -1,624 +0,0 @@ - - - - - HTTP headers on request for HTML "script" element source - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-script.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-script.sub.html deleted file mode 100644 index 47ed5fa9f8f..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-script.sub.html +++ /dev/null @@ -1,578 +0,0 @@ - - - - - HTTP headers on request for HTML "script" element source - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-video-poster.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-video-poster.https.sub.html deleted file mode 100644 index ddd870af7c2..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-video-poster.https.sub.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - - HTTP headers on request for HTML "video" element "poster" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-video-poster.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-video-poster.sub.html deleted file mode 100644 index aeb3d4f9b5e..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-video-poster.sub.html +++ /dev/null @@ -1,228 +0,0 @@ - - - - - HTTP headers on request for HTML "video" element "poster" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-video.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-video.https.sub.html deleted file mode 100644 index c900807820c..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-video.https.sub.html +++ /dev/null @@ -1,352 +0,0 @@ - - - - - HTTP headers on request for HTML "video" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/element-video.sub.html b/test/fixtures/wpt/fetch/metadata/generated/element-video.sub.html deleted file mode 100644 index bf357e9da0e..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/element-video.sub.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - HTTP headers on request for HTML "video" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html deleted file mode 100644 index c3b0599b9da..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html +++ /dev/null @@ -1,745 +0,0 @@ - - - - - - HTTP headers on request using the "fetch" API and passing through a Serive Worker - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/fetch.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/fetch.https.sub.html deleted file mode 100644 index 486f404d4bd..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/fetch.https.sub.html +++ /dev/null @@ -1,329 +0,0 @@ - - - - - HTTP headers on request using the "fetch" API - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/fetch.sub.html b/test/fixtures/wpt/fetch/metadata/generated/fetch.sub.html deleted file mode 100644 index 7d9536efcfb..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/fetch.sub.html +++ /dev/null @@ -1,259 +0,0 @@ - - - - - HTTP headers on request using the "fetch" API - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/form-submission.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/form-submission.https.sub.html deleted file mode 100644 index 0935f846111..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/form-submission.https.sub.html +++ /dev/null @@ -1,566 +0,0 @@ - - - - - - HTTP headers on request for HTML form navigation - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/form-submission.sub.html b/test/fixtures/wpt/fetch/metadata/generated/form-submission.sub.html deleted file mode 100644 index b0f56f60942..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/form-submission.sub.html +++ /dev/null @@ -1,466 +0,0 @@ - - - - - - HTTP headers on request for HTML form navigation - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/header-link.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/header-link.https.sub.html deleted file mode 100644 index 8e7d029dc25..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/header-link.https.sub.html +++ /dev/null @@ -1,587 +0,0 @@ - - - - - HTTP headers on request for HTTP "Link" header - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/header-link.https.sub.tentative.html b/test/fixtures/wpt/fetch/metadata/generated/header-link.https.sub.tentative.html deleted file mode 100644 index 307c37fbf77..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/header-link.https.sub.tentative.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - HTTP headers on request for HTTP "Link" header - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/header-link.sub.html b/test/fixtures/wpt/fetch/metadata/generated/header-link.sub.html deleted file mode 100644 index b0823071345..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/header-link.sub.html +++ /dev/null @@ -1,544 +0,0 @@ - - - - - HTTP headers on request for HTTP "Link" header - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/header-refresh.https.optional.sub.html b/test/fixtures/wpt/fetch/metadata/generated/header-refresh.https.optional.sub.html deleted file mode 100644 index 0f7e9e42b79..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/header-refresh.https.optional.sub.html +++ /dev/null @@ -1,297 +0,0 @@ - - - - - - HTTP headers on request for HTTP "Refresh" header - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/header-refresh.optional.sub.html b/test/fixtures/wpt/fetch/metadata/generated/header-refresh.optional.sub.html deleted file mode 100644 index 873013e821c..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/header-refresh.optional.sub.html +++ /dev/null @@ -1,258 +0,0 @@ - - - - - - HTTP headers on request for HTTP "Refresh" header - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/script-json-module-import-static.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/script-json-module-import-static.https.sub.html deleted file mode 100644 index 55c7014b9cb..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/script-json-module-import-static.https.sub.html +++ /dev/null @@ -1,373 +0,0 @@ - - - - - HTTP headers on request for static ECMAScript module import - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/script-json-module-import-static.sub.html b/test/fixtures/wpt/fetch/metadata/generated/script-json-module-import-static.sub.html deleted file mode 100644 index 2b554df255a..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/script-json-module-import-static.sub.html +++ /dev/null @@ -1,378 +0,0 @@ - - - - - HTTP headers on request for static ECMAScript module import - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/script-module-import-dynamic.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/script-module-import-dynamic.https.sub.html deleted file mode 100644 index 94ef41b9228..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/script-module-import-dynamic.https.sub.html +++ /dev/null @@ -1,280 +0,0 @@ - - - - - HTTP headers on request for dynamic ECMAScript module import - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/script-module-import-dynamic.sub.html b/test/fixtures/wpt/fetch/metadata/generated/script-module-import-dynamic.sub.html deleted file mode 100644 index 16aefa11176..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/script-module-import-dynamic.sub.html +++ /dev/null @@ -1,253 +0,0 @@ - - - - - HTTP headers on request for dynamic ECMAScript module import - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/script-module-import-static.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/script-module-import-static.https.sub.html deleted file mode 100644 index 62968f318f5..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/script-module-import-static.https.sub.html +++ /dev/null @@ -1,316 +0,0 @@ - - - - - HTTP headers on request for static ECMAScript module import - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/script-module-import-static.sub.html b/test/fixtures/wpt/fetch/metadata/generated/script-module-import-static.sub.html deleted file mode 100644 index c4da1576606..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/script-module-import-static.sub.html +++ /dev/null @@ -1,288 +0,0 @@ - - - - - HTTP headers on request for static ECMAScript module import - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/serviceworker.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/serviceworker.https.sub.html deleted file mode 100644 index 12e37369a42..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/serviceworker.https.sub.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - HTTP headers on request for Service Workers - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/svg-image.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/svg-image.https.sub.html deleted file mode 100644 index 9dd0da15f9d..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/svg-image.https.sub.html +++ /dev/null @@ -1,396 +0,0 @@ - - - - - - HTTP headers on request for SVG "image" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/svg-image.sub.html b/test/fixtures/wpt/fetch/metadata/generated/svg-image.sub.html deleted file mode 100644 index 56427bedaa6..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/svg-image.sub.html +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - HTTP headers on request for SVG "image" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/window-history.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/window-history.https.sub.html deleted file mode 100644 index 64e282e7789..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/window-history.https.sub.html +++ /dev/null @@ -1,281 +0,0 @@ - - - - - HTTP headers on request for navigation via the HTML History API - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/window-history.sub.html b/test/fixtures/wpt/fetch/metadata/generated/window-history.sub.html deleted file mode 100644 index 83670496477..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/window-history.sub.html +++ /dev/null @@ -1,426 +0,0 @@ - - - - - - HTTP headers on request for navigation via the HTML History API - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/window-location.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/window-location.https.sub.html deleted file mode 100644 index 387039bcf25..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/window-location.https.sub.html +++ /dev/null @@ -1,1296 +0,0 @@ - - - - - - HTTP headers on request for navigation via the HTML Location API - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/window-location.sub.html b/test/fixtures/wpt/fetch/metadata/generated/window-location.sub.html deleted file mode 100644 index e307b909dce..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/window-location.sub.html +++ /dev/null @@ -1,1062 +0,0 @@ - - - - - - HTTP headers on request for navigation via the HTML Location API - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html deleted file mode 100644 index 86f17607554..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - HTTP headers on request for dedicated worker via the "Worker" constructor - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-constructor.sub.html b/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-constructor.sub.html deleted file mode 100644 index e05e12df395..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-constructor.sub.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - HTTP headers on request for dedicated worker via the "Worker" constructor - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html b/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html deleted file mode 100644 index 5b48e59e716..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html +++ /dev/null @@ -1,295 +0,0 @@ - - - - - HTTP headers on request for dedicated worker via the "importScripts" API - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-importscripts.sub.html b/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-importscripts.sub.html deleted file mode 100644 index ad4792b8a60..00000000000 --- a/test/fixtures/wpt/fetch/metadata/generated/worker-dedicated-importscripts.sub.html +++ /dev/null @@ -1,267 +0,0 @@ - - - - - HTTP headers on request for dedicated worker via the "importScripts" API - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/navigation.https.sub.html b/test/fixtures/wpt/fetch/metadata/navigation.https.sub.html deleted file mode 100644 index 32c9cf77f90..00000000000 --- a/test/fixtures/wpt/fetch/metadata/navigation.https.sub.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/object.https.sub.html b/test/fixtures/wpt/fetch/metadata/object.https.sub.html deleted file mode 100644 index fae5b37b592..00000000000 --- a/test/fixtures/wpt/fetch/metadata/object.https.sub.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/paint-worklet.https.html b/test/fixtures/wpt/fetch/metadata/paint-worklet.https.html deleted file mode 100644 index 49fc7765f62..00000000000 --- a/test/fixtures/wpt/fetch/metadata/paint-worklet.https.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/preload.https.sub.html b/test/fixtures/wpt/fetch/metadata/preload.https.sub.html deleted file mode 100644 index 29042a85474..00000000000 --- a/test/fixtures/wpt/fetch/metadata/preload.https.sub.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html deleted file mode 100644 index 0f8f3200165..00000000000 --- a/test/fixtures/wpt/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade.sub.html deleted file mode 100644 index fa765b66d03..00000000000 --- a/test/fixtures/wpt/fetch/metadata/redirect/redirect-http-upgrade.sub.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade.sub.html b/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade.sub.html deleted file mode 100644 index 4e5a48e6f6e..00000000000 --- a/test/fixtures/wpt/fetch/metadata/redirect/redirect-https-downgrade.sub.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/report.https.sub.html b/test/fixtures/wpt/fetch/metadata/report.https.sub.html deleted file mode 100644 index b65f7c0a244..00000000000 --- a/test/fixtures/wpt/fetch/metadata/report.https.sub.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/report.https.sub.html.sub.headers b/test/fixtures/wpt/fetch/metadata/report.https.sub.html.sub.headers deleted file mode 100644 index 1ec5df78f30..00000000000 --- a/test/fixtures/wpt/fetch/metadata/report.https.sub.html.sub.headers +++ /dev/null @@ -1,3 +0,0 @@ -Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri /fetch/metadata/resources/record-header.py?file=report-same-origin -Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-same-site -Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-cross-site diff --git a/test/fixtures/wpt/fetch/metadata/resources/appcache-iframe.sub.html b/test/fixtures/wpt/fetch/metadata/resources/appcache-iframe.sub.html deleted file mode 100644 index cea9a4feaec..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/appcache-iframe.sub.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/test/fixtures/wpt/fetch/metadata/resources/dedicatedWorker.js b/test/fixtures/wpt/fetch/metadata/resources/dedicatedWorker.js deleted file mode 100644 index 18626d3d845..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/dedicatedWorker.js +++ /dev/null @@ -1 +0,0 @@ -self.postMessage("Loaded"); diff --git a/test/fixtures/wpt/fetch/metadata/resources/echo-as-json.py b/test/fixtures/wpt/fetch/metadata/resources/echo-as-json.py deleted file mode 100644 index 44f68e8fe9e..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/echo-as-json.py +++ /dev/null @@ -1,29 +0,0 @@ -import json - -from wptserve.utils import isomorphic_decode - -def main(request, response): - headers = [(b"Content-Type", b"application/json"), - (b"Access-Control-Allow-Credentials", b"true")] - - if b"origin" in request.headers: - headers.append((b"Access-Control-Allow-Origin", request.headers[b"origin"])) - - body = u"" - - # If we're in a preflight, verify that `Sec-Fetch-Mode` is `cors`. - if request.method == u'OPTIONS': - if request.headers.get(b"sec-fetch-mode") != b"cors": - return (403, b"Failed"), [], body - - headers.append((b"Access-Control-Allow-Methods", b"*")) - headers.append((b"Access-Control-Allow-Headers", b"*")) - else: - body = json.dumps({ - u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), - u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), - u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), - u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), - }) - - return headers, body diff --git a/test/fixtures/wpt/fetch/metadata/resources/echo-as-script.py b/test/fixtures/wpt/fetch/metadata/resources/echo-as-script.py deleted file mode 100644 index 1e7bc91184f..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/echo-as-script.py +++ /dev/null @@ -1,14 +0,0 @@ -import json - -from wptserve.utils import isomorphic_decode - -def main(request, response): - headers = [(b"Content-Type", b"text/javascript")] - body = u"var header = %s;" % json.dumps({ - u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), - u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), - u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), - u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), - }) - - return headers, body diff --git a/test/fixtures/wpt/fetch/metadata/resources/es-json-module.sub.js b/test/fixtures/wpt/fetch/metadata/resources/es-json-module.sub.js deleted file mode 100644 index df5d44cb890..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/es-json-module.sub.js +++ /dev/null @@ -1 +0,0 @@ -import '{{GET[moduleId]}}' with { type: 'json' }; diff --git a/test/fixtures/wpt/fetch/metadata/resources/es-module.sub.js b/test/fixtures/wpt/fetch/metadata/resources/es-module.sub.js deleted file mode 100644 index f9668a3dc67..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/es-module.sub.js +++ /dev/null @@ -1 +0,0 @@ -import '{{GET[moduleId]}}'; diff --git a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js deleted file mode 100644 index 09858b2663f..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js +++ /dev/null @@ -1,3 +0,0 @@ -self.addEventListener('fetch', function(event) { - // Empty event handler - will fallback to the network. -}); diff --git a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js deleted file mode 100644 index 8bf8d8f2217..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js +++ /dev/null @@ -1,3 +0,0 @@ -self.addEventListener('fetch', function(event) { - event.respondWith(fetch(event.request)); -}); diff --git a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker-frame.html b/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker-frame.html deleted file mode 100644 index 98798025005..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/fetch-via-serviceworker-frame.html +++ /dev/null @@ -1,3 +0,0 @@ - - -Page Title diff --git a/test/fixtures/wpt/fetch/metadata/resources/header-link.py b/test/fixtures/wpt/fetch/metadata/resources/header-link.py deleted file mode 100644 index de891163a33..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/header-link.py +++ /dev/null @@ -1,15 +0,0 @@ -def main(request, response): - """ - Respond with a blank HTML document and a `Link` header which describes - a link relation specified by the requests `location` and `rel` query string - parameters - """ - headers = [ - (b'Content-Type', b'text/html'), - ( - b'Link', - b'<' + request.GET.first(b'location') + b'>; rel=' + request.GET.first(b'rel') - ) - ] - return (200, headers, b'') - diff --git a/test/fixtures/wpt/fetch/metadata/resources/helper.js b/test/fixtures/wpt/fetch/metadata/resources/helper.js deleted file mode 100644 index 725f9a7e43b..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/helper.js +++ /dev/null @@ -1,42 +0,0 @@ -function validate_expectations(key, expected, tag) { - return fetch("/fetch/metadata/resources/record-header.py?retrieve=true&file=" + key) - .then(response => response.text()) - .then(text => { - assert_not_equals(text, "No header has been recorded"); - let value = JSON.parse(text); - test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); - test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); - test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); - test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); - }); -} - -function validate_expectations_custom_url(url, header, expected, tag) { - return fetch(url, header) - .then(response => response.text()) - .then(text => { - assert_not_equals(text, "No header has been recorded"); - let value = JSON.parse(text); - test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); - test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); - test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); - test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); - }); -} - -/** - * @param {object} value - * @param {object} expected - * @param {string} tag - **/ -function assert_header_equals(value, expected, tag) { - if (typeof(value) === "string"){ - assert_not_equals(value, "No header has been recorded"); - value = JSON.parse(value); - } - - test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); - test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); - test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); - test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); -} diff --git a/test/fixtures/wpt/fetch/metadata/resources/helper.sub.js b/test/fixtures/wpt/fetch/metadata/resources/helper.sub.js deleted file mode 100644 index fd179fe6f25..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/helper.sub.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -/** - * Construct a URL which, when followed, will trigger redirection through zero - * or more specified origins and ultimately resolve in the Python handler - * `record-headers.py`. - * - * @param {string} key - the WPT server "stash" name where the request's - * headers should be stored - * @param {string[]} [origins] - zero or more origin names through which the - * request should pass; see the function - * implementation for a completel list of names - * and corresponding origins; If specified, the - * final origin will be used to access the - * `record-headers.py` hander. - * @param {object} [params] - a collection of key-value pairs to include as - * URL "search" parameters in the final request to - * `record-headers.py` - * - * @returns {string} an absolute URL - */ -function makeRequestURL(key, origins, params) { - const byName = { - httpOrigin: 'http://{{host}}:{{ports[http][0]}}', - httpSameSite: 'http://{{hosts[][www]}}:{{ports[http][0]}}', - httpCrossSite: 'http://{{hosts[alt][]}}:{{ports[http][0]}}', - httpsOrigin: 'https://{{host}}:{{ports[https][0]}}', - httpsSameSite: 'https://{{hosts[][www]}}:{{ports[https][0]}}', - httpsCrossSite: 'https://{{hosts[alt][]}}:{{ports[https][0]}}' - }; - const redirectPath = '/fetch/api/resources/redirect.py?location='; - const path = '/fetch/metadata/resources/record-headers.py?key=' + key; - - let requestUrl = path; - if (params) { - requestUrl += '&' + new URLSearchParams(params).toString(); - } - - if (origins && origins.length) { - requestUrl = byName[origins.pop()] + requestUrl; - - while (origins.length) { - requestUrl = byName[origins.pop()] + redirectPath + - encodeURIComponent(requestUrl); - } - } else { - requestUrl = byName.httpsOrigin + requestUrl; - } - - return requestUrl; -} - -function retrieve(key, options) { - return fetch('/fetch/metadata/resources/record-headers.py?retrieve&key=' + key) - .then((response) => { - if (response.status === 204 && options && options.poll) { - return new Promise((resolve) => setTimeout(resolve, 300)) - .then(() => retrieve(key, options)); - } - - if (response.status !== 200) { - throw new Error('Failed to query for recorded headers.'); - } - - return response.text().then((text) => JSON.parse(text)); - }); -} diff --git a/test/fixtures/wpt/fetch/metadata/resources/message-opener.html b/test/fixtures/wpt/fetch/metadata/resources/message-opener.html deleted file mode 100644 index eb2af7b250b..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/message-opener.html +++ /dev/null @@ -1,17 +0,0 @@ - diff --git a/test/fixtures/wpt/fetch/metadata/resources/post-to-owner.py b/test/fixtures/wpt/fetch/metadata/resources/post-to-owner.py deleted file mode 100644 index 2d4896867bb..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/post-to-owner.py +++ /dev/null @@ -1,26 +0,0 @@ -import json - -from wptserve.utils import isomorphic_decode - -def main(request, response): - headers = [ - (b"Content-Type", b"text/html"), - (b"Cache-Control", b"no-cache, no-store, must-revalidate") - ] - - body = u""" - - - """ % (json.dumps({ - u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), - u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), - u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), - u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), - })) - return headers, body diff --git a/test/fixtures/wpt/fetch/metadata/resources/record-header.py b/test/fixtures/wpt/fetch/metadata/resources/record-header.py deleted file mode 100644 index 29ff2ed7985..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/record-header.py +++ /dev/null @@ -1,145 +0,0 @@ -import os -import hashlib -import json - -from wptserve.utils import isomorphic_decode - -def main(request, response): - ## Get the query parameter (key) from URL ## - ## Tests will record POST requests (CSP Report) and GET (rest) ## - if request.GET: - key = request.GET[b'file'] - elif request.POST: - key = request.POST[b'file'] - - ## Convert the key from String to UUID valid String ## - testId = hashlib.md5(key).hexdigest() - - ## Handle the header retrieval request ## - if b'retrieve' in request.GET: - response.writer.write_status(200) - response.writer.write_header(b"Connection", b"close") - response.writer.end_headers() - try: - header_value = request.server.stash.take(testId) - response.writer.write(header_value) - except (KeyError, ValueError) as e: - response.writer.write(u"No header has been recorded") - pass - - response.close_connection = True - - ## Record incoming fetch metadata header value - else: - try: - ## Return a serialized JSON object with one member per header. If the ## - ## header isn't present, the member will contain an empty string. ## - header = json.dumps({ - u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), - u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), - u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), - u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), - }) - request.server.stash.put(testId, header) - except KeyError: - ## The header is already recorded or it doesn't exist - pass - - ## Prevent the browser from caching returned responses and allow CORS ## - response.headers.set(b"Access-Control-Allow-Origin", b"*") - response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate") - response.headers.set(b"Pragma", b"no-cache") - response.headers.set(b"Expires", b"0") - - ## Add a valid ServiceWorker Content-Type ## - if key.startswith(b"serviceworker"): - response.headers.set(b"Content-Type", b"application/javascript") - - ## Add a valid image Content-Type ## - if key.startswith(b"image"): - response.headers.set(b"Content-Type", b"image/png") - file = open(os.path.join(request.doc_root, u"media", u"1x1-green.png"), u"rb") - image = file.read() - file.close() - return image - - ## Return a valid .vtt content for the tag ## - if key.startswith(b"track"): - return b"WEBVTT" - - ## Return a valid SharedWorker ## - if key.startswith(b"sharedworker"): - response.headers.set(b"Content-Type", b"application/javascript") - file = open(os.path.join(request.doc_root, u"fetch", u"metadata", - u"resources", u"sharedWorker.js"), u"rb") - shared_worker = file.read() - file.close() - return shared_worker - - ## Return a valid font content and Content-Type ## - if key.startswith(b"font"): - response.headers.set(b"Content-Type", b"application/x-font-ttf") - file = open(os.path.join(request.doc_root, u"fonts", u"Ahem.ttf"), u"rb") - font = file.read() - file.close() - return font - - ## Return a valid audio content and Content-Type ## - if key.startswith(b"audio"): - response.headers.set(b"Content-Type", b"audio/mpeg") - file = open(os.path.join(request.doc_root, u"media", u"sound_5.mp3"), u"rb") - audio = file.read() - file.close() - return audio - - ## Return a valid video content and Content-Type ## - if key.startswith(b"video"): - response.headers.set(b"Content-Type", b"video/mp4") - file = open(os.path.join(request.doc_root, u"media", u"A4.mp4"), u"rb") - video = file.read() - file.close() - return video - - ## Return valid style content and Content-Type ## - if key.startswith(b"style"): - response.headers.set(b"Content-Type", b"text/css") - return b"div { }" - - ## Return a valid embed/object content and Content-Type ## - if key.startswith(b"embed") or key.startswith(b"object"): - response.headers.set(b"Content-Type", b"text/html") - return b"EMBED!" - - ## Return a valid image content and Content-Type for redirect requests ## - if key.startswith(b"redirect"): - response.headers.set(b"Content-Type", b"image/jpeg") - file = open(os.path.join(request.doc_root, u"media", u"1x1-green.png"), u"rb") - image = file.read() - file.close() - return image - - ## Return a valid dedicated worker - if key.startswith(b"worker"): - response.headers.set(b"Content-Type", b"application/javascript") - return b"self.postMessage('loaded');" - - ## Return a valid worklet - if key.startswith(b"worklet"): - response.headers.set(b"Content-Type", b"application/javascript") - return b"" - - ## Return a valid XSLT - if key.startswith(b"xslt"): - response.headers.set(b"Content-Type", b"text/xsl") - return b""" - - - - - - -""" - - if key.startswith(b"script"): - response.headers.set(b"Content-Type", b"application/javascript") - return b"void 0;" diff --git a/test/fixtures/wpt/fetch/metadata/resources/record-headers.py b/test/fixtures/wpt/fetch/metadata/resources/record-headers.py deleted file mode 100644 index 0362fe228c2..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/record-headers.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -import uuid -import hashlib -import time -import json - - -def bytes_to_strings(d): - # Recursively convert bytes to strings in `d`. - if not isinstance(d, dict): - if isinstance(d, (tuple,list,set)): - v = [bytes_to_strings(x) for x in d] - return v - else: - if isinstance(d, bytes): - d = d.decode() - return d - - result = {} - for k,v in d.items(): - if isinstance(k, bytes): - k = k.decode() - if isinstance(v, dict): - v = bytes_to_strings(v) - elif isinstance(v, (tuple,list,set)): - v = [bytes_to_strings(x) for x in v] - elif isinstance(v, bytes): - v = v.decode() - result[k] = v - return result - - -def main(request, response): - # This condition avoids false positives from CORS preflight checks, where the - # request under test may be followed immediately by a request to the same URL - # using a different HTTP method. - if b'requireOPTIONS' in request.GET and request.method != b'OPTIONS': - return - - if b'key' in request.GET: - key = request.GET[b'key'] - elif b'key' in request.POST: - key = request.POST[b'key'] - - ## Convert the key from String to UUID valid String ## - testId = hashlib.md5(key).hexdigest() - - ## Handle the header retrieval request ## - if b'retrieve' in request.GET: - recorded_headers = request.server.stash.take(testId) - - if recorded_headers is None: - return (204, [], b'') - - return (200, [], recorded_headers) - - ## Record incoming fetch metadata header value - else: - try: - request.server.stash.put(testId, json.dumps(bytes_to_strings(request.headers))) - except KeyError: - ## The header is already recorded or it doesn't exist - pass - - ## Prevent the browser from caching returned responses and allow CORS ## - response.headers.set(b"Access-Control-Allow-Origin", b"*") - response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate") - response.headers.set(b"Pragma", b"no-cache") - response.headers.set(b"Expires", b"0") - if b"mime" in request.GET: - response.headers.set(b"Content-Type", request.GET.first(b"mime")) - - return request.GET.first(b"body", request.POST.first(b"body", b"")) diff --git a/test/fixtures/wpt/fetch/metadata/resources/redirectTestHelper.sub.js b/test/fixtures/wpt/fetch/metadata/resources/redirectTestHelper.sub.js deleted file mode 100644 index 1bfbbae70c2..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/redirectTestHelper.sub.js +++ /dev/null @@ -1,167 +0,0 @@ -function createVideoElement() { - let el = document.createElement('video'); - el.src = '/media/movie_5.mp4'; - el.setAttribute('controls', ''); - el.setAttribute('crossorigin', ''); - return el; -} - -function createTrack() { - let el = document.createElement('track'); - el.setAttribute('default', ''); - el.setAttribute('kind', 'captions'); - el.setAttribute('srclang', 'en'); - return el; -} - -let secureRedirectURL = 'https://{{host}}:{{ports[https][0]}}/fetch/api/resources/redirect.py?location='; -let insecureRedirectURL = 'http://{{host}}:{{ports[http][0]}}/fetch/api/resources/redirect.py?location='; -let secureTestURL = 'https://{{host}}:{{ports[https][0]}}/fetch/metadata/'; -let insecureTestURL = 'http://{{host}}:{{ports[http][0]}}/fetch/metadata/'; - -// Helper to craft an URL that will go from HTTPS => HTTP => HTTPS to -// simulate us downgrading then upgrading again during the same redirect chain. -function MultipleRedirectTo(partialPath) { - let finalURL = insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath); - return secureRedirectURL + encodeURIComponent(finalURL); -} - -// Helper to craft an URL that will go from HTTP => HTTPS to simulate upgrading a -// given request. -function upgradeRedirectTo(partialPath) { - return insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath); -} - -// Helper to craft an URL that will go from HTTPS => HTTP to simulate downgrading a -// given request. -function downgradeRedirectTo(partialPath) { - return secureRedirectURL + encodeURIComponent(insecureTestURL + partialPath); -} - -// Helper to run common redirect test cases that don't require special setup on -// the test page itself. -function RunCommonRedirectTests(testNamePrefix, urlHelperMethod, expectedResults) { - async_test(t => { - let testWindow = window.open(urlHelperMethod('resources/post-to-owner.py?top-level-navigation' + nonce)); - t.add_cleanup(_ => testWindow.close()); - window.addEventListener('message', t.step_func(e => { - if (e.source != testWindow) { - return; - } - - let expectation = { ...expectedResults }; - if (expectation['mode'] != '') - expectation['mode'] = 'navigate'; - if (expectation['dest'] == 'font') - expectation['dest'] = 'document'; - assert_header_equals(e.data, expectation, testNamePrefix + ' top level navigation'); - t.done(); - })); - }, testNamePrefix + ' top level navigation'); - - promise_test(t => { - return new Promise((resolve, reject) => { - let key = 'embed-https-redirect' + nonce; - let e = document.createElement('embed'); - e.src = urlHelperMethod('resources/record-header.py?file=' + key); - e.onload = e => { - let expectation = { ...expectedResults }; - if (expectation['mode'] != '') - expectation['mode'] = 'navigate'; - if (expectation['dest'] == 'font') - expectation['dest'] = 'embed'; - fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) - .then(response => response.text()) - .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' embed'))) - .then(resolve) - .catch(e => reject(e)); - }; - document.body.appendChild(e); - }); - }, testNamePrefix + ' embed'); - - promise_test(t => { - return new Promise((resolve, reject) => { - let key = 'object-https-redirect' + nonce; - let e = document.createElement('object'); - e.data = urlHelperMethod('resources/record-header.py?file=' + key); - e.onload = e => { - let expectation = { ...expectedResults }; - if (expectation['mode'] != '') - expectation['mode'] = 'navigate'; - if (expectation['dest'] == 'font') - expectation['dest'] = 'object'; - fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) - .then(response => response.text()) - .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' object'))) - .then(resolve) - .catch(e => reject(e)); - }; - document.body.appendChild(e); - }); - }, testNamePrefix + ' object'); - - if (document.createElement('link').relList.supports('preload')) { - async_test(t => { - let key = 'preload' + nonce; - let e = document.createElement('link'); - e.rel = 'preload'; - e.href = urlHelperMethod('resources/record-header.py?file=' + key); - e.setAttribute('as', 'track'); - e.onload = e.onerror = t.step_func_done(e => { - let expectation = { ...expectedResults }; - if (expectation['mode'] != '') - expectation['mode'] = 'cors'; - fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) - .then(t.step_func(response => response.text())) - .then(t.step_func_done(text => assert_header_equals(text, expectation, testNamePrefix + ' preload'))) - .catch(t.unreached_func()); - }); - document.head.appendChild(e); - }, testNamePrefix + ' preload'); - } - - promise_test(t => { - return new Promise((resolve, reject) => { - let key = 'style-https-redirect' + nonce; - let e = document.createElement('link'); - e.rel = 'stylesheet'; - e.href = urlHelperMethod('resources/record-header.py?file=' + key); - e.onload = e => { - let expectation = { ...expectedResults }; - if (expectation['mode'] != '') - expectation['mode'] = 'no-cors'; - if (expectation['dest'] == 'font') - expectation['dest'] = 'style'; - fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) - .then(response => response.text()) - .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' stylesheet'))) - .then(resolve) - .catch(e => reject(e)); - }; - document.body.appendChild(e); - }); - }, testNamePrefix + ' stylesheet'); - - promise_test(t => { - return new Promise((resolve, reject) => { - let key = 'track-https-redirect' + nonce; - let video = createVideoElement(); - let el = createTrack(); - el.src = urlHelperMethod('resources/record-header.py?file=' + key); - el.onload = t.step_func(_ => { - let expectation = { ...expectedResults }; - if (expectation['mode'] != '') - expectation['mode'] = 'cors'; - if (expectation['dest'] == 'font') - expectation['dest'] = 'track'; - fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) - .then(response => response.text()) - .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' track'))) - .then(resolve); - }); - video.appendChild(el); - document.body.appendChild(video); - }); - }, testNamePrefix + ' track'); -} diff --git a/test/fixtures/wpt/fetch/metadata/resources/serviceworker-accessors-frame.html b/test/fixtures/wpt/fetch/metadata/resources/serviceworker-accessors-frame.html deleted file mode 100644 index 98798025005..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/serviceworker-accessors-frame.html +++ /dev/null @@ -1,3 +0,0 @@ - - -Page Title diff --git a/test/fixtures/wpt/fetch/metadata/resources/serviceworker-accessors.sw.js b/test/fixtures/wpt/fetch/metadata/resources/serviceworker-accessors.sw.js deleted file mode 100644 index 36c55a77860..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/serviceworker-accessors.sw.js +++ /dev/null @@ -1,14 +0,0 @@ -addEventListener("fetch", event => { - event.waitUntil(async function () { - if (!event.clientId) return; - const client = await clients.get(event.clientId); - if (!client) return; - - client.postMessage({ - "dest": event.request.headers.get("sec-fetch-dest"), - "mode": event.request.headers.get("sec-fetch-mode"), - "site": event.request.headers.get("sec-fetch-site"), - "user": event.request.headers.get("sec-fetch-user") - }); - }()); -}); diff --git a/test/fixtures/wpt/fetch/metadata/resources/sharedWorker.js b/test/fixtures/wpt/fetch/metadata/resources/sharedWorker.js deleted file mode 100644 index 5eb89cb4f68..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/sharedWorker.js +++ /dev/null @@ -1,9 +0,0 @@ -onconnect = function(e) { - var port = e.ports[0]; - - port.addEventListener('message', function(e) { - port.postMessage("Ready"); - }); - - port.start(); -} diff --git a/test/fixtures/wpt/fetch/metadata/resources/unload-with-beacon.html b/test/fixtures/wpt/fetch/metadata/resources/unload-with-beacon.html deleted file mode 100644 index b00c9a5776a..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/unload-with-beacon.html +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/test/fixtures/wpt/fetch/metadata/resources/xslt-test.sub.xml b/test/fixtures/wpt/fetch/metadata/resources/xslt-test.sub.xml deleted file mode 100644 index acb478ab641..00000000000 --- a/test/fixtures/wpt/fetch/metadata/resources/xslt-test.sub.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/serviceworker-accessors.https.sub.html b/test/fixtures/wpt/fetch/metadata/serviceworker-accessors.https.sub.html deleted file mode 100644 index 03a8321d4ca..00000000000 --- a/test/fixtures/wpt/fetch/metadata/serviceworker-accessors.https.sub.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/sharedworker.https.sub.html b/test/fixtures/wpt/fetch/metadata/sharedworker.https.sub.html deleted file mode 100644 index 4df858208a7..00000000000 --- a/test/fixtures/wpt/fetch/metadata/sharedworker.https.sub.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/style.https.sub.html b/test/fixtures/wpt/fetch/metadata/style.https.sub.html deleted file mode 100644 index a30d81d70dd..00000000000 --- a/test/fixtures/wpt/fetch/metadata/style.https.sub.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/README.md b/test/fixtures/wpt/fetch/metadata/tools/README.md deleted file mode 100644 index 1c3bac2be5b..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# Fetch Metadata test generation framework - -This directory defines a command-line tool for procedurally generating WPT -tests. - -## Motivation - -Many features of the web platform involve the browser making one or more HTTP -requests to remote servers. Only some aspects of these requests are specified -within the standard that defines the relevant feature. Other aspects are -specified by external standards which span the entire platform (e.g. [Fetch -Metadata Request Headers](https://w3c.github.io/webappsec-fetch-metadata/)). - -This state of affairs makes it difficult to maintain test coverage for two -reasons: - -- When a new feature introduces a new kind of web request, it must be verified - to integrate with every cross-cutting standard. -- When a new cross-cutting standard is introduced, it must be verified to - integrate with every kind of web request. - -The tool in this directory attempts to reduce this tension. It allows -maintainers to express instructions for making web requests in an abstract -sense. These generic instructions can be reused by to produce a different suite -of tests for each cross-cutting feature. - -When a new kind of request is proposed, a single generic template can be -defined here. This will provide the maintainers of all cross-cutting features -with clear instruction on how to extend their test suite with the new feature. - -Similarly, when a new cross-cutting feature is proposed, the authors can use -this tool to build a test suite which spans the entire platform. - -## Build script - -To generate the Fetch Metadata tests, run `./wpt update-built --include fetch` -in the root of the repository. - -## Configuration - -The test generation tool requires a YAML-formatted configuration file as its -input. The file should define a dictionary with the following keys: - -- `templates` - a string describing the filesystem path from which template - files should be loaded -- `output_directory` - a string describing the filesystem path where the - generated test files should be written -- `cases` - a list of dictionaries describing how the test templates should be - expanded with individual subtests; each dictionary should have the following - keys: - - `all_subtests` - properties which should be defined for every expansion - - `common_axis` - a list of dictionaries - - `template_axes` - a dictionary relating template names to properties that - should be used when expanding that particular template - -Internally, the tool creates a set of "subtests" for each template. This set is -the Cartesian product of the `common_axis` and the given template's entry in -the `template_axes` dictionary. It uses this set of subtests to expand the -template, creating an output file. Refer to the next section for a concrete -example of how the expansion is performed. - -In general, the tool will output a single file for each template. However, the -`filename_flags` attribute has special semantics. It is used to separate -subtests for the same template file. This is intended to accommodate [the -web-platform-test's filename-based -conventions](https://web-platform-tests.org/writing-tests/file-names.html). - -For instance, when `.https` is present in a test file's name, the WPT test -harness will load that test using the HTTPS protocol. Subtests which include -the value `https` in the `filename_flags` property will be expanded using the -appropriate template but written to a distinct file whose name includes -`.https`. - -The generation tool requires that the configuration file references every -template in the `templates` directory. Because templates and configuration -files may be contributed by different people, this requirement ensures that -configuration authors are aware of all available templates. Some templates may -not be relevant for some features; in those cases, the configuration file can -include an empty array for the template's entry in the `template_axes` -dictionary (as in `template3.html` in the example which follows). - -## Expansion example - -In the following example configuration file, `a`, `b`, `s`, `w`, `x`, `y`, and -`z` all represent associative arrays. - -```yaml -templates: path/to/templates -output_directory: path/to/output -cases: - - every_subtest: s - common_axis: [a, b] - template_axes: - template1.html: [w] - template2.html: [x, y, z] - template3.html: [] -``` - -When run with such a configuration file, the tool would generate two files, -expanded with data as described below (where `(a, b)` represents the union of -`a` and `b`): - - template1.html: [(a, w), (b, w)] - template2.html: [(a, x), (b, x), (a, y), (b, y), (a, z), (b, z)] - template3.html: (zero tests; not expanded) - -## Design Considerations - -**Efficiency of generated output** The tool is capable of generating a large -number of tests given a small amount of input. Naively structured, this could -result in test suites which take large amount of time and computational -resources to complete. The tool has been designed to help authors structure the -generated output to reduce these resource requirements. - -**Literalness of generated output** Because the generated output is how most -people will interact with the tests, it is important that it be approachable. -This tool avoids outputting abstractions which would frustrate attempts to read -the source code or step through its execution environment. - -**Simplicity** The test generation logic itself was written to be approachable. -This makes it easier to anticipate how the tool will behave with new input, and -it lowers the bar for others to contribute improvements. - -Non-goals include conciseness of template files (verbosity makes the potential -expansions more predictable) and conciseness of generated output (verbosity -aids in the interpretation of results). diff --git a/test/fixtures/wpt/fetch/metadata/tools/fetch-metadata.conf.yml b/test/fixtures/wpt/fetch/metadata/tools/fetch-metadata.conf.yml deleted file mode 100644 index 425a4e1c86f..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/fetch-metadata.conf.yml +++ /dev/null @@ -1,943 +0,0 @@ ---- -templates: templates -output_directory: ../generated -cases: - - all_subtests: - expected: NULL - filename_flags: [] - common_axis: - - headerName: sec-fetch-site - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-site - origins: [httpSameSite] - description: Not sent to non-trustworthy same-site destination - - headerName: sec-fetch-site - origins: [httpCrossSite] - description: Not sent to non-trustworthy cross-site destination - - headerName: sec-fetch-mode - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-mode - origins: [httpSameSite] - description: Not sent to non-trustworthy same-site destination - - headerName: sec-fetch-mode - origins: [httpCrossSite] - description: Not sent to non-trustworthy cross-site destination - - headerName: sec-fetch-dest - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-dest - origins: [httpSameSite] - description: Not sent to non-trustworthy same-site destination - - headerName: sec-fetch-dest - origins: [httpCrossSite] - description: Not sent to non-trustworthy cross-site destination - - headerName: sec-fetch-user - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-user - origins: [httpSameSite] - description: Not sent to non-trustworthy same-site destination - - headerName: sec-fetch-user - origins: [httpCrossSite] - description: Not sent to non-trustworthy cross-site destination - - headerName: sec-fetch-storage-access - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-storage-access - origins: [httpSameSite] - description: Not sent to non-trustworthy same-site destination - - headerName: sec-fetch-storage-access - origins: [httpCrossSite] - description: Not sent to non-trustworthy cross-site destination - template_axes: - # The `AudioWorklet` interface is only available in secure contexts - # https://webaudio.github.io/web-audio-api/#AudioWorklet - audioworklet.https.sub.html: [] - # Service workers are only available in secure context - fetch-via-serviceworker.https.sub.html: [] - # Service workers are only available in secure context - serviceworker.https.sub.html: [] - - css-images.sub.html: - - filename_flags: [tentative] - css-font-face.sub.html: - - filename_flags: [tentative] - element-a.sub.html: [{}] - element-area.sub.html: [{}] - element-audio.sub.html: [{}] - element-embed.sub.html: [{}] - element-frame.sub.html: [{}] - element-iframe.sub.html: [{}] - element-img.sub.html: - - sourceAttr: src - - sourceAttr: srcset - element-img-environment-change.sub.html: [{}] - element-input-image.sub.html: [{}] - element-link-icon.sub.html: [{}] - element-link-prefetch.optional.sub.html: [{}] - element-meta-refresh.optional.sub.html: [{}] - element-picture.sub.html: [{}] - element-script.sub.html: - - {} - - elementAttrs: { type: module } - element-video.sub.html: [{}] - element-video-poster.sub.html: [{}] - fetch.sub.html: [{}] - form-submission.sub.html: - - method: GET - - method: POST - header-link.sub.html: - - rel: icon - - rel: stylesheet - header-refresh.optional.sub.html: [{}] - window-location.sub.html: [{}] - script-module-import-dynamic.sub.html: [{}] - script-module-import-static.sub.html: [{}] - script-json-module-import-static.sub.html: [{}] - svg-image.sub.html: [{}] - window-history.sub.html: [{}] - worker-dedicated-importscripts.sub.html: [{}] - # `new Worker()` only makes same-origin requests, therefore we split it - # out into the next block. - worker-dedicated-constructor.sub.html: [] - - - all_subtests: - expected: NULL - filename_flags: [] - common_axis: - - headerName: sec-fetch-site - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-mode - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-dest - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-user - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - - headerName: sec-fetch-storage-access - origins: [httpOrigin] - description: Not sent to non-trustworthy same-origin destination - template_axes: - # All the templates in this block are unused with the exception of - # `worker-dedicated-constructor` - audioworklet.https.sub.html: [] - fetch-via-serviceworker.https.sub.html: [] - serviceworker.https.sub.html: [] - css-images.sub.html: [] - css-font-face.sub.html: [] - element-a.sub.html: [] - element-area.sub.html: [] - element-audio.sub.html: [] - element-embed.sub.html: [] - element-frame.sub.html: [] - element-iframe.sub.html: [] - element-img.sub.html: [] - element-img-environment-change.sub.html: [] - element-input-image.sub.html: [] - element-link-icon.sub.html: [] - element-link-prefetch.optional.sub.html: [] - element-meta-refresh.optional.sub.html: [] - element-picture.sub.html: [] - element-script.sub.html: [] - element-video.sub.html: [] - element-video-poster.sub.html: [] - fetch.sub.html: [] - form-submission.sub.html: [] - header-link.sub.html: [] - header-refresh.optional.sub.html: [] - window-location.sub.html: [] - script-module-import-dynamic.sub.html: [] - script-module-import-static.sub.html: [] - script-json-module-import-static.sub.html: [] - svg-image.sub.html: [] - window-history.sub.html: [] - worker-dedicated-importscripts.sub.html: [] - # `new Worker()` only makes same-origin requests, so we populate its - # generated tests here. - worker-dedicated-constructor.sub.html: [{}] - - # Sec-Fetch-Site - direct requests - - all_subtests: - headerName: sec-fetch-site - filename_flags: [https] - common_axis: - - description: Same origin - origins: [httpsOrigin] - expected: same-origin - - description: Cross-site - origins: [httpsCrossSite] - expected: cross-site - - description: Same site - origins: [httpsSameSite] - expected: same-site - template_axes: - # Unused - # - the request mode of all "classic" worker scripts is set to - # "same-origin" - # https://html.spec.whatwg.org/#fetch-a-classic-worker-script - # - the request mode of all "top-level "module" worker scripts is set to - # "same-origin": - # https://html.spec.whatwg.org/#fetch-a-single-module-script - worker-dedicated-constructor.sub.html: [] - - audioworklet.https.sub.html: [{}] - css-images.sub.html: - - filename_flags: [tentative] - css-font-face.sub.html: - - filename_flags: [tentative] - element-a.sub.html: [{}] - element-area.sub.html: [{}] - element-audio.sub.html: [{}] - element-embed.sub.html: [{}] - element-frame.sub.html: [{}] - element-iframe.sub.html: [{}] - element-img.sub.html: - - sourceAttr: src - - sourceAttr: srcset - element-img-environment-change.sub.html: [{}] - element-input-image.sub.html: [{}] - element-link-icon.sub.html: [{}] - element-link-prefetch.optional.sub.html: [{}] - element-meta-refresh.optional.sub.html: [{}] - element-picture.sub.html: [{}] - element-script.sub.html: - - {} - - elementAttrs: { type: module } - element-video.sub.html: [{}] - element-video-poster.sub.html: [{}] - fetch.sub.html: [{ init: { mode: no-cors } }] - fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors } }] - form-submission.sub.html: - - method: GET - - method: POST - header-link.sub.html: - - rel: icon - - rel: stylesheet - header-refresh.optional.sub.html: [{}] - window-location.sub.html: [{}] - script-module-import-dynamic.sub.html: [{}] - script-module-import-static.sub.html: [{}] - script-json-module-import-static.sub.html: [{}] - serviceworker.https.sub.html: [{}] - svg-image.sub.html: [{}] - window-history.sub.html: [{}] - worker-dedicated-importscripts.sub.html: [{}] - - # Sec-Fetch-Site - redirection from HTTP - - all_subtests: - headerName: sec-fetch-site - filename_flags: [] - common_axis: - - description: HTTPS downgrade (header not sent) - origins: [httpsOrigin, httpOrigin] - expected: NULL - - description: HTTPS upgrade - origins: [httpOrigin, httpsOrigin] - expected: cross-site - - description: HTTPS downgrade-upgrade - origins: [httpsOrigin, httpOrigin, httpsOrigin] - expected: cross-site - template_axes: - # Unused - # The `AudioWorklet` interface is only available in secure contexts - # https://webaudio.github.io/web-audio-api/#AudioWorklet - audioworklet.https.sub.html: [] - # Service workers are only available in secure context - fetch-via-serviceworker.https.sub.html: [] - # Service workers' redirect mode is "error" - serviceworker.https.sub.html: [] - # Interstitial locations in an HTTP redirect chain are not added to the - # session history, so these requests cannot be initiated using the - # History API. - window-history.sub.html: [] - # Unused - # - the request mode of all "classic" worker scripts is set to - # "same-origin" - # https://html.spec.whatwg.org/#fetch-a-classic-worker-script - # - the request mode of all "top-level "module" worker scripts is set to - # "same-origin": - # https://html.spec.whatwg.org/#fetch-a-single-module-script - worker-dedicated-constructor.sub.html: [] - - css-images.sub.html: - - filename_flags: [tentative] - css-font-face.sub.html: - - filename_flags: [tentative] - element-a.sub.html: [{}] - element-area.sub.html: [{}] - element-audio.sub.html: [{}] - element-embed.sub.html: [{}] - element-frame.sub.html: [{}] - element-iframe.sub.html: [{}] - element-img.sub.html: - - sourceAttr: src - - sourceAttr: srcset - element-img-environment-change.sub.html: [{}] - element-input-image.sub.html: [{}] - element-link-icon.sub.html: [{}] - element-link-prefetch.optional.sub.html: [{}] - element-meta-refresh.optional.sub.html: [{}] - element-picture.sub.html: [{}] - element-script.sub.html: - - {} - - elementAttrs: { type: module } - element-video.sub.html: [{}] - element-video-poster.sub.html: [{}] - fetch.sub.html: [{}] - form-submission.sub.html: - - method: GET - - method: POST - header-link.sub.html: - - rel: icon - - rel: stylesheet - header-refresh.optional.sub.html: [{}] - window-location.sub.html: [{}] - script-module-import-dynamic.sub.html: [{}] - script-module-import-static.sub.html: [{}] - script-json-module-import-static.sub.html: [{}] - svg-image.sub.html: [{}] - worker-dedicated-importscripts.sub.html: [{}] - - # Sec-Fetch-Site - redirection from HTTPS - - all_subtests: - headerName: sec-fetch-site - filename_flags: [https] - common_axis: - - description: Same-Origin -> Cross-Site -> Same-Origin redirect - origins: [httpsOrigin, httpsCrossSite, httpsOrigin] - expected: cross-site - - description: Same-Origin -> Same-Site -> Same-Origin redirect - origins: [httpsOrigin, httpsSameSite, httpsOrigin] - expected: same-site - - description: Cross-Site -> Same Origin - origins: [httpsCrossSite, httpsOrigin] - expected: cross-site - - description: Cross-Site -> Same-Site - origins: [httpsCrossSite, httpsSameSite] - expected: cross-site - - description: Cross-Site -> Cross-Site - origins: [httpsCrossSite, httpsCrossSite] - expected: cross-site - - description: Same-Origin -> Same Origin - origins: [httpsOrigin, httpsOrigin] - expected: same-origin - - description: Same-Origin -> Same-Site - origins: [httpsOrigin, httpsSameSite] - expected: same-site - - description: Same-Origin -> Cross-Site - origins: [httpsOrigin, httpsCrossSite] - expected: cross-site - - description: Same-Site -> Same Origin - origins: [httpsSameSite, httpsOrigin] - expected: same-site - - description: Same-Site -> Same-Site - origins: [httpsSameSite, httpsSameSite] - expected: same-site - - description: Same-Site -> Cross-Site - origins: [httpsSameSite, httpsCrossSite] - expected: cross-site - template_axes: - # Service Workers' redirect mode is "error" - serviceworker.https.sub.html: [] - # Interstitial locations in an HTTP redirect chain are not added to the - # session history, so these requests cannot be initiated using the - # History API. - window-history.sub.html: [] - # Unused - # - the request mode of all "classic" worker scripts is set to - # "same-origin" - # https://html.spec.whatwg.org/#fetch-a-classic-worker-script - # - the request mode of all "top-level "module" worker scripts is set to - # "same-origin": - # https://html.spec.whatwg.org/#fetch-a-single-module-script - worker-dedicated-constructor.sub.html: [] - - audioworklet.https.sub.html: [{}] - css-images.sub.html: - - filename_flags: [tentative] - css-font-face.sub.html: - - filename_flags: [tentative] - element-a.sub.html: [{}] - element-area.sub.html: [{}] - element-audio.sub.html: [{}] - element-embed.sub.html: [{}] - element-frame.sub.html: [{}] - element-iframe.sub.html: [{}] - element-img.sub.html: - - sourceAttr: src - - sourceAttr: srcset - element-img-environment-change.sub.html: [{}] - element-input-image.sub.html: [{}] - element-link-icon.sub.html: [{}] - element-link-prefetch.optional.sub.html: [{}] - element-meta-refresh.optional.sub.html: [{}] - element-picture.sub.html: [{}] - element-script.sub.html: - - {} - - elementAttrs: { type: module } - element-video.sub.html: [{}] - element-video-poster.sub.html: [{}] - fetch.sub.html: [{ init: { mode: no-cors } }] - fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors } }] - form-submission.sub.html: - - method: GET - - method: POST - header-link.sub.html: - - rel: icon - - rel: stylesheet - header-refresh.optional.sub.html: [{}] - window-location.sub.html: [{}] - script-module-import-dynamic.sub.html: [{}] - script-module-import-static.sub.html: [{}] - script-json-module-import-static.sub.html: [{}] - svg-image.sub.html: [{}] - worker-dedicated-importscripts.sub.html: [{}] - - # Sec-Fetch-Site - redirection with mixed content - # These tests verify the effect that redirection has on the request's "site". - # The initial request must be made to a resource that is "same-site" with its - # origin. This avoids false positives because if the request were made to a - # cross-site resource, the value of "cross-site" would be assigned regardless - # of the subseqent redirection. - # - # Because these conditions necessarily warrant mixed content, only templates - # which can be configured to allow mixed content [1] can be used. - # - # [1] https://w3c.github.io/webappsec-mixed-content/#should-block-fetch - - - common_axis: - - description: HTTPS downgrade-upgrade - headerName: sec-fetch-site - origins: [httpsOrigin, httpOrigin, httpsOrigin] - expected: cross-site - filename_flags: [https] - template_axes: - # Mixed Content considers only a small subset of requests as - # "optionally-blockable." These are the only requests that can be tested - # for the "downgrade-upgrade" scenario, so all other templates must be - # explicitly ignored. - audioworklet.https.sub.html: [] - css-font-face.sub.html: [] - element-embed.sub.html: [] - element-frame.sub.html: [] - element-iframe.sub.html: [] - element-img-environment-change.sub.html: [] - element-link-icon.sub.html: [] - element-link-prefetch.optional.sub.html: [] - element-picture.sub.html: [] - element-script.sub.html: [] - fetch.sub.html: [] - fetch-via-serviceworker.https.sub.html: [] - header-link.sub.html: [] - script-module-import-static.sub.html: [] - script-module-import-dynamic.sub.html: [] - script-json-module-import-static.sub.html: [] - # Service Workers' redirect mode is "error" - serviceworker.https.sub.html: [] - # Interstitial locations in an HTTP redirect chain are not added to the - # session history, so these requests cannot be initiated using the - # History API. - window-history.sub.html: [] - worker-dedicated-constructor.sub.html: [] - worker-dedicated-importscripts.sub.html: [] - # Avoid duplicate subtest for 'sec-fetch-site - HTTPS downgrade-upgrade' - css-images.sub.html: - - filename_flags: [tentative] - element-a.sub.html: [{}] - element-area.sub.html: [{}] - element-audio.sub.html: [{}] - element-img.sub.html: - # srcset omitted because it is not "optionally-blockable" - # https://w3c.github.io/webappsec-mixed-content/#category-optionally-blockable - - sourceAttr: src - element-input-image.sub.html: [{}] - element-meta-refresh.optional.sub.html: [{}] - element-video.sub.html: [{}] - element-video-poster.sub.html: [{}] - form-submission.sub.html: - - method: GET - - method: POST - header-refresh.optional.sub.html: [{}] - svg-image.sub.html: [{}] - window-location.sub.html: [{}] - - # Sec-Fetch-Mode - # These tests are served over HTTPS so the induced requests will be both - # same-origin with the document [1] and a potentially-trustworthy URL [2]. - # - # [1] https://html.spec.whatwg.org/multipage/origin.html#same-origin - # [2] https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-url - - common_axis: - - headerName: sec-fetch-mode - filename_flags: [https] - origins: [] - template_axes: - audioworklet.https.sub.html: - # https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script - - expected: cors - css-images.sub.html: - - expected: no-cors - filename_flags: [tentative] - css-font-face.sub.html: - - expected: cors - filename_flags: [tentative] - element-a.sub.html: - - expected: navigate - # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks - - elementAttrs: {download: ''} - expected: no-cors - element-area.sub.html: - - expected: navigate - # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks - - elementAttrs: {download: ''} - expected: no-cors - element-audio.sub.html: - - expected: no-cors - - expected: cors - elementAttrs: { crossorigin: '' } - - expected: cors - elementAttrs: { crossorigin: anonymous } - - expected: cors - elementAttrs: { crossorigin: use-credentials } - element-embed.sub.html: - - expected: no-cors - element-frame.sub.html: - - expected: navigate - element-iframe.sub.html: - - expected: navigate - element-img.sub.html: - - sourceAttr: src - expected: no-cors - - sourceAttr: src - expected: cors - elementAttrs: { crossorigin: '' } - - sourceAttr: src - expected: cors - elementAttrs: { crossorigin: anonymous } - - sourceAttr: src - expected: cors - elementAttrs: { crossorigin: use-credentials } - - sourceAttr: srcset - expected: no-cors - - sourceAttr: srcset - expected: cors - elementAttrs: { crossorigin: '' } - - sourceAttr: srcset - expected: cors - elementAttrs: { crossorigin: anonymous } - - sourceAttr: srcset - expected: cors - elementAttrs: { crossorigin: use-credentials } - element-img-environment-change.sub.html: - - expected: no-cors - - expected: cors - elementAttrs: { crossorigin: '' } - - expected: cors - elementAttrs: { crossorigin: anonymous } - - expected: cors - elementAttrs: { crossorigin: use-credentials } - element-input-image.sub.html: - - expected: no-cors - element-link-icon.sub.html: - - expected: no-cors - - expected: cors - elementAttrs: { crossorigin: '' } - - expected: cors - elementAttrs: { crossorigin: anonymous } - - expected: cors - elementAttrs: { crossorigin: use-credentials } - element-link-prefetch.optional.sub.html: - - expected: no-cors - - expected: cors - elementAttrs: { crossorigin: '' } - - expected: cors - elementAttrs: { crossorigin: anonymous } - - expected: cors - elementAttrs: { crossorigin: use-credentials } - element-meta-refresh.optional.sub.html: - - expected: navigate - element-picture.sub.html: - - expected: no-cors - - expected: cors - elementAttrs: { crossorigin: '' } - - expected: cors - elementAttrs: { crossorigin: anonymous } - - expected: cors - elementAttrs: { crossorigin: use-credentials } - element-script.sub.html: - - expected: no-cors - - expected: cors - elementAttrs: { type: module } - - expected: cors - elementAttrs: { crossorigin: '' } - - expected: cors - elementAttrs: { crossorigin: anonymous } - - expected: cors - elementAttrs: { crossorigin: use-credentials } - element-video.sub.html: - - expected: no-cors - - expected: cors - elementAttrs: { crossorigin: '' } - - expected: cors - elementAttrs: { crossorigin: anonymous } - - expected: cors - elementAttrs: { crossorigin: use-credentials } - element-video-poster.sub.html: - - expected: no-cors - fetch.sub.html: - - expected: cors - - expected: cors - init: { mode: cors } - - expected: no-cors - init: { mode: no-cors } - - expected: same-origin - init: { mode: same-origin } - fetch-via-serviceworker.https.sub.html: - - expected: cors - - expected: cors - init: { mode: cors } - - expected: no-cors - init: { mode: no-cors } - - expected: same-origin - init: { mode: same-origin } - form-submission.sub.html: - - method: GET - expected: navigate - - method: POST - expected: navigate - header-link.sub.html: - - rel: icon - expected: no-cors - - rel: stylesheet - expected: no-cors - header-refresh.optional.sub.html: - - expected: navigate - window-history.sub.html: - - expected: navigate - window-location.sub.html: - - expected: navigate - script-module-import-dynamic.sub.html: - - expected: cors - script-module-import-static.sub.html: - - expected: cors - script-json-module-import-static.sub.html: - - expected: cors - # https://svgwg.org/svg2-draft/linking.html#processingURL-fetch - svg-image.sub.html: - - expected: no-cors - - expected: cors - elementAttrs: { crossorigin: '' } - - expected: cors - elementAttrs: { crossorigin: anonymous } - - expected: cors - elementAttrs: { crossorigin: use-credentials } - serviceworker.https.sub.html: - - expected: same-origin - options: { type: 'classic' } - # https://github.com/whatwg/html/pull/5875 - - expected: same-origin - worker-dedicated-constructor.sub.html: - - expected: same-origin - - options: { type: module } - expected: same-origin - worker-dedicated-importscripts.sub.html: - - expected: no-cors - - # Sec-Fetch-Dest - - common_axis: - - headerName: sec-fetch-dest - filename_flags: [https] - origins: [] - template_axes: - audioworklet.https.sub.html: - # https://github.com/WebAudio/web-audio-api/issues/2203 - - expected: audioworklet - css-images.sub.html: - - expected: image - filename_flags: [tentative] - css-font-face.sub.html: - - expected: font - filename_flags: [tentative] - element-a.sub.html: - - expected: document - # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks - - elementAttrs: {download: ''} - expected: empty - element-area.sub.html: - - expected: document - # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks - - elementAttrs: {download: ''} - expected: empty - element-audio.sub.html: - - expected: audio - element-embed.sub.html: - - expected: embed - element-frame.sub.html: - # https://github.com/whatwg/html/pull/4976 - - expected: frame - element-iframe.sub.html: - # https://github.com/whatwg/html/pull/4976 - - expected: iframe - element-img.sub.html: - - sourceAttr: src - expected: image - - sourceAttr: srcset - expected: image - element-img-environment-change.sub.html: - - expected: image - element-input-image.sub.html: - - expected: image - element-link-icon.sub.html: - - expected: empty - element-link-prefetch.optional.sub.html: - - expected: empty - - elementAttrs: { as: audio } - expected: audio - - elementAttrs: { as: document } - expected: document - - elementAttrs: { as: embed } - expected: embed - - elementAttrs: { as: fetch } - expected: fetch - - elementAttrs: { as: font } - expected: font - - elementAttrs: { as: image } - expected: image - - elementAttrs: { as: object } - expected: object - - elementAttrs: { as: script } - expected: script - - elementAttrs: { as: style } - expected: style - - elementAttrs: { as: track } - expected: track - - elementAttrs: { as: video } - expected: video - - elementAttrs: { as: worker } - expected: worker - element-meta-refresh.optional.sub.html: - - expected: document - element-picture.sub.html: - - expected: image - element-script.sub.html: - - expected: script - element-video.sub.html: - - expected: video - element-video-poster.sub.html: - - expected: image - fetch.sub.html: - - expected: empty - fetch-via-serviceworker.https.sub.html: - - expected: empty - form-submission.sub.html: - - method: GET - expected: document - - method: POST - expected: document - header-link.sub.html: - - rel: icon - expected: empty - - rel: stylesheet - filename_flags: [tentative] - expected: style - header-refresh.optional.sub.html: - - expected: document - window-history.sub.html: - - expected: document - window-location.sub.html: - - expected: document - script-module-import-dynamic.sub.html: - - expected: script - script-module-import-static.sub.html: - - expected: script - script-json-module-import-static.sub.html: - - expected: json - serviceworker.https.sub.html: - - expected: serviceworker - # Implemented as "image" in Chromium and Firefox, but specified as - # "empty" - # https://github.com/w3c/svgwg/issues/782 - svg-image.sub.html: - - expected: empty - worker-dedicated-constructor.sub.html: - - expected: worker - - options: { type: module } - expected: worker - worker-dedicated-importscripts.sub.html: - - expected: script - - # Sec-Fetch-User - - common_axis: - - headerName: sec-fetch-user - filename_flags: [https] - origins: [] - template_axes: - audioworklet.https.sub.html: - - expected: NULL - css-images.sub.html: - - expected: NULL - filename_flags: [tentative] - css-font-face.sub.html: - - expected: NULL - filename_flags: [tentative] - element-a.sub.html: - - expected: NULL - - userActivated: TRUE - expected: ?1 - element-area.sub.html: - - expected: NULL - - userActivated: TRUE - expected: ?1 - element-audio.sub.html: - - expected: NULL - element-embed.sub.html: - - expected: NULL - element-frame.sub.html: - - expected: NULL - - userActivated: TRUE - expected: ?1 - element-iframe.sub.html: - - expected: NULL - - userActivated: TRUE - expected: ?1 - element-img.sub.html: - - sourceAttr: src - expected: NULL - - sourceAttr: srcset - expected: NULL - element-img-environment-change.sub.html: - - expected: NULL - element-input-image.sub.html: - - expected: NULL - element-link-icon.sub.html: - - expected: NULL - element-link-prefetch.optional.sub.html: - - expected: NULL - element-meta-refresh.optional.sub.html: - - expected: NULL - element-picture.sub.html: - - expected: NULL - element-script.sub.html: - - expected: NULL - element-video.sub.html: - - expected: NULL - element-video-poster.sub.html: - - expected: NULL - fetch.sub.html: - - expected: NULL - fetch-via-serviceworker.https.sub.html: - - expected: NULL - form-submission.sub.html: - - method: GET - expected: NULL - - method: GET - userActivated: TRUE - expected: ?1 - - method: POST - expected: NULL - - method: POST - userActivated: TRUE - expected: ?1 - header-link.sub.html: - - rel: icon - expected: NULL - - rel: stylesheet - expected: NULL - header-refresh.optional.sub.html: - - expected: NULL - window-history.sub.html: - - expected: NULL - window-location.sub.html: - - expected: NULL - - userActivated: TRUE - expected: ?1 - script-module-import-dynamic.sub.html: - - expected: NULL - script-module-import-static.sub.html: - - expected: NULL - script-json-module-import-static.sub.html: - - expected: NULL - serviceworker.https.sub.html: - - expected: NULL - svg-image.sub.html: - - expected: NULL - worker-dedicated-constructor.sub.html: - - expected: NULL - - options: { type: module } - expected: NULL - worker-dedicated-importscripts.sub.html: - - expected: NULL - # Sec-Fetch-Storage-Access - - all_subtests: - headerName: sec-fetch-storage-access - filename_flags: [https] - common_axis: - - description: Cross-site - origins: [httpsCrossSite] - expected: none - - description: Same site - origins: [httpsSameSite] - expected: NULL - template_axes: - # Service Workers' redirect mode is "error" - serviceworker.https.sub.html: [{}] - # Interstitial locations in an HTTP redirect chain are not added to the - # session history, so these requests cannot be initiated using the - # History API. - css-images.sub.html: - - filename_flags: [tentative] - element-audio.sub.html: [{}] - element-embed.sub.html: [{}] - element-frame.sub.html: [{}] - element-iframe.sub.html: [{}] - element-img.sub.html: - - sourceAttr: src - - sourceAttr: srcset - element-img-environment-change.sub.html: [{}] - element-input-image.sub.html: [{}] - element-link-icon.sub.html: [{}] - element-link-prefetch.optional.sub.html: [{}] - element-picture.sub.html: [{}] - element-script.sub.html: [{}] - element-video.sub.html: [{}] - element-video-poster.sub.html: [{}] - fetch.sub.html: [{ init: { mode: no-cors , credentials: include } }] - fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors , credentials: include } }] - header-link.sub.html: - - rel: icon - - rel: stylesheet - svg-image.sub.html: [{}] - worker-dedicated-constructor.sub.html: [] - worker-dedicated-importscripts.sub.html: [{}] - # The following are cases where the Sec-Fetch-Storage-Access header should - # not be attached at all. - - all_subtests: - headerName: sec-fetch-storage-access - filename_flags: [https] - common_axis: - - description: Cross-site - origins: [httpsCrossSite] - expected: NULL - - description: Same site - origins: [httpsSameSite] - expected: NULL - template_axes: - audioworklet.https.sub.html: [{}] - css-font-face.sub.html: - - filename_flags: [tentative] - element-meta-refresh.optional.sub.html: [{}] - element-a.sub.html: [{}] - element-area.sub.html: [{}] - form-submission.sub.html: - - method: GET - - method: POST - header-refresh.optional.sub.html: [{}] - script-module-import-dynamic.sub.html: [{}] - script-module-import-static.sub.html: [{}] - window-history.sub.html: [{}] - window-location.sub.html: [{}] diff --git a/test/fixtures/wpt/fetch/metadata/tools/generate.py b/test/fixtures/wpt/fetch/metadata/tools/generate.py deleted file mode 100755 index fa850c8c8a0..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/generate.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python3 - -import itertools -import os - -import jinja2 -import yaml - -HERE = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.join(HERE, '..', '..', '..') - -def find_templates(starting_directory): - for directory, subdirectories, file_names in os.walk(starting_directory): - for file_name in file_names: - if file_name.startswith('.'): - continue - yield file_name, os.path.join(directory, file_name) - -def test_name(directory, template_name, subtest_flags): - ''' - Create a test name based on a template and the WPT file name flags [1] - required for a given subtest. This name is used to determine how subtests - may be grouped together. In order to promote grouping, the combination uses - a few aspects of how file name flags are interpreted: - - - repeated flags have no effect, so duplicates are removed - - flag sequence does not matter, so flags are consistently sorted - - directory | template_name | subtest_flags | result - ----------|------------------|-----------------|------- - cors | image.html | [] | cors/image.html - cors | image.https.html | [] | cors/image.https.html - cors | image.html | [https] | cors/image.https.html - cors | image.https.html | [https] | cors/image.https.html - cors | image.https.html | [https] | cors/image.https.html - cors | image.sub.html | [https] | cors/image.https.sub.html - cors | image.https.html | [sub] | cors/image.https.sub.html - - [1] docs/writing-tests/file-names.md - ''' - template_name_parts = template_name.split('.') - flags = set(subtest_flags) | set(template_name_parts[1:-1]) - test_name_parts = ( - [template_name_parts[0]] + - sorted(flags) + - [template_name_parts[-1]] - ) - return os.path.join(directory, '.'.join(test_name_parts)) - -def merge(a, b): - if type(a) != type(b): - raise Exception('Cannot merge disparate types') - if type(a) == list: - return a + b - if type(a) == dict: - merged = {} - - for key in a: - if key in b: - merged[key] = merge(a[key], b[key]) - else: - merged[key] = a[key] - - for key in b: - if not key in a: - merged[key] = b[key] - - return merged - - raise Exception('Cannot merge {} type'.format(type(a).__name__)) - -def product(a, b): - ''' - Given two lists of objects, compute their Cartesian product by merging the - elements together. For example, - - product( - [{'a': 1}, {'b': 2}], - [{'c': 3}, {'d': 4}, {'e': 5}] - ) - - returns the following list: - - [ - {'a': 1, 'c': 3}, - {'a': 1, 'd': 4}, - {'a': 1, 'e': 5}, - {'b': 2, 'c': 3}, - {'b': 2, 'd': 4}, - {'b': 2, 'e': 5} - ] - ''' - result = [] - - for a_object in a: - for b_object in b: - result.append(merge(a_object, b_object)) - - return result - -def make_provenance(project_root, cases, template): - return '\n'.join([ - 'This test was procedurally generated. Please do not modify it directly.', - 'Sources:', - '- {}'.format(os.path.relpath(cases, project_root)), - '- {}'.format(os.path.relpath(template, project_root)) - ]) - -def collection_filter(obj, title): - if not obj: - return 'no {}'.format(title) - - members = [] - for name, value in obj.items(): - if value == '': - members.append(name) - else: - members.append('{}={}'.format(name, value)) - - return '{}: {}'.format(title, ', '.join(members)) - -def pad_filter(value, side, padding): - if not value: - return '' - if side == 'start': - return padding + value - - return value + padding - -def main(config_file): - with open(config_file, 'r') as handle: - config = yaml.safe_load(handle.read()) - - templates_directory = os.path.normpath( - os.path.join(os.path.dirname(config_file), config['templates']) - ) - - environment = jinja2.Environment( - variable_start_string='[%', - variable_end_string='%]' - ) - environment.filters['collection'] = collection_filter - environment.filters['pad'] = pad_filter - templates = {} - subtests = {} - - for template_name, path in find_templates(templates_directory): - subtests[template_name] = [] - with open(path, 'r') as handle: - templates[template_name] = environment.from_string(handle.read()) - - for case in config['cases']: - unused_templates = set(templates) - set(case['template_axes']) - - # This warning is intended to help authors avoid mistakenly omitting - # templates. It can be silenced by extending the`template_axes` - # dictionary with an empty list for templates which are intentionally - # unused. - if unused_templates: - print( - 'Warning: case does not reference the following templates:' - ) - print('\n'.join('- {}'.format(name) for name in unused_templates)) - - common_axis = product( - case['common_axis'], [case.get('all_subtests', {})] - ) - - for template_name, template_axis in case['template_axes'].items(): - subtests[template_name].extend(product(common_axis, template_axis)) - - for template_name, template in templates.items(): - provenance = make_provenance( - PROJECT_ROOT, - config_file, - os.path.join(templates_directory, template_name) - ) - get_filename = lambda subtest: test_name( - config['output_directory'], - template_name, - subtest['filename_flags'] - ) - subtests_by_filename = itertools.groupby( - sorted(subtests[template_name], key=get_filename), - key=get_filename - ) - for filename, some_subtests in subtests_by_filename: - with open(filename, 'w') as handle: - handle.write(templates[template_name].render( - subtests=list(some_subtests), - provenance=provenance - ) + '\n') - -if __name__ == '__main__': - main('fetch-metadata.conf.yml') diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/audioworklet.https.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/audioworklet.https.sub.html deleted file mode 100644 index 7be309c5068..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/audioworklet.https.sub.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - HTTP headers on request for AudioWorklet module - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/css-font-face.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/css-font-face.sub.html deleted file mode 100644 index 94b33f4e6b0..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/css-font-face.sub.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - HTTP headers on request for CSS font-face - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/css-images.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/css-images.sub.html deleted file mode 100644 index e394f9f5b06..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/css-images.sub.html +++ /dev/null @@ -1,137 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for CSS image-accepting properties - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-a.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-a.sub.html deleted file mode 100644 index 2bd8e8a40e0..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-a.sub.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for HTML "a" element navigation - - - {%- if subtests|selectattr('userActivated')|list %} - - - {%- endif %} - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-area.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-area.sub.html deleted file mode 100644 index 0cef5b22941..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-area.sub.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for HTML "area" element navigation - - - {%- if subtests|selectattr('userActivated')|list %} - - - {%- endif %} - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-audio.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-audio.sub.html deleted file mode 100644 index 92bc22198e7..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-audio.sub.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - HTTP headers on request for HTML "audio" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-embed.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-embed.sub.html deleted file mode 100644 index 18ce09e5fdc..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-embed.sub.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - HTTP headers on request for HTML "embed" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-frame.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-frame.sub.html deleted file mode 100644 index ce90171779a..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-frame.sub.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - HTTP headers on request for HTML "frame" element source - - - {%- if subtests|selectattr('userActivated')|list %} - - - {%- endif %} - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-iframe.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-iframe.sub.html deleted file mode 100644 index 43a632a15cd..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-iframe.sub.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - HTTP headers on request for HTML "frame" element source - - - {%- if subtests|selectattr('userActivated')|list %} - - - {%- endif %} - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-img-environment-change.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-img-environment-change.sub.html deleted file mode 100644 index 5a65114f184..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-img-environment-change.sub.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - HTTP headers on image request triggered by change to environment - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-img.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-img.sub.html deleted file mode 100644 index 1dac5843ecd..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-img.sub.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - HTTP headers on request for HTML "img" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-input-image.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-input-image.sub.html deleted file mode 100644 index 3c50008433c..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-input-image.sub.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - HTTP headers on request for HTML "input" element with type="button" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-link-icon.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-link-icon.sub.html deleted file mode 100644 index 18ce12a6898..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-link-icon.sub.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for HTML "link" element with rel="icon" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html deleted file mode 100644 index 59d677d8d67..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for HTML "link" element with rel="prefetch" - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html deleted file mode 100644 index 5a8d8f8ecd2..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - HTTP headers on request for HTML "meta" element with http-equiv="refresh" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-picture.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-picture.sub.html deleted file mode 100644 index 903aeed1f37..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-picture.sub.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - - HTTP headers on request for HTML "picture" element source - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-script.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-script.sub.html deleted file mode 100644 index 4a281ae5192..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-script.sub.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - HTTP headers on request for HTML "script" element source - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-video-poster.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-video-poster.sub.html deleted file mode 100644 index 9cdaf063ace..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-video-poster.sub.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - HTTP headers on request for HTML "video" element "poster" - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/element-video.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/element-video.sub.html deleted file mode 100644 index 1b7b976d7c4..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/element-video.sub.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - HTTP headers on request for HTML "video" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html deleted file mode 100644 index eead7102008..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request using the "fetch" API and passing through a Serive Worker - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/fetch.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/fetch.sub.html deleted file mode 100644 index a8dc5368f8b..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/fetch.sub.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - HTTP headers on request using the "fetch" API - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/form-submission.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/form-submission.sub.html deleted file mode 100644 index 4c9c8c50f82..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/form-submission.sub.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - HTTP headers on request for HTML form navigation - - - {%- if subtests|selectattr('userActivated')|list %} - - - {%- endif %} - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/header-link.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/header-link.sub.html deleted file mode 100644 index 2831f221d5c..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/header-link.sub.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - HTTP headers on request for HTTP "Link" header - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/header-refresh.optional.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/header-refresh.optional.sub.html deleted file mode 100644 index ec963d5cc00..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/header-refresh.optional.sub.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for HTTP "Refresh" header - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/script-json-module-import-static.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/script-json-module-import-static.sub.html deleted file mode 100644 index 76aecbdb220..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/script-json-module-import-static.sub.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - HTTP headers on request for static ECMAScript module import - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html deleted file mode 100644 index 653d3cdec44..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - HTTP headers on request for dynamic ECMAScript module import - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/script-module-import-static.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/script-module-import-static.sub.html deleted file mode 100644 index c8d5f9532a9..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/script-module-import-static.sub.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - HTTP headers on request for static ECMAScript module import - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/serviceworker.https.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/serviceworker.https.sub.html deleted file mode 100644 index 82843255469..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/serviceworker.https.sub.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - HTTP headers on request for Service Workers - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/svg-image.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/svg-image.sub.html deleted file mode 100644 index 52f7806b33c..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/svg-image.sub.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for SVG "image" element source - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/window-history.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/window-history.sub.html deleted file mode 100644 index 286d019887d..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/window-history.sub.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for navigation via the HTML History API - - - {%- if subtests|selectattr('userActivated')|list %} - - - {%- endif %} - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/window-location.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/window-location.sub.html deleted file mode 100644 index 96f39123616..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/window-location.sub.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - - {%- if subtests|length > 10 %} - - {%- endif %} - HTTP headers on request for navigation via the HTML Location API - - - {%- if subtests|selectattr('userActivated')|list %} - - - {%- endif %} - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html deleted file mode 100644 index fede5965d3a..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - HTTP headers on request for dedicated worker via the "Worker" constructor - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html b/test/fixtures/wpt/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html deleted file mode 100644 index 93e6374d54b..00000000000 --- a/test/fixtures/wpt/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - HTTP headers on request for dedicated worker via the "importScripts" API - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/track.https.sub.html b/test/fixtures/wpt/fetch/metadata/track.https.sub.html deleted file mode 100644 index 346798fdc0e..00000000000 --- a/test/fixtures/wpt/fetch/metadata/track.https.sub.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/trailing-dot.https.sub.any.js b/test/fixtures/wpt/fetch/metadata/trailing-dot.https.sub.any.js deleted file mode 100644 index 5e32fc4e7f6..00000000000 --- a/test/fixtures/wpt/fetch/metadata/trailing-dot.https.sub.any.js +++ /dev/null @@ -1,30 +0,0 @@ -// META: global=window,worker -// META: script=/fetch/metadata/resources/helper.js - -// Site -promise_test(t => { - return validate_expectations_custom_url("https://{{host}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { - "site": "cross-site", - "user": "", - "mode": "cors", - "dest": "empty" - }, "Fetching a resource from the same origin, but spelled with a trailing dot."); -}, "Fetching a resource from the same origin, but spelled with a trailing dot."); - -promise_test(t => { - return validate_expectations_custom_url("https://{{hosts[][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { - "site": "cross-site", - "user": "", - "mode": "cors", - "dest": "empty" - }, "Fetching a resource from the same site, but spelled with a trailing dot."); -}, "Fetching a resource from the same site, but spelled with a trailing dot."); - -promise_test(t => { - return validate_expectations_custom_url("https://{{hosts[alt][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { - "site": "cross-site", - "user": "", - "mode": "cors", - "dest": "empty" - }, "Fetching a resource from a cross-site host, spelled with a trailing dot."); -}, "Fetching a resource from a cross-site host, spelled with a trailing dot."); diff --git a/test/fixtures/wpt/fetch/metadata/unload.https.sub.html b/test/fixtures/wpt/fetch/metadata/unload.https.sub.html deleted file mode 100644 index bc26048c810..00000000000 --- a/test/fixtures/wpt/fetch/metadata/unload.https.sub.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/window-open.https.sub.html b/test/fixtures/wpt/fetch/metadata/window-open.https.sub.html deleted file mode 100644 index 94ba76a19ff..00000000000 --- a/test/fixtures/wpt/fetch/metadata/window-open.https.sub.html +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/worker.https.sub.html b/test/fixtures/wpt/fetch/metadata/worker.https.sub.html deleted file mode 100644 index 20a4fe54166..00000000000 --- a/test/fixtures/wpt/fetch/metadata/worker.https.sub.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/test/fixtures/wpt/fetch/metadata/xslt.https.sub.html b/test/fixtures/wpt/fetch/metadata/xslt.https.sub.html deleted file mode 100644 index dc72d7b8a67..00000000000 --- a/test/fixtures/wpt/fetch/metadata/xslt.https.sub.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/test/fixtures/wpt/fetch/nosniff/image.html b/test/fixtures/wpt/fetch/nosniff/image.html deleted file mode 100644 index 9dfdb94cf62..00000000000 --- a/test/fixtures/wpt/fetch/nosniff/image.html +++ /dev/null @@ -1,39 +0,0 @@ - - -
- diff --git a/test/fixtures/wpt/fetch/nosniff/importscripts.html b/test/fixtures/wpt/fetch/nosniff/importscripts.html deleted file mode 100644 index 920b6bdd409..00000000000 --- a/test/fixtures/wpt/fetch/nosniff/importscripts.html +++ /dev/null @@ -1,14 +0,0 @@ - - -
- diff --git a/test/fixtures/wpt/fetch/nosniff/importscripts.js b/test/fixtures/wpt/fetch/nosniff/importscripts.js deleted file mode 100644 index 18952805bb7..00000000000 --- a/test/fixtures/wpt/fetch/nosniff/importscripts.js +++ /dev/null @@ -1,28 +0,0 @@ -// Testing importScripts() -function log(w) { this.postMessage(w) } -function f() { log("FAIL") } -function p() { log("PASS") } - -const get_url = (mime, outcome) => { - let url = "resources/js.py" - if (mime != null) { - url += "?type=" + encodeURIComponent(mime) - } - if (outcome) { - url += "&outcome=p" - } - return url -} - -[null, "", "x", "x/x", "text/html", "text/json"].forEach(function(mime) { - try { - importScripts(get_url(mime)) - } catch(e) { - (e.name == "NetworkError") ? p() : log("FAIL (no NetworkError exception): " + mime) - } - -}) -importScripts(get_url("text/javascript", true)) -importScripts(get_url("text/ecmascript", true)) -importScripts(get_url("text/ecmascript;blah", true)) -log("END") diff --git a/test/fixtures/wpt/fetch/nosniff/parsing-nosniff.window.js b/test/fixtures/wpt/fetch/nosniff/parsing-nosniff.window.js deleted file mode 100644 index 2a2648653ca..00000000000 --- a/test/fixtures/wpt/fetch/nosniff/parsing-nosniff.window.js +++ /dev/null @@ -1,27 +0,0 @@ -promise_test(() => fetch("resources/x-content-type-options.json").then(res => res.json()).then(runTests), "Loading JSON…"); - -function runTests(allTestData) { - for (let i = 0; i < allTestData.length; i++) { - const testData = allTestData[i], - input = encodeURIComponent(testData.input); - promise_test(t => { - let resolve; - const promise = new Promise(r => resolve = r); - const script = document.createElement("script"); - t.add_cleanup(() => script.remove()); - // A - -
- diff --git a/test/fixtures/wpt/fetch/nosniff/stylesheet.html b/test/fixtures/wpt/fetch/nosniff/stylesheet.html deleted file mode 100644 index 8f2b5476e90..00000000000 --- a/test/fixtures/wpt/fetch/nosniff/stylesheet.html +++ /dev/null @@ -1,60 +0,0 @@ - - - -
- diff --git a/test/fixtures/wpt/fetch/nosniff/worker.html b/test/fixtures/wpt/fetch/nosniff/worker.html deleted file mode 100644 index c8c1076df5c..00000000000 --- a/test/fixtures/wpt/fetch/nosniff/worker.html +++ /dev/null @@ -1,28 +0,0 @@ - - -
- diff --git a/test/fixtures/wpt/fetch/orb/resources/data.json b/test/fixtures/wpt/fetch/orb/resources/data.json deleted file mode 100644 index f2a886f39de..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/data.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hello": "world" -} diff --git a/test/fixtures/wpt/fetch/orb/resources/data_non_ascii.json b/test/fixtures/wpt/fetch/orb/resources/data_non_ascii.json deleted file mode 100644 index 64566c50c14..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/data_non_ascii.json +++ /dev/null @@ -1 +0,0 @@ -["你好"] diff --git a/test/fixtures/wpt/fetch/orb/resources/empty.json b/test/fixtures/wpt/fetch/orb/resources/empty.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/empty.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/test/fixtures/wpt/fetch/orb/resources/font.ttf b/test/fixtures/wpt/fetch/orb/resources/font.ttf deleted file mode 100644 index 9023592ef5a..00000000000 Binary files a/test/fixtures/wpt/fetch/orb/resources/font.ttf and /dev/null differ diff --git a/test/fixtures/wpt/fetch/orb/resources/image.png b/test/fixtures/wpt/fetch/orb/resources/image.png deleted file mode 100644 index 820f8cace21..00000000000 Binary files a/test/fixtures/wpt/fetch/orb/resources/image.png and /dev/null differ diff --git a/test/fixtures/wpt/fetch/orb/resources/js-unlabeled-utf16-without-bom.json b/test/fixtures/wpt/fetch/orb/resources/js-unlabeled-utf16-without-bom.json deleted file mode 100644 index 157a8f54308..00000000000 Binary files a/test/fixtures/wpt/fetch/orb/resources/js-unlabeled-utf16-without-bom.json and /dev/null differ diff --git a/test/fixtures/wpt/fetch/orb/resources/js-unlabeled.js b/test/fixtures/wpt/fetch/orb/resources/js-unlabeled.js deleted file mode 100644 index a880a5bc724..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/js-unlabeled.js +++ /dev/null @@ -1 +0,0 @@ -window.has_executed_script = true; diff --git a/test/fixtures/wpt/fetch/orb/resources/png-mislabeled-as-html.png b/test/fixtures/wpt/fetch/orb/resources/png-mislabeled-as-html.png deleted file mode 100644 index 820f8cace21..00000000000 Binary files a/test/fixtures/wpt/fetch/orb/resources/png-mislabeled-as-html.png and /dev/null differ diff --git a/test/fixtures/wpt/fetch/orb/resources/png-mislabeled-as-html.png.headers b/test/fixtures/wpt/fetch/orb/resources/png-mislabeled-as-html.png.headers deleted file mode 100644 index 156209f9c81..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/png-mislabeled-as-html.png.headers +++ /dev/null @@ -1 +0,0 @@ -Content-Type: text/html diff --git a/test/fixtures/wpt/fetch/orb/resources/png-unlabeled.png b/test/fixtures/wpt/fetch/orb/resources/png-unlabeled.png deleted file mode 100644 index 820f8cace21..00000000000 Binary files a/test/fixtures/wpt/fetch/orb/resources/png-unlabeled.png and /dev/null differ diff --git a/test/fixtures/wpt/fetch/orb/resources/script-asm-js-invalid.js b/test/fixtures/wpt/fetch/orb/resources/script-asm-js-invalid.js deleted file mode 100644 index 8d1bbd6abc8..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/script-asm-js-invalid.js +++ /dev/null @@ -1,4 +0,0 @@ -function f() { - "use asm"; - return; -} diff --git a/test/fixtures/wpt/fetch/orb/resources/script-asm-js-valid.js b/test/fixtures/wpt/fetch/orb/resources/script-asm-js-valid.js deleted file mode 100644 index 79b375fe054..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/script-asm-js-valid.js +++ /dev/null @@ -1,4 +0,0 @@ -function f() { - "use asm"; - return {}; -} diff --git a/test/fixtures/wpt/fetch/orb/resources/script-iso-8559-1.js b/test/fixtures/wpt/fetch/orb/resources/script-iso-8559-1.js deleted file mode 100644 index 3bccb6af93e..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/script-iso-8559-1.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -function fn() { - return "An"; -} diff --git a/test/fixtures/wpt/fetch/orb/resources/script-utf16-bom.js b/test/fixtures/wpt/fetch/orb/resources/script-utf16-bom.js deleted file mode 100644 index 16b76e9d5e4..00000000000 Binary files a/test/fixtures/wpt/fetch/orb/resources/script-utf16-bom.js and /dev/null differ diff --git a/test/fixtures/wpt/fetch/orb/resources/script-utf16-without-bom.js b/test/fixtures/wpt/fetch/orb/resources/script-utf16-without-bom.js deleted file mode 100644 index d983086b036..00000000000 Binary files a/test/fixtures/wpt/fetch/orb/resources/script-utf16-without-bom.js and /dev/null differ diff --git a/test/fixtures/wpt/fetch/orb/resources/script.js b/test/fixtures/wpt/fetch/orb/resources/script.js deleted file mode 100644 index 19675d25d83..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/script.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -function fn() { - return 42; -} diff --git a/test/fixtures/wpt/fetch/orb/resources/sound.mp3 b/test/fixtures/wpt/fetch/orb/resources/sound.mp3 deleted file mode 100644 index a15d1de328f..00000000000 Binary files a/test/fixtures/wpt/fetch/orb/resources/sound.mp3 and /dev/null differ diff --git a/test/fixtures/wpt/fetch/orb/resources/text.txt b/test/fixtures/wpt/fetch/orb/resources/text.txt deleted file mode 100644 index 270c611ee72..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/text.txt +++ /dev/null @@ -1 +0,0 @@ -hello, world! diff --git a/test/fixtures/wpt/fetch/orb/resources/utils.js b/test/fixtures/wpt/fetch/orb/resources/utils.js deleted file mode 100644 index 45fbc4cb38e..00000000000 --- a/test/fixtures/wpt/fetch/orb/resources/utils.js +++ /dev/null @@ -1,101 +0,0 @@ -function header(name, value) { - return `header(${name},${value})`; -} - -function contentType(type) { - return header("Content-Type", type); -} - -function contentTypeOptions(type) { - return header("X-Content-Type-Options", type); -} - -function testFetchNoCors(_t, path, { headers }) { - return fetch(path, { - ...(headers ? { headers } : {}), - mode: "no-cors", - }); -} - -function testElementInitiator(t, path, name) { - let element = document.createElement(name); - element.src = path; - t.add_cleanup(() => element.remove()); - return new Promise((resolve, reject) => { - element.onerror = e => reject(new TypeError()); - element.onload = resolve; - - document.body.appendChild(element); - }); -} - -function testImageInitiator(t, path) { - return testElementInitiator(t, path, "img"); -} - -function testAudioInitiator(t, path) { - return testElementInitiator(t, path, "audio"); -} - -function testVideoInitiator(t, path) { - return testElementInitiator(t, path, "video"); -} - -function testScriptInitiator(t, path) { - return testElementInitiator(t, path, "script"); -} - -function runTest(t, test, file, options, ...pipe) { - const path = `${file}${pipe.length ? `?pipe=${pipe.join("|")}` : ""}`; - return test(t, path, options) -} - -function testRunAll(file, testCallback, adapter, options) { - let testcase = function (test, message, skip) { - return {test, message, skip}; - }; - - const name = "..."; - [ testcase(testFetchNoCors, `fetch(${name}, {mode: "no-cors"})`, false || options.skip.includes("fetch")), - testcase(testImageInitiator, ``, options.onlyFetch || options.skip.includes("image")), - testcase(testAudioInitiator, `