├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── renovate.json └── workflows │ ├── browserslist.yml │ ├── ci.yml │ ├── lock.yml │ └── prettier.yml ├── .gitignore ├── .releaserc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── middleware.js ├── modules.d.ts ├── package-lock.json ├── package.config.ts ├── package.json ├── src ├── createRequester.ts ├── index.browser.ts ├── index.react-server.ts ├── index.ts ├── middleware.browser.ts ├── middleware.ts ├── middleware │ ├── agent │ │ ├── browser-agent.ts │ │ └── node-agent.ts │ ├── base.ts │ ├── debug.ts │ ├── defaultOptionsProcessor.ts │ ├── defaultOptionsValidator.ts │ ├── headers.ts │ ├── httpErrors.ts │ ├── injectResponse.ts │ ├── jsonRequest.ts │ ├── jsonResponse.ts │ ├── keepAlive.ts │ ├── mtls.ts │ ├── observable.ts │ ├── progress │ │ ├── browser-progress.ts │ │ └── node-progress.ts │ ├── promise.ts │ ├── proxy.ts │ ├── retry │ │ ├── browser-retry.ts │ │ ├── node-retry.ts │ │ └── shared-retry.ts │ └── urlEncoded.ts ├── request │ ├── browser-request.ts │ ├── browser │ │ └── fetchXhr.ts │ ├── node-request.ts │ └── node │ │ ├── proxy.ts │ │ ├── simpleConcat.ts │ │ ├── timedOut.ts │ │ └── tunnel.ts ├── types.ts └── util │ ├── browser-shouldRetry.ts │ ├── global.ts │ ├── isBrowserOptions.ts │ ├── isBuffer.ts │ ├── isPlainObject.ts │ ├── lowerCaseHeaders.ts │ ├── middlewareReducer.ts │ ├── node-shouldRetry.ts │ ├── progress-stream.ts │ ├── pubsub.ts │ └── speedometer.ts ├── test-deno ├── import_map.json └── test.ts ├── test-esm ├── test.cjs └── test.mjs ├── test ├── .eslintrc.json ├── abort.test.ts ├── agent.test.ts ├── basics.test.ts ├── certs │ ├── invalid-mtls │ │ ├── README.md │ │ ├── ca.pem │ │ ├── client.key │ │ ├── client.pem │ │ ├── server.key │ │ └── server.pem │ ├── mtls │ │ ├── README.md │ │ ├── ca.pem │ │ ├── client.key │ │ ├── client.pem │ │ ├── server.key │ │ └── server.pem │ └── server │ │ ├── cert.pem │ │ └── key.pem ├── debug.test.ts ├── errors.test.ts ├── fetch.test.ts ├── headers.test.ts ├── helpers │ ├── debugRequest.ts │ ├── expectEvent.ts │ ├── expectRequest.ts │ ├── failOnError.ts │ ├── globalSetup.http.ts │ ├── globalSetup.https.ts │ ├── globalSetup.proxy.http.ts │ ├── globalSetup.proxy.https.ts │ ├── index.ts │ ├── mtls.ts │ ├── noop.ts │ ├── proxy.ts │ └── server.ts ├── inject.test.ts ├── json.test.ts ├── keepAlive.test.ts ├── mtls.test.ts ├── node.fetch.test.ts ├── observable.test.ts ├── progress.test.ts ├── promise.test.ts ├── proxy.test.ts ├── queryStrings.test.ts ├── redirect.test.ts ├── retry.test.ts ├── socket.test.ts ├── stream.test.ts ├── timeouts.test.ts └── urlEncoded.test.ts ├── tsconfig.dist.json ├── tsconfig.json ├── tsconfig.settings.json ├── vite.config.ts ├── vitest.browser.config.ts ├── vitest.edge.config.ts └── vitest.react-server.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | # ESLint isn't configured to deal with ESM yet 4 | /test-esm 5 | # ESLint isn't configured to deal with Deno yet 6 | /test-deno 7 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:prettier/recommended', 10 | 'plugin:@typescript-eslint/eslint-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | ecmaVersion: 2018, 16 | }, 17 | plugins: ['@typescript-eslint', 'simple-import-sort', 'prettier'], 18 | rules: { 19 | '@typescript-eslint/no-explicit-any': 'warn', 20 | '@typescript-eslint/member-delimiter-style': 'off', 21 | '@typescript-eslint/no-empty-interface': 'off', 22 | 'simple-import-sort/imports': 'warn', 23 | 'simple-import-sort/exports': 'warn', 24 | 'no-console': 'error', 25 | 'no-shadow': 'error', 26 | 'no-warning-comments': ['warn', {location: 'start', terms: ['todo', '@todo', 'fixme']}], 27 | }, 28 | 29 | overrides: [ 30 | { 31 | files: ['**/*.js'], 32 | rules: { 33 | '@typescript-eslint/explicit-module-boundary-types': 'off', 34 | }, 35 | }, 36 | ], 37 | } 38 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>sanity-io/renovate-config", ":reviewer(team:ecosystem)"], 4 | "ignorePresets": [":ignoreModulesAndTests", "github>sanity-io/renovate-config:group-non-major"], 5 | "packageRules": [ 6 | { 7 | "matchDepTypes": ["dependencies"], 8 | "rangeStrategy": "bump" 9 | }, 10 | { 11 | "matchPackageNames": ["debug", "parse-headers"], 12 | "rangeStrategy": "bump", 13 | "semanticCommitType": "fix" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/browserslist.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update Browserslist database 3 | 4 | on: 5 | schedule: 6 | - cron: "0 2 1,15 * *" 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read # for checkout 11 | 12 | jobs: 13 | update-browserslist-database: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | - run: npx update-browserslist-db@latest 21 | - uses: actions/create-github-app-token@v2 22 | id: app-token 23 | with: 24 | app-id: ${{ secrets.ECOSPARK_APP_ID }} 25 | private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} 26 | - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7 27 | with: 28 | author: github-actions <41898282+github-actions[bot]@users.noreply.github.com> 29 | body: I ran `npx update-browserslist-db@latest` 🧑‍💻 30 | branch: actions/update-browserslist-database-if-needed 31 | commit-message: "chore: update browserslist db" 32 | labels: 🤖 bot 33 | sign-commits: true 34 | title: "chore: update browserslist db" 35 | token: ${{ steps.app-token.outputs.token }} 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI & Release 2 | 3 | on: 4 | # Build on pushes to release branches 5 | push: 6 | branches: [main] 7 | # Build on pull requests targeting release branches 8 | pull_request: 9 | branches: [main] 10 | # https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow 11 | # https://github.com/sanity-io/semantic-release-preset/actions/workflows/ci.yml 12 | workflow_dispatch: 13 | inputs: 14 | release: 15 | description: "Release new version" 16 | required: true 17 | default: false 18 | type: boolean 19 | 20 | concurrency: 21 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 22 | cancel-in-progress: true 23 | 24 | permissions: 25 | contents: read # for checkout 26 | 27 | jobs: 28 | build: 29 | name: Build, lint and test coverage 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-node@v4 34 | with: 35 | cache: npm 36 | node-version: lts/* 37 | - run: npm ci 38 | - run: npx ls-engines 39 | - run: npm run typecheck 40 | - run: npm run build 41 | - run: npm run lint -- --quiet 42 | - run: npm run coverage 43 | 44 | test: 45 | name: Node.js ${{ matrix.node }} on ${{ matrix.os }} 46 | needs: build 47 | runs-on: ${{ matrix.os }} 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | # Run the testing suite on each major OS with the latest LTS release of Node.js 52 | os: [macos-latest, ubuntu-latest, windows-latest] 53 | node: [lts/*] 54 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner 55 | include: 56 | - os: ubuntu-latest 57 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life 58 | node: lts/-1 59 | - os: ubuntu-latest 60 | # Test the actively developed version that will become the latest LTS release next October 61 | node: current 62 | # The `build` job already runs the testing suite in ubuntu and lts/* 63 | exclude: 64 | - os: ubuntu-latest 65 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life 66 | node: lts/* 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: actions/setup-node@v4 70 | with: 71 | cache: npm 72 | node-version: ${{ matrix.node }} 73 | - run: npm i 74 | - run: npx ls-engines 75 | - run: npm run build 76 | - run: npm test 77 | # Only run the RSC testing suite if native `fetch` is available 78 | - run: node -e "fetch" && npm run test:react-server || true 79 | 80 | test-esm: 81 | name: Test ESM exports 82 | needs: build 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v4 86 | - uses: actions/setup-node@v4 87 | with: 88 | cache: npm 89 | # The testing suite uses the native test runner introduced in Node.js v18 90 | # https://nodejs.org/api/test.html 91 | node-version: lts/* 92 | - run: npm ci 93 | - run: npm run build 94 | - run: npm run test:esm 95 | # This test will run both in a CJS and an ESM mode in Node.js to ensure backwards compatibility 96 | name: Ensure pkg.exports don't break anything in modern Node.js envs 97 | - run: npm run test:esm:browser 98 | # This test is just ensuring the pkg.exports defined by 'browser' conditionals don't point to files that don't exist and have valid syntax (no CJS) 99 | # Please note that this test DOES support Node.js APIs, we need to test in a Cloudflare Worker v8 runtime, or Vercel Edge Runtime, to fully test e2e 100 | name: Test the 'browser' conditional using Node.js 101 | 102 | test-deno: 103 | name: Test Deno 104 | needs: build 105 | runs-on: ubuntu-latest 106 | steps: 107 | - uses: actions/checkout@v4 108 | - uses: actions/setup-node@v4 109 | with: 110 | cache: npm 111 | node-version: lts/* 112 | - run: npm ci 113 | - run: npm run build 114 | - uses: denoland/setup-deno@v1 115 | with: 116 | deno-version: vx.x.x 117 | - run: npm run test:esm:deno 118 | name: Test that Deno can import `get-it` 119 | 120 | test-browser: 121 | name: Test Browser 122 | needs: build 123 | runs-on: ubuntu-latest 124 | steps: 125 | - uses: actions/checkout@v4 126 | - uses: actions/setup-node@v4 127 | with: 128 | cache: npm 129 | node-version: lts/* 130 | - run: npm ci 131 | - run: npm run test:browser -- run --coverage 132 | 133 | test-edge: 134 | name: Test Edge Runtime 135 | needs: build 136 | runs-on: ubuntu-latest 137 | steps: 138 | - uses: actions/checkout@v4 139 | - uses: actions/setup-node@v4 140 | with: 141 | cache: npm 142 | node-version: lts/* 143 | - run: npm ci 144 | - run: npm run test:edge-runtime -- run --coverage 145 | 146 | release: 147 | permissions: 148 | id-token: write # to enable use of OIDC for npm provenance 149 | name: Semantic release 150 | runs-on: ubuntu-latest 151 | needs: [test, test-esm, test-deno, test-edge, test-browser] 152 | if: inputs.release == true 153 | steps: 154 | - uses: actions/create-github-app-token@v2 155 | id: app-token 156 | with: 157 | app-id: ${{ secrets.ECOSPARK_APP_ID }} 158 | private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} 159 | - uses: actions/checkout@v4 160 | with: 161 | # Need to fetch entire commit history to 162 | # analyze every commit since last release 163 | fetch-depth: 0 164 | # Uses generated token to allow pushing commits back 165 | token: ${{ steps.app-token.outputs.token }} 166 | # Make sure the value of GITHUB_TOKEN will not be persisted in repo's config 167 | persist-credentials: false 168 | - uses: actions/setup-node@v4 169 | with: 170 | cache: npm 171 | node-version: lts/* 172 | - run: npm ci 173 | - run: npx semantic-release 174 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state 175 | # e.g. git tags were pushed but it exited before `npm publish` 176 | if: always() 177 | env: 178 | NPM_CONFIG_PROVENANCE: true 179 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 180 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 181 | # Should release fail, dry rerun with debug on for richer logs 182 | - run: npx semantic-release --dry-run --debug 183 | if: ${{ failure() }} 184 | env: 185 | NPM_CONFIG_PROVENANCE: true 186 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 187 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 188 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Lock Threads" 3 | 4 | on: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | workflow_dispatch: 8 | 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | 13 | concurrency: 14 | group: ${{ github.workflow }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | action: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5 22 | with: 23 | issue-inactive-days: 0 24 | pr-inactive-days: 7 25 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Prettier 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | run: 18 | name: Can the code be prettier? 🤔 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | cache: npm 25 | node-version: lts/* 26 | - run: npm ci --ignore-scripts --only-dev 27 | - uses: actions/cache@v4 28 | with: 29 | path: node_modules/.cache/prettier/.prettier-cache 30 | key: prettier-${{ hashFiles('package-lock.json') }}-${{ hashFiles('.gitignore') }} 31 | - run: npx prettier --ignore-path .gitignore --cache --write . 32 | - run: git restore .github/workflows 33 | - uses: actions/create-github-app-token@v2 34 | id: app-token 35 | with: 36 | app-id: ${{ secrets.ECOSPARK_APP_ID }} 37 | private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }} 38 | - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7 39 | with: 40 | author: github-actions <41898282+github-actions[bot]@users.noreply.github.com> 41 | body: I ran `npx prettier --ignore-path .gitignore --cache --write .` 🧑‍💻 42 | branch: actions/prettier 43 | commit-message: "chore(prettier): 🤖 ✨" 44 | labels: 🤖 bot 45 | sign-commits: true 46 | title: "chore(prettier): 🤖 ✨" 47 | token: ${{ steps.app-token.outputs.token }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Built sources 11 | /dist 12 | 13 | # Coverage directory used by tools like c8 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependencies 23 | node_modules 24 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.enablePaths": ["test-deno"] 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2025, Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | // Necessary for `get-it/middleware` imports to work with setups not setup to be ESM native, like older `jest` configs. 2 | module.exports = require('./dist/middleware.cjs') // eslint-disable-line @typescript-eslint/no-require-imports 3 | -------------------------------------------------------------------------------- /modules.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module 'tunnel-agent' { 3 | export function httpOverHttp(options: any): any 4 | export function httpsOverHttp(options: any): any 5 | export function httpOverHttps(options: any): any 6 | export function httpsOverHttps(options: any): any 7 | } 8 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | tsconfig: 'tsconfig.dist.json', 5 | bundles: [ 6 | { 7 | source: './src/index.react-server.ts', 8 | import: './dist/index.react-server.js', 9 | }, 10 | ], 11 | // Setting up Terser here to workaround a issue where Next.js fails to import from a ESM file that has a reference to `module.exports` somewhere in the file: 12 | // https://github.com/vercel/next.js/issues/57962 13 | // By enabling minification the problematic reference is changed so the issue is avoided. 14 | // The reason this happens is because we're inlining the `debug` module, which is CJS only. We can't stop inlining this module as it breaks Hydrogen compatibility. 15 | minify: true, 16 | }) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-it", 3 | "version": "8.6.10", 4 | "description": "Generic HTTP request library for node, browsers and workers", 5 | "keywords": [ 6 | "request", 7 | "http", 8 | "fetch" 9 | ], 10 | "homepage": "https://github.com/sanity-io/get-it#readme", 11 | "bugs": { 12 | "url": "https://github.com/sanity-io/get-it/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/sanity-io/get-it.git" 17 | }, 18 | "license": "MIT", 19 | "author": "Sanity.io ", 20 | "sideEffects": false, 21 | "type": "module", 22 | "exports": { 23 | ".": { 24 | "source": "./src/index.ts", 25 | "browser": { 26 | "source": "./src/index.browser.ts", 27 | "import": "./dist/index.browser.js", 28 | "require": "./dist/index.browser.cjs" 29 | }, 30 | "react-native": { 31 | "import": "./dist/index.browser.js", 32 | "require": "./dist/index.browser.cjs" 33 | }, 34 | "react-server": "./dist/index.react-server.js", 35 | "bun": "./dist/index.browser.js", 36 | "deno": "./dist/index.browser.js", 37 | "edge-light": "./dist/index.browser.js", 38 | "worker": "./dist/index.browser.js", 39 | "sanity-function": "./dist/index.browser.js", 40 | "import": "./dist/index.js", 41 | "require": "./dist/index.cjs", 42 | "default": "./dist/index.js" 43 | }, 44 | "./middleware": { 45 | "source": "./src/middleware.ts", 46 | "browser": { 47 | "source": "./src/middleware.browser.ts", 48 | "import": "./dist/middleware.browser.js", 49 | "require": "./dist/middleware.browser.cjs" 50 | }, 51 | "react-native": { 52 | "import": "./dist/middleware.browser.js", 53 | "require": "./dist/middleware.browser.cjs" 54 | }, 55 | "react-server": "./dist/middleware.browser.js", 56 | "bun": "./dist/middleware.browser.js", 57 | "deno": "./dist/middleware.browser.js", 58 | "edge-light": "./dist/middleware.browser.js", 59 | "worker": "./dist/middleware.browser.js", 60 | "sanity-function": "./dist/middleware.browser.js", 61 | "import": "./dist/middleware.js", 62 | "require": "./dist/middleware.cjs", 63 | "default": "./dist/middleware.js" 64 | }, 65 | "./package.json": "./package.json" 66 | }, 67 | "main": "./dist/index.cjs", 68 | "module": "./dist/index.js", 69 | "browser": { 70 | "./dist/index.cjs": "./dist/index.browser.cjs", 71 | "./dist/index.js": "./dist/index.browser.js", 72 | "./dist/middleware.cjs": "./dist/middleware.browser.cjs", 73 | "./dist/middleware.js": "./dist/middleware.browser.js" 74 | }, 75 | "types": "./dist/index.d.ts", 76 | "typesVersions": { 77 | "*": { 78 | "middleware": [ 79 | "./dist/middleware.d.ts" 80 | ] 81 | } 82 | }, 83 | "files": [ 84 | "dist", 85 | "src", 86 | "middleware.js" 87 | ], 88 | "scripts": { 89 | "build": "pkg build --strict --check --clean", 90 | "coverage": "vitest run --coverage", 91 | "lint": "eslint . --ext .cjs,.js,.ts --report-unused-disable-directives", 92 | "prepublishOnly": "npm run build", 93 | "test": "vitest", 94 | "test:browser": "npm test -- --config ./vitest.browser.config.ts --dom", 95 | "test:edge-runtime": "npm test -- --config ./vitest.edge.config.ts", 96 | "test:esm": "(cd test-esm && node --test) | faucet", 97 | "test:esm:browser": "node -C browser --test test-esm/test.mjs | faucet", 98 | "test:esm:deno": "deno test --allow-read --allow-net --allow-env --import-map=test-deno/import_map.json test-deno", 99 | "test:react-server": "npm test -- --config ./vitest.react-server.config.ts", 100 | "typecheck": "tsc --noEmit" 101 | }, 102 | "browserslist": "extends @sanity/browserslist-config", 103 | "prettier": "@sanity/prettier-config", 104 | "dependencies": { 105 | "@types/follow-redirects": "^1.14.4", 106 | "decompress-response": "^7.0.0", 107 | "follow-redirects": "^1.15.9", 108 | "is-retry-allowed": "^2.2.0", 109 | "through2": "^4.0.2", 110 | "tunnel-agent": "^0.6.0" 111 | }, 112 | "devDependencies": { 113 | "@edge-runtime/vm": "^5.0.0", 114 | "@sanity/pkg-utils": "^7.9.0", 115 | "@sanity/prettier-config": "^2.0.0", 116 | "@sanity/semantic-release-preset": "^5.0.0", 117 | "@types/bun": "^1.2.17", 118 | "@types/debug": "^4.1.12", 119 | "@types/node": "^20.8.8", 120 | "@types/through2": "^2.0.41", 121 | "@types/zen-observable": "^0.8.7", 122 | "@typescript-eslint/eslint-plugin": "^8.35.1", 123 | "@typescript-eslint/parser": "^8.35.1", 124 | "@vitest/coverage-v8": "^3.2.4", 125 | "debug": "4.4.1", 126 | "eslint": "^8.57.1", 127 | "eslint-config-prettier": "^10.1.5", 128 | "eslint-plugin-prettier": "^5.5.1", 129 | "eslint-plugin-simple-import-sort": "^12.1.1", 130 | "faucet": "^0.0.4", 131 | "get-uri": "^6.0.4", 132 | "happy-dom": "^18.0.1", 133 | "ls-engines": "^0.9.3", 134 | "node-fetch": "^2.6.7", 135 | "parse-headers": "2.0.6", 136 | "prettier": "^3.6.2", 137 | "semantic-release": "^24.2.6", 138 | "typescript": "5.8.3", 139 | "vite": "^7.0.0", 140 | "vitest": "^3.2.4", 141 | "zen-observable": "^0.10.0" 142 | }, 143 | "packageManager": "npm@11.4.2", 144 | "engines": { 145 | "node": ">=14.0.0" 146 | }, 147 | "publishConfig": { 148 | "access": "public" 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/createRequester.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import {processOptions} from './middleware/defaultOptionsProcessor' 3 | import {validateOptions} from './middleware/defaultOptionsValidator' 4 | import type { 5 | HttpContext, 6 | HttpRequest, 7 | HttpRequestOngoing, 8 | Middleware, 9 | MiddlewareChannels, 10 | MiddlewareHooks, 11 | MiddlewareReducer, 12 | MiddlewareResponse, 13 | Middlewares, 14 | Requester, 15 | RequestOptions, 16 | } from './types' 17 | import {middlewareReducer} from './util/middlewareReducer' 18 | import {createPubSub} from './util/pubsub' 19 | 20 | const channelNames = [ 21 | 'request', 22 | 'response', 23 | 'progress', 24 | 'error', 25 | 'abort', 26 | ] satisfies (keyof MiddlewareChannels)[] 27 | const middlehooks = [ 28 | 'processOptions', 29 | 'validateOptions', 30 | 'interceptRequest', 31 | 'finalizeOptions', 32 | 'onRequest', 33 | 'onResponse', 34 | 'onError', 35 | 'onReturn', 36 | 'onHeaders', 37 | ] satisfies (keyof MiddlewareHooks)[] 38 | 39 | /** @public */ 40 | export function createRequester(initMiddleware: Middlewares, httpRequest: HttpRequest): Requester { 41 | const loadedMiddleware: Middlewares = [] 42 | const middleware: MiddlewareReducer = middlehooks.reduce( 43 | (ware, name) => { 44 | ware[name] = ware[name] || [] 45 | return ware 46 | }, 47 | { 48 | processOptions: [processOptions], 49 | validateOptions: [validateOptions], 50 | } as any, 51 | ) 52 | 53 | function request(opts: RequestOptions | string) { 54 | const onResponse = (reqErr: Error | null, res: MiddlewareResponse, ctx: HttpContext) => { 55 | let error = reqErr 56 | let response: MiddlewareResponse | null = res 57 | 58 | // We're processing non-errors first, in case a middleware converts the 59 | // response into an error (for instance, status >= 400 == HttpError) 60 | if (!error) { 61 | try { 62 | response = applyMiddleware('onResponse', res, ctx) 63 | } catch (err: any) { 64 | response = null 65 | error = err 66 | } 67 | } 68 | 69 | // Apply error middleware - if middleware return the same (or a different) error, 70 | // publish as an error event. If we *don't* return an error, assume it has been handled 71 | error = error && applyMiddleware('onError', error, ctx) 72 | 73 | // Figure out if we should publish on error/response channels 74 | if (error) { 75 | channels.error.publish(error) 76 | } else if (response) { 77 | channels.response.publish(response) 78 | } 79 | } 80 | 81 | const channels: MiddlewareChannels = channelNames.reduce((target, name) => { 82 | target[name] = createPubSub() as MiddlewareChannels[typeof name] 83 | return target 84 | }, {} as any) 85 | 86 | // Prepare a middleware reducer that can be reused throughout the lifecycle 87 | const applyMiddleware = middlewareReducer(middleware) 88 | 89 | // Parse the passed options 90 | const options = applyMiddleware('processOptions', opts as RequestOptions) 91 | 92 | // Validate the options 93 | applyMiddleware('validateOptions', options) 94 | 95 | // Build a context object we can pass to child handlers 96 | const context = {options, channels, applyMiddleware} 97 | 98 | // We need to hold a reference to the current, ongoing request, 99 | // in order to allow cancellation. In the case of the retry middleware, 100 | // a new request might be triggered 101 | let ongoingRequest: HttpRequestOngoing | undefined 102 | const unsubscribe = channels.request.subscribe((ctx) => { 103 | // Let request adapters (node/browser) perform the actual request 104 | ongoingRequest = httpRequest(ctx, (err, res) => onResponse(err, res!, ctx)) 105 | }) 106 | 107 | // If we abort the request, prevent further requests from happening, 108 | // and be sure to cancel any ongoing request (obviously) 109 | channels.abort.subscribe(() => { 110 | unsubscribe() 111 | if (ongoingRequest) { 112 | ongoingRequest.abort() 113 | } 114 | }) 115 | 116 | // See if any middleware wants to modify the return value - for instance 117 | // the promise or observable middlewares 118 | const returnValue = applyMiddleware('onReturn', channels, context) 119 | 120 | // If return value has been modified by a middleware, we expect the middleware 121 | // to publish on the 'request' channel. If it hasn't been modified, we want to 122 | // trigger it right away 123 | if (returnValue === channels) { 124 | channels.request.publish(context) 125 | } 126 | 127 | return returnValue 128 | } 129 | 130 | request.use = function use(newMiddleware: Middleware) { 131 | if (!newMiddleware) { 132 | throw new Error('Tried to add middleware that resolved to falsey value') 133 | } 134 | 135 | if (typeof newMiddleware === 'function') { 136 | throw new Error( 137 | 'Tried to add middleware that was a function. It probably expects you to pass options to it.', 138 | ) 139 | } 140 | 141 | if (newMiddleware.onReturn && middleware.onReturn.length > 0) { 142 | throw new Error( 143 | 'Tried to add new middleware with `onReturn` handler, but another handler has already been registered for this event', 144 | ) 145 | } 146 | 147 | middlehooks.forEach((key) => { 148 | if (newMiddleware[key]) { 149 | middleware[key].push(newMiddleware[key] as any) 150 | } 151 | }) 152 | 153 | loadedMiddleware.push(newMiddleware) 154 | return request 155 | } 156 | 157 | request.clone = () => createRequester(loadedMiddleware, httpRequest) 158 | 159 | initMiddleware.forEach(request.use) 160 | 161 | return request 162 | } 163 | -------------------------------------------------------------------------------- /src/index.browser.ts: -------------------------------------------------------------------------------- 1 | import {createRequester} from './createRequester' 2 | import {httpRequester} from './request/browser-request' 3 | import type {ExportEnv, HttpRequest, Middlewares, Requester} from './types' 4 | 5 | export type * from './types' 6 | 7 | /** @public */ 8 | export const getIt = ( 9 | initMiddleware: Middlewares = [], 10 | httpRequest: HttpRequest = httpRequester, 11 | ): Requester => createRequester(initMiddleware, httpRequest) 12 | 13 | /** @public */ 14 | export const environment = 'browser' satisfies ExportEnv 15 | 16 | /** @public */ 17 | export {adapter} from './request/browser-request' 18 | -------------------------------------------------------------------------------- /src/index.react-server.ts: -------------------------------------------------------------------------------- 1 | import type {ExportEnv} from './types' 2 | 3 | export * from './index.browser' 4 | 5 | /** @public */ 6 | export const environment = 'react-server' satisfies ExportEnv 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {createRequester} from './createRequester' 2 | import {httpRequester} from './request/node-request' 3 | import type {ExportEnv, HttpRequest, Middlewares, Requester} from './types' 4 | 5 | export type * from './types' 6 | 7 | /** @public */ 8 | export const getIt = ( 9 | initMiddleware: Middlewares = [], 10 | httpRequest: HttpRequest = httpRequester, 11 | ): Requester => createRequester(initMiddleware, httpRequest) 12 | 13 | /** @public */ 14 | export const environment: ExportEnv = 'node' 15 | 16 | /** @public */ 17 | export {adapter} from './request/node-request' 18 | -------------------------------------------------------------------------------- /src/middleware.browser.ts: -------------------------------------------------------------------------------- 1 | export * from './middleware/agent/browser-agent' 2 | export * from './middleware/base' 3 | export * from './middleware/debug' 4 | export * from './middleware/defaultOptionsProcessor' 5 | export * from './middleware/defaultOptionsValidator' 6 | export * from './middleware/headers' 7 | export * from './middleware/httpErrors' 8 | export * from './middleware/injectResponse' 9 | export * from './middleware/jsonRequest' 10 | export * from './middleware/jsonResponse' 11 | export * from './middleware/mtls' 12 | export * from './middleware/observable' 13 | export * from './middleware/progress/browser-progress' 14 | export * from './middleware/promise' 15 | export * from './middleware/proxy' 16 | export * from './middleware/retry/browser-retry' 17 | export * from './middleware/urlEncoded' 18 | 19 | import {agent} from './middleware/agent/browser-agent' 20 | import {buildKeepAlive} from './middleware/keepAlive' 21 | /** @public */ 22 | export const keepAlive = buildKeepAlive(agent) 23 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | export * from './middleware/agent/node-agent' 2 | export * from './middleware/base' 3 | export * from './middleware/debug' 4 | export * from './middleware/defaultOptionsProcessor' 5 | export * from './middleware/defaultOptionsValidator' 6 | export * from './middleware/headers' 7 | export * from './middleware/httpErrors' 8 | export * from './middleware/injectResponse' 9 | export * from './middleware/jsonRequest' 10 | export * from './middleware/jsonResponse' 11 | export * from './middleware/mtls' 12 | export * from './middleware/observable' 13 | export * from './middleware/progress/node-progress' 14 | export * from './middleware/promise' 15 | export * from './middleware/proxy' 16 | export * from './middleware/retry/node-retry' 17 | export * from './middleware/urlEncoded' 18 | 19 | import {agent} from './middleware/agent/node-agent' 20 | import {buildKeepAlive} from './middleware/keepAlive' 21 | /** @public */ 22 | export const keepAlive = buildKeepAlive(agent) 23 | -------------------------------------------------------------------------------- /src/middleware/agent/browser-agent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This middleware only has an effect in Node.js. 3 | * @public 4 | */ 5 | export function agent( 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | _opts?: any, 8 | ): any { 9 | return {} 10 | } 11 | -------------------------------------------------------------------------------- /src/middleware/agent/node-agent.ts: -------------------------------------------------------------------------------- 1 | import {Agent as HttpAgent, type AgentOptions} from 'http' 2 | import {Agent as HttpsAgent} from 'https' 3 | import {type Middleware} from 'get-it' 4 | 5 | const isHttpsProto = /^https:/i 6 | 7 | /** 8 | * Constructs a http.Agent and uses it for all requests. 9 | * This can be used to override settings such as `maxSockets`, `maxTotalSockets` (to limit concurrency) or change the `timeout`. 10 | * @public 11 | */ 12 | export function agent(opts?: AgentOptions) { 13 | const httpAgent = new HttpAgent(opts) 14 | const httpsAgent = new HttpsAgent(opts) 15 | const agents = {http: httpAgent, https: httpsAgent} 16 | 17 | return { 18 | finalizeOptions: (options: any) => { 19 | if (options.agent) { 20 | return options 21 | } 22 | 23 | // When maxRedirects>0 we're using the follow-redirects package and this supports the `agents` option. 24 | if (options.maxRedirects > 0) { 25 | return {...options, agents} 26 | } 27 | 28 | // ... otherwise we'll have to detect which agent to use: 29 | const isHttps = isHttpsProto.test(options.href || options.protocol) 30 | return {...options, agent: isHttps ? httpsAgent : httpAgent} 31 | }, 32 | } satisfies Middleware 33 | } 34 | -------------------------------------------------------------------------------- /src/middleware/base.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | const leadingSlash = /^\// 4 | const trailingSlash = /\/$/ 5 | 6 | /** @public */ 7 | export function base(baseUrl: string) { 8 | const baseUri = baseUrl.replace(trailingSlash, '') 9 | return { 10 | processOptions: (options) => { 11 | if (/^https?:\/\//i.test(options.url)) { 12 | return options // Already prefixed 13 | } 14 | 15 | const url = [baseUri, options.url.replace(leadingSlash, '')].join('/') 16 | return Object.assign({}, options, {url}) 17 | }, 18 | } satisfies Middleware 19 | } 20 | -------------------------------------------------------------------------------- /src/middleware/debug.ts: -------------------------------------------------------------------------------- 1 | import debugIt from 'debug' 2 | import type {Middleware} from 'get-it' 3 | 4 | const SENSITIVE_HEADERS = ['cookie', 'authorization'] 5 | 6 | const hasOwn = Object.prototype.hasOwnProperty 7 | const redactKeys = (source: any, redacted: any) => { 8 | const target: any = {} 9 | for (const key in source) { 10 | if (hasOwn.call(source, key)) { 11 | target[key] = redacted.indexOf(key.toLowerCase()) > -1 ? '' : source[key] 12 | } 13 | } 14 | return target 15 | } 16 | 17 | /** @public */ 18 | export function debug(opts: any = {}) { 19 | const verbose = opts.verbose 20 | const namespace = opts.namespace || 'get-it' 21 | const defaultLogger = debugIt(namespace) 22 | const log = opts.log || defaultLogger 23 | const shortCircuit = log === defaultLogger && !debugIt.enabled(namespace) 24 | let requestId = 0 25 | 26 | return { 27 | processOptions: (options) => { 28 | options.debug = log 29 | options.requestId = options.requestId || ++requestId 30 | return options 31 | }, 32 | 33 | onRequest: (event) => { 34 | // Short-circuit if not enabled, to save some CPU cycles with formatting stuff 35 | if (shortCircuit || !event) { 36 | return event 37 | } 38 | 39 | const options = event.options 40 | 41 | log('[%s] HTTP %s %s', options.requestId, options.method, options.url) 42 | 43 | if (verbose && options.body && typeof options.body === 'string') { 44 | log('[%s] Request body: %s', options.requestId, options.body) 45 | } 46 | 47 | if (verbose && options.headers) { 48 | const headers = 49 | opts.redactSensitiveHeaders === false 50 | ? options.headers 51 | : redactKeys(options.headers, SENSITIVE_HEADERS) 52 | 53 | log('[%s] Request headers: %s', options.requestId, JSON.stringify(headers, null, 2)) 54 | } 55 | 56 | return event 57 | }, 58 | 59 | onResponse: (res, context) => { 60 | // Short-circuit if not enabled, to save some CPU cycles with formatting stuff 61 | if (shortCircuit || !res) { 62 | return res 63 | } 64 | 65 | const reqId = context.options.requestId 66 | 67 | log('[%s] Response code: %s %s', reqId, res.statusCode, res.statusMessage) 68 | 69 | if (verbose && res.body) { 70 | log('[%s] Response body: %s', reqId, stringifyBody(res)) 71 | } 72 | 73 | return res 74 | }, 75 | 76 | onError: (err, context) => { 77 | const reqId = context.options.requestId 78 | if (!err) { 79 | log('[%s] Error encountered, but handled by an earlier middleware', reqId) 80 | return err 81 | } 82 | 83 | log('[%s] ERROR: %s', reqId, err.message) 84 | return err 85 | }, 86 | } satisfies Middleware 87 | } 88 | 89 | function stringifyBody(res: any) { 90 | const contentType = (res.headers['content-type'] || '').toLowerCase() 91 | const isJson = contentType.indexOf('application/json') !== -1 92 | return isJson ? tryFormat(res.body) : res.body 93 | } 94 | 95 | // Attempt pretty-formatting JSON 96 | function tryFormat(body: any) { 97 | try { 98 | const parsed = typeof body === 'string' ? JSON.parse(body) : body 99 | return JSON.stringify(parsed, null, 2) 100 | } catch { 101 | return body 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/middleware/defaultOptionsProcessor.ts: -------------------------------------------------------------------------------- 1 | import type {MiddlewareHooks, RequestOptions} from 'get-it' 2 | 3 | const isReactNative = typeof navigator === 'undefined' ? false : navigator.product === 'ReactNative' 4 | 5 | const defaultOptions = {timeout: isReactNative ? 60000 : 120000} satisfies Partial 6 | 7 | /** @public */ 8 | export const processOptions = function processOptions(opts) { 9 | const options = { 10 | ...defaultOptions, 11 | ...(typeof opts === 'string' ? {url: opts} : opts), 12 | } satisfies RequestOptions 13 | 14 | // Normalize timeouts 15 | options.timeout = normalizeTimeout(options.timeout) 16 | 17 | // Shallow-merge (override) existing query params 18 | if (options.query) { 19 | const {url, searchParams} = splitUrl(options.url) 20 | 21 | for (const [key, value] of Object.entries(options.query)) { 22 | if (value !== undefined) { 23 | if (Array.isArray(value)) { 24 | for (const v of value) { 25 | searchParams.append(key, v as string) 26 | } 27 | } else { 28 | searchParams.append(key, value as string) 29 | } 30 | } 31 | 32 | // Merge back params into url 33 | const search = searchParams.toString() 34 | if (search) { 35 | options.url = `${url}?${search}` 36 | } 37 | } 38 | } 39 | 40 | // Implicit POST if we have not specified a method but have a body 41 | options.method = 42 | options.body && !options.method ? 'POST' : (options.method || 'GET').toUpperCase() 43 | 44 | return options 45 | } satisfies MiddlewareHooks['processOptions'] 46 | 47 | /** 48 | * Given a string URL, extracts the query string and URL from each other, and returns them. 49 | * Note that we cannot use the `URL` constructor because of old React Native versions which are 50 | * majorly broken and returns incorrect results: 51 | * 52 | * (`new URL('http://foo/?a=b').toString()` == 'http://foo/?a=b/') 53 | */ 54 | function splitUrl(url: string): {url: string; searchParams: URLSearchParams} { 55 | const qIndex = url.indexOf('?') 56 | if (qIndex === -1) { 57 | return {url, searchParams: new URLSearchParams()} 58 | } 59 | 60 | const base = url.slice(0, qIndex) 61 | const qs = url.slice(qIndex + 1) 62 | 63 | // React Native's URL and URLSearchParams are broken, so passing a string to URLSearchParams 64 | // does not work, leading to an empty query string. For other environments, this should be enough 65 | if (!isReactNative) { 66 | return {url: base, searchParams: new URLSearchParams(qs)} 67 | } 68 | 69 | // Sanity-check; we do not know of any environment where this is the case, 70 | // but if it is, we should not proceed without giving a descriptive error 71 | if (typeof decodeURIComponent !== 'function') { 72 | throw new Error( 73 | 'Broken `URLSearchParams` implementation, and `decodeURIComponent` is not defined', 74 | ) 75 | } 76 | 77 | const params = new URLSearchParams() 78 | for (const pair of qs.split('&')) { 79 | const [key, value] = pair.split('=') 80 | if (key) { 81 | params.append(decodeQueryParam(key), decodeQueryParam(value || '')) 82 | } 83 | } 84 | 85 | return {url: base, searchParams: params} 86 | } 87 | 88 | function decodeQueryParam(value: string): string { 89 | return decodeURIComponent(value.replace(/\+/g, ' ')) 90 | } 91 | 92 | function normalizeTimeout(time: RequestOptions['timeout']) { 93 | if (time === false || time === 0) { 94 | return false 95 | } 96 | 97 | if (time.connect || time.socket) { 98 | return time 99 | } 100 | 101 | const delay = Number(time) 102 | if (isNaN(delay)) { 103 | return normalizeTimeout(defaultOptions.timeout) 104 | } 105 | 106 | return {connect: delay, socket: delay} 107 | } 108 | -------------------------------------------------------------------------------- /src/middleware/defaultOptionsValidator.ts: -------------------------------------------------------------------------------- 1 | import type {MiddlewareHooks} from 'get-it' 2 | 3 | const validUrl = /^https?:\/\//i 4 | 5 | /** @public */ 6 | export const validateOptions = function validateOptions(options) { 7 | if (!validUrl.test(options.url)) { 8 | throw new Error(`"${options.url}" is not a valid URL`) 9 | } 10 | } satisfies MiddlewareHooks['validateOptions'] 11 | -------------------------------------------------------------------------------- /src/middleware/headers.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | /** @public */ 4 | export function headers(_headers: any, opts: any = {}) { 5 | return { 6 | processOptions: (options) => { 7 | const existing = options.headers || {} 8 | options.headers = opts.override 9 | ? Object.assign({}, existing, _headers) 10 | : Object.assign({}, _headers, existing) 11 | 12 | return options 13 | }, 14 | } satisfies Middleware 15 | } 16 | -------------------------------------------------------------------------------- /src/middleware/httpErrors.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | class HttpError extends Error { 4 | response: any 5 | request: any 6 | constructor(res: any, ctx: any) { 7 | super() 8 | const truncatedUrl = res.url.length > 400 ? `${res.url.slice(0, 399)}…` : res.url 9 | let msg = `${res.method}-request to ${truncatedUrl} resulted in ` 10 | msg += `HTTP ${res.statusCode} ${res.statusMessage}` 11 | 12 | this.message = msg.trim() 13 | this.response = res 14 | this.request = ctx.options 15 | } 16 | } 17 | 18 | /** @public */ 19 | export function httpErrors() { 20 | return { 21 | onResponse: (res, ctx) => { 22 | const isHttpError = res.statusCode >= 400 23 | if (!isHttpError) { 24 | return res 25 | } 26 | 27 | throw new HttpError(res, ctx) 28 | }, 29 | } satisfies Middleware 30 | } 31 | -------------------------------------------------------------------------------- /src/middleware/injectResponse.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware, MiddlewareHooks, MiddlewareResponse} from 'get-it' 2 | 3 | /** @public */ 4 | export function injectResponse( 5 | opts: { 6 | inject: ( 7 | event: Parameters[1], 8 | prevValue: Parameters[0], 9 | ) => Partial 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | } = {} as any, 12 | ) { 13 | if (typeof opts.inject !== 'function') { 14 | throw new Error('`injectResponse` middleware requires a `inject` function') 15 | } 16 | 17 | const inject = function inject(prevValue, event) { 18 | const response = opts.inject(event, prevValue) 19 | if (!response) { 20 | return prevValue 21 | } 22 | 23 | // Merge defaults so we don't have to provide the most basic of details unless we want to 24 | const options = event.context.options 25 | return { 26 | body: '', 27 | url: options.url, 28 | method: options.method!, 29 | headers: {}, 30 | statusCode: 200, 31 | statusMessage: 'OK', 32 | ...response, 33 | } satisfies MiddlewareResponse 34 | } satisfies Middleware['interceptRequest'] 35 | 36 | return {interceptRequest: inject} satisfies Middleware 37 | } 38 | -------------------------------------------------------------------------------- /src/middleware/jsonRequest.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | import {isBuffer} from '../util/isBuffer' 4 | import {isPlainObject} from '../util/isPlainObject' 5 | 6 | const serializeTypes = ['boolean', 'string', 'number'] 7 | 8 | /** @public */ 9 | export function jsonRequest() { 10 | return { 11 | processOptions: (options) => { 12 | const body = options.body 13 | if (!body) { 14 | return options 15 | } 16 | 17 | const isStream = typeof body.pipe === 'function' 18 | const shouldSerialize = 19 | !isStream && 20 | !isBuffer(body) && 21 | (serializeTypes.indexOf(typeof body) !== -1 || Array.isArray(body) || isPlainObject(body)) 22 | 23 | if (!shouldSerialize) { 24 | return options 25 | } 26 | 27 | return Object.assign({}, options, { 28 | body: JSON.stringify(options.body), 29 | headers: Object.assign({}, options.headers, { 30 | 'Content-Type': 'application/json', 31 | }), 32 | }) 33 | }, 34 | } satisfies Middleware 35 | } 36 | -------------------------------------------------------------------------------- /src/middleware/jsonResponse.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | /** @public */ 4 | export function jsonResponse(opts?: any) { 5 | return { 6 | onResponse: (response) => { 7 | const contentType = response.headers['content-type'] || '' 8 | const shouldDecode = (opts && opts.force) || contentType.indexOf('application/json') !== -1 9 | if (!response.body || !contentType || !shouldDecode) { 10 | return response 11 | } 12 | 13 | return Object.assign({}, response, {body: tryParse(response.body)}) 14 | }, 15 | 16 | processOptions: (options) => 17 | Object.assign({}, options, { 18 | headers: Object.assign({Accept: 'application/json'}, options.headers), 19 | }), 20 | } satisfies Middleware 21 | 22 | function tryParse(body: any) { 23 | try { 24 | return JSON.parse(body) 25 | } catch (err: any) { 26 | err.message = `Failed to parsed response body as JSON: ${err.message}` 27 | throw err 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/middleware/keepAlive.ts: -------------------------------------------------------------------------------- 1 | import type {AgentOptions} from 'http' 2 | import type {Middleware} from 'get-it' 3 | 4 | import {NodeRequestError} from '../request/node-request' 5 | 6 | type KeepAliveOptions = { 7 | ms?: number 8 | maxFree?: number 9 | 10 | /** 11 | How many times to retry in case of ECONNRESET error. Default: 3 12 | */ 13 | maxRetries?: number 14 | } 15 | 16 | export function buildKeepAlive(agent: (opts: AgentOptions) => Pick) { 17 | return function keepAlive(config: KeepAliveOptions = {}): any { 18 | const {maxRetries = 3, ms = 1000, maxFree = 256} = config 19 | 20 | const {finalizeOptions} = agent({ 21 | keepAlive: true, 22 | keepAliveMsecs: ms, 23 | maxFreeSockets: maxFree, 24 | }) 25 | 26 | return { 27 | finalizeOptions, 28 | onError: (err, context) => { 29 | // When sending request through a keep-alive enabled agent, the underlying socket might be reused. But if server closes connection at unfortunate time, client may run into a 'ECONNRESET' error. 30 | // We retry three times in case of ECONNRESET error. 31 | // https://nodejs.org/docs/latest-v20.x/api/http.html#requestreusedsocket 32 | if ( 33 | (context.options.method === 'GET' || context.options.method === 'POST') && 34 | err instanceof NodeRequestError && 35 | err.code === 'ECONNRESET' && 36 | err.request.reusedSocket 37 | ) { 38 | const attemptNumber = context.options.attemptNumber || 0 39 | if (attemptNumber < maxRetries) { 40 | // Create a new context with an increased attempt number, so we can exit if we reach a limit 41 | const newContext = Object.assign({}, context, { 42 | options: Object.assign({}, context.options, {attemptNumber: attemptNumber + 1}), 43 | }) 44 | // If this is a reused socket we retry immediately 45 | setImmediate(() => context.channels.request.publish(newContext)) 46 | 47 | return null 48 | } 49 | } 50 | 51 | return err 52 | }, 53 | } satisfies Middleware 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/middleware/mtls.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | import {isBrowserOptions} from '../util/isBrowserOptions' 4 | 5 | /** @public */ 6 | export function mtls(config: any = {}) { 7 | if (!config.ca) { 8 | throw new Error('Required mtls option "ca" is missing') 9 | } 10 | if (!config.cert) { 11 | throw new Error('Required mtls option "cert" is missing') 12 | } 13 | if (!config.key) { 14 | throw new Error('Required mtls option "key" is missing') 15 | } 16 | 17 | return { 18 | finalizeOptions: (options) => { 19 | if (isBrowserOptions(options)) { 20 | return options 21 | } 22 | 23 | const mtlsOpts = { 24 | cert: config.cert, 25 | key: config.key, 26 | ca: config.ca, 27 | } 28 | return Object.assign({}, options, mtlsOpts) 29 | }, 30 | } satisfies Middleware 31 | } 32 | -------------------------------------------------------------------------------- /src/middleware/observable.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | import global from '../util/global' 4 | 5 | /** @public */ 6 | export function observable( 7 | opts: { 8 | implementation?: any 9 | } = {}, 10 | ) { 11 | const Observable = 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- @TODO consider dropping checking for a global Observable since it's not on a standards track 13 | opts.implementation || (global as any).Observable 14 | if (!Observable) { 15 | throw new Error( 16 | '`Observable` is not available in global scope, and no implementation was passed', 17 | ) 18 | } 19 | 20 | return { 21 | onReturn: (channels, context) => 22 | new Observable((observer: any) => { 23 | channels.error.subscribe((err) => observer.error(err)) 24 | channels.progress.subscribe((event) => 25 | observer.next(Object.assign({type: 'progress'}, event)), 26 | ) 27 | channels.response.subscribe((response) => { 28 | observer.next(Object.assign({type: 'response'}, response)) 29 | observer.complete() 30 | }) 31 | 32 | channels.request.publish(context) 33 | return () => channels.abort.publish() 34 | }), 35 | } satisfies Middleware 36 | } 37 | -------------------------------------------------------------------------------- /src/middleware/progress/browser-progress.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | /** @public */ 4 | export function progress() { 5 | return { 6 | onRequest: (evt) => { 7 | if (evt.adapter !== 'xhr') { 8 | return 9 | } 10 | 11 | const xhr = evt.request 12 | const context = evt.context 13 | 14 | if ('upload' in xhr && 'onprogress' in xhr.upload) { 15 | xhr.upload.onprogress = handleProgress('upload') 16 | } 17 | 18 | if ('onprogress' in xhr) { 19 | xhr.onprogress = handleProgress('download') 20 | } 21 | 22 | function handleProgress(stage: 'download' | 'upload') { 23 | return (event: any) => { 24 | const percent = event.lengthComputable ? (event.loaded / event.total) * 100 : -1 25 | context.channels.progress.publish({ 26 | stage, 27 | percent, 28 | total: event.total, 29 | loaded: event.loaded, 30 | lengthComputable: event.lengthComputable, 31 | }) 32 | } 33 | } 34 | }, 35 | } satisfies Middleware 36 | } 37 | -------------------------------------------------------------------------------- /src/middleware/progress/node-progress.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | import {type Progress, progressStream} from '../../util/progress-stream' 4 | 5 | function normalizer(stage: 'download' | 'upload') { 6 | return (prog: Pick) => ({ 7 | stage, 8 | percent: prog.percentage, 9 | total: prog.length, 10 | loaded: prog.transferred, 11 | lengthComputable: !(prog.length === 0 && prog.percentage === 0), 12 | }) 13 | } 14 | 15 | /** @public */ 16 | export function progress() { 17 | let didEmitUpload = false 18 | const onDownload = normalizer('download') 19 | const onUpload = normalizer('upload') 20 | return { 21 | onHeaders: (response, evt) => { 22 | const stream = progressStream({time: 32}) 23 | 24 | stream.on('progress', (prog) => evt.context.channels.progress.publish(onDownload(prog))) 25 | return response.pipe(stream) 26 | }, 27 | 28 | onRequest: (evt) => { 29 | if (!evt.progress) { 30 | return 31 | } 32 | 33 | evt.progress.on('progress', (prog: Progress) => { 34 | didEmitUpload = true 35 | evt.context.channels.progress.publish(onUpload(prog)) 36 | }) 37 | }, 38 | 39 | onResponse: (res, evt) => { 40 | if (!didEmitUpload && typeof evt.options.body !== 'undefined') { 41 | evt.channels.progress.publish(onUpload({length: 0, transferred: 0, percentage: 100})) 42 | } 43 | 44 | return res 45 | }, 46 | } satisfies Middleware 47 | } 48 | -------------------------------------------------------------------------------- /src/middleware/promise.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | /** @public */ 4 | export const promise = ( 5 | options: {onlyBody?: boolean; implementation?: PromiseConstructor} = {}, 6 | ) => { 7 | const PromiseImplementation = options.implementation || Promise 8 | if (!PromiseImplementation) { 9 | throw new Error('`Promise` is not available in global scope, and no implementation was passed') 10 | } 11 | 12 | return { 13 | onReturn: (channels, context) => 14 | new PromiseImplementation((resolve, reject) => { 15 | const cancel = context.options.cancelToken 16 | if (cancel) { 17 | cancel.promise.then((reason: any) => { 18 | channels.abort.publish(reason) 19 | reject(reason) 20 | }) 21 | } 22 | 23 | channels.error.subscribe(reject) 24 | channels.response.subscribe((response) => { 25 | resolve(options.onlyBody ? (response as any).body : response) 26 | }) 27 | 28 | // Wait until next tick in case cancel has been performed 29 | setTimeout(() => { 30 | try { 31 | channels.request.publish(context) 32 | } catch (err) { 33 | reject(err) 34 | } 35 | }, 0) 36 | }), 37 | } satisfies Middleware 38 | } 39 | 40 | /** 41 | * The cancel token API is based on the [cancelable promises proposal](https://github.com/tc39/proposal-cancelable-promises), which is currently at Stage 1. 42 | * 43 | * Code shamelessly stolen/borrowed from MIT-licensed [axios](https://github.com/mzabriskie/axios). Thanks to [Nick Uraltsev](https://github.com/nickuraltsev), [Matt Zabriskie](https://github.com/mzabriskie) and the other contributors of that project! 44 | */ 45 | /** @public */ 46 | export class Cancel { 47 | __CANCEL__ = true 48 | 49 | message: string | undefined 50 | 51 | constructor(message: string | undefined) { 52 | this.message = message 53 | } 54 | 55 | toString() { 56 | return `Cancel${this.message ? `: ${this.message}` : ''}` 57 | } 58 | } 59 | 60 | /** @public */ 61 | export class CancelToken { 62 | promise: Promise 63 | reason?: Cancel 64 | 65 | constructor(executor: (cb: (message?: string) => void) => void) { 66 | if (typeof executor !== 'function') { 67 | throw new TypeError('executor must be a function.') 68 | } 69 | 70 | let resolvePromise: any = null 71 | 72 | this.promise = new Promise((resolve) => { 73 | resolvePromise = resolve 74 | }) 75 | 76 | executor((message?: string) => { 77 | if (this.reason) { 78 | // Cancellation has already been requested 79 | return 80 | } 81 | 82 | this.reason = new Cancel(message) 83 | resolvePromise(this.reason) 84 | }) 85 | } 86 | 87 | static source = () => { 88 | let cancel: (message?: string) => void 89 | const token = new CancelToken((can) => { 90 | cancel = can 91 | }) 92 | 93 | return { 94 | token: token, 95 | cancel: cancel!, 96 | } 97 | } 98 | } 99 | 100 | const isCancel = (value: any): value is Cancel => !!(value && value?.__CANCEL__) 101 | 102 | promise.Cancel = Cancel 103 | promise.CancelToken = CancelToken 104 | promise.isCancel = isCancel 105 | -------------------------------------------------------------------------------- /src/middleware/proxy.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | /** @public */ 4 | export function proxy(_proxy: any) { 5 | if (_proxy !== false && (!_proxy || !_proxy.host)) { 6 | throw new Error('Proxy middleware takes an object of host, port and auth properties') 7 | } 8 | 9 | return { 10 | processOptions: (options) => Object.assign({proxy: _proxy}, options), 11 | } satisfies Middleware 12 | } 13 | -------------------------------------------------------------------------------- /src/middleware/retry/browser-retry.ts: -------------------------------------------------------------------------------- 1 | import type {RetryOptions} from 'get-it' 2 | 3 | import defaultShouldRetry from '../../util/browser-shouldRetry' 4 | import sharedRetry from './shared-retry' 5 | 6 | /** @public */ 7 | export const retry = (opts: Partial = {}) => 8 | sharedRetry({shouldRetry: defaultShouldRetry, ...opts}) 9 | 10 | retry.shouldRetry = defaultShouldRetry 11 | -------------------------------------------------------------------------------- /src/middleware/retry/node-retry.ts: -------------------------------------------------------------------------------- 1 | import type {RetryOptions} from 'get-it' 2 | 3 | import defaultShouldRetry from '../../util/node-shouldRetry' 4 | import sharedRetry from './shared-retry' 5 | 6 | /** @public */ 7 | export const retry = (opts: Partial = {}) => 8 | sharedRetry({shouldRetry: defaultShouldRetry, ...opts}) 9 | 10 | retry.shouldRetry = defaultShouldRetry 11 | -------------------------------------------------------------------------------- /src/middleware/retry/shared-retry.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware, RetryOptions} from 'get-it' 2 | 3 | const isStream = (stream: any) => 4 | stream !== null && typeof stream === 'object' && typeof stream.pipe === 'function' 5 | 6 | /** @public */ 7 | export default (opts: RetryOptions) => { 8 | const maxRetries = opts.maxRetries || 5 9 | const retryDelay = opts.retryDelay || getRetryDelay 10 | const allowRetry = opts.shouldRetry 11 | 12 | return { 13 | onError: (err, context) => { 14 | const options = context.options 15 | const max = options.maxRetries || maxRetries 16 | const delay = options.retryDelay || retryDelay 17 | const shouldRetry = options.shouldRetry || allowRetry 18 | const attemptNumber = options.attemptNumber || 0 19 | 20 | // We can't retry if body is a stream, since it'll be drained 21 | if (isStream(options.body)) { 22 | return err 23 | } 24 | 25 | // Give up? 26 | if (!shouldRetry(err, attemptNumber, options) || attemptNumber >= max) { 27 | return err 28 | } 29 | 30 | // Create a new context with an increased attempt number, so we can exit if we reach a limit 31 | const newContext = Object.assign({}, context, { 32 | options: Object.assign({}, options, {attemptNumber: attemptNumber + 1}), 33 | }) 34 | 35 | // Wait a given amount of time before doing the request again 36 | setTimeout(() => context.channels.request.publish(newContext), delay(attemptNumber)) 37 | 38 | // Signal that we've handled the error and that it should not propagate further 39 | return null 40 | }, 41 | } satisfies Middleware 42 | } 43 | 44 | function getRetryDelay(attemptNum: number) { 45 | return 100 * Math.pow(2, attemptNum) + Math.random() * 100 46 | } 47 | -------------------------------------------------------------------------------- /src/middleware/urlEncoded.ts: -------------------------------------------------------------------------------- 1 | import type {Middleware} from 'get-it' 2 | 3 | import {isBuffer} from '../util/isBuffer' 4 | import {isPlainObject} from '../util/isPlainObject' 5 | 6 | function encode(data: Record>): string { 7 | const query = new URLSearchParams() 8 | 9 | const nest = (name: string, _value: unknown) => { 10 | const value = _value instanceof Set ? Array.from(_value) : _value 11 | if (Array.isArray(value)) { 12 | if (value.length) { 13 | for (const index in value) { 14 | nest(`${name}[${index}]`, value[index]) 15 | } 16 | } else { 17 | query.append(`${name}[]`, '') 18 | } 19 | } else if (typeof value === 'object' && value !== null) { 20 | for (const [key, obj] of Object.entries(value)) { 21 | nest(`${name}[${key}]`, obj) 22 | } 23 | } else { 24 | query.append(name, value as string) 25 | } 26 | } 27 | 28 | for (const [key, value] of Object.entries(data)) { 29 | nest(key, value) 30 | } 31 | 32 | return query.toString() 33 | } 34 | 35 | /** @public */ 36 | export function urlEncoded() { 37 | return { 38 | processOptions: (options) => { 39 | const body = options.body 40 | if (!body) { 41 | return options 42 | } 43 | 44 | const isStream = typeof body.pipe === 'function' 45 | const shouldSerialize = !isStream && !isBuffer(body) && isPlainObject(body) 46 | 47 | if (!shouldSerialize) { 48 | return options 49 | } 50 | 51 | return { 52 | ...options, 53 | body: encode(options.body), 54 | headers: { 55 | ...options.headers, 56 | 'Content-Type': 'application/x-www-form-urlencoded', 57 | }, 58 | } 59 | }, 60 | } satisfies Middleware 61 | } 62 | -------------------------------------------------------------------------------- /src/request/browser-request.ts: -------------------------------------------------------------------------------- 1 | import type {HttpRequest, MiddlewareResponse, RequestOptions} from 'get-it' 2 | import parseHeaders from 'parse-headers' 3 | import type {RequestAdapter} from '../types' 4 | 5 | import {FetchXhr} from './browser/fetchXhr' 6 | 7 | /** 8 | * Use fetch if it's available, non-browser environments such as Deno, Edge Runtime and more provide fetch as a global but doesn't provide xhr 9 | * @public 10 | */ 11 | export const adapter = ( 12 | typeof XMLHttpRequest === 'function' ? ('xhr' as const) : ('fetch' as const) 13 | ) satisfies RequestAdapter 14 | 15 | // Fallback to fetch-based XHR polyfill for non-browser environments like Workers 16 | const XmlHttpRequest = adapter === 'xhr' ? XMLHttpRequest : FetchXhr 17 | 18 | export const httpRequester: HttpRequest = (context, callback) => { 19 | const opts = context.options 20 | const options = context.applyMiddleware('finalizeOptions', opts) as RequestOptions 21 | const timers: any = {} 22 | 23 | // Allow middleware to inject a response, for instance in the case of caching or mocking 24 | const injectedResponse = context.applyMiddleware('interceptRequest', undefined, { 25 | adapter, 26 | context, 27 | }) 28 | 29 | // If middleware injected a response, treat it as we normally would and return it 30 | // Do note that the injected response has to be reduced to a cross-environment friendly response 31 | if (injectedResponse) { 32 | const cbTimer = setTimeout(callback, 0, null, injectedResponse) 33 | const cancel = () => clearTimeout(cbTimer) 34 | return {abort: cancel} 35 | } 36 | 37 | // We'll want to null out the request on success/failure 38 | let xhr = new XmlHttpRequest() 39 | 40 | if (xhr instanceof FetchXhr && typeof options.fetch === 'object') { 41 | xhr.setInit(options.fetch, options.useAbortSignal ?? true) 42 | } 43 | 44 | const headers = options.headers 45 | const delays = options.timeout 46 | 47 | // Request state 48 | let aborted = false 49 | let loaded = false 50 | let timedOut = false 51 | 52 | // Apply event handlers 53 | xhr.onerror = (event: ProgressEvent) => { 54 | // If fetch is used then rethrow the original error 55 | if (xhr instanceof FetchXhr) { 56 | onError( 57 | event instanceof Error 58 | ? event 59 | : new Error(`Request error while attempting to reach is ${options.url}`, {cause: event}), 60 | ) 61 | } else { 62 | onError( 63 | new Error( 64 | `Request error while attempting to reach is ${options.url}${ 65 | event.lengthComputable ? `(${event.loaded} of ${event.total} bytes transferred)` : '' 66 | }`, 67 | ), 68 | ) 69 | } 70 | } 71 | xhr.ontimeout = (event: ProgressEvent) => { 72 | onError( 73 | new Error( 74 | `Request timeout while attempting to reach ${options.url}${ 75 | event.lengthComputable ? `(${event.loaded} of ${event.total} bytes transferred)` : '' 76 | }`, 77 | ), 78 | ) 79 | } 80 | xhr.onabort = () => { 81 | stopTimers(true) 82 | aborted = true 83 | } 84 | 85 | xhr.onreadystatechange = function () { 86 | // Prevent request from timing out 87 | resetTimers() 88 | 89 | if (aborted || !xhr || xhr.readyState !== 4) { 90 | return 91 | } 92 | 93 | // Will be handled by onError 94 | if (xhr.status === 0) { 95 | return 96 | } 97 | 98 | onLoad() 99 | } 100 | 101 | // @todo two last options to open() is username/password 102 | xhr.open( 103 | options.method!, 104 | options.url, 105 | true, // Always async 106 | ) 107 | 108 | // Some options need to be applied after open 109 | xhr.withCredentials = !!options.withCredentials 110 | 111 | // Set headers 112 | if (headers && xhr.setRequestHeader) { 113 | for (const key in headers) { 114 | // eslint-disable-next-line no-prototype-builtins 115 | if (headers.hasOwnProperty(key)) { 116 | xhr.setRequestHeader(key, headers[key]) 117 | } 118 | } 119 | } 120 | 121 | if (options.rawBody) { 122 | xhr.responseType = 'arraybuffer' 123 | } 124 | 125 | // Let middleware know we're about to do a request 126 | context.applyMiddleware('onRequest', {options, adapter, request: xhr, context}) 127 | 128 | xhr.send(options.body || null) 129 | 130 | // Figure out which timeouts to use (if any) 131 | if (delays) { 132 | timers.connect = setTimeout(() => timeoutRequest('ETIMEDOUT'), delays.connect) 133 | } 134 | 135 | return {abort} 136 | 137 | function abort() { 138 | aborted = true 139 | 140 | if (xhr) { 141 | xhr.abort() 142 | } 143 | } 144 | 145 | function timeoutRequest(code: any) { 146 | timedOut = true 147 | xhr.abort() 148 | const error: any = new Error( 149 | code === 'ESOCKETTIMEDOUT' 150 | ? `Socket timed out on request to ${options.url}` 151 | : `Connection timed out on request to ${options.url}`, 152 | ) 153 | error.code = code 154 | context.channels.error.publish(error) 155 | } 156 | 157 | function resetTimers() { 158 | if (!delays) { 159 | return 160 | } 161 | 162 | stopTimers() 163 | timers.socket = setTimeout(() => timeoutRequest('ESOCKETTIMEDOUT'), delays.socket) 164 | } 165 | 166 | function stopTimers(force?: boolean) { 167 | // Only clear the connect timeout if we've got a connection 168 | if (force || aborted || (xhr && xhr.readyState >= 2 && timers.connect)) { 169 | clearTimeout(timers.connect) 170 | } 171 | 172 | if (timers.socket) { 173 | clearTimeout(timers.socket) 174 | } 175 | } 176 | 177 | function onError(error: Error) { 178 | if (loaded) { 179 | return 180 | } 181 | 182 | // Clean up 183 | stopTimers(true) 184 | loaded = true 185 | ;(xhr as any) = null 186 | 187 | // Annoyingly, details are extremely scarce and hidden from us. 188 | // We only really know that it is a network error 189 | const err = (error || 190 | new Error(`Network error while attempting to reach ${options.url}`)) as Error & { 191 | isNetworkError: boolean 192 | request?: typeof options 193 | } 194 | err.isNetworkError = true 195 | err.request = options 196 | callback(err) 197 | } 198 | 199 | function reduceResponse(): MiddlewareResponse { 200 | return { 201 | body: 202 | xhr.response || 203 | (xhr.responseType === '' || xhr.responseType === 'text' ? xhr.responseText : ''), 204 | url: options.url, 205 | method: options.method!, 206 | headers: parseHeaders(xhr.getAllResponseHeaders()), 207 | statusCode: xhr.status!, 208 | statusMessage: xhr.statusText!, 209 | } 210 | } 211 | 212 | function onLoad() { 213 | if (aborted || loaded || timedOut) { 214 | return 215 | } 216 | 217 | if (xhr.status === 0) { 218 | onError(new Error('Unknown XHR error')) 219 | return 220 | } 221 | 222 | // Prevent being called twice 223 | stopTimers() 224 | loaded = true 225 | callback(null, reduceResponse()) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/request/browser/fetchXhr.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mimicks the XMLHttpRequest API with only the parts needed for get-it's XHR adapter 3 | */ 4 | export class FetchXhr 5 | implements Pick 6 | { 7 | /** 8 | * Public interface, interop with real XMLHttpRequest 9 | */ 10 | onabort: (() => void) | undefined 11 | onerror: ((error?: any) => void) | undefined 12 | onreadystatechange: (() => void) | undefined 13 | ontimeout: XMLHttpRequest['ontimeout'] | undefined 14 | /** 15 | * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState 16 | */ 17 | readyState: 0 | 1 | 2 | 3 | 4 = 0 18 | response: XMLHttpRequest['response'] 19 | responseText: XMLHttpRequest['responseText'] = '' 20 | responseType: XMLHttpRequest['responseType'] = '' 21 | status: XMLHttpRequest['status'] | undefined 22 | statusText: XMLHttpRequest['statusText'] | undefined 23 | withCredentials: XMLHttpRequest['withCredentials'] | undefined 24 | 25 | /** 26 | * Private implementation details 27 | */ 28 | #method!: string 29 | #url!: string 30 | #resHeaders!: string 31 | #headers: Record = {} 32 | #controller?: AbortController 33 | #init: RequestInit = {} 34 | #useAbortSignal?: boolean 35 | // eslint-disable-next-line @typescript-eslint/no-unused-vars -- _async is only declared for typings compatibility 36 | open(method: string, url: string, _async?: boolean) { 37 | this.#method = method 38 | this.#url = url 39 | this.#resHeaders = '' 40 | this.readyState = 1 // Open 41 | this.onreadystatechange?.() 42 | this.#controller = undefined 43 | } 44 | abort() { 45 | if (this.#controller) { 46 | this.#controller.abort() 47 | } 48 | } 49 | getAllResponseHeaders() { 50 | return this.#resHeaders 51 | } 52 | setRequestHeader(name: string, value: string) { 53 | this.#headers[name] = value 54 | } 55 | // Allow setting extra fetch init options, needed for runtimes such as Vercel Edge to set `cache` and other options in React Server Components 56 | setInit(init: RequestInit, useAbortSignal = true) { 57 | this.#init = init 58 | this.#useAbortSignal = useAbortSignal 59 | } 60 | send(body: BodyInit) { 61 | const textBody = this.responseType !== 'arraybuffer' 62 | const options: RequestInit = { 63 | ...this.#init, 64 | method: this.#method, 65 | headers: this.#headers, 66 | body, 67 | } 68 | if (typeof AbortController === 'function' && this.#useAbortSignal) { 69 | this.#controller = new AbortController() 70 | // The instanceof check ensures environments like Edge Runtime, Node 18 with built-in fetch 71 | // and more don't throw if `signal` doesn't implement`EventTarget` 72 | // Native browser AbortSignal implements EventTarget, so we can use it 73 | if (typeof EventTarget !== 'undefined' && this.#controller.signal instanceof EventTarget) { 74 | options.signal = this.#controller.signal 75 | } 76 | } 77 | 78 | // Some environments (like CloudFlare workers) don't support credentials in 79 | // RequestInitDict, and there doesn't seem to be any easy way to check for it, 80 | // so for now let's just make do with a document check :/ 81 | if (typeof document !== 'undefined') { 82 | options.credentials = this.withCredentials ? 'include' : 'omit' 83 | } 84 | 85 | fetch(this.#url, options) 86 | .then((res): Promise => { 87 | res.headers.forEach((value: any, key: any) => { 88 | this.#resHeaders += `${key}: ${value}\r\n` 89 | }) 90 | this.status = res.status 91 | this.statusText = res.statusText 92 | this.readyState = 3 // Loading 93 | this.onreadystatechange?.() 94 | return textBody ? res.text() : res.arrayBuffer() 95 | }) 96 | .then((resBody) => { 97 | if (typeof resBody === 'string') { 98 | this.responseText = resBody 99 | } else { 100 | this.response = resBody 101 | } 102 | this.readyState = 4 // Done 103 | this.onreadystatechange?.() 104 | }) 105 | .catch((err: Error) => { 106 | if (err.name === 'AbortError') { 107 | this.onabort?.() 108 | return 109 | } 110 | 111 | this.onerror?.(err) 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/request/node/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Code borrowed from https://github.com/request/request 3 | * Apache License 2.0 4 | */ 5 | 6 | import url, {type UrlWithStringQuery} from 'url' 7 | 8 | import type {ProxyOptions, RequestOptions} from '../../types' 9 | 10 | function formatHostname(hostname: string) { 11 | // canonicalize the hostname, so that 'oogle.com' won't match 'google.com' 12 | return hostname.replace(/^\.*/, '.').toLowerCase() 13 | } 14 | 15 | function parseNoProxyZone(zoneStr: string) { 16 | const zone = zoneStr.trim().toLowerCase() 17 | 18 | const zoneParts = zone.split(':', 2) 19 | const zoneHost = formatHostname(zoneParts[0]) 20 | const zonePort = zoneParts[1] 21 | const hasPort = zone.indexOf(':') > -1 22 | 23 | return {hostname: zoneHost, port: zonePort, hasPort: hasPort} 24 | } 25 | 26 | function uriInNoProxy(uri: UrlWithStringQuery, noProxy: string) { 27 | const port = uri.port || (uri.protocol === 'https:' ? '443' : '80') 28 | const hostname = formatHostname(uri.hostname || '') 29 | const noProxyList = noProxy.split(',') 30 | 31 | // iterate through the noProxyList until it finds a match. 32 | return noProxyList.map(parseNoProxyZone).some((noProxyZone) => { 33 | const isMatchedAt = hostname.indexOf(noProxyZone.hostname) 34 | const hostnameMatched = 35 | isMatchedAt > -1 && isMatchedAt === hostname.length - noProxyZone.hostname.length 36 | 37 | if (noProxyZone.hasPort) { 38 | return port === noProxyZone.port && hostnameMatched 39 | } 40 | 41 | return hostnameMatched 42 | }) 43 | } 44 | 45 | function getProxyFromUri(uri: UrlWithStringQuery): string | null { 46 | // Decide the proper request proxy to use based on the request URI object and the 47 | // environmental variables (NO_PROXY, HTTP_PROXY, etc.) 48 | // respect NO_PROXY environment variables (see: http://lynx.isc.org/current/breakout/lynx_help/keystrokes/environments.html) 49 | const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'] || '' 50 | 51 | // if the noProxy is a wildcard then return null 52 | if (noProxy === '*') { 53 | return null 54 | } 55 | 56 | // if the noProxy is not empty and the uri is found return null 57 | if (noProxy !== '' && uriInNoProxy(uri, noProxy)) { 58 | return null 59 | } 60 | 61 | // Check for HTTP or HTTPS Proxy in environment, else default to null 62 | if (uri.protocol === 'http:') { 63 | return process.env['HTTP_PROXY'] || process.env['http_proxy'] || null 64 | } 65 | 66 | if (uri.protocol === 'https:') { 67 | return ( 68 | process.env['HTTPS_PROXY'] || 69 | process.env['https_proxy'] || 70 | process.env['HTTP_PROXY'] || 71 | process.env['http_proxy'] || 72 | null 73 | ) 74 | } 75 | 76 | // if none of that works, return null 77 | // (What uri protocol are you using then?) 78 | return null 79 | } 80 | 81 | function getHostFromUri(uri: UrlWithStringQuery) { 82 | let host = uri.host 83 | 84 | // Drop :port suffix from Host header if known protocol. 85 | if (uri.port) { 86 | if ( 87 | (uri.port === '80' && uri.protocol === 'http:') || 88 | (uri.port === '443' && uri.protocol === 'https:') 89 | ) { 90 | host = uri.hostname 91 | } 92 | } 93 | 94 | return host 95 | } 96 | 97 | function getHostHeaderWithPort(uri: UrlWithStringQuery) { 98 | const port = uri.port || (uri.protocol === 'https:' ? '443' : '80') 99 | return `${uri.hostname}:${port}` 100 | } 101 | 102 | export function rewriteUriForProxy( 103 | reqOpts: RequestOptions & UrlWithStringQuery, 104 | uri: UrlWithStringQuery, 105 | proxy: UrlWithStringQuery | ProxyOptions, 106 | ) { 107 | const headers = reqOpts.headers || {} 108 | const options = Object.assign({}, reqOpts, {headers}) 109 | headers.host = headers.host || getHostHeaderWithPort(uri) 110 | options.protocol = proxy.protocol || options.protocol 111 | options.hostname = ( 112 | proxy.host || 113 | ('hostname' in proxy && proxy.hostname) || 114 | options.hostname || 115 | '' 116 | ).replace(/:\d+/, '') 117 | options.port = proxy.port ? `${proxy.port}` : options.port 118 | options.host = getHostFromUri(Object.assign({}, uri, proxy)) 119 | options.href = `${options.protocol}//${options.host}${options.path}` 120 | options.path = url.format(uri) 121 | return options 122 | } 123 | 124 | export function getProxyOptions(options: RequestOptions): UrlWithStringQuery | ProxyOptions | null { 125 | const proxy = 126 | typeof options.proxy === 'undefined' ? getProxyFromUri(url.parse(options.url)) : options.proxy 127 | 128 | return typeof proxy === 'string' ? url.parse(proxy) : proxy || null 129 | } 130 | -------------------------------------------------------------------------------- /src/request/node/simpleConcat.ts: -------------------------------------------------------------------------------- 1 | /*! simple-concat. MIT License. Feross Aboukhadijeh */ 2 | export function concat(stream: any, cb: any) { 3 | const chunks: any = [] 4 | stream.on('data', function (chunk: any) { 5 | chunks.push(chunk) 6 | }) 7 | stream.once('end', function () { 8 | if (cb) cb(null, Buffer.concat(chunks)) 9 | cb = null 10 | }) 11 | stream.once('error', function (err: any) { 12 | if (cb) cb(err) 13 | cb = null 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/request/node/timedOut.ts: -------------------------------------------------------------------------------- 1 | // Copied from `@sanity/timed-out` 2 | 3 | import type {IncomingMessage} from 'node:http' 4 | import type {Socket} from 'node:net' 5 | 6 | export function timedOut(req: any, time: any) { 7 | if (req.timeoutTimer) { 8 | return req 9 | } 10 | 11 | const delays = isNaN(time) ? time : {socket: time, connect: time} 12 | const hostHeader = req.getHeader('host') 13 | const host = hostHeader ? ' to ' + hostHeader : '' 14 | 15 | if (delays.connect !== undefined) { 16 | req.timeoutTimer = setTimeout(function timeoutHandler() { 17 | const e: NodeJS.ErrnoException = new Error('Connection timed out on request' + host) 18 | e.code = 'ETIMEDOUT' 19 | req.destroy(e) 20 | }, delays.connect) 21 | } 22 | 23 | // Clear the connection timeout timer once a socket is assigned to the 24 | // request and is connected. 25 | req.on('socket', function assign(socket: Socket) { 26 | // Socket may come from Agent pool and may be already connected. 27 | if (!socket.connecting) { 28 | connect(socket) 29 | return 30 | } 31 | 32 | socket.once('connect', () => connect(socket)) 33 | }) 34 | 35 | function clear() { 36 | if (req.timeoutTimer) { 37 | clearTimeout(req.timeoutTimer) 38 | req.timeoutTimer = null 39 | } 40 | } 41 | 42 | function connect(socket: Socket) { 43 | clear() 44 | 45 | if (delays.socket !== undefined) { 46 | const socketTimeoutHandler = () => { 47 | const e: NodeJS.ErrnoException = new Error('Socket timed out on request' + host) 48 | e.code = 'ESOCKETTIMEDOUT' 49 | socket.destroy(e) 50 | } 51 | 52 | socket.setTimeout(delays.socket, socketTimeoutHandler) 53 | req.once('response', (response: IncomingMessage) => { 54 | response.once('end', () => { 55 | socket.removeListener('timeout', socketTimeoutHandler) 56 | }) 57 | }) 58 | } 59 | } 60 | 61 | return req.on('error', clear) 62 | } 63 | -------------------------------------------------------------------------------- /src/request/node/tunnel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Code borrowed from https://github.com/request/request 3 | * Modified to be less request-specific, more functional 4 | * Apache License 2.0 5 | */ 6 | import * as tunnel from 'tunnel-agent' 7 | import url from 'url' 8 | 9 | const uriParts = [ 10 | 'protocol', 11 | 'slashes', 12 | 'auth', 13 | 'host', 14 | 'port', 15 | 'hostname', 16 | 'hash', 17 | 'search', 18 | 'query', 19 | 'pathname', 20 | 'path', 21 | 'href', 22 | ] 23 | 24 | const defaultProxyHeaderWhiteList = [ 25 | 'accept', 26 | 'accept-charset', 27 | 'accept-encoding', 28 | 'accept-language', 29 | 'accept-ranges', 30 | 'cache-control', 31 | 'content-encoding', 32 | 'content-language', 33 | 'content-location', 34 | 'content-md5', 35 | 'content-range', 36 | 'content-type', 37 | 'connection', 38 | 'date', 39 | 'expect', 40 | 'max-forwards', 41 | 'pragma', 42 | 'referer', 43 | 'te', 44 | 'user-agent', 45 | 'via', 46 | ] 47 | 48 | const defaultProxyHeaderExclusiveList = ['proxy-authorization'] 49 | 50 | export function shouldEnable(options: any) { 51 | // Tunnel HTTPS by default. Allow the user to override this setting. 52 | 53 | // If user has specified a specific tunnel override... 54 | if (typeof options.tunnel !== 'undefined') { 55 | return Boolean(options.tunnel) 56 | } 57 | 58 | // If the destination is HTTPS, tunnel. 59 | const uri = url.parse(options.url) 60 | if (uri.protocol === 'https:') { 61 | return true 62 | } 63 | 64 | // Otherwise, do not use tunnel. 65 | return false 66 | } 67 | 68 | export function applyAgent(opts: any = {}, proxy: any) { 69 | const options = Object.assign({}, opts) 70 | 71 | // Setup proxy header exclusive list and whitelist 72 | const proxyHeaderWhiteList = defaultProxyHeaderWhiteList 73 | .concat(options.proxyHeaderWhiteList || []) 74 | .map((header) => header.toLowerCase()) 75 | 76 | const proxyHeaderExclusiveList = defaultProxyHeaderExclusiveList 77 | .concat(options.proxyHeaderExclusiveList || []) 78 | .map((header) => header.toLowerCase()) 79 | 80 | // Get the headers we should send to the proxy 81 | const proxyHeaders = getAllowedProxyHeaders(options.headers, proxyHeaderWhiteList) 82 | proxyHeaders.host = constructProxyHost(options) 83 | 84 | // Reduce headers to the ones not exclusive for the proxy 85 | options.headers = Object.keys(options.headers || {}).reduce((headers, header) => { 86 | const isAllowed = proxyHeaderExclusiveList.indexOf(header.toLowerCase()) === -1 87 | if (isAllowed) { 88 | headers[header] = options.headers[header] 89 | } 90 | 91 | return headers 92 | }, {} as any) 93 | 94 | const tunnelFn = getTunnelFn(options, proxy) 95 | const tunnelOptions = constructTunnelOptions(options, proxy, proxyHeaders) 96 | options.agent = tunnelFn(tunnelOptions) 97 | 98 | return options 99 | } 100 | 101 | function getTunnelFn(options: any, proxy: any) { 102 | const uri = getUriParts(options) 103 | const tunnelFnName = constructTunnelFnName(uri, proxy) 104 | return tunnel[tunnelFnName] 105 | } 106 | 107 | function getUriParts(options: any) { 108 | return uriParts.reduce((uri, part) => { 109 | uri[part] = options[part] 110 | return uri 111 | }, {} as any) 112 | } 113 | 114 | type UriProtocol = `http` | `https` 115 | type ProxyProtocol = `Http` | `Https` 116 | function constructTunnelFnName(uri: any, proxy: any): `${UriProtocol}Over${ProxyProtocol}` { 117 | const uriProtocol = uri.protocol === 'https:' ? 'https' : 'http' 118 | const proxyProtocol = proxy.protocol === 'https:' ? 'Https' : 'Http' 119 | return `${uriProtocol}Over${proxyProtocol}` 120 | } 121 | 122 | function constructProxyHost(uri: any) { 123 | const port = uri.port 124 | const protocol = uri.protocol 125 | let proxyHost = `${uri.hostname}:` 126 | 127 | if (port) { 128 | proxyHost += port 129 | } else if (protocol === 'https:') { 130 | proxyHost += '443' 131 | } else { 132 | proxyHost += '80' 133 | } 134 | 135 | return proxyHost 136 | } 137 | 138 | function getAllowedProxyHeaders(headers: any, whiteList: any): any { 139 | return Object.keys(headers) 140 | .filter((header) => whiteList.indexOf(header.toLowerCase()) !== -1) 141 | .reduce((set: any, header: any) => { 142 | set[header] = headers[header] 143 | return set 144 | }, {}) 145 | } 146 | 147 | function constructTunnelOptions(options: any, proxy: any, proxyHeaders: any) { 148 | return { 149 | proxy: { 150 | host: proxy.hostname, 151 | port: +proxy.port, 152 | proxyAuth: proxy.auth, 153 | headers: proxyHeaders, 154 | }, 155 | headers: options.headers, 156 | ca: options.ca, 157 | cert: options.cert, 158 | key: options.key, 159 | passphrase: options.passphrase, 160 | pfx: options.pfx, 161 | ciphers: options.ciphers, 162 | rejectUnauthorized: options.rejectUnauthorized, 163 | secureOptions: options.secureOptions, 164 | secureProtocol: options.secureProtocol, 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type {IncomingHttpHeaders, IncomingMessage} from 'http' 2 | import type {UrlWithStringQuery} from 'url' 3 | 4 | import type {ProgressStream} from './util/progress-stream' 5 | 6 | /** @public */ 7 | export interface RequestOptions { 8 | url: string 9 | body?: any 10 | bodySize?: number 11 | cancelToken?: any 12 | compress?: boolean 13 | headers?: any 14 | maxRedirects?: number 15 | maxRetries?: number 16 | retryDelay?: (attemptNumber: number) => number 17 | method?: string 18 | proxy?: string | false | null | ProxyOptions 19 | query?: any 20 | rawBody?: boolean 21 | shouldRetry?: any 22 | stream?: boolean 23 | timeout?: any 24 | tunnel?: boolean 25 | debug?: any 26 | requestId?: number 27 | attemptNumber?: number 28 | withCredentials?: boolean 29 | /** 30 | * Enables using the native `fetch` API instead of the default `http` module, and allows setting its options like `cache` 31 | */ 32 | fetch?: boolean | Omit 33 | /** 34 | * Some frameworks have special behavior for `fetch` when an `AbortSignal` is used, and may want to disable it unless userland specifically opts-in. 35 | */ 36 | useAbortSignal?: boolean 37 | } 38 | 39 | /** 40 | * @public 41 | */ 42 | export interface ProxyOptions { 43 | host: string 44 | port: number 45 | protocol?: 'http:' | 'https:' 46 | auth?: {username?: string; password?: string} 47 | } 48 | 49 | /** @public */ 50 | export interface Subscriber { 51 | (event: Event): void 52 | } 53 | /** @public */ 54 | export interface PubSub { 55 | publish: (message: Message) => void 56 | subscribe: (subscriber: Subscriber) => () => void 57 | } 58 | 59 | /** @public */ 60 | export interface MiddlewareChannels { 61 | request: PubSub 62 | response: PubSub 63 | progress: PubSub 64 | error: PubSub 65 | abort: PubSub 66 | } 67 | 68 | /** @public */ 69 | export interface FinalizeNodeOptionsPayload extends UrlWithStringQuery { 70 | method: RequestOptions['method'] 71 | headers: RequestOptions['headers'] 72 | maxRedirects: RequestOptions['maxRedirects'] 73 | agent?: any 74 | cert?: any 75 | key?: any 76 | ca?: any 77 | } 78 | 79 | /** @public */ 80 | export interface MiddlewareHooks { 81 | processOptions: (options: RequestOptions) => RequestOptions 82 | validateOptions: (options: RequestOptions) => void | undefined 83 | interceptRequest: ( 84 | prevValue: MiddlewareResponse | undefined, 85 | event: {adapter: RequestAdapter; context: HttpContext}, 86 | ) => MiddlewareResponse | undefined | void 87 | finalizeOptions: ( 88 | options: FinalizeNodeOptionsPayload | RequestOptions, 89 | ) => FinalizeNodeOptionsPayload | RequestOptions 90 | onRequest: (evt: HookOnRequestEvent) => void 91 | onResponse: (response: MiddlewareResponse, context: HttpContext) => MiddlewareResponse 92 | onError: (err: Error | null, context: HttpContext) => any 93 | onReturn: (channels: MiddlewareChannels, context: HttpContext) => any 94 | onHeaders: ( 95 | response: IncomingMessage, 96 | evt: {headers: IncomingHttpHeaders; adapter: RequestAdapter; context: HttpContext}, 97 | ) => ProgressStream 98 | } 99 | 100 | /** @public */ 101 | export interface HookOnRequestEventBase { 102 | options: RequestOptions 103 | context: HttpContext 104 | request: any 105 | } 106 | /** @public */ 107 | export interface HookOnRequestEventNode extends HookOnRequestEventBase { 108 | adapter: 'node' 109 | progress?: any 110 | } 111 | /** @public */ 112 | export interface HookOnRequestEventBrowser extends HookOnRequestEventBase { 113 | adapter: Exclude 114 | progress?: undefined 115 | } 116 | /** @public */ 117 | export type HookOnRequestEvent = HookOnRequestEventNode | HookOnRequestEventBrowser 118 | 119 | /** @public */ 120 | export interface HttpContext { 121 | options: RequestOptions 122 | channels: MiddlewareChannels 123 | applyMiddleware: ApplyMiddleware 124 | } 125 | 126 | /** @public */ 127 | export type MiddlewareReducer = { 128 | [T in keyof MiddlewareHooks]: (( 129 | ...args: Parameters 130 | ) => ReturnType)[] 131 | } 132 | 133 | /** @public */ 134 | export type ApplyMiddleware = ( 135 | hook: T, 136 | value: MiddlewareHooks[T] extends (defaultValue: infer V, ...rest: any[]) => any ? V : never, 137 | ...args: MiddlewareHooks[T] extends (defaultValue: any, ...rest: infer P) => any ? P : never 138 | ) => ReturnType 139 | 140 | /** @public */ 141 | export type DefineApplyMiddleware = (middleware: MiddlewareReducer) => ApplyMiddleware 142 | 143 | /** @public */ 144 | export type MiddlewareHookName = keyof MiddlewareHooks 145 | 146 | /** @public */ 147 | export type Middleware = Partial 148 | 149 | /** @public */ 150 | export type Middlewares = Middleware[] 151 | 152 | /** @public */ 153 | export interface HttpRequestOngoing { 154 | abort: () => void 155 | } 156 | 157 | /** @public */ 158 | export interface MiddlewareRequest {} // eslint-disable-line @typescript-eslint/no-empty-object-type 159 | 160 | /** @public */ 161 | export interface MiddlewareResponse { 162 | body: any 163 | url: string 164 | method: string 165 | headers: any 166 | statusCode: number 167 | statusMessage: string 168 | } 169 | 170 | /** 171 | * request-node in node, browser-request in browsers 172 | * @public 173 | */ 174 | export type HttpRequest = ( 175 | context: HttpContext, 176 | callback: (err: Error | null, response?: MiddlewareResponse) => void, 177 | ) => HttpRequestOngoing 178 | 179 | /** @public */ 180 | export interface RetryOptions { 181 | shouldRetry: (err: any, num: number, options: any) => boolean 182 | maxRetries?: number 183 | retryDelay?: (attemptNumber: number) => number 184 | } 185 | 186 | /** 187 | * Reports the environment as either "node" or "browser", depending on what entry point was used to aid bundler debugging. 188 | * If 'browser' is used, then the globally available `fetch` class is used. While `node` will always use either `node:https` or `node:http` depending on the protocol. 189 | * @public 190 | */ 191 | export type ExportEnv = 'node' | 'react-server' | 'browser' 192 | 193 | /** 194 | * Reports the request adapter in use. `node` is only available if `ExportEnv` is also `node`. 195 | * When `ExportEnv` is `browser` then the adapter can be either `xhr` or `fetch`. 196 | * In the future `fetch` will be available in `node` as well. 197 | * @public 198 | */ 199 | export type RequestAdapter = 'node' | 'xhr' | 'fetch' 200 | 201 | /** @public */ 202 | export type Requester = { 203 | use: (middleware: Middleware) => Requester 204 | clone: () => Requester 205 | (options: RequestOptions | string): any 206 | } 207 | -------------------------------------------------------------------------------- /src/util/browser-shouldRetry.ts: -------------------------------------------------------------------------------- 1 | export default (err: any, _attempt: any, options: any) => { 2 | if (options.method !== 'GET' && options.method !== 'HEAD') { 3 | return false 4 | } 5 | 6 | return err.isNetworkError || false 7 | } 8 | -------------------------------------------------------------------------------- /src/util/global.ts: -------------------------------------------------------------------------------- 1 | let actualGlobal = {} as typeof globalThis 2 | 3 | if (typeof globalThis !== 'undefined') { 4 | actualGlobal = globalThis 5 | } else if (typeof window !== 'undefined') { 6 | actualGlobal = window 7 | } else if (typeof global !== 'undefined') { 8 | actualGlobal = global 9 | } else if (typeof self !== 'undefined') { 10 | actualGlobal = self 11 | } 12 | 13 | export default actualGlobal 14 | -------------------------------------------------------------------------------- /src/util/isBrowserOptions.ts: -------------------------------------------------------------------------------- 1 | import type {RequestOptions} from 'get-it' 2 | 3 | export function isBrowserOptions(options: unknown): options is RequestOptions { 4 | return typeof options === 'object' && options !== null && !('protocol' in options) 5 | } 6 | -------------------------------------------------------------------------------- /src/util/isBuffer.ts: -------------------------------------------------------------------------------- 1 | export const isBuffer = 2 | typeof Buffer === 'undefined' ? () => false : (obj: unknown) => Buffer.isBuffer(obj) 3 | -------------------------------------------------------------------------------- /src/util/isPlainObject.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * is-plain-object 3 | * 4 | * Copyright (c) 2014-2017, Jon Schlinkert. 5 | * Released under the MIT License. 6 | */ 7 | 8 | function isObject(o: unknown): o is Record { 9 | return Object.prototype.toString.call(o) === '[object Object]' 10 | } 11 | 12 | export function isPlainObject(o: unknown): boolean { 13 | if (isObject(o) === false) return false 14 | 15 | // If has modified constructor 16 | const ctor = o.constructor 17 | if (ctor === undefined) return true 18 | 19 | // If has modified prototype 20 | const prot = ctor.prototype 21 | if (isObject(prot) === false) return false 22 | 23 | // If constructor does not have an Object-specific method 24 | if ( 25 | // eslint-disable-next-line no-prototype-builtins 26 | prot.hasOwnProperty('isPrototypeOf') === false 27 | ) { 28 | return false 29 | } 30 | 31 | // Most likely a plain Object 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /src/util/lowerCaseHeaders.ts: -------------------------------------------------------------------------------- 1 | export function lowerCaseHeaders(headers: any) { 2 | return Object.keys(headers || {}).reduce((acc, header) => { 3 | acc[header.toLowerCase()] = headers[header] 4 | return acc 5 | }, {} as any) 6 | } 7 | -------------------------------------------------------------------------------- /src/util/middlewareReducer.ts: -------------------------------------------------------------------------------- 1 | import type {ApplyMiddleware, MiddlewareReducer} from 'get-it' 2 | 3 | export const middlewareReducer = (middleware: MiddlewareReducer) => 4 | function applyMiddleware(hook, defaultValue, ...args) { 5 | const bailEarly = hook === 'onError' 6 | 7 | let value = defaultValue 8 | for (let i = 0; i < middleware[hook].length; i++) { 9 | const handler = middleware[hook][i] 10 | // @ts-expect-error -- find a better way to deal with argument tuples 11 | value = handler(value, ...args) 12 | 13 | if (bailEarly && !value) { 14 | break 15 | } 16 | } 17 | 18 | return value 19 | } as ApplyMiddleware 20 | -------------------------------------------------------------------------------- /src/util/node-shouldRetry.ts: -------------------------------------------------------------------------------- 1 | import allowed from 'is-retry-allowed' 2 | 3 | export default (err: any, _num: number, options: any) => { 4 | if (options.method !== 'GET' && options.method !== 'HEAD') { 5 | return false 6 | } 7 | 8 | // Don't allow retries if we get any http status code by default 9 | if (err.response && err.response.statusCode) { 10 | return false 11 | } 12 | 13 | return allowed(err) 14 | } 15 | -------------------------------------------------------------------------------- /src/util/progress-stream.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inlined, reduced variant of npm `progress-stream` (https://github.com/freeall/progress-stream), 3 | * that fixes a bug with `content-length` header. BSD 2-Clause Simplified License, 4 | * Copyright (c) Tobias Baunbæk . 5 | */ 6 | import type {Transform} from 'stream' 7 | import through from 'through2' 8 | 9 | import {speedometer} from './speedometer' 10 | 11 | export interface Progress { 12 | percentage: number 13 | transferred: number 14 | length: number 15 | remaining: number 16 | eta: number 17 | runtime: number 18 | delta: number 19 | speed: number 20 | } 21 | 22 | export interface ProgressStream extends Transform { 23 | progress(): Progress 24 | } 25 | 26 | export function progressStream(options: {time: number; length?: number}): ProgressStream { 27 | let length = options.length || 0 28 | let transferred = 0 29 | let nextUpdate = Date.now() + options.time 30 | let delta = 0 31 | const speed = speedometer(5) 32 | const startTime = Date.now() 33 | 34 | const update = { 35 | percentage: 0, 36 | transferred: transferred, 37 | length: length, 38 | remaining: length, 39 | eta: 0, 40 | runtime: 0, 41 | speed: 0, 42 | delta: 0, 43 | } 44 | 45 | const emit = function (ended: boolean) { 46 | update.delta = delta 47 | update.percentage = ended ? 100 : length ? (transferred / length) * 100 : 0 48 | update.speed = speed.getSpeed(delta) 49 | update.eta = Math.round(update.remaining / update.speed) 50 | update.runtime = Math.floor((Date.now() - startTime) / 1000) 51 | nextUpdate = Date.now() + options.time 52 | 53 | delta = 0 54 | 55 | tr.emit('progress', update) 56 | } 57 | 58 | const write = function ( 59 | chunk: Buffer, 60 | _enc: string, 61 | callback: (err: Error | null, data?: Buffer) => void, 62 | ) { 63 | const len = chunk.length 64 | transferred += len 65 | delta += len 66 | update.transferred = transferred 67 | update.remaining = length >= transferred ? length - transferred : 0 68 | 69 | if (Date.now() >= nextUpdate) emit(false) 70 | callback(null, chunk) 71 | } 72 | 73 | const end = function (callback: (err?: Error | null) => void) { 74 | emit(true) 75 | speed.clear() 76 | callback() 77 | } 78 | 79 | const tr = through({}, write, end) as ProgressStream 80 | const onlength = function (newLength: number) { 81 | length = newLength 82 | update.length = length 83 | update.remaining = length - update.transferred 84 | tr.emit('length', length) 85 | } 86 | 87 | tr.on('pipe', function (stream) { 88 | if (length > 0) return 89 | 90 | // Support http module 91 | if ( 92 | stream.readable && 93 | !('writable' in stream) && 94 | 'headers' in stream && 95 | isRecord(stream.headers) 96 | ) { 97 | const contentLength = 98 | typeof stream.headers['content-length'] === 'string' 99 | ? parseInt(stream.headers['content-length'], 10) 100 | : 0 101 | return onlength(contentLength) 102 | } 103 | 104 | // Support streams with a length property 105 | if ('length' in stream && typeof stream.length === 'number') { 106 | return onlength(stream.length) 107 | } 108 | 109 | // Support request module 110 | stream.on('response', function (res) { 111 | if (!res || !res.headers) return 112 | if (res.headers['content-encoding'] === 'gzip') return 113 | if (res.headers['content-length']) { 114 | return onlength(parseInt(res.headers['content-length'])) 115 | } 116 | }) 117 | }) 118 | 119 | tr.progress = function () { 120 | update.speed = speed.getSpeed(0) 121 | update.eta = Math.round(update.remaining / update.speed) 122 | 123 | return update 124 | } 125 | 126 | return tr 127 | } 128 | 129 | function isRecord(value: unknown): value is Record { 130 | return typeof value === 'object' && value !== null && !Array.isArray(value) 131 | } 132 | -------------------------------------------------------------------------------- /src/util/pubsub.ts: -------------------------------------------------------------------------------- 1 | // Code borrowed from https://github.com/bjoerge/nano-pubsub 2 | 3 | import type {PubSub, Subscriber} from 'get-it' 4 | 5 | export function createPubSub(): PubSub { 6 | const subscribers: {[id: string]: Subscriber} = Object.create(null) 7 | let nextId = 0 8 | function subscribe(subscriber: Subscriber) { 9 | const id = nextId++ 10 | subscribers[id] = subscriber 11 | return function unsubscribe() { 12 | delete subscribers[id] 13 | } 14 | } 15 | 16 | function publish(event: Message) { 17 | for (const id in subscribers) { 18 | subscribers[id](event) 19 | } 20 | } 21 | 22 | return { 23 | publish, 24 | subscribe, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/util/speedometer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inlined variant of npm `speedometer` (https://github.com/mafintosh/speedometer), 3 | * MIT-licensed, Copyright (c) 2013 Mathias Buus. 4 | */ 5 | 6 | let tick = 1 7 | const maxTick = 65535 8 | const resolution = 4 9 | let timer: ReturnType | null = null 10 | 11 | const inc = function () { 12 | tick = (tick + 1) & maxTick 13 | } 14 | 15 | export function speedometer(seconds: number) { 16 | if (!timer) { 17 | timer = setInterval(inc, (1000 / resolution) | 0) 18 | if (timer.unref) timer.unref() 19 | } 20 | 21 | const size = resolution * (seconds || 5) 22 | const buffer = [0] 23 | let pointer = 1 24 | let last = (tick - 1) & maxTick 25 | 26 | return { 27 | getSpeed: function (delta: number) { 28 | let dist = (tick - last) & maxTick 29 | if (dist > size) dist = size 30 | last = tick 31 | 32 | while (dist--) { 33 | if (pointer === size) pointer = 0 34 | buffer[pointer] = buffer[pointer === 0 ? size - 1 : pointer - 1] 35 | pointer++ 36 | } 37 | 38 | if (delta) buffer[pointer - 1] += delta 39 | 40 | const top = buffer[pointer - 1] 41 | const btm = buffer.length < size ? 0 : buffer[pointer === size ? 0 : pointer] 42 | 43 | return buffer.length < resolution ? top : ((top - btm) * resolution) / buffer.length 44 | }, 45 | clear: function () { 46 | if (timer) { 47 | clearInterval(timer) 48 | timer = null 49 | } 50 | }, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test-deno/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "debug": "https://esm.sh/debug@^4.3.4", 4 | "decompress-response": "https://esm.sh/decompress-response@^7.0.0", 5 | "follow-redirects": "https://esm.sh/follow-redirects@^1.15.2", 6 | "is-retry-allowed": "https://esm.sh/is-retry-allowed@^2.2.0", 7 | "parse-headers": "https://esm.sh/parse-headers@^2.0.5", 8 | "tunnel-agent": "https://esm.sh/tunnel-agent@^0.6.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test-deno/test.ts: -------------------------------------------------------------------------------- 1 | import {assertEquals, assertExists} from 'https://deno.land/std@0.171.0/testing/asserts.ts' 2 | 3 | Deno.test('top-level import', async () => { 4 | const {getIt} = await import('../dist/index.browser.js') 5 | console.log({getIt: typeof getIt}) 6 | assertEquals(typeof getIt, 'function') 7 | }) 8 | 9 | Deno.test('importing package json', async () => { 10 | const { 11 | default: {version}, 12 | } = await import('../package.json', {with: {type: 'json'}}) 13 | assertExists(version) 14 | }) 15 | 16 | Deno.test('named middleware imports', async () => { 17 | const {jsonRequest, jsonResponse, httpErrors, headers, promise} = await import( 18 | '../dist/middleware.browser.js' 19 | ) 20 | 21 | assertEquals(typeof jsonRequest, 'function') 22 | assertEquals(typeof jsonResponse, 'function') 23 | assertEquals(typeof httpErrors, 'function') 24 | assertEquals(typeof headers, 'function') 25 | assertEquals(typeof promise, 'function') 26 | }) 27 | -------------------------------------------------------------------------------- /test-esm/test.cjs: -------------------------------------------------------------------------------- 1 | const test = require('node:test') 2 | const assert = require('node:assert/strict') 3 | 4 | test('top-level imports', async (t) => { 5 | await t.test('get-it', (t) => { 6 | const {getIt} = require('get-it') 7 | assert.equal(typeof getIt, 'function') 8 | }) 9 | 10 | await t.test('get-it/package.json', (t) => { 11 | const {version} = require('get-it/package.json') 12 | assert.equal(typeof version, 'string') 13 | }) 14 | 15 | await t.test('get-it/middleware', async () => { 16 | const middlewares = require('get-it/middleware') 17 | const entries = Object.entries(middlewares) 18 | for (const [name, middleware] of entries) { 19 | assert.equal(typeof middleware, 'function', `${name} is not a function`) 20 | } 21 | assert.deepEqual( 22 | Object.keys(middlewares).sort(), 23 | Object.keys(await import('get-it/middleware')).sort(), 24 | 'ESM and CJS exports are not the same', 25 | ) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test-esm/test.mjs: -------------------------------------------------------------------------------- 1 | import test from 'node:test' 2 | import assert from 'node:assert/strict' 3 | import {createRequire} from 'node:module' 4 | 5 | import {getIt} from 'get-it' 6 | import * as middlewares from 'get-it/middleware' 7 | 8 | const require = createRequire(import.meta.url) 9 | 10 | test('top-level imports', async (t) => { 11 | await t.test('get-it', async () => { 12 | assert.equal(typeof getIt, 'function') 13 | }) 14 | 15 | await t.test('get-it/package.json', async () => { 16 | const { 17 | default: {version}, 18 | } = await import('get-it/package.json', {with: {type: 'json'}}) 19 | assert.equal(typeof version, 'string') 20 | }) 21 | 22 | await t.test('get-it/middleware', async () => { 23 | for (const [name, middleware] of Object.entries(middlewares)) { 24 | assert.equal(typeof middleware, 'function', `${name} is not a function`) 25 | } 26 | assert.deepEqual( 27 | Object.keys(middlewares).sort(), 28 | Object.keys(require('get-it/middleware')).sort(), 29 | 'ESM and CJS exports are not the same', 30 | ) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "rules": { 7 | "max-nested-callbacks": [2, {"max": 4}], 8 | "@typescript-eslint/no-explicit-any": "off", 9 | "no-warning-comments": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/abort.test.ts: -------------------------------------------------------------------------------- 1 | import {adapter, environment, getIt} from 'get-it' 2 | import {describe, it} from 'vitest' 3 | 4 | import {baseUrl, debugRequest} from './helpers' 5 | 6 | describe('aborting requests', () => { 7 | // TODO fix the test in happy-dom 8 | it.skipIf(environment === 'browser' && adapter === 'xhr')( 9 | 'should be able to abort requests', 10 | () => { 11 | return new Promise((resolve, reject) => { 12 | const request = getIt([baseUrl, debugRequest]) 13 | const req = request({url: '/delay'}) 14 | 15 | req.error.subscribe((err: any) => 16 | reject( 17 | new Error(`error channel should not be called when aborting, got:\n\n${err.message}`), 18 | ), 19 | ) 20 | req.response.subscribe(() => 21 | reject(new Error('response channel should not be called when aborting')), 22 | ) 23 | 24 | setTimeout(() => req.abort.publish(), 15) 25 | setTimeout(resolve, 250) 26 | }) 27 | }, 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /test/agent.test.ts: -------------------------------------------------------------------------------- 1 | import {environment, getIt} from 'get-it' 2 | import {agent, jsonResponse} from 'get-it/middleware' 3 | import {describe, it} from 'vitest' 4 | 5 | import {baseUrl, debugRequest, expectRequestBody} from './helpers' 6 | 7 | describe.runIf(environment === 'node')('agent middleware', () => { 8 | it('can set keepAlive=true', async () => { 9 | const request = getIt([baseUrl, agent({keepAlive: true}), jsonResponse(), debugRequest]) 10 | const req = request({url: '/debug'}) 11 | await expectRequestBody(req).resolves.toMatchObject({headers: {connection: 'keep-alive'}}) 12 | }) 13 | 14 | it('can set keepAlive=false', async () => { 15 | const request = getIt([baseUrl, agent({keepAlive: false}), jsonResponse(), debugRequest]) 16 | const req = request({url: '/debug'}) 17 | await expectRequestBody(req).resolves.toMatchObject({headers: {connection: 'close'}}) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /test/basics.test.ts: -------------------------------------------------------------------------------- 1 | import {adapter, environment, getIt} from 'get-it' 2 | import {jsonResponse} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | 5 | import { 6 | baseUrl, 7 | baseUrlPrefix, 8 | debugRequest, 9 | expectRequest, 10 | expectRequestBody, 11 | promiseRequest, 12 | } from './helpers' 13 | 14 | describe('basics', {timeout: 15000}, () => { 15 | it('should return same instance when calling use()', () => { 16 | const request = getIt([baseUrl]) 17 | return expect(request).to.equal(request.use(jsonResponse())) 18 | }) 19 | 20 | it('should throw when requesting with invalid URL', () => { 21 | const request = getIt() 22 | return expect(() => request({url: 'heisann'})).to.throw(/valid URL/) 23 | }) 24 | 25 | it('should be able to request a basic, plain-text file', async () => { 26 | const body = 'Just some plain text for you to consume' 27 | const request = getIt([baseUrl, debugRequest]) 28 | const req = request({url: '/plain-text'}) 29 | 30 | await expectRequest(req).resolves.toHaveProperty('body', body) 31 | }) 32 | 33 | it('should transform string to url option', async () => { 34 | const body = 'Just some plain text for you to consume' 35 | const request = getIt([baseUrl, debugRequest]) 36 | const req = request('/plain-text') 37 | 38 | await expectRequest(req).resolves.toHaveProperty('body', body) 39 | }) 40 | 41 | it.skipIf(adapter === 'xhr')('should be able to post a Buffer as body', async () => { 42 | const request = getIt([baseUrl, debugRequest]) 43 | const req = request({url: '/echo', body: Buffer.from('Foo bar')}) 44 | await expectRequestBody(req).resolves.toEqual('Foo bar') 45 | }) 46 | it.runIf(adapter === 'fetch')('[fetch] fetch is more permissive in what `body` can be', () => { 47 | const request = getIt([baseUrl, debugRequest]) 48 | expect(() => { 49 | request({url: '/echo', method: 'post', body: {}}) 50 | }).not.toThrow() 51 | }) 52 | it.runIf(adapter === 'node')('[node] should throw when trying to post invalid stuff', () => { 53 | const request = getIt([baseUrl, debugRequest]) 54 | expect(() => { 55 | request({url: '/echo', method: 'post', body: {}}) 56 | }).toThrow(/string, buffer or stream/) 57 | }) 58 | 59 | // @TODO make the test work in happy-dom 60 | it.skipIf(environment === 'browser')( 61 | 'should be able to get a raw, unparsed body back', 62 | async () => { 63 | const request = getIt([baseUrl, debugRequest]) 64 | const req = request({url: '/plain-text', rawBody: true}) 65 | switch (adapter) { 66 | case 'node': 67 | // Node.js (buffer) 68 | return await expectRequestBody(req).resolves.toEqual( 69 | Buffer.from('Just some plain text for you to consume'), 70 | ) 71 | case 'xhr': 72 | return await expectRequestBody(req).resolves.toBeTypeOf('string') 73 | case 'fetch': 74 | // Browser (ArrayBuffer) 75 | return await expectRequestBody(req).resolves.toMatchInlineSnapshot('ArrayBuffer []') 76 | } 77 | }, 78 | ) 79 | 80 | it.skipIf(environment === 'browser')( 81 | 'should request compressed responses by default', 82 | async () => { 83 | const request = getIt([baseUrl, jsonResponse()]) 84 | const req = request({url: '/debug'}) 85 | 86 | const body = await promiseRequest(req).then((res) => res.body) 87 | expect(body).toHaveProperty('headers') 88 | expect(body.headers).toHaveProperty('accept-encoding') 89 | expect(body.headers['accept-encoding']).toMatch(/br|gzip|deflate/i) 90 | }, 91 | ) 92 | 93 | it.skipIf(environment === 'browser')('should decompress compressed responses', async () => { 94 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 95 | const req = request({url: '/gzip'}) 96 | const res = await promiseRequest(req) 97 | expect(res).toHaveProperty('body') 98 | expect(res.body).toEqual(['harder', 'better', 'faster', 'stronger']) 99 | }) 100 | 101 | it.runIf(adapter === 'node')( 102 | 'should not request compressed responses for HEAD requests', 103 | async () => { 104 | const request = getIt([baseUrl, jsonResponse()]) 105 | const req = request({url: '/maybeCompress', method: 'HEAD'}) 106 | 107 | const res = await promiseRequest(req) 108 | expect(res).toHaveProperty('headers') 109 | expect(res.headers).not.toHaveProperty('content-encoding') 110 | }, 111 | ) 112 | 113 | it.runIf(adapter === 'node')('should decompress brotli-encoded responses', async () => { 114 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 115 | const req = request({url: '/maybeCompress'}) 116 | const res = await promiseRequest(req) 117 | expect(res).toHaveProperty('body') 118 | expect(res.body).toEqual(['smaller', 'better', 'faster', 'stronger']) 119 | }) 120 | 121 | it.runIf(adapter === 'node')('should be able to disable compression', async () => { 122 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 123 | const req = request({url: '/maybeCompress', compress: false}) 124 | const res = await promiseRequest(req) 125 | expect(res).toHaveProperty('body') 126 | expect(res.body).toEqual(['larger', 'worse', 'slower', 'weaker']) 127 | }) 128 | 129 | it('should not return a body on HEAD-requests', async () => { 130 | const request = getIt([baseUrl, jsonResponse()]) 131 | const req = request({url: '/gzip', method: 'HEAD'}) 132 | await expectRequest(req).resolves.toMatchObject({ 133 | statusCode: 200, 134 | method: 'HEAD', 135 | }) 136 | }) 137 | 138 | it('should be able to send PUT-requests with raw bodies', async () => { 139 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 140 | const req = request({url: '/debug', method: 'PUT', body: 'just a plain body'}) 141 | await expectRequestBody(req).resolves.toMatchObject({ 142 | method: 'PUT', 143 | body: 'just a plain body', 144 | }) 145 | }) 146 | 147 | it('should handle https without issues', async () => { 148 | const request = getIt() 149 | const req = request({url: 'https://api.sanity.io/v1/ping'}) 150 | const res = await promiseRequest(req) 151 | expect(res).toHaveProperty('body') 152 | expect(res.body).toContain('PONG') 153 | }) 154 | 155 | it('should handle cross-protocol redirects without issues', async () => { 156 | const request = getIt() 157 | const req = request({url: `http://api.sanity.io/v1/ping?cb=${Date.now()}`}) 158 | const res = await promiseRequest(req) 159 | expect(res).toHaveProperty('body') 160 | expect(res.body).toMatch('PONG') 161 | }) 162 | 163 | it('should not allow base middleware to add prefix on absolute urls', async () => { 164 | const request = getIt([baseUrl, jsonResponse()]) 165 | const req = request({url: `${baseUrlPrefix}/debug`}) 166 | await expectRequestBody(req).resolves.toHaveProperty('url', '/req-test/debug') 167 | }) 168 | 169 | it('should be able to clone a requester, keeping the same middleware', () => 170 | new Promise((resolve) => { 171 | let i = 0 172 | const onRequest = () => i++ 173 | const base = getIt([baseUrl, {onRequest}]) 174 | const cloned = base.clone() 175 | 176 | base('/plain-text') 177 | cloned('/plain-text') 178 | 179 | setTimeout(() => { 180 | expect(i).to.equal(2, 'two requests should have been initiated') 181 | resolve() 182 | }, 15) 183 | })) 184 | }) 185 | -------------------------------------------------------------------------------- /test/certs/invalid-mtls/README.md: -------------------------------------------------------------------------------- 1 | # Used for testing MLTS handling invalid certificates 2 | 3 | Do not regenerate valid ones, they are supposed to fail. 4 | -------------------------------------------------------------------------------- /test/certs/invalid-mtls/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICBzCCAXACCQDe3kCVnwXlKTANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECBMCVkExFDASBgNVBAoTC0V2ZW50U291cmNlMRYwFAYDVQQDEw1F 4 | dmVudFNvdXJjZUNBMB4XDTE2MDIyOTIwMTMyNloXDTI2MDIyNjIwMTMyNlowSDEL 5 | MAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMRQwEgYDVQQKEwtFdmVudFNvdXJjZTEW 6 | MBQGA1UEAxMNRXZlbnRTb3VyY2VDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC 7 | gYEAujWjBi18dawJfTPMd1vtozoE0VRD5aP32d2UU3kps8nDfKlwcWcaYcKtGhlT 8 | KXYKn4zrXd6wa5J3RdWEwukN5aAkLYDPCJX12w8KacDOoqdYyHi635QXXq1N+7rK 9 | nDKaDwtg015fPsOumLDqk6x4VBpFDbjrcblT0ILqsurd0OMCAwEAATANBgkqhkiG 10 | 9w0BAQUFAAOBgQABgZZnsnsDsGctScBD7vSoTM9+aOetlUlPZx6N52ADe1L77rP7 11 | 0rPxL/+yD3VGVTAhAHrtC1JpJpo+JBssuC2EkwZ2RWFU/rkYQBO7wbgRFAoEWylT 12 | j3PaeDzUZumSL5ZuVyQC94XNCodNeWBDK+WzQbjPa0BPd6vflUnAb/stwg== 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /test/certs/invalid-mtls/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,6040D9A5C438CD94 4 | 5 | nIsbCSSu74WqCDkABopP4WfV1qt1Om0Se27KNWYQdPxBgD7APTcf2OBb3QZH5IpX 6 | esGvGcCAfmE9VXmT9z3Kpwaloo1kZ3kdEEdYIH7QWc0mbIlMLghgvjfYy43LupoG 7 | GD9Hlhvtpynhrc+VE+R3DKKboA8NnT//NuekPkiVdp5qVM7uKNLCiYrW2iKHZDiA 8 | mRCbmWmLAC9YPou9mEu0wJxk+MULXjIjgls8IdCcNdcEJO6sRyzPOgBv/6WipGxq 9 | 15xYJViBUVl8W33JdkGWFTrlodJs49X7/AjHw55GqCixONrPzhzycsgFo8FOCROI 10 | BqPHD5N/itGtMu61JGCmvBuXyhUYd9xiBrinalVOxJV+XUJWLGw7KurNQrPa/RwU 11 | m9E0cSxwyYWEalpr0fTMpvo2NYcKmJLs30jj9G/Kpqs/7LWjrp7Tm/stHFSpa4Jm 12 | Qt1nX7x8Vjt1cnMup0f/dK8eMsx4JsF0YqyklYObyJdJhe1akoz2YhSnD4CNE0T+ 13 | oIoxrU74w6iK0PF33+w7sc2ZRODZdJcA09r9yZfh68yhGPkcp91koaZso+0uHlHw 14 | sN204cy1jhS2s7EJ/J8abHdOnznEw1g/1YIPkhfgEh3ccpGCWPbpAHc12eth+fEl 15 | SL5rmFrQtowic2lOYOabWbkP+ZDd5deSlTouOB/iYtsRZP7UINBlqcoYvThx40ba 16 | yHKeFE1pitNB3oh3AVGGu8xbzObzjVC32s8caMZqr2QwIyk18oIGQPD4rhcbkRQ8 17 | WIeog03dLVEBa+qhgaOZX7zJrYX6eilRGBXmjZueV57VOl3pTQCnpg== 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /test/certs/invalid-mtls/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICAjCCAWsCCQDcY7oXL2AdXjANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECBMCVkExFDASBgNVBAoTC0V2ZW50U291cmNlMRYwFAYDVQQDEw1F 4 | dmVudFNvdXJjZUNBMB4XDTE2MDIyOTIwMTU0MVoXDTI2MDIyNjIwMTU0MVowQzEL 5 | MAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMRQwEgYDVQQKEwtFdmVudFNvdXJjZTER 6 | MA8GA1UEAxMIdGVzdHVzZXIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPKw 7 | mifd5tbJg5ZDkoabQsgqqu24MsNbJMtrbTkH/DtK12Qg6m5n8TR2aUt/CfpXBNkh 8 | C40KzbFirkoTwLtLon39f6HFqaQCvdRZOb9e3SmHapm1W/ROx2M4L17DdKaVdkVn 9 | 7HA4zBL+kfZSI914dWTI4s0l9ohwJdVmBBBsmEO9AgMBAAEwDQYJKoZIhvcNAQEF 10 | BQADgYEAdDfnGB9/n3Q2Hao4t8svM5s/Wyvy/sc8iZUn4R6PwFKSmeeuOS6NjkIy 11 | rq59z3D4qHTES5RFoO7d8km884UcYCz6mq081hAm+Dlcd3uZhsWyboHCgkWlhVIS 12 | XGEeNAUBir6aGvBkfU69HFdXfprTiTvHlt2NukwTPCYoqPQQH3w= 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /test/certs/invalid-mtls/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,5A4FE8B32AF89EBF 4 | 5 | Falz9rSy6LdpFlmXQKkmIGVzYqkmKJ8rA1E/xt5YFOGk7tO6kwCKm5/wOrWv3TrH 6 | 6v90CTNNJHerwFe7QinEAMiwKaLssR+YUxdNplJrYCbTTRrOo/DER0qVO8bw4yht 7 | MawMDY1+pI1YyDpkwNt8QsVn80DFUBTqnXb2A+u0Pk+PaL/hji7t1piGD+yRkPsC 8 | Fbx54F3IzZByFyhMQ2YoTzT8OHEyMDEzqxjla7DsWC9PGuFTtp7h6NWA4kT1FBb5 9 | kkEIYai/DuN/ng750+7eAVv6K3Fz+byb9t4VMpb3FHpreqk32D8StCVG1N1vUVK7 10 | gN8D3X1GBDRKmOEP/hSJOoDZj8W9UXq4SQ9GzRtyVGq2ARMr32P/e/EOq5L4tSy1 11 | WVJk0pBVIIUrY2Z0UO5tpK7+pkZvjOCWr68qnXIWcyEptyPqEiFZNqoztRbtaHyf 12 | 7m8FEDXSRBKQpFyi5p9AWut1hlaf6YyKbw3z8Gqfd1nqbzcqqH2jSN7WdZcJj0Ma 13 | JWTMaFBT1wZYQ/mQleLk4TakQf+X/wlq/B9gfrEWR0Zkmc7tuXwDaMU8muQdXx6d 14 | Olu73ICqrsajrmILX2tmJo2HsSvfNjxOXC/VORqVwD+Hp09p3UZnrxxNohCm8Qka 15 | Dgghd+oU0o1RfdBa+c5ZL6l41ghk2a3zMi/GCGl+KprmMonlRIfUSkMc3tdRQiZV 16 | GubJSwASXFxVDrIb64fDdyEryHFxKjyFeHOc9fFyP11TawLJgCKPaiqSnUivvvPr 17 | PBei3pkaFiq9mpF3pF3GO7I6ENGpBmU0O/6rVh7hYtg= 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /test/certs/invalid-mtls/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICAzCCAWwCCQDcY7oXL2AdXTANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECBMCVkExFDASBgNVBAoTC0V2ZW50U291cmNlMRYwFAYDVQQDEw1F 4 | dmVudFNvdXJjZUNBMB4XDTE2MDIyOTIwMTQyMVoXDTI2MDIyNjIwMTQyMVowRDEL 5 | MAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMRQwEgYDVQQKEwtFdmVudFNvdXJjZTES 6 | MBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJ 7 | 5BXHRbLlvlMGg19SQ1Bg4iDA8Tt9IJ4P/tocgwamQyCjFpngOi+uDmnXLZFf1x7P 8 | ueXEtKVLLGlehcqM3LNS3Z4SUUc+OkcO2ztFZHjWell7FOuDTUuus3BjcFQDDNhd 9 | GBNBKI79/oxRVjRFcDdMvTWG0r1UIALvlECTj8uwswIDAQABMA0GCSqGSIb3DQEB 10 | BQUAA4GBAFqvIsJWnsV/drGNeuftEMG/zwD+5j8Xe9xCUIewMH5Er20/MXK0owLi 11 | V3XI84LpVKi9hfUwJji91EW6Qi18Z4LKdA/bXvLdWwtZMCybYTTGKnLmUELhqIyn 12 | VZbgEXyYpiUzRUnIotjbOwQIpP1aj+8Gys6DrHgBEbqrMuI6tiKF 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /test/certs/mtls/README.md: -------------------------------------------------------------------------------- 1 | # MTLS 2 | 3 | ## Generate new certiticate 4 | 5 | note: Set common name to localhost 6 | 7 | ```bash 8 | 9 | # CA 10 | openssl genrsa -aes256 -passout pass:xxxx -out ca.pass.key 4096 11 | openssl rsa -passin pass:xxxx -in ca.pass.key -out ca.key 12 | rm ca.pass.key 13 | 14 | openssl req -new -x509 -days 3650 -key ca.key -out ca.pem 15 | 16 | 17 | # Client 18 | openssl genrsa -aes256 -passout pass:xxxx -out client.pass.key 4096 19 | openssl rsa -passin pass:xxxx -in client.pass.key -out client.key 20 | rm client.pass.key 21 | 22 | openssl req -new -key client.key -out client.csr 23 | openssl x509 -req -sha512 -days 3650 -in client.csr -CA ca.pem -CAkey ca.key -set_serial "01" -out client.pem 24 | 25 | # Server 26 | openssl genrsa -aes256 -passout pass:xxxx -out server.pass.key 4096 27 | openssl rsa -passin pass:xxxx -in server.pass.key -out server.key 28 | rm server.pass.key 29 | 30 | openssl req -new -key server.key -out server.csr 31 | openssl x509 -req -sha512 -days 3650 -in server.csr -CA ca.pem -CAkey ca.key -set_serial "02" -out server.pem 32 | ``` 33 | -------------------------------------------------------------------------------- /test/certs/mtls/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEyjCCArICCQCZ58E/wg2V4TANBgkqhkiG9w0BAQ0FADAnMQswCQYDVQQGEwJO 3 | TzEYMBYGA1UEAwwPY2EudGVzdC5nZXQtaXQgMB4XDTIzMDEwMzE2NDgxNVoXDTMy 4 | MTIzMTE2NDgxNVowJzELMAkGA1UEBhMCTk8xGDAWBgNVBAMMD2NhLnRlc3QuZ2V0 5 | LWl0IDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMQXc1cJs/AOQIuP 6 | JVKx+m49HntCs/Y3GDipewZLP4/1EG5k16zrVbEQpHLCiCc5jcz1leYmmvzqMXyD 7 | UbW8VW6+MX3UPDUJfXwbHGnaTpyN8Vi3mM2D6X9iikfz1ynUAPuMkDL8KuUtOIPe 8 | j2R2pD+NLOlEDFLIi2VCcfINdK+K+xcHLymJCPC1rSyEHjg1nKFts/6XcAPwM0r0 9 | h0ih7jqyHXEKY8htyWN9tDt07+uV5bRa8w269NIOUwermjRuymQBaRFxI3dzKvJG 10 | wWLPTnI1PL52E47WWaNs7PSJJjlIkcOrvgSQU4MePTy40U6mF4pXQBIJMIPZIvux 11 | yzLXVNABxtSCXlVwQy/X7z4OcN9fcWx6pgrcBuA1uq0Ps3HZUwTTXfkjOZH+5sKv 12 | G02+RRb63QIBvHm6QXitRHRDsM2vqxIOGIGqMhRwbkP708XYFm61Vo/WPCSXJa10 13 | FWSKEkqbln2A/I85zEANffUVTxz4CxubTE7odz3z234l0DVtUqtZhkGpSK0rhPhE 14 | 7AFMYPRrRP/ggu225vU3EVpB4ClXvZx55z4PzUE6E6bfDiBKh1acqzZs6YNn1yRr 15 | Mew/SDkFQZs8LMw0yQMbfPR69wPtOlOPaJrXH+DlgUvPK0A6+kuIMR53Hd0wlWKc 16 | ce4a3bfo96ZjG2XxhcoZvmAwI/TfAgMBAAEwDQYJKoZIhvcNAQENBQADggIBALC1 17 | /Q7Rcb1YPB0FMeGMDn8loul8MYOT766Lf+aHAFmYahiC8CcYYrCojg3MD6IOpPp2 18 | uLO+9y34Ze/70BZzjskJYlhe0ufM9LSIGa4KMi7IHiCFh12V6SzN3YqFo3/TajYi 19 | pTesgEO6Ct23hWEZRrfdMhIviuWdIJTZMflpVWDg+pEkXorPtPNuI8/gol9jC9yz 20 | O8fJpI6uvg7G+Scuwh3q3tsjiiHcmalibtHjRS3jAMGOZbJM4ei6OKCjWXUZ1qKU 21 | +5jziGh7fb/XkqLRehnBQyFvAOXJiRYsR7K2TaBgAt16StvrBGMkKBSlLnlKyxkO 22 | uo/ssyTmRGzys67e+ZG3jtESYLHnyaryzLgXOP9gxJqL3Mkk19egoTAd8Z3gqZKs 23 | aJzuOJKLFMeNMWcJh0bH4Ae9XFjzo+p3QdoMQexy0FWiV06hzZCINFUBsIztsMD8 24 | bd+Z7OFtwR97dpiqxm9RwSuR8mYPeKj/+8IGr1OSvviEV35gAhewOhdYv3b9od6I 25 | twdqGx6BplEC5t2OsMKOdeGd+1sNdMWP4YH0Butm2S8Rdgkpr9I+NDhvhXqZS/sa 26 | BuZc9OruoF42Wvz5MnvNFGqfg549iSkcIquaFxs/9LAKNheZaBv/jszenA4qBC0P 27 | oI/3ebPf8kTI/OOJnH1/bKT0nJGFWw/iw1gxzQf2 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/certs/mtls/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEAuX0cfOk8rEOdtyBXXQIrGTZVy4DpgtTpxby8bZnllvIpgKt+ 3 | G/Kpv5Hdr9v8ZnvTBQ1OpIFDbL47DqKqBlSSrFHmXv38zMlmZmIRrgUUWpDM1Q7F 4 | vI3fJ977PYYL7wAtPVuFvNPmSbiEYflGennn/1RGgHu0ZcYqf7GNLtnEomAp8XYV 5 | XW2EWYpcZKeq2flvu7Agd1QRWbaBRFWYzZwTmZ7bmF3BQ5APQD4/NOESXSd4YAIx 6 | G7FfFznswL4l0GZEG0a2LKUgP+k3TNNh1WbFYW/WaZ3TgtiekIn5JfDa+T7/3k75 7 | 2BAbfOBh4+9R/MqJKi/BGHSI6Rj5erdwdiPC1cdQtwhaPNVPOzfPDTZ8+ns3J1P0 8 | AxuBS/GMor67RY0ProAkriU+pDGN38x3viAGurw6YAIlcv3JcwkUvsl+1T+d9+KM 9 | Sk5LufT4J84CxQ/mL+ROpgvorNXGrrSxrjvaR0GnBbwyxE30P0VFOuCQtnDYSzFe 10 | EkuXr71qLY9mb+EdRtLlM/j+SljfoPtNzdoq6TwZb5scVlHUnLhG1AiXzB3LqZpn 11 | Ba9fC2d1/rIKEmWLKJ+azgBWmTq3kvVGv/tV3eK8axLFAZADrApdRKo232iAPewL 12 | z5G5R0MWgHsz3mdmVrz9wBVP4d9wJ5ee05w7DL7gpIbyenrWgm15rogaB6UCAwEA 13 | AQKCAgB89oTSwm9VvtfqH6e7yVIv0iNKzraCpd44kUaAsEW0w8CiJub2/SzPGv43 14 | FAWfJZyssqJgwZUjFaXD3mKdkSyfWy3QoVxGuwh5wWgXzORBf3v/rcwZk5rbyaWJ 15 | dV1kzsGrrJUU+c0TrXPCbaXFrYtWwPgeZzjJuA+5p2xuO9f1bMPVILCUGMwpZsWI 16 | Kk04PIvB7o5w1fo6bnH4D5K6MoQh4pwesksZsVwU89qxTVCJ4aV/SPq8pWpiw2t1 17 | DqV7GOqZVbI1salgZUYUQ/SNSIeBdU8GKIXuiPGKfumTXgioEoFey+YWYgOixagt 18 | X3Xk0K5H9ZQpXZk/MMwimKLzdHQ1WlUpVTamTpl7dM+GcDXXQfWjIkos4xwoCwwY 19 | nKeh18y8WJBNsZ7LUn5QdTnbwgIG6DYXdrM3hrhhzNbXR4wJ1dYMhZ4lYx9h1mZO 20 | URnTNrBX2Pkw4g1WnXIR8PgjwgYSa1I7vDy94/neChQq6HXjq52CW2o5BVVYHmAS 21 | EuRsp6uzC0MQOLeTBw9GMv8/NgAVpSkUMREFKL4W9AM4j2npDFOJTYAkngPsIO/1 22 | jwGWdUoKVh0A5jVhVTALdixwvg+WdajctBc8bApSazofSy2BAPeLd8Yh+fvT3yrn 23 | gXB0h1PnRoXKz1L1neBW4Zj15siV1BskgLRNd365d8r/TnTHoQKCAQEA9KLZioLh 24 | bcro5icMBTHO56tplDstv1I7Yh7RHDR7gJh41iycRYpkF4iqJF/1A+uxVHPBZluy 25 | y9z2feCgPHH4xOagWSbWcgThAYPAIFsvHB+eHkIg3l94Zisdw5q+l8AdGS/TbsLK 26 | hgBMgLmOe9Db3yjrFySGkdmkUkbv3cXG+MN+pJyQHmevX0GoPF1UqzVkFFQ4rn0/ 27 | Euneh7LCBuMO6CoOvO1Z30t0yCIZ8qWh3jOFDBqjubLa8DFp3uX2Qj6AyAm4q7uD 28 | U0D5aGQNQqwtL4Uq6v8ME8XDZp2Nqrx+lFryYe+6NRWkZ325NIqVxCk1pErTatJU 29 | PI9M1hxXjRgXnwKCAQEAwhrlRUBaTpv3UjITGUmkgPahLRFLePumzn0ms3SpTdSm 30 | 3AnAsEW7brXTz0ZlozE0HPlSC4+YD4GoiHaQwr69k3Cu/3pDGixhDt4W7NSZzJsd 31 | bo90ddxeoWAC2KHs2GoDpneekw8QjVLLXSPhelBdvNzkHAHbmnz7HaB6fNmTu3Kg 32 | 8eazXvkPOW4GdnQ9r+n+1vmHe6Sq1VNMBgfSQhMJnb1Ph4+8/GrrVwks+bQZAZxi 33 | T7B1Nz/szPOIdzshJQcaR3a1BInWiSoNj9tgSRXT3FinZPRO+L7hW0AWgXj2k92G 34 | b0u1Is2fqUQuRXx42PW0Pck1R5V32/at1JE0IwOqOwKCAQAV2RlummPBp3aHX4Ne 35 | T2V4IUHroCFkzdZJ3BC24SUKhhN8pT1LpKFtzKHNX9iFAh6zOSdTmers9A37jf0h 36 | iNg71ZEKhUJvqmnh9b9J5Hrfmx9G+obu9T3OehlNZ9uPt/OhVTkf0ju+Hfa3JFtK 37 | SYvpcUEVrwhprDNVucogV1J+0w7TarwgSwhJjJaW1YsDQ2BtxFvPkZX5fXOHUPqt 38 | HMgvCdqJGvWE0LtcSFdi6VH8g0Nloldu9T9CZldbNdR5dBWSR/P7OmLvI4ViwlTA 39 | 2Jmsfcsoc6DDf00FNBr73Zu8aibtgjhyFz24lGze+WWIwFlvE9Ov3ZryUaX+FUfo 40 | bPdnAoIBAAn+NHuCOqP9Z5g1t8H/hpaVG+skDsyluAq9aramdOjq38RIREFO9b1X 41 | YxYyu5zRNaugMlciB2QU+sY7xGwiQcRLctb4RmgcjMqlKGz87QYZnkRI4mgG9mA8 42 | fykD/RuPKazyT5mmluWPs6SR6lPPu2Ozw5KljdbARHVcA7JVyUNHPX32sJldHSmJ 43 | vo9uoJZj55jNs+nrqlfdN/a/hFWegUo7qtKB1erw3jjW4hfg68CnenA7120Gv3w1 44 | tXrd8nDjkrjHJb6cEg1xus0DMEvS8dtQPR33bfFkclmuTPpRbfBi4T7tmN+30lZ2 45 | iiNNqzMQQz1DFJkg2tDaPEzLRPHgiG0CggEBAPPJPtQ8ZyvalZH/GOgJG9yBicj0 46 | XLUKRbDhsXy6Ng7vNmXEraCXbfMvESuoHjlee4d9m+0HRbxRmBT1B41dZCO8AH+G 47 | vJjGF11Z9CCqTdyTvHWM+cEcL8CjWz7RzZzF9/bLdXHKx4Ho+/OTt90rKJFYjv3n 48 | cJvC9VcREcKy25PG0aShRdp8f3Z3t5GHfWWMtrYe2IMMZ64YvA+tPMv4YgX9W1Qs 49 | iztiMbaA/bzGSA6B/VwSTko2STFhIZGW2Kca6q3+bpqOxtJKKBjHfWvizFs8dq4w 50 | K9i8OCjGaOX3zf3vEqtRN+frm/40VOEeQ+wjOSFGVOjUUWYHE3wkovKBHl8= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /test/certs/mtls/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIExTCCAq0CAQEwDQYJKoZIhvcNAQENBQAwJzELMAkGA1UEBhMCTk8xGDAWBgNV 3 | BAMMD2NhLnRlc3QuZ2V0LWl0IDAeFw0yMzAxMDMxNjUwMDJaFw0zMjEyMzExNjUw 4 | MDJaMCoxCzAJBgNVBAYTAk5PMRswGQYDVQQDDBJjbGllbnQudGVzdC5nZXQtaXQw 5 | ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC5fRx86TysQ523IFddAisZ 6 | NlXLgOmC1OnFvLxtmeWW8imAq34b8qm/kd2v2/xme9MFDU6kgUNsvjsOoqoGVJKs 7 | UeZe/fzMyWZmYhGuBRRakMzVDsW8jd8n3vs9hgvvAC09W4W80+ZJuIRh+UZ6eef/ 8 | VEaAe7Rlxip/sY0u2cSiYCnxdhVdbYRZilxkp6rZ+W+7sCB3VBFZtoFEVZjNnBOZ 9 | ntuYXcFDkA9APj804RJdJ3hgAjEbsV8XOezAviXQZkQbRrYspSA/6TdM02HVZsVh 10 | b9ZpndOC2J6Qifkl8Nr5Pv/eTvnYEBt84GHj71H8yokqL8EYdIjpGPl6t3B2I8LV 11 | x1C3CFo81U87N88NNnz6ezcnU/QDG4FL8YyivrtFjQ+ugCSuJT6kMY3fzHe+IAa6 12 | vDpgAiVy/clzCRS+yX7VP5334oxKTku59PgnzgLFD+Yv5E6mC+is1cautLGuO9pH 13 | QacFvDLETfQ/RUU64JC2cNhLMV4SS5evvWotj2Zv4R1G0uUz+P5KWN+g+03N2irp 14 | PBlvmxxWUdScuEbUCJfMHcupmmcFr18LZ3X+sgoSZYson5rOAFaZOreS9Ua/+1Xd 15 | 4rxrEsUBkAOsCl1EqjbfaIA97AvPkblHQxaAezPeZ2ZWvP3AFU/h33Anl57TnDsM 16 | vuCkhvJ6etaCbXmuiBoHpQIDAQABMA0GCSqGSIb3DQEBDQUAA4ICAQC4V26NB9fW 17 | 6FnhtUFEZAp7poiuds4qG19QLHArOBMSnFN4R6pcUiz3GUdY/0+OCkFuzmjRgJFr 18 | BDVOAmGmL5gSlucMhlD6ob+KgdTM5rQVgp8R5N4PVyF3xStn9ckaUTzMMR+aJoPF 19 | Oys/w0UhnqVZ151nDySTpceite0hOYwznBHNMIHHIQmVj6c+gHlGk8MLmDi9p+FP 20 | pPwB2eBWY1NygbeW1FoRBY2DSUOX7CCAFqqGU9uKzLAhz4ZsySR7iklcnwh/s5xC 21 | nHw6HcCGVmMDklPBL+zkdkPtA8KLgoWvvajCYc1W5zAC6IsCiT4z3RtLZPhE1LZT 22 | lb/eBoO/fjy/pNAl6Kily1ZjC4aY0M5ZgqVODXjhSq6AKHhk8Fjb15C4B8rLbHQP 23 | 07nOLIrFIMW92BlYTfDA+xbSi9zKBHpA0cHqUnjlv/+XoyxOr5QfLdP3gz4A/OLU 24 | KvZqIL/5w8tBrFsLHtoaFdtSocCteovqzqkcHb5DulYlPQCg4YJjzurHcvvbOiWs 25 | bAV4fIHCklh2mCgjuFXYk7iX0EZ2WYTJh8btoeahj9IwIGvhbfDma4/Sp07cAt5X 26 | UI+yIv4+txSFpGitiCJOXMfnxX9cIZQHhWk/BbYieyFT22OCmoYCF3650QL8qeaa 27 | GH/iUF2UUdKqSiPTJZsuWVLpRrdPFyWE0A== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/certs/mtls/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEAvXz+Hqu2BBoQj/0SBTsUhjytUqC/mQ+KjM8dPjgK2Qbokqqu 3 | aL/TVkL9B2tQWqGg0U/3LxyOJIlMSDGNzrxt8PBa4xZ8F2Nh/2DVg8tIwwnm5XWe 4 | MIMFSZv50457Cv8wZZ5Vkz4oYCVafu7/1BAauRHE5r+FR0VrETWeLs5B/YiPo5RO 5 | +UiDUbduykp+37+ZG1DpBxrN9y4gVU4HFRb8CUv+1/uFcEkDTa235ij5MLkXqeUe 6 | ex5piZVF6psN77zKrlyKfl58bzHksx0ywA7+89ePeSP8u+qWJtwVBCvyfSYupphg 7 | vrp2A2SZJqj8NQtU1vu+DmRDapWYNwQCJAQLdkatV5OPQYsi3PrsKEgPcakFqw1Z 8 | LsJHXK+uzW1GbPz1JZM6Rh1IjU7rYxryS+gRUobITQ3C4XfvSi2uslrq6ry/C5ye 9 | 1nWjLj1na+g4JulEfd2R/nnGY6sTk5zq2FA+aSzvxu34Hd1KPKMl9M+C/A7LOqBb 10 | KyBzGetwB/B4/I6glndWRJUJ6D5nDkYfxkbdA7rhXRz78FKXhNQN/l9gswe5o50B 11 | BJkPtjUFut5uePw/aEx2F6IkVvspQrdePESx1OwX/WurJhF7absJIKHuBtM33kFP 12 | deb66x99tgBQzBqdwgIPDpAX8iWhHRWKhfFVF8e4Byl7RZ6AHFuKoYKtkEECAwEA 13 | AQKCAgAihnSy1+yJIMqlwaGX7GTX9JRL+tgOJmPDNjWI0aNAWd7kYk727QSvsfLy 14 | eB4i9VIc8SoDnntTsuSAPu8snO6XOOQGfmFLzaLcaiNdKRvv1Y0Jf/27rxO2jsHd 15 | RxeqMEb8LPZAptGqewPbHubkF68Wy2MJ/BlnnAFfGFrjAlfJvykUz//3sbujlv2L 16 | xDkLwZg+/uYGkl62y+O+R0JJGHABraQYbE6q39LHQ4C/YfXNZ9wk64c7PZZTKCye 17 | C3Rlmwt+64OcdNDNXdTiwc4uak0P1tXgZ6sz/hEvsWfu46cCIIPdfOQbvFOmWJ0D 18 | C0Uw0Qi7RxEhBCm4cNtUItnVABuqN2tRlp10/UbgS1KvbzGyIaDA77vI1d9CGkCQ 19 | 82a0ih8QCL7gKLGMjaeSDPFeCuxMLQCa7qqyZCpYqVXX9OKBIV3g6D8/KcTuCuPv 20 | eaWqWRiiJe0VHUJ93qB/KcD2ky3m3IXMJUIngdzDIJnLmdP8IJ5XNbdv+TzL8Kg7 21 | YKwE64auLRacdj8yjFbQ99Nc4hMXotFvQnyTVlUnWQsOSE71yyfeusqazp6F+BM+ 22 | vrcTQYRg7yY7OiT6MrtmL8zURQmphbOUSeGuK/A4EnvTpVYcveJc7ukqzaQ4XLI1 23 | tZuTb1z+y45kLxXjwiDoJtrkx90nUfnuGwG9TAjMqnFaTn1EAQKCAQEA+JCjyOGg 24 | Wn62HdQwnE8+hk+R7aM1b3W08AVTt9Sw2/TTJa5JFiE1X7NGNzQ7cNqGeiTrPL22 25 | qL2szQKxBaFHvqtPtgFDVFSB4ex2IwqCjg6BY1/1caynkDLzC1x9Upxy8SZjN2SP 26 | +vCXczvVPV1uKjjlZlauYZvxCaw71ThjsKcO6V7BiiXFSErDnqrohpn2dTLPKO0d 27 | 1828TusYC9SMiCMQ8Psf1bqtF+tIlG23TimpH1/P4+M3U9YxivdXlRS4WAlRNlfn 28 | 78neR4F5KD4plJPliUVkaEtJIHU7xf/uUb0tXuSyXqiU9WwREPItIevAuiHhgrOD 29 | 8wRW7MVf5PcPYQKCAQEAwyf6mvRizxkLhmVS2jooY0mKrKe0eDpBY6HyMffFamUE 30 | M7p9tmZ0L7RK1+C7J5rOgiYNeHxzfViojdUeUvvbSxs10fz35E/mOW+h7qjDymSA 31 | yQVQLfLHHzi+tMF60wCR9JiPJZnntk8pdNd9Hb5DbFVHcLcfG6dY9ZIPBNGjkyQB 32 | i8Nh7Dg5RIERjIs2LSvXAz3e7d2aROxvZR6gXSsS1io+1RIvxbnp6FEIiFnFVVYe 33 | HfKemtrKCJRJFNclIjoH/UbHMJZ+sRw+v/YPmYzU57RE141YCE0Dtz5zlfBeof/m 34 | My/aoOo2Gud/PB7XBnezNyae+YQGaEVdunLqXSCM4QKCAQEA1BBKu/LLJQngBDjp 35 | NCJjnKE8Rfstdqd8tB5HW5opPa91iTtVfXzdlYVUzXpRkCnP+1uOGqdctEAGsm2u 36 | a7g6hpCPlB8lu0fR+9cKv0CTO/FiME9JDy+XUe417yZ8gUOayOTQVw0Dzbr/6z9L 37 | 4WvZtkoOZS5k3j/1+COqdHIEk00j6rR9+Ifa9Z9bn/3+HgAzBbBQj4ElKd2L3+Sx 38 | Aj7XmroYFRCbC3SVgseoh4HHlvbyJCNRMQLETFF8uSvm2/jugaWJVQzQg4K9klIC 39 | 9PN79BfuCBxc2qIIhJmgYn/EapY0pDYHe7zmQTBuqv+Cw+Ln7aVESYcwdt/n7V9D 40 | PgwOYQKCAQAidMWPZESp0f8x7GPTed81oH9menmnnIl9ANPVNKzbWxiB1ZRqhu5O 41 | meN4+AmzNWbuna9VLYvqfqPL3uvqtOMEALrg52wRGHZKf8Y0cAGZk2MVmuWMJ0g2 42 | 3/rzyRFZfclHSURE3EaH7rQgGAfUH4qh7vCdGwfYBxTyzqXuMOFTklDfj1CTt12N 43 | Fci2asHW+d2Niff5QV4Ce0gzr1Oa/4bk3zKWxg4/N9LvnJcIE+l+ZqUMnpCYQLbZ 44 | Cu1YS6AXye0tL7jtgDTAKn99vwEEGFRi0HYzJJZ9aLOWP+WCfJ0Itzi6ouFT9eY1 45 | m9fgEoXec3Xl7+CzpzbGZDDnQXDVnnChAoIBABVQNvRTV9wo1F6FGPKkEKIiNDgX 46 | /KGPCsIJKqhPfsxmWdx/b8Ou3keislklkt8i5cEe32R0WRK9zncefN1phfwtXIrM 47 | oLSL/45a3ot/PSyhfA6+YOIlMcUl64C8XB0ibCxwNRDS89WP18L39Hl0CdNF9KJp 48 | Dj+TAyaaaCHHMyMigGgVZKlHxeqN4gIT1/Z62KuY26ePTxrueS2f+SMa3/h9S6d5 49 | xrILmVt2gRf8OMfL6d7Uct0WxSzxr5nYKQ1rxIiZIcAc/ZGAQ5R0spM1VUoCJD30 50 | JMyjj42IeU/DRZAEWPhmWUbjJP7Op8+vreJW89gYVOUYK1a1XbiJR1GyEXI= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /test/certs/mtls/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEvDCCAqQCAQIwDQYJKoZIhvcNAQENBQAwJzELMAkGA1UEBhMCTk8xGDAWBgNV 3 | BAMMD2NhLnRlc3QuZ2V0LWl0IDAeFw0yMzAxMDMxNjUzMDZaFw0zMjEyMzExNjUz 4 | MDZaMCExCzAJBgNVBAYTAk5PMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqG 5 | SIb3DQEBAQUAA4ICDwAwggIKAoICAQC9fP4eq7YEGhCP/RIFOxSGPK1SoL+ZD4qM 6 | zx0+OArZBuiSqq5ov9NWQv0Ha1BaoaDRT/cvHI4kiUxIMY3OvG3w8FrjFnwXY2H/ 7 | YNWDy0jDCebldZ4wgwVJm/nTjnsK/zBlnlWTPihgJVp+7v/UEBq5EcTmv4VHRWsR 8 | NZ4uzkH9iI+jlE75SINRt27KSn7fv5kbUOkHGs33LiBVTgcVFvwJS/7X+4VwSQNN 9 | rbfmKPkwuRep5R57HmmJlUXqmw3vvMquXIp+XnxvMeSzHTLADv7z1495I/y76pYm 10 | 3BUEK/J9Ji6mmGC+unYDZJkmqPw1C1TW+74OZENqlZg3BAIkBAt2Rq1Xk49BiyLc 11 | +uwoSA9xqQWrDVkuwkdcr67NbUZs/PUlkzpGHUiNTutjGvJL6BFShshNDcLhd+9K 12 | La6yWurqvL8LnJ7WdaMuPWdr6Dgm6UR93ZH+ecZjqxOTnOrYUD5pLO/G7fgd3Uo8 13 | oyX0z4L8Dss6oFsrIHMZ63AH8Hj8jqCWd1ZElQnoPmcORh/GRt0DuuFdHPvwUpeE 14 | 1A3+X2CzB7mjnQEEmQ+2NQW63m54/D9oTHYXoiRW+ylCt148RLHU7Bf9a6smEXtp 15 | uwkgoe4G0zfeQU915vrrH322AFDMGp3CAg8OkBfyJaEdFYqF8VUXx7gHKXtFnoAc 16 | W4qhgq2QQQIDAQABMA0GCSqGSIb3DQEBDQUAA4ICAQC4cLilAS9s4MB6Zmuq+0QB 17 | 5a5O/6zfor8j+7gzJGM0itx7/exZ36IEYOHar+bxbNPnj5eQ3ouZFDI4VxkTXfSv 18 | c7U5IhNaQVkxinFQAfZyGHOG+2yOtYdKbxMBf/OUveDFAzgkxMtu+fflsL56jfSO 19 | +Z9cv0l0iMETGoAWynmiVE+7zRzA1bq7wN/PecSQFJcaxxMwl9UoOWBF7GgSeYJ7 20 | j1KCBYduEWrMT+aO5W6oNa9uRLR1SUeH7cAFrqiDFgD298SrBvTmCrVyJNV1RwqD 21 | QXoRXSUT8ZZm9Oo/TqARz/YgW3pnuwLaGqLhQF+6Sdik03B9Ob93cA6rQBRghOWN 22 | i4mEoHw9zmoZ25vdXwNlIllQ1amAv2Bcgl1KeXKe9V9GYtIlYe7eWNb++jNZRPiP 23 | 0ZS7c7vGA3iLPvlu1Q7tZqUyh/b5FJo8uYr5gvkf7hIhJqTuQfeLbXGgWGpwitNC 24 | n3Pr7YrkiJq2ohn8BCDvv+aXl91mn0wcqPMve13ml7WOKrv57q/ailPb9jVUF5Z2 25 | 3oDAJp7NgNCovc3bt+slO0RKAJ1W0tzZzAe4JUYGYFQcujHPHkRCbr4D13JWVExh 26 | KESck1krPCu+nzeSChhIY3gLp0eR7cPFZGSAyYHnGkzsWPkAR5+zOvgVl6NKcHwI 27 | cqg0FXcszS6oDu4uh+RwtA== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/certs/server/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBuzCCASQCCQCMLdUY2C26YzANBgkqhkiG9w0BAQsFADAhMQswCQYDVQQGEwJO 3 | TzESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIzMDExODAwMDE1M1oYDzIwNTAwNjA0 4 | MDAwMTUzWjAhMQswCQYDVQQGEwJOTzESMBAGA1UEAwwJbG9jYWxob3N0MIGfMA0G 5 | CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2Wm 6 | O8CbBFc0qfEChrR/3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3M 7 | Fe4ZJmvbtO0WZxXgf72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2 8 | A8XWFmEIEwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAHPTpRgd4ZglU32WJuDKWhr2 9 | QHOX1eNnh6HPHFuvhViMlCT+yzWIZVTtc5l7fW/F/UD2stmY6JQwmTBUs4y6woLl 10 | UO82nPHrxJ2jHS8uZtlSEUcbiYBffmQdnrv4ZEz6ksM3PQBtRCs6rvOLcF81kYHO 11 | eB55rzjJpjVXm/71BEP5 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /test/certs/server/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEChrR/ 3 | 3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0WZxXg 4 | f72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwIDAQAB 5 | AoGAAlVY8sHi/aE+9xT77twWX3mGHV0SzdjfDnly40fx6S1Gc7bOtVdd9DC7pk6l 6 | 3ENeJVR02IlgU8iC5lMHq4JEHPE272jtPrLlrpWLTGmHEqoVFv9AITPqUDLhB9Kk 7 | Hjl7h8NYBKbr2JHKICr3DIPKOT+RnXVb1PD4EORbJ3ooYmkCQQDfknUnVxPgxUGs 8 | ouABw1WJIOVgcCY/IFt4Ihf6VWTsxBgzTJKxn3HtgvE0oqTH7V480XoH0QxHhjLq 9 | DrgobWU9AkEA0TRJ8/ouXGnFEPAXjWr9GdPQRZ1Use2MrFjneH2+Sxc0CmYtwwqL 10 | Kr5kS6mqJrxprJeluSjBd+3/ElxURrEXjwJAUvmlN1OPEhXDmRHd92mKnlkyKEeX 11 | OkiFCiIFKih1S5Y/sRJTQ0781nyJjtJqO7UyC3pnQu1oFEePL+UEniRztQJAMfav 12 | AtnpYKDSM+1jcp7uu9BemYGtzKDTTAYfoiNF42EzSJiGrWJDQn4eLgPjY0T0aAf/ 13 | yGz3Z9ErbhMm/Ysl+QJBAL4kBxRT8gM4ByJw4sdOvSeCCANFq8fhbgm8pGWlCPb5 14 | JGmX3/GHFM8x2tbWMGpyZP1DLtiNEFz7eCGktWK5rqE= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/debug.test.ts: -------------------------------------------------------------------------------- 1 | import {getIt} from 'get-it' 2 | import {debug, jsonRequest, jsonResponse} from 'get-it/middleware' 3 | import util from 'util' 4 | import {describe, expect, it} from 'vitest' 5 | 6 | import {baseUrl} from './helpers' 7 | 8 | describe('debug middleware', () => { 9 | const log = (str: any) => expect(str).to.be.a('string') 10 | 11 | it('should be able to use default options', () => { 12 | expect(() => debug()).to.not.throw() 13 | }) 14 | 15 | it('should be able to pass custom logger', () => 16 | new Promise((resolve) => { 17 | const logger = debug({log}) 18 | const request = getIt([baseUrl, logger]) 19 | request({url: '/plain-text'}).response.subscribe(() => resolve(undefined)) 20 | })) 21 | 22 | it('should be able to pass custom logger (verbose mode)', () => 23 | new Promise((resolve) => { 24 | const logger = debug({log, verbose: true}) 25 | const request = getIt([baseUrl, logger]) 26 | request({url: '/plain-text'}).response.subscribe(() => resolve(undefined)) 27 | })) 28 | 29 | it('should be able to pass custom logger (verbose mode + json request body)', () => 30 | new Promise((resolve) => { 31 | const logger = debug({log, verbose: true}) 32 | const request = getIt([baseUrl, jsonRequest(), jsonResponse(), logger]) 33 | request({url: '/json-echo', method: 'PUT', body: {foo: 'bar'}}).response.subscribe(() => 34 | resolve(undefined), 35 | ) 36 | })) 37 | 38 | it('should be able to pass custom logger (verbose mode + text request body)', () => 39 | new Promise((resolve) => { 40 | const logger = debug({log, verbose: true}) 41 | const request = getIt([baseUrl, logger]) 42 | request({url: '/echo', body: 'Just some text'}).response.subscribe(() => resolve(undefined)) 43 | })) 44 | 45 | it('should be able to pass custom logger (invalid JSON in response)', () => 46 | new Promise((resolve) => { 47 | const logger = debug({log, verbose: true}) 48 | const request = getIt([baseUrl, logger]) 49 | request({url: '/invalid-json'}).response.subscribe(() => resolve(undefined)) 50 | })) 51 | 52 | it('should redact sensitive headers in verbose mode', () => 53 | new Promise((resolve) => { 54 | const lines: any[] = [] 55 | const logIt = (line: any, ...args: any[]) => lines.push(util.format(line, ...args)) 56 | const logger = debug({log: logIt, verbose: true}) 57 | const request = getIt([baseUrl, logger]) 58 | request({ 59 | url: '/echo', 60 | headers: {CoOkIe: 'yes cookie', authorization: 'bearer auth'}, 61 | body: 'Just some text', 62 | }).response.subscribe(() => { 63 | expect(lines.join('\n')).not.to.contain('yes cookie') 64 | expect(lines.join('\n')).to.contain('') 65 | resolve(undefined) 66 | }) 67 | })) 68 | }) 69 | -------------------------------------------------------------------------------- /test/errors.test.ts: -------------------------------------------------------------------------------- 1 | import {getIt} from 'get-it' 2 | import {httpErrors} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | 5 | import {baseUrl, baseUrlPrefix, expectRequest} from './helpers' 6 | 7 | describe('errors', () => { 8 | it('should not respond with errors on HTTP >= 400 by default', async () => { 9 | const request = getIt([baseUrl]) 10 | const req = request({url: '/status?code=400'}) 11 | await expectRequest(req).resolves.toHaveProperty('statusCode', 400) 12 | }) 13 | 14 | it('should error when httpErrors middleware is enabled and response code is >= 400', async () => { 15 | const request = getIt([baseUrl, httpErrors()]) 16 | const req = request({url: '/status?code=400', headers: {foo: 'bar'}}) 17 | req.response.subscribe(() => { 18 | throw new Error('Response channel called when error channel should have been triggered') 19 | }) 20 | const err: any = await new Promise((resolve) => req.error.subscribe(resolve)) 21 | expect(err).to.be.an.instanceOf(Error) 22 | expect(err.message).to.eq( 23 | 'GET-request to http://localhost:9980/req-test/status?code=400 resulted in HTTP 400 Bad Request', 24 | ) 25 | expect(err.message).to.include('HTTP 400').and.include('Bad Request') 26 | expect(err) 27 | .to.have.property('response') 28 | .and.containSubset({ 29 | url: `${baseUrlPrefix}/status?code=400`, 30 | method: 'GET', 31 | statusCode: 400, 32 | statusMessage: 'Bad Request', 33 | body: '---', 34 | }) 35 | 36 | expect(err.request.headers).toMatchObject({ 37 | foo: 'bar', 38 | }) 39 | }) 40 | 41 | it('should truncate really long URLs from error message', async () => { 42 | const request = getIt([baseUrl, httpErrors()]) 43 | const rep = new Array(1024).join('a') 44 | const req = request({url: `/status?code=400&foo=${rep}`, headers: {foo: 'bar'}}) 45 | req.response.subscribe(() => { 46 | throw new Error('Response channel called when error channel should have been triggered') 47 | }) 48 | const err: any = await new Promise((resolve) => req.error.subscribe(resolve)) 49 | expect(err).to.be.an.instanceOf(Error) 50 | expect(err.message).to.have.length.lessThan(600) 51 | }) 52 | 53 | it('should not error when httpErrors middleware is enabled and response code is < 400', async () => { 54 | const request = getIt([baseUrl, httpErrors()]) 55 | const req = request({url: '/plain-text'}) 56 | await expectRequest(req).resolves.toMatchObject({ 57 | statusCode: 200, 58 | body: 'Just some plain text for you to consume', 59 | }) 60 | }) 61 | 62 | it('should only call onError middlewares up to the first one that returns null', async () => { 63 | const errs: any[] = [] 64 | const first = {onError: (err: any) => errs.push(err) && err} 65 | const second = { 66 | onError: (err: any, ctx: any) => { 67 | errs.push(err) 68 | ctx.channels.response.publish({ 69 | body: 'works', 70 | method: 'GET', 71 | headers: {}, 72 | statusCode: 200, 73 | statusMessage: 'OK', 74 | }) 75 | }, 76 | } 77 | const third = {onError: (err: any) => errs.push(err)} 78 | const request = getIt([baseUrl, first, second, third]) 79 | const req = request({url: '/permafail'}) 80 | 81 | await Promise.all([ 82 | expectRequest(req).resolves.toMatchObject({statusCode: 200}), 83 | new Promise((resolve) => setTimeout(resolve, 500)).then(() => { 84 | expect(errs).to.have.length(2) 85 | }), 86 | ]) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/fetch.test.ts: -------------------------------------------------------------------------------- 1 | import {adapter, environment, getIt} from 'get-it' 2 | import {jsonRequest, jsonResponse} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | 5 | import {httpRequester as browserRequest} from '../src/request/browser-request' 6 | import {baseUrl, expectRequest, expectRequestBody, promiseRequest} from './helpers' 7 | 8 | describe.skipIf(typeof fetch === 'undefined' && typeof XMLHttpRequest === 'undefined')( 9 | 'fetch', 10 | {timeout: 15000}, 11 | () => { 12 | it('can use browser request with fetch polyfill', () => { 13 | getIt([baseUrl], browserRequest) 14 | }) 15 | 16 | it('should be able to read plain text response', async () => { 17 | const body = 'Just some plain text for you to consume' 18 | const request = getIt([baseUrl], browserRequest) 19 | const req = request('/plain-text') 20 | await expectRequest(req).resolves.toHaveProperty('body', body) 21 | }) 22 | 23 | it('should be able to post a Buffer as body', async () => { 24 | const request = getIt([baseUrl], browserRequest) 25 | const req = request({url: '/echo', body: Buffer.from('Foo bar')}) 26 | await expectRequestBody(req).resolves.toEqual('Foo bar') 27 | }) 28 | 29 | it('should be able to post a string as body', async () => { 30 | const request = getIt([baseUrl], browserRequest) 31 | const req = request({url: '/echo', body: 'Does this work?'}) 32 | await expectRequestBody(req).resolves.toEqual('Does this work?') 33 | }) 34 | 35 | it.skipIf(adapter === 'fetch' && environment === 'browser')( 36 | 'should be able to use JSON request middleware', 37 | async () => { 38 | const request = getIt([baseUrl, jsonRequest()], browserRequest) 39 | const req = request({url: '/echo', body: {foo: 'bar'}}) 40 | await expectRequestBody(req).resolves.toEqual('{"foo":"bar"}') 41 | }, 42 | ) 43 | 44 | it('should be able to set http headers', async () => { 45 | const request = getIt([baseUrl, jsonResponse()], browserRequest) 46 | const req = request({url: '/debug', headers: {'X-My-Awesome-Header': 'forsure'}}) 47 | 48 | const body = await promiseRequest(req).then((res) => res.body) 49 | expect(body).toHaveProperty('headers') 50 | expect(body.headers).toHaveProperty('x-my-awesome-header', 'forsure') 51 | }) 52 | 53 | it('should return the response headers', async () => { 54 | const request = getIt([baseUrl], browserRequest) 55 | const req = request({url: '/headers'}) 56 | const res = await promiseRequest(req) 57 | expect(res).toHaveProperty('headers') 58 | expect(res.headers).toMatchObject({ 59 | 'x-custom-header': 'supercustom', 60 | 'content-type': 'text/markdown', 61 | }) 62 | }) 63 | 64 | // @TODO fix the test so it works in happy-dom 65 | it.skipIf(environment === 'browser')( 66 | 'should be able to abort requests', 67 | () => 68 | new Promise((resolve, reject) => { 69 | const request = getIt([baseUrl], browserRequest) 70 | const req = request({url: '/delay'}) 71 | 72 | req.error.subscribe((err: any) => 73 | reject( 74 | new Error( 75 | `error channel should not be called when aborting, got:\n\n${err.message}`, 76 | { 77 | cause: err, 78 | }, 79 | ), 80 | ), 81 | ) 82 | req.response.subscribe(() => 83 | reject(new Error('response channel should not be called when aborting')), 84 | ) 85 | 86 | setTimeout(() => req.abort.publish(), 15) 87 | setTimeout(() => resolve(undefined), 250) 88 | }), 89 | ) 90 | 91 | it.skipIf(typeof ArrayBuffer === 'undefined' || environment === 'browser')( 92 | 'should be able to get arraybuffer back', 93 | async () => { 94 | const request = getIt([baseUrl], browserRequest) 95 | const req = request({url: '/plain-text', rawBody: true}) 96 | await expectRequestBody(req).resolves.toBeInstanceOf(ArrayBuffer) 97 | }, 98 | ) 99 | 100 | it('should emit errors on error channel', async () => { 101 | expect.assertions(2) 102 | await new Promise((resolve, reject) => { 103 | const request = getIt([baseUrl], browserRequest) 104 | const req = request({url: '/permafail'}) 105 | req.response.subscribe(() => { 106 | reject(new Error('Response channel called when error channel should have been triggered')) 107 | }) 108 | req.error.subscribe((err: any) => { 109 | try { 110 | expect(err).to.be.an.instanceOf(Error) 111 | expect(err.message).to.have.length.lessThan(600) 112 | resolve(undefined) 113 | // eslint-disable-next-line no-shadow 114 | } catch (err: any) { 115 | reject(err) 116 | } 117 | }) 118 | }) 119 | }) 120 | }, 121 | ) 122 | -------------------------------------------------------------------------------- /test/headers.test.ts: -------------------------------------------------------------------------------- 1 | import {adapter, getIt} from 'get-it' 2 | import {headers, jsonResponse} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | 5 | import {baseUrl, promiseRequest} from './helpers' 6 | 7 | describe('headers', () => { 8 | it('should be able to set http headers', async () => { 9 | const request = getIt([baseUrl, jsonResponse()]) 10 | const req = request({url: '/debug', headers: {'X-My-Awesome-Header': 'forsure'}}) 11 | 12 | const body = await promiseRequest(req).then((res) => res.body) 13 | expect(body).toHaveProperty('headers') 14 | expect(body.headers).toHaveProperty('x-my-awesome-header', 'forsure') 15 | }) 16 | 17 | it('should return the response headers', async () => { 18 | const request = getIt([baseUrl]) 19 | const req = request({url: '/headers'}) 20 | 21 | const res = await promiseRequest(req) 22 | expect(res).toHaveProperty('headers') 23 | expect(res.headers).toMatchObject({ 24 | 'x-custom-header': 'supercustom', 25 | 'content-type': 'text/markdown', 26 | }) 27 | }) 28 | 29 | it('should be able to set default headers using headers middleware', async () => { 30 | const defHeaders = headers({'X-Name': 'Something', 'X-Dont-Override': 'You'}) 31 | const request = getIt([baseUrl, jsonResponse(), defHeaders]) 32 | const req = request({url: '/debug', headers: {'X-Dont-Override': 'Me'}}) 33 | const body = await promiseRequest(req).then((res) => res.body) 34 | expect(body).toHaveProperty('headers') 35 | expect(body.headers).toMatchObject({ 36 | 'x-name': 'Something', 37 | 'x-dont-override': 'Me', 38 | }) 39 | }) 40 | 41 | it('should be able to set overriding headers using headers middleware', async () => { 42 | const defHeaders = headers({'X-Name': 'Something', 'X-Dont-Override': 'You'}, {override: true}) 43 | const request = getIt([baseUrl, jsonResponse(), defHeaders]) 44 | const req = request({url: '/debug', headers: {'X-Dont-Override': 'Me'}}) 45 | const body = await promiseRequest(req).then((res) => res.body) 46 | expect(body).toHaveProperty('headers') 47 | expect(body.headers).toMatchObject({ 48 | 'x-name': 'Something', 49 | 'x-dont-override': 'You', 50 | }) 51 | }) 52 | 53 | it.skipIf(adapter === 'xhr')('should set Content-Length based on body (Buffer)', async () => { 54 | const request = getIt([baseUrl, jsonResponse()]) 55 | const req = request({method: 'POST', url: '/debug', body: Buffer.from('hello')}) 56 | 57 | const body = await promiseRequest(req).then((res) => res.body) 58 | expect(body).toHaveProperty('headers') 59 | expect(body.headers).toHaveProperty('content-length', '5') 60 | }) 61 | 62 | it.skipIf(adapter === 'xhr')('should set Content-Length based on body (string)', async () => { 63 | const request = getIt([baseUrl, jsonResponse()]) 64 | const req = request({method: 'POST', url: '/debug', body: 'hello'}) 65 | 66 | const body = await promiseRequest(req).then((res) => res.body) 67 | expect(body).toHaveProperty('headers') 68 | expect(body.headers).toHaveProperty('content-length', '5') 69 | }) 70 | 71 | it.skipIf(adapter === 'xhr')('should set Content-Length based on body (string)', async () => { 72 | const request = getIt([baseUrl, jsonResponse()]) 73 | const req = request({method: 'POST', url: '/debug', body: 'hello 🚀'}) 74 | 75 | const body = await promiseRequest(req).then((res) => res.body) 76 | expect(body).toHaveProperty('headers') 77 | expect(body.headers).toHaveProperty('content-length', '10') 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /test/helpers/debugRequest.ts: -------------------------------------------------------------------------------- 1 | export default function debugRequest(req: any, body: any) { 2 | return { 3 | headers: req.headers, 4 | method: req.method, 5 | url: req.url, 6 | body: body ? body.toString() : null, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/helpers/expectEvent.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'vitest' 2 | 3 | export default (channel: any) => expect(new Promise((resolve) => channel.subscribe(resolve))) 4 | -------------------------------------------------------------------------------- /test/helpers/expectRequest.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'vitest' 2 | 3 | import type {MiddlewareChannels} from '../../src/types' 4 | 5 | export const promiseRequest = (channels: MiddlewareChannels) => 6 | new Promise((resolve, reject) => { 7 | let completed = false 8 | channels.error.subscribe((err) => { 9 | if (completed) throw new Error('error received after promise completed') 10 | completed = true 11 | reject(err) 12 | }) 13 | channels.response.subscribe((evt) => { 14 | if (completed) throw new Error('response received after promise completed') 15 | completed = true 16 | resolve(evt) 17 | }) 18 | }) 19 | 20 | export const expectRequest = (channels: MiddlewareChannels) => expect(promiseRequest(channels)) 21 | 22 | export const expectRequestBody = (channels: MiddlewareChannels) => 23 | expect(promiseRequest(channels).then((res) => res.body)) 24 | -------------------------------------------------------------------------------- /test/helpers/failOnError.ts: -------------------------------------------------------------------------------- 1 | export default (err: any) => { 2 | if (err) { 3 | throw err 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/helpers/globalSetup.http.ts: -------------------------------------------------------------------------------- 1 | import {createServer} from './server' 2 | 3 | export async function setup() { 4 | const server = await createServer('http') 5 | 6 | return () => new Promise((resolve) => server.close(resolve)) 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/globalSetup.https.ts: -------------------------------------------------------------------------------- 1 | import {createServer} from './server' 2 | 3 | export async function setup() { 4 | const server = await createServer('https') 5 | 6 | return () => new Promise((resolve) => server.close(resolve)) 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/globalSetup.proxy.http.ts: -------------------------------------------------------------------------------- 1 | import {createProxyServer} from './proxy' 2 | 3 | export async function setup() { 4 | const server = await createProxyServer('http') 5 | 6 | return () => new Promise((resolve) => server.close(resolve)) 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/globalSetup.proxy.https.ts: -------------------------------------------------------------------------------- 1 | import {createProxyServer} from './proxy' 2 | 3 | export async function setup() { 4 | const server = await createProxyServer('https') 5 | 6 | return () => new Promise((resolve) => server.close(resolve)) 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import {base, debug} from 'get-it/middleware' 2 | 3 | export {expectRequest, expectRequestBody, promiseRequest} from './expectRequest' 4 | 5 | export const hostname = 6 | (typeof window !== 'undefined' && (window as any).location?.hostname) || 'localhost' 7 | export const debugRequest = debug({verbose: true}) 8 | export const serverUrl = `http://${hostname}:9980` 9 | export const serverUrlHttps = `https://${hostname}:9443` 10 | export const baseUrlPrefix = `${serverUrl}/req-test` 11 | export const baseUrlPrefixHttps = `${serverUrlHttps}/req-test` 12 | export const baseUrl = base(baseUrlPrefix) 13 | export const baseUrlHttps = base(baseUrlPrefixHttps.replace(/^http:/, 'https:')) 14 | -------------------------------------------------------------------------------- /test/helpers/mtls.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import https from 'node:https' 3 | import path from 'node:path' 4 | 5 | export default (port: number, serverOpts = {}) => 6 | new Promise((resolve, reject) => { 7 | const httpsServerOptions = { 8 | ca: fs.readFileSync(path.join(__dirname, '..', 'certs', 'mtls', 'ca.pem')), 9 | key: fs.readFileSync(path.join(__dirname, '..', 'certs', 'mtls', 'server.key')), 10 | cert: fs.readFileSync(path.join(__dirname, '..', 'certs', 'mtls', 'server.pem')), 11 | } 12 | 13 | const options = Object.assign({}, httpsServerOptions, serverOpts) 14 | const server = https 15 | .createServer(options, (_request, response) => { 16 | response.end('hello from mtls') 17 | }) 18 | .on('error', reject) 19 | .listen(port, () => resolve(server)) 20 | }) 21 | -------------------------------------------------------------------------------- /test/helpers/noop.ts: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /test/helpers/proxy.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import http from 'node:http' 3 | import https from 'node:https' 4 | import path from 'node:path' 5 | import url from 'node:url' 6 | 7 | const httpsServerOptions = { 8 | key: fs.readFileSync(path.join(__dirname, '..', 'certs', 'server', 'key.pem')), 9 | cert: fs.readFileSync(path.join(__dirname, '..', 'certs', 'server', 'cert.pem')), 10 | } 11 | 12 | const httpPort = 4000 13 | const httpsPort = 4443 14 | 15 | export function createProxyServer(proto: 'http' | 'https' = 'http') { 16 | return new Promise((resolve, reject) => { 17 | const isHttp = proto === 'http' 18 | const protoPort = isHttp ? httpPort : httpsPort 19 | const protoOpts = isHttp ? {} : httpsServerOptions 20 | const requestHandler = (request: any, response: any) => { 21 | const parsed = url.parse(request.url) 22 | const opts = { 23 | host: parsed.hostname, 24 | port: parsed.port, 25 | path: parsed.path, 26 | rejectUnauthorized: false, 27 | } 28 | 29 | const transport = parsed.protocol === 'https:' ? https : http 30 | transport.get(opts, (res) => { 31 | let body = '' 32 | res.on('data', (data) => { 33 | body += data 34 | }) 35 | res.on('end', () => { 36 | response.setHeader('X-Proxy-Auth', request.headers['proxy-authorization'] || 'none') 37 | response.setHeader('X-Proxy-Host', request.headers.host) 38 | response.setHeader('Content-Type', 'text/plain; charset=UTF-8') 39 | response.end(`${body} + proxy`) 40 | }) 41 | }) 42 | } 43 | const server = isHttp 44 | ? http.createServer(requestHandler) 45 | : https.createServer(protoOpts, requestHandler) 46 | server.on('error', reject) 47 | server.listen(protoPort, () => resolve(server)) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /test/helpers/server.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import http from 'node:http' 3 | import https from 'node:https' 4 | import path from 'node:path' 5 | import qs from 'node:querystring' 6 | import url from 'node:url' 7 | import zlib from 'node:zlib' 8 | 9 | import {concat} from '../../src/request/node/simpleConcat' 10 | import debugRequest from './debugRequest' 11 | 12 | const httpsServerOptions: https.ServerOptions = { 13 | key: fs.readFileSync(path.join(__dirname, '..', 'certs', 'server', 'key.pem')), 14 | cert: fs.readFileSync(path.join(__dirname, '..', 'certs', 'server', 'cert.pem')), 15 | } 16 | 17 | const createError = (code: any, msg?: string) => { 18 | const err: any = new Error(msg || code) 19 | err.code = code 20 | return err 21 | } 22 | 23 | const httpPort = 9980 24 | const httpsPort = 9443 25 | const state: any = {failures: {}} 26 | 27 | function getResponseHandler(proto = 'http'): any { 28 | const isSecure = proto === 'https' 29 | return (req: any, res: any, next: any) => { 30 | const parts = url.parse(req.url, true) 31 | const num = Number(parts.query['n']) 32 | const atMax = num >= 10 33 | const uuid: any = parts.query['uuid'] 34 | const acceptedEncodings = (req.headers['accept-encoding'] || '').split(/\s*,\s*/) 35 | const noCache = () => res.setHeader('Cache-Control', 'private,max-age=0,no-cache,no-store') 36 | const incrementFailureCount = () => { 37 | if (!state.failures[uuid]) { 38 | state.failures[uuid] = 0 39 | } 40 | 41 | return ++state.failures[uuid] 42 | } 43 | 44 | if (parts.pathname === '/req-test/stall') { 45 | return 46 | } 47 | 48 | const tempFail = parts.pathname === '/req-test/fail' 49 | const permaFail = parts.pathname === '/req-test/permafail' 50 | if (tempFail || permaFail) { 51 | if (tempFail && incrementFailureCount() >= (num || 4)) { 52 | noCache() 53 | res.end('Success after failure') 54 | return 55 | } 56 | 57 | res.destroy(createError(parts.query['error'] || 'ECONNREFUSED')) 58 | return 59 | } 60 | 61 | // For all other requests, set no-cache 62 | noCache() 63 | 64 | switch (parts.pathname) { 65 | case '/req-test/query-string': 66 | res.setHeader('Content-Type', 'application/json') 67 | res.end(JSON.stringify(parts.query)) 68 | break 69 | case '/req-test/plain-text': 70 | res.setHeader('Content-Type', 'text/plain') 71 | res.end( 72 | isSecure 73 | ? 'Just some secure, plain text for you to consume' 74 | : 'Just some plain text for you to consume', 75 | ) 76 | break 77 | case '/req-test/custom-json': 78 | res.setHeader('Content-Type', 'application/vnd.npm.install-v1+json') 79 | res.end(JSON.stringify({foo: 'bar'})) 80 | break 81 | case '/req-test/json': 82 | res.setHeader('Content-Type', 'application/json') 83 | res.end(JSON.stringify({foo: 'bar'})) 84 | break 85 | case '/req-test/json-echo': 86 | res.setHeader('Content-Type', 'application/json') 87 | req.pipe(res) 88 | break 89 | case '/req-test/urlencoded': 90 | concat(req, (_unused: any, body: any) => { 91 | res.setHeader('Content-Type', 'application/json') 92 | res.end(JSON.stringify(qs.parse(body.toString()))) 93 | }) 94 | break 95 | case '/req-test/echo': 96 | req.pipe(res) 97 | break 98 | case '/req-test/debug': 99 | res.setHeader('Content-Type', 'application/json') 100 | concat(req, (_unused: any, body: any) => { 101 | res.end(JSON.stringify(debugRequest(req, body))) 102 | }) 103 | break 104 | case '/req-test/maybeCompress': 105 | res.setHeader('Content-Type', 'application/json') 106 | if (acceptedEncodings.includes('br')) { 107 | res.setHeader('Content-Encoding', 'br') 108 | zlib.brotliCompress( 109 | JSON.stringify(['smaller', 'better', 'faster', 'stronger']), 110 | (_err, result) => res.end(result), 111 | ) 112 | } else { 113 | res.end(JSON.stringify(['larger', 'worse', 'slower', 'weaker'])) 114 | } 115 | break 116 | case '/req-test/gzip': 117 | res.setHeader('Content-Type', 'application/json') 118 | res.setHeader('Content-Encoding', 'gzip') 119 | zlib.gzip(JSON.stringify(['harder', 'better', 'faster', 'stronger']), (_unused, result) => 120 | res.end(result), 121 | ) 122 | break 123 | case '/req-test/invalid-json': 124 | res.setHeader('Content-Type', 'application/json') 125 | res.end('{"foo":"bar') 126 | break 127 | case '/req-test/headers': 128 | res.setHeader('X-Custom-Header', 'supercustom') 129 | res.setHeader('Content-Type', 'text/markdown') 130 | res.end("# Memorable tweets\n\n> they're good dogs Brent") 131 | break 132 | case '/req-test/redirect': 133 | res.statusCode = atMax ? 200 : 302 134 | res.setHeader( 135 | atMax ? 'Content-Type' : 'Location', 136 | atMax ? 'text/plain' : `/req-test/redirect?n=${num + 1}`, 137 | ) 138 | res.end(atMax ? 'Done redirecting' : '') 139 | break 140 | case '/req-test/status': 141 | res.statusCode = Number(parts.query['code'] || 200) 142 | res.end('---') 143 | break 144 | case '/req-test/stall-after-initial': 145 | // Need a bit of data before browsers will usually accept it as "open" 146 | res.writeHead(200, {'Content-Type': 'text/plain'}) 147 | res.write(new Array(2048).join('.')) 148 | setTimeout(() => res.end(new Array(1024).join('.')), 6000) 149 | break 150 | case '/req-test/stall-after-initial-gzip': 151 | res.setHeader('Content-Encoding', 'gzip') 152 | res.writeHead(200, {'Content-Type': 'text/plain'}) 153 | zlib.gzip(JSON.stringify(['harder', 'better', 'faster', 'stronger']), (_unused, result) => { 154 | res.write(result) 155 | setTimeout(() => res.end(), 6000) 156 | }) 157 | break 158 | case '/req-test/delay': 159 | setTimeout(() => res.end('Hello future'), Number(parts.query['delay'] || 1000)) 160 | break 161 | case '/req-test/drip': 162 | drip(res) 163 | break 164 | case '/req-test/remote-port': 165 | res.setHeader('Content-Type', 'text/plain') 166 | res.end(`${req.connection.remotePort}`) 167 | break 168 | default: 169 | if (next) { 170 | next() 171 | return 172 | } 173 | 174 | res.statusCode = 404 175 | res.end('File not found') 176 | } 177 | } 178 | } 179 | 180 | function drip(res: http.ServerResponse) { 181 | let iterations = 0 182 | let interval: any = null 183 | 184 | setTimeout(() => { 185 | res.writeHead(200, {'Content-Type': 'text/plain', 'Content-Length': '45'}) 186 | interval = setInterval(() => { 187 | if (++iterations === 10) { 188 | clearInterval(interval) 189 | res.end() 190 | return 191 | } 192 | 193 | res.write('chunk') 194 | }, 50) 195 | }, 500) 196 | } 197 | 198 | export function createServer(proto?: 'http'): Promise 199 | export function createServer(proto: 'https'): Promise 200 | export function createServer(proto: 'http' | 'https' = 'http') { 201 | const isHttp = proto === 'http' 202 | const protoPort = isHttp ? httpPort : httpsPort 203 | const server = isHttp 204 | ? http.createServer(getResponseHandler(proto)) 205 | : https.createServer(httpsServerOptions, getResponseHandler(proto)) 206 | 207 | return new Promise((resolve, reject) => { 208 | server.on('error', reject) 209 | server.listen(protoPort, () => resolve(server)) 210 | }) 211 | } 212 | -------------------------------------------------------------------------------- /test/inject.test.ts: -------------------------------------------------------------------------------- 1 | import {getIt} from 'get-it' 2 | import {injectResponse} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | 5 | import {baseUrl, expectRequestBody, promiseRequest} from './helpers' 6 | 7 | describe('inject response', () => { 8 | it('should throw if not provided with an `inject` function', () => { 9 | expect(injectResponse).to.throw(/inject/) 10 | }) 11 | 12 | it('should be able to inject before dns resolution', async () => { 13 | const inject = () => ({body: 'foo'}) 14 | const request = getIt([injectResponse({inject})]) 15 | const req = request({url: 'http://some-unknown-host'}) 16 | 17 | await expectRequestBody(req).resolves.toEqual('foo') 18 | }) 19 | 20 | it('should be able to specify headers', async () => { 21 | const headers = {'x-my-mock': 'is-mocked'} 22 | const inject = () => ({headers}) 23 | const request = getIt([baseUrl, injectResponse({inject})]) 24 | const req = request({url: '/headers'}) 25 | 26 | const res = await promiseRequest(req) 27 | expect(res).toHaveProperty('headers') 28 | expect(res.headers).toHaveProperty('x-my-mock', 'is-mocked') 29 | }) 30 | 31 | it('should be able to use real request on a per-request basis', async () => { 32 | const mock = {body: 'Just some mocked text'} 33 | const inject = (evt: any) => evt.context.options.url.includes('/mocked') && mock 34 | const request = getIt([baseUrl, injectResponse({inject})]) 35 | const normalReq = request({url: '/plain-text'}) 36 | const mockedReq = request({url: '/mocked'}) 37 | 38 | await Promise.all([ 39 | expectRequestBody(normalReq).resolves.toMatch('Just some plain text'), 40 | expectRequestBody(mockedReq).resolves.toMatch('Just some mocked text'), 41 | ]) 42 | }) 43 | 44 | it('should be able to immediately cancel request', () => 45 | new Promise((resolve, reject) => { 46 | const inject = () => ({body: 'foo'}) 47 | const request = getIt([injectResponse({inject})]) 48 | const req = request({url: 'http://blah-blah'}) 49 | 50 | req.error.subscribe((err: any) => 51 | reject( 52 | new Error(`error channel should not be called when aborting, got:\n\n${err.message}`), 53 | ), 54 | ) 55 | req.response.subscribe(() => 56 | reject(new Error('response channel should not be called when aborting')), 57 | ) 58 | 59 | req.abort.publish() 60 | 61 | setTimeout(() => resolve(undefined), 250) 62 | })) 63 | }) 64 | -------------------------------------------------------------------------------- /test/json.test.ts: -------------------------------------------------------------------------------- 1 | import {adapter, environment, getIt} from 'get-it' 2 | import {jsonRequest, jsonResponse} from 'get-it/middleware' 3 | import {Readable} from 'stream' 4 | import {describe, it} from 'vitest' 5 | 6 | import {baseUrl, debugRequest, expectRequest, expectRequestBody} from './helpers' 7 | 8 | describe('json middleware', () => { 9 | it.runIf(environment === 'node')( 10 | 'should be able to request data from a JSON-responding endpoint as JSON', 11 | async () => { 12 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 13 | const req = request({url: '/json'}) 14 | await expectRequestBody(req).resolves.toHaveProperty('foo', 'bar') 15 | }, 16 | ) 17 | 18 | it('should be able to force response body as JSON regardless of content type', async () => { 19 | const request = getIt([baseUrl, jsonResponse({force: true}), debugRequest]) 20 | const req = request({url: '/custom-json'}) 21 | await expectRequestBody(req).resolves.toHaveProperty('foo', 'bar') 22 | }) 23 | 24 | it('should be able to send JSON-data data to a JSON endpoint and get JSON back', async () => { 25 | const request = getIt([baseUrl, jsonResponse(), jsonRequest(), debugRequest]) 26 | const body = {randomValue: Date.now()} 27 | const req = request({url: '/json-echo', body}) 28 | await expectRequestBody(req).resolves.toEqual(body) 29 | }) 30 | 31 | it.runIf(environment === 'node')( 32 | 'should be able to use json response body parser on non-json responses', 33 | async () => { 34 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 35 | const req = request({url: '/plain-text'}) 36 | await expectRequestBody(req).resolves.toEqual('Just some plain text for you to consume') 37 | }, 38 | ) 39 | 40 | it('should be able to use json response body parser on non-json responses (no content type)', async () => { 41 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 42 | const req = request({url: '/echo', body: 'Foobar'}) 43 | await expectRequestBody(req).resolves.toEqual('Foobar') 44 | }) 45 | 46 | it('should be able to use json request body parser without response body', async () => { 47 | const request = getIt([baseUrl, jsonResponse(), jsonRequest(), debugRequest]) 48 | const req = request({url: '/debug', method: 'post'}) 49 | 50 | await expectRequestBody(req).resolves.toMatchObject({ 51 | method: 'POST', 52 | body: '', 53 | }) 54 | }) 55 | 56 | it.runIf(environment === 'node')( 57 | 'should be able to send PUT-requests with json bodies', 58 | async () => { 59 | const request = getIt([baseUrl, jsonRequest(), jsonResponse(), debugRequest]) 60 | const req = request({url: '/json-echo', method: 'PUT', body: {foo: 'bar'}}) 61 | await expectRequestBody(req).resolves.toEqual({foo: 'bar'}) 62 | }, 63 | ) 64 | 65 | it('should throw if response body is not valid JSON', async () => { 66 | const request = getIt([baseUrl, jsonResponse()]) 67 | const req = request({url: '/invalid-json'}) 68 | await expectRequest(req).rejects.toThrow(/response body as json/i) 69 | }) 70 | 71 | it('should serialize plain values (numbers, strings)', async () => { 72 | const request = getIt([baseUrl, jsonRequest(), jsonResponse(), debugRequest]) 73 | const url = '/json-echo' 74 | await Promise.all([ 75 | expectRequestBody(request({url, body: 'string'})).resolves.toEqual('string'), 76 | expectRequestBody(request({url, body: 1337})).resolves.toEqual(1337), 77 | ]) 78 | }) 79 | 80 | it.skipIf(adapter === 'xhr')('should serialize arrays', async () => { 81 | const request = getIt([baseUrl, jsonRequest(), jsonResponse(), debugRequest]) 82 | const body = ['foo', 'bar', 'baz'] 83 | const req = request({url: '/json-echo', method: 'PUT', body}) 84 | await expectRequestBody(req).resolves.toEqual(body) 85 | }) 86 | 87 | it.skipIf(adapter === 'xhr')('should not serialize buffers', async () => { 88 | const request = getIt([baseUrl, jsonRequest(), jsonResponse(), debugRequest]) 89 | const body = Buffer.from('blåbærsyltetøy', 'utf8') 90 | const req = request({url: '/echo', method: 'PUT', body}) 91 | await expectRequestBody(req).resolves.toEqual('blåbærsyltetøy') 92 | }) 93 | 94 | it.runIf(environment === 'node')('should not serialize streams', async () => { 95 | const request = getIt([baseUrl, jsonRequest(), jsonResponse(), debugRequest]) 96 | const body = Readable.from('unicorn') 97 | const req = request({url: '/echo', method: 'PUT', body}) 98 | await expectRequestBody(req).resolves.toEqual('unicorn') 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /test/keepAlive.test.ts: -------------------------------------------------------------------------------- 1 | import {environment, getIt} from 'get-it' 2 | import {agent, keepAlive} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | 5 | import {baseUrl, promiseRequest} from './helpers' 6 | 7 | describe.runIf(environment === 'node')('keepAlive middleware', () => { 8 | // This is just verifying that our method of detecting if keepAlive is enabled works. 9 | it('should be able to detect that keepAlive is disabled', async () => { 10 | const request = getIt([baseUrl, agent({keepAlive: false})]) 11 | 12 | const remotePort1 = (await promiseRequest(request('/remote-port'))).body 13 | await new Promise((resolve) => setTimeout(resolve, 50)) 14 | const remotePort2 = (await promiseRequest(request('/remote-port'))).body 15 | 16 | expect(remotePort1).not.toBe(remotePort2) 17 | }) 18 | 19 | it('should work with redirects', async () => { 20 | const request = getIt([baseUrl, keepAlive()]) 21 | 22 | const remotePort1 = (await promiseRequest(request('/remote-port'))).body 23 | await new Promise((resolve) => setTimeout(resolve, 50)) 24 | const remotePort2 = (await promiseRequest(request('/remote-port'))).body 25 | 26 | expect(remotePort1).toBe(remotePort2) 27 | }) 28 | 29 | it('should work without redirects', async () => { 30 | const request = getIt([baseUrl, keepAlive()]) 31 | const options = {url: '/remote-port', maxRedirects: 0} 32 | 33 | const remotePort1 = (await promiseRequest(request(options))).body 34 | await new Promise((resolve) => setTimeout(resolve, 50)) 35 | const remotePort2 = (await promiseRequest(request(options))).body 36 | 37 | expect(remotePort1).toBe(remotePort2) 38 | }) 39 | 40 | it('should retry on econnreset', async () => { 41 | let count = 0 42 | const request = getIt([ 43 | baseUrl, 44 | keepAlive(), 45 | { 46 | onRequest: (req) => { 47 | count += 1 48 | if (count === 3) { 49 | const err: NodeJS.ErrnoException = new Error('ECONNRESET') 50 | err.code = 'ECONNRESET' 51 | req.request.destroy(err) 52 | } 53 | }, 54 | }, 55 | ]) 56 | const options = {url: '/remote-port', maxRedirects: 0} 57 | 58 | const remotePort1 = (await promiseRequest(request(options))).body 59 | const remotePort2 = (await promiseRequest(request(options))).body 60 | expect(remotePort1).toBe(remotePort2) 61 | 62 | const remotePort3 = (await promiseRequest(request(options))).body 63 | expect(remotePort2).not.toBe(remotePort3) 64 | expect(remotePort1).not.toBe(remotePort3) 65 | }) 66 | 67 | it('should respect maxRetries', async () => { 68 | let count = 0 69 | const request = getIt([ 70 | baseUrl, 71 | keepAlive({maxRetries: 0}), 72 | { 73 | onRequest: (req) => { 74 | count += 1 75 | if (count === 3) { 76 | const err: NodeJS.ErrnoException = new Error('ECONNRESET') 77 | err.code = 'ECONNRESET' 78 | req.request.destroy(err) 79 | } 80 | }, 81 | }, 82 | ]) 83 | const options = {url: '/remote-port', maxRedirects: 0} 84 | 85 | const remotePort1 = (await promiseRequest(request(options))).body 86 | const remotePort2 = (await promiseRequest(request(options))).body 87 | expect(remotePort1).toBe(remotePort2) 88 | 89 | // Now the connection is broken and usage should throw: 90 | await expect(promiseRequest(request(options))).rejects.toThrow() 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /test/mtls.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | 4 | import {environment, getIt} from 'get-it' 5 | import {base, mtls} from 'get-it/middleware' 6 | import {describe, expect, it} from 'vitest' 7 | 8 | import {expectRequestBody} from './helpers' 9 | import getMtls from './helpers/mtls' 10 | 11 | const port = 4444 12 | const baseUrl = `https://localhost:${port}/req-test` 13 | 14 | describe.runIf(environment === 'node')('mtls middleware', () => { 15 | it('should throw on missing options', () => { 16 | expect(() => getIt([base(baseUrl), mtls()])).to.throw(/Required mtls option "ca" is missing/) 17 | }) 18 | 19 | it('should handle mtls', async () => { 20 | const body = 'hello from mtls' 21 | const mtlsOpts = { 22 | ca: fs.readFileSync(path.join(__dirname, 'certs', 'mtls', 'ca.pem')), 23 | key: fs.readFileSync(path.join(__dirname, 'certs', 'mtls', 'client.key')), 24 | cert: fs.readFileSync(path.join(__dirname, 'certs', 'mtls', 'client.pem')), 25 | } 26 | const request = getIt([base(baseUrl), mtls(mtlsOpts)]) 27 | await getMtls(port).then(async (server: any) => { 28 | await expectRequestBody(request({url: '/plain-text'})).resolves.toEqual(body) 29 | return server.close() 30 | }) 31 | }) 32 | 33 | it('should fail on invalid mtls cert', async () => { 34 | const request = getIt([ 35 | base(baseUrl), 36 | mtls({ 37 | ca: fs.readFileSync(path.join(__dirname, 'certs', 'mtls', 'ca.pem')).toString(), 38 | key: fs 39 | .readFileSync(path.join(__dirname, 'certs', 'invalid-mtls', 'client.key')) 40 | .toString(), 41 | cert: fs 42 | .readFileSync(path.join(__dirname, 'certs', 'invalid-mtls', 'client.pem')) 43 | .toString(), 44 | }), 45 | ]) 46 | 47 | const server: any = await getMtls(port) 48 | await expect(() => request({url: '/plain-text'})).to.throw() 49 | 50 | return server.close() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/node.fetch.test.ts: -------------------------------------------------------------------------------- 1 | import {environment, getIt, type Middleware} from 'get-it' 2 | import {jsonRequest, jsonResponse, keepAlive, promise} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | 5 | import {httpRequester as nodeRequest} from '../src/request/node-request' 6 | import {baseUrl, expectRequestBody, promiseRequest} from './helpers' 7 | 8 | /** 9 | * We're asserting that two requests responses are equal, but the date header might 10 | * differ by a second, so we remove it from the response. 11 | */ 12 | function withoutDateHeader(): Middleware { 13 | return { 14 | onResponse: (response) => { 15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 16 | const {date, ...rest} = response.headers 17 | return {...response, headers: rest} 18 | }, 19 | } 20 | } 21 | 22 | describe.runIf(typeof fetch !== 'undefined' && environment === 'node')( 23 | 'fetch', 24 | {timeout: 15000}, 25 | () => { 26 | it('can return a regular response', async () => { 27 | const request = getIt([baseUrl, keepAlive(), promise(), withoutDateHeader()], nodeRequest) 28 | const expected = await request({url: '/plain-text', fetch: false}) 29 | const actual = await request({url: '/plain-text', fetch: true}) 30 | expect(actual).toEqual(expected) 31 | }) 32 | 33 | it.skipIf(process.versions.node.split('.')[0] >= '20')('can turn off keep-alive', async () => { 34 | const request = getIt([baseUrl, promise(), withoutDateHeader()], nodeRequest) 35 | const expected = await request({url: '/plain-text', fetch: false}) 36 | const actual = await request({ 37 | url: '/plain-text', 38 | fetch: {headers: {connection: 'close'}}, 39 | }) 40 | expect(actual).toEqual(expected) 41 | expect(actual.headers).toHaveProperty('connection', 'close') 42 | }) 43 | 44 | it('should allow sending cache options', async () => { 45 | const request = getIt([baseUrl, keepAlive(), promise(), withoutDateHeader()], nodeRequest) 46 | const expected = await request({url: '/plain-text', fetch: false}) 47 | const actual = await request({url: '/plain-text', fetch: {cache: 'no-store'}}) 48 | expect(actual).toEqual(expected) 49 | }) 50 | 51 | it('should be able to post a Buffer as body', async () => { 52 | const request = getIt([baseUrl, withoutDateHeader()], nodeRequest) 53 | const req = request({url: '/echo', fetch: true, body: Buffer.from('Foo bar')}) 54 | await expectRequestBody(req).resolves.toEqual('Foo bar') 55 | }) 56 | 57 | it.skipIf(typeof FormData === 'undefined' || typeof Blob === 'undefined')( 58 | 'should be able to post a File as body', 59 | async () => { 60 | const request = getIt([baseUrl, withoutDateHeader()], nodeRequest) 61 | const formData = new FormData() 62 | const file = new Blob(['Foo bar'], {type: 'text/plain'}) 63 | formData.set('cody', file) 64 | const req = request({url: '/echo', fetch: true, body: formData}) 65 | await expectRequestBody(req).resolves.toContain('Foo bar') 66 | }, 67 | ) 68 | 69 | it('should be able to post a string as body', async () => { 70 | const request = getIt([baseUrl, withoutDateHeader()], nodeRequest) 71 | const req = request({url: '/echo', fetch: true, body: 'Does this work?'}) 72 | await expectRequestBody(req).resolves.toEqual('Does this work?') 73 | }) 74 | 75 | it('should be able to use JSON request middleware', async () => { 76 | const request = getIt([baseUrl, jsonRequest(), withoutDateHeader()], nodeRequest) 77 | const req = request({url: '/echo', fetch: true, body: {foo: 'bar'}}) 78 | await expectRequestBody(req).resolves.toEqual('{"foo":"bar"}') 79 | }) 80 | 81 | it('should be able to set http headers', async () => { 82 | const request = getIt([baseUrl, jsonResponse(), withoutDateHeader()], nodeRequest) 83 | const req = request({url: '/debug', fetch: {headers: {'X-My-Awesome-Header': 'forsure'}}}) 84 | 85 | const body = await promiseRequest(req).then((res) => res.body) 86 | expect(body).toHaveProperty('headers') 87 | expect(body.headers).toHaveProperty('x-my-awesome-header', 'forsure') 88 | }) 89 | 90 | it('should return the response headers', async () => { 91 | const request = getIt([baseUrl, withoutDateHeader()], nodeRequest) 92 | const req = request({url: '/headers', fetch: true}) 93 | const res = await promiseRequest(req) 94 | expect(res).toHaveProperty('headers') 95 | expect(res.headers).toMatchObject({ 96 | 'x-custom-header': 'supercustom', 97 | 'content-type': 'text/markdown', 98 | }) 99 | }) 100 | 101 | it('should be able to abort requests', () => 102 | new Promise((resolve, reject) => { 103 | const request = getIt([baseUrl, withoutDateHeader()], nodeRequest) 104 | const req = request({url: '/delay', fetch: true}) 105 | 106 | req.error.subscribe((err: any) => 107 | reject( 108 | new Error(`error channel should not be called when aborting, got:\n\n${err.message}`, { 109 | cause: err, 110 | }), 111 | ), 112 | ) 113 | req.response.subscribe(() => 114 | reject(new Error('response channel should not be called when aborting')), 115 | ) 116 | 117 | setTimeout(() => req.abort.publish(), 15) 118 | setTimeout(() => resolve(undefined), 250) 119 | })) 120 | 121 | it.skipIf(typeof ReadableStream === 'undefined')( 122 | 'should be able to get a ReadableStream back', 123 | async () => { 124 | const request = getIt([baseUrl, promise(), withoutDateHeader()], nodeRequest) 125 | const res = await request({url: '/plain-text', rawBody: true, fetch: true}) 126 | expect(res.body).toBeInstanceOf(ReadableStream) 127 | }, 128 | ) 129 | 130 | it('should emit errors on error channel', async () => { 131 | expect.assertions(2) 132 | await new Promise((resolve, reject) => { 133 | const request = getIt([baseUrl, withoutDateHeader()], nodeRequest) 134 | const req = request({url: '/permafail', fetch: true}) 135 | req.response.subscribe(() => { 136 | reject(new Error('Response channel called when error channel should have been triggered')) 137 | }) 138 | req.error.subscribe((err: any) => { 139 | try { 140 | expect(err).to.be.an.instanceOf(Error) 141 | expect(err.message).to.have.length.lessThan(600) 142 | resolve(undefined) 143 | // eslint-disable-next-line no-shadow 144 | } catch (err: any) { 145 | reject(err) 146 | } 147 | }) 148 | }) 149 | }) 150 | }, 151 | ) 152 | -------------------------------------------------------------------------------- /test/observable.test.ts: -------------------------------------------------------------------------------- 1 | import {getIt} from 'get-it' 2 | import {httpErrors, observable} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | import zenObservable from 'zen-observable' 5 | 6 | import {baseUrl} from './helpers' 7 | 8 | describe('observable middleware', () => { 9 | const implementation = zenObservable 10 | 11 | it('should turn the return value into an observable', () => 12 | new Promise((resolve) => { 13 | const request = getIt([baseUrl, observable({implementation})]) 14 | request({url: '/plain-text'}) 15 | .filter((ev: any) => ev.type === 'response') 16 | .subscribe((res: any) => { 17 | expect(res).to.containSubset({ 18 | body: 'Just some plain text for you to consume', 19 | method: 'GET', 20 | statusCode: 200, 21 | }) 22 | 23 | resolve(undefined) 24 | }) 25 | })) 26 | 27 | it('should trigger error handler on failures', () => 28 | new Promise((resolve, reject) => { 29 | const request = getIt([baseUrl, httpErrors(), observable({implementation})]) 30 | request({url: '/status?code=500'}).subscribe({ 31 | next: () => reject(new Error('next() called when error() should have been')), 32 | error: (err: any) => { 33 | expect(err.message).to.match(/HTTP 500/i) 34 | resolve(undefined) 35 | }, 36 | }) 37 | })) 38 | 39 | it('should not trigger request unless subscribe is called', () => 40 | new Promise((resolve, reject) => { 41 | const onRequest = () => reject(new Error('Request triggered without subscribe()')) 42 | const request = getIt([baseUrl, observable({implementation}), {onRequest}]) 43 | request({url: '/plain-text'}) 44 | setTimeout(() => resolve(undefined), 100) 45 | })) 46 | 47 | it('should cancel the request when unsubscribing from observable', () => 48 | new Promise((resolve, reject) => { 49 | const request = getIt([baseUrl, observable({implementation})]) 50 | const subscriber = request({url: '/delay'}).subscribe({ 51 | next: () => reject(new Error('response channel should not be called when aborting')), 52 | error: (err: any) => 53 | reject( 54 | new Error(`error channel should not be called when aborting, got:\n\n${err.message}`), 55 | ), 56 | }) 57 | 58 | setTimeout(() => subscriber.unsubscribe(), 15) 59 | setTimeout(() => resolve(undefined), 250) 60 | })) 61 | 62 | // @todo test timeout errors 63 | }) 64 | -------------------------------------------------------------------------------- /test/progress.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | 3 | import {adapter, environment, getIt} from 'get-it' 4 | import {observable, progress} from 'get-it/middleware' 5 | import {describe, expect, it} from 'vitest' 6 | import implementation from 'zen-observable' 7 | 8 | import {baseUrl} from './helpers' 9 | 10 | describe('progress', () => { 11 | it('should be able to use progress middleware without side-effects', () => 12 | new Promise((resolve, reject) => { 13 | const request = getIt([baseUrl, progress()]) 14 | const req = request({url: '/plain-text'}) 15 | 16 | req.error.subscribe((err: any) => 17 | reject(new Error(`error channel should not be called, got:\n\n${err.message}`)), 18 | ) 19 | req.response.subscribe(() => resolve(undefined)) 20 | })) 21 | it('should be able to use progress middleware without side-effects', async () => { 22 | expect.hasAssertions() 23 | await expect( 24 | new Promise((resolve, reject) => { 25 | const request = getIt([baseUrl, progress()]) 26 | const req = request({url: '/plain-text'}) 27 | 28 | req.error.subscribe((err: any) => 29 | reject(new Error(`error channel should not be called, got:\n\n${err.message}`)), 30 | ) 31 | req.response.subscribe(() => resolve(undefined)) 32 | }), 33 | ).resolves.toBeUndefined() 34 | }) 35 | 36 | // @TODO add support for `adapter = fetch` when `ReadableStream` is available on `Response` 37 | it.skipIf(adapter === 'fetch')( 38 | 'should emit download progress events', 39 | {timeout: 10000}, 40 | async () => { 41 | expect.hasAssertions() 42 | await expect( 43 | new Promise((resolve, reject) => { 44 | const request = getIt([baseUrl, progress()]) 45 | const req = request({url: '/drip'}) 46 | let events = 0 47 | 48 | req.progress.subscribe((evt: any) => { 49 | events++ 50 | expect(evt).to.containSubset({ 51 | stage: 'download', 52 | lengthComputable: true, 53 | }) 54 | }) 55 | 56 | req.error.subscribe((err: any) => 57 | reject(new Error(`error channel should not be called, got:\n\n${err.message}`)), 58 | ) 59 | req.response.subscribe(() => { 60 | expect(events).to.be.above(0) 61 | resolve(undefined) 62 | }) 63 | }), 64 | ).resolves.toBeUndefined() 65 | }, 66 | ) 67 | 68 | // @TODO support upload events in fetch if Request.body supports ReadableStream 69 | // @TODO make this test work in happy-dom 70 | it.skipIf(adapter === 'fetch' || environment === 'browser')( 71 | 'should emit upload progress events on strings', 72 | async () => { 73 | expect.assertions(2) 74 | const promise = new Promise((resolve, reject) => { 75 | const request = getIt([baseUrl, progress()]) 76 | const req = request({url: '/plain-text', body: new Array(100).join('-')}) 77 | let events = 0 78 | 79 | req.progress.subscribe((evt: any) => { 80 | if (evt.stage !== 'upload') { 81 | return 82 | } 83 | 84 | events++ 85 | expect(evt).to.containSubset({ 86 | stage: 'upload', 87 | lengthComputable: true, 88 | }) 89 | }) 90 | 91 | req.error.subscribe((err: any) => 92 | reject(new Error(`error channel should not be called, got:\n\n${err.message}`)), 93 | ) 94 | req.response.subscribe(() => { 95 | if (events > 0) { 96 | resolve(events) 97 | } 98 | }) 99 | }) 100 | await expect(promise).resolves.toBeGreaterThan(0) 101 | }, 102 | ) 103 | 104 | // @TODO add support for `adapter = fetch` 105 | it.skipIf( 106 | environment === 'browser' || 107 | adapter === 'fetch' || 108 | (process.env['GITHUB_ACTIONS'] === 'true' && process.platform === 'darwin'), 109 | )('can tell requester how large the body is', {timeout: 10000}, async () => { 110 | expect.hasAssertions() 111 | await expect( 112 | new Promise((resolve, reject) => { 113 | const request = getIt([baseUrl, progress()]) 114 | const body = fs.createReadStream(__filename) 115 | const bodySize = fs.statSync(__filename).size 116 | const req = request({url: '/plain-text', body, bodySize}) 117 | let events = 0 118 | 119 | req.progress.subscribe((evt: any) => { 120 | if (evt.stage !== 'upload') { 121 | return 122 | } 123 | 124 | events++ 125 | expect(evt).to.containSubset({ 126 | stage: 'upload', 127 | lengthComputable: true, 128 | }) 129 | }) 130 | 131 | req.error.subscribe((err: any) => 132 | reject(new Error(`error channel should not be called, got:\n\n${err.message}`)), 133 | ) 134 | req.response.subscribe(() => { 135 | resolve(events) 136 | }) 137 | }), 138 | 'should have received progress events', 139 | ).resolves.toBeGreaterThan(0) 140 | }) 141 | 142 | // @TODO add support for `adapter = fetch` when `ReadableStream` is available on `Response` 143 | it.skipIf(adapter === 'fetch')('progress events should be emitted on observable', async () => { 144 | expect.hasAssertions() 145 | await expect( 146 | new Promise((resolve) => { 147 | const request = getIt([baseUrl, progress(), observable({implementation})]) 148 | const obs = request({url: '/drip'}) 149 | .filter((ev: any) => ev.type === 'progress') 150 | .subscribe((evt: any) => { 151 | expect(evt).to.containSubset({ 152 | stage: 'download', 153 | lengthComputable: true, 154 | }) 155 | 156 | obs.unsubscribe() 157 | resolve(undefined) 158 | }) 159 | }), 160 | ).resolves.toBeUndefined() 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /test/promise.test.ts: -------------------------------------------------------------------------------- 1 | import {getIt} from 'get-it' 2 | import {httpErrors, promise} from 'get-it/middleware' 3 | import {describe, expect, it} from 'vitest' 4 | 5 | import {baseUrl, debugRequest} from './helpers' 6 | 7 | describe('promise middleware', {timeout: 5000}, () => { 8 | it('should turn the return value into a promise', async () => { 9 | const request = getIt([baseUrl, promise()]) 10 | const req = request({url: '/plain-text'}) 11 | await expect(req).resolves.toMatchObject({ 12 | body: 'Just some plain text for you to consume', 13 | method: 'GET', 14 | statusCode: 200, 15 | }) 16 | }) 17 | 18 | it('should be able to resolve only the response body', async () => { 19 | const request = getIt([baseUrl, promise({onlyBody: true})]) 20 | const req = request({url: '/plain-text'}) 21 | await expect(req).resolves.toEqual('Just some plain text for you to consume') 22 | }) 23 | 24 | it('should reject network errors', async () => { 25 | const request = getIt([baseUrl, promise()]) 26 | const req = request({url: '/permafail'}) 27 | await expect(req).rejects.toThrow(/(socket|network|Request error|fetch failed)/i) 28 | }) 29 | 30 | it('should reject http errors (if middleware is loaded)', async () => { 31 | const request = getIt([baseUrl, httpErrors(), promise()]) 32 | const req = request({url: '/status?code=500'}) 33 | await expect(req).rejects.toThrow(/HTTP 500/i) 34 | }) 35 | 36 | it('can cancel using cancel tokens', () => 37 | new Promise((resolve, reject) => { 38 | const source = promise.CancelToken.source() 39 | 40 | const request = getIt([baseUrl, promise()]) 41 | request({url: '/delay', cancelToken: source.token}) 42 | .then(() => reject(new Error('Should not be resolved when cancelled'))) 43 | .catch((err: any) => { 44 | if (promise.isCancel(err)) { 45 | expect(err.toString()).to.equal('Cancel: Cancelled by user') 46 | resolve(undefined) 47 | return 48 | } 49 | 50 | reject(new Error(`Should be rejected with cancellation, got:\n\n${err.message}`)) 51 | }) 52 | 53 | setTimeout(() => source.cancel('Cancelled by user'), 15) 54 | })) 55 | 56 | it('does not execute requests that are already cancelled', () => 57 | new Promise((resolve, reject) => { 58 | const source = promise.CancelToken.source() 59 | source.cancel() 60 | 61 | const request = getIt([baseUrl, debugRequest, promise()]) 62 | request({url: '/delay', cancelToken: source.token}) 63 | .then(() => reject(new Error('Should not be resolved when cancelled'))) 64 | .catch((err: any) => { 65 | if (promise.isCancel(err)) { 66 | expect(err.toString()).to.equal('Cancel') 67 | resolve(undefined) 68 | return 69 | } 70 | 71 | reject(new Error(`Should be rejected with cancellation, got:\n\n${err.message}`)) 72 | }) 73 | })) 74 | 75 | // @todo test timeout errors 76 | // @todo cancelation 77 | }) 78 | -------------------------------------------------------------------------------- /test/queryStrings.test.ts: -------------------------------------------------------------------------------- 1 | import {getIt} from 'get-it' 2 | import {jsonResponse} from 'get-it/middleware' 3 | import {describe, it} from 'vitest' 4 | 5 | import {baseUrl, debugRequest, expectRequestBody} from './helpers' 6 | 7 | describe('query strings', () => { 8 | it('should serialize query strings', async () => { 9 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 10 | const query = {foo: 'bar', baz: 'bing'} 11 | const req = request({url: '/query-string', query}) 12 | await expectRequestBody(req).resolves.toEqual(query) 13 | }) 14 | 15 | it('should merge existing and explicit query params', async () => { 16 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 17 | const query = {baz: 3} 18 | const req = request({url: '/query-string?foo=1&bar=2', query}) 19 | await expectRequestBody(req).resolves.toEqual({foo: '1', bar: '2', baz: '3'}) 20 | }) 21 | 22 | it('should serialize arrays correctly', async () => { 23 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 24 | const query = {it: ['hai', 'there']} 25 | const req = request({url: '/query-string?foo=1&bar=2', query}) 26 | await expectRequestBody(req).resolves.toEqual({ 27 | foo: '1', 28 | bar: '2', 29 | it: ['hai', 'there'], 30 | }) 31 | }) 32 | 33 | it('should remove undefined values from query strings', async () => { 34 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 35 | const query = {foo: undefined, bar: 'baz'} 36 | const req = request({url: '/query-string', query}) 37 | await expectRequestBody(req).resolves.toEqual({bar: 'baz'}) 38 | }) 39 | 40 | it('should handle URLs with duplicate query params', async () => { 41 | const request = getIt([baseUrl, jsonResponse()]) 42 | const req = request({url: '/debug?dupe=1&dupe=2&lone=3'}) 43 | await expectRequestBody(req).resolves.toHaveProperty( 44 | 'url', 45 | '/req-test/debug?dupe=1&dupe=2&lone=3', 46 | ) 47 | }) 48 | 49 | it('should append explicitly passed query parameters with existing params in URL', async () => { 50 | const request = getIt([baseUrl, jsonResponse()]) 51 | const req = request({url: '/debug?dupe=a', query: {dupe: 'b', lone: 'c'}}) 52 | await expectRequestBody(req).resolves.toHaveProperty( 53 | 'url', 54 | '/req-test/debug?dupe=a&dupe=b&lone=c', 55 | ) 56 | }) 57 | 58 | it('should handle query parameter values with escaped `&`, `=`, and `?` (in uri)', async () => { 59 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 60 | const req = request({ 61 | url: '/query-string?and=this%26that&equals=this%3Dthat&question=this%3Fthat', 62 | }) 63 | await expectRequestBody(req).resolves.toEqual({ 64 | and: 'this&that', 65 | equals: 'this=that', 66 | question: 'this?that', 67 | }) 68 | }) 69 | 70 | it('should handle query parameter values with `&`, `=`, and `?` (in `query` option)', async () => { 71 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 72 | const req = request({ 73 | url: '/query-string', 74 | query: {and: 'this&that', equals: 'this=that', question: 'this?that'}, 75 | }) 76 | await expectRequestBody(req).resolves.toEqual({ 77 | and: 'this&that', 78 | equals: 'this=that', 79 | question: 'this?that', 80 | }) 81 | }) 82 | 83 | it('should handle query parameter values with `&`, `=`, and `?` (mixed)', async () => { 84 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 85 | const req = request({ 86 | url: '/query-string?and=this%26that&question=this%3Fthat', 87 | query: {equals: 'this=that'}, 88 | }) 89 | await expectRequestBody(req).resolves.toEqual({ 90 | and: 'this&that', 91 | equals: 'this=that', 92 | question: 'this?that', 93 | }) 94 | }) 95 | 96 | it('should handle query parameter values with double equals (uri)', async () => { 97 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 98 | const req = request({ 99 | url: '/query-string?query=_type%20%3D%3D+%22test%22', 100 | }) 101 | await expectRequestBody(req).resolves.toEqual({ 102 | query: '_type == "test"', 103 | }) 104 | }) 105 | 106 | it('should handle query parameter values with double equals (uri + query option)', async () => { 107 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 108 | const req = request({ 109 | url: '/query-string?query=_type+%3D%3D%20%22test%22', 110 | query: {$type: 'itsa == test'}, 111 | }) 112 | await expectRequestBody(req).resolves.toEqual({ 113 | query: '_type == "test"', 114 | $type: 'itsa == test', 115 | }) 116 | }) 117 | 118 | it('should handle query parameters with empty values', async () => { 119 | const request = getIt([baseUrl, jsonResponse(), debugRequest]) 120 | const req = request({ 121 | url: '/query-string?a=', 122 | query: {b: ''}, 123 | }) 124 | await expectRequestBody(req).resolves.toEqual({ 125 | a: '', 126 | b: '', 127 | }) 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /test/redirect.test.ts: -------------------------------------------------------------------------------- 1 | import {environment, getIt} from 'get-it' 2 | import {describe, it} from 'vitest' 3 | 4 | import {baseUrl, baseUrlPrefix, expectRequest} from './helpers' 5 | 6 | describe.runIf(environment === 'node')('redirects', () => { 7 | it('should handle redirects', async () => { 8 | const request = getIt([baseUrl]) 9 | const req = request({url: '/redirect?n=8'}) 10 | return expectRequest(req).resolves.toMatchObject({ 11 | statusCode: 200, 12 | body: 'Done redirecting', 13 | url: `${baseUrlPrefix}/redirect?n=10`, 14 | }) 15 | }) 16 | 17 | it('should be able to set max redirects (node)', () => { 18 | const request = getIt([baseUrl]) 19 | const req = request({url: '/redirect?n=7', maxRedirects: 2}) 20 | return expectRequest(req).rejects.toThrow(/(Max redirects)|(Maximum number of redirects)/) 21 | }) 22 | 23 | it('should be able to be told NOT to follow redirects', () => { 24 | const request = getIt([baseUrl]) 25 | const req = request({url: '/redirect?n=8', maxRedirects: 0}) 26 | return expectRequest(req).resolves.toMatchObject({statusCode: 302}) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/retry.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | 3 | import {environment, getIt} from 'get-it' 4 | import {httpErrors, retry} from 'get-it/middleware' 5 | import {describe, expect, it} from 'vitest' 6 | 7 | import {baseUrl, debugRequest, expectRequest} from './helpers' 8 | 9 | describe('retry middleware', {timeout: 15000}, () => { 10 | const retry5xx = (err: any) => err.response.statusCode >= 500 11 | 12 | it('exposes default "shouldRetry" function', () => { 13 | expect(retry.shouldRetry).to.be.a('function') 14 | }) 15 | 16 | it('should handle retries when retry middleware is used', () => { 17 | const request = getIt([baseUrl, debugRequest, retry()]) 18 | const req = request({url: `/fail?uuid=${Math.random()}&n=4`}) 19 | 20 | return expectRequest(req).resolves.toMatchObject({ 21 | statusCode: 200, 22 | body: 'Success after failure', 23 | }) 24 | }) 25 | 26 | it('should be able to set max retries', {timeout: 400}, () => { 27 | const request = getIt([baseUrl, httpErrors(), retry({maxRetries: 1, shouldRetry: retry5xx})]) 28 | const req = request({url: '/status?code=500'}) 29 | return expectRequest(req).rejects.toThrow(/HTTP 500/i) 30 | }) 31 | 32 | it.runIf(environment === 'node')('should not retry if body is a stream', {timeout: 400}, () => { 33 | const request = getIt([baseUrl, httpErrors(), retry({maxRetries: 5, shouldRetry: retry5xx})]) 34 | const req = request({url: '/status?code=500', body: fs.createReadStream(__filename)}) 35 | return expectRequest(req).rejects.toThrow(/HTTP 500/i) 36 | }) 37 | 38 | it('should be able to set max retries on a per-request basis', {timeout: 400}, () => { 39 | const request = getIt([baseUrl, httpErrors(), retry({maxRetries: 5, shouldRetry: retry5xx})]) 40 | const req = request({url: '/status?code=500', maxRetries: 1}) 41 | return expectRequest(req).rejects.toThrow(/HTTP 500/i) 42 | }) 43 | 44 | it('should be able to set a custom function on whether or not we should retry', () => { 45 | const shouldRetry = (_error: any, retryCount: any) => retryCount !== 1 46 | const request = getIt([baseUrl, debugRequest, httpErrors(), retry({shouldRetry})]) 47 | const req = request({url: '/status?code=503'}) 48 | return expectRequest(req).rejects.toThrow(/HTTP 503/) 49 | }) 50 | 51 | it('should be able to set a custom function on whether or not we should retry (per-request basis)', () => { 52 | const shouldRetry = (_error: any, retryCount: any) => retryCount !== 1 53 | const request = getIt([baseUrl, debugRequest, httpErrors(), retry()]) 54 | const req = request({url: '/status?code=503', shouldRetry}) 55 | return expectRequest(req).rejects.toThrow(/HTTP 503/) 56 | }) 57 | 58 | it.skipIf(environment === 'browser')('should not retry non-GET-requests by default', () => { 59 | // Browsers have a weird thing where they might auto-retry on network errors 60 | const request = getIt([baseUrl, debugRequest, retry()]) 61 | const req = request({url: `/fail?uuid=${Math.random()}&n=2`, method: 'POST', body: 'Heisann'}) 62 | return expectRequest(req).rejects.toThrow(Error) 63 | }) 64 | 65 | // @todo Browsers are really flaky with retries, revisit later 66 | it.skipIf(environment === 'browser')('should handle retries with a delay function ', () => { 67 | const retryDelay = () => 375 68 | const request = getIt([baseUrl, retry({retryDelay})]) 69 | 70 | const startTime = Date.now() 71 | const req = request({url: `/fail?uuid=${Math.random()}&n=4`}) 72 | return expectRequest(req).resolves.toSatisfy(() => { 73 | const timeUsed = Date.now() - startTime 74 | return timeUsed > 1000 && timeUsed < 1750 75 | }, 'respects the retry delay (roughly)') 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /test/socket.test.ts: -------------------------------------------------------------------------------- 1 | import {environment, getIt} from 'get-it' 2 | import {keepAlive} from 'get-it/middleware' 3 | import {describe, it} from 'vitest' 4 | 5 | import {baseUrl, promiseRequest} from './helpers' 6 | 7 | describe.runIf(environment === 'node')('socket', () => { 8 | process.on('warning', (e) => { 9 | if (e.name === 'MaxListenersExceededWarning') { 10 | throw e 11 | } 12 | }) 13 | 14 | it(`doesn't leak handlers`, async () => { 15 | const request = getIt([baseUrl]) 16 | 17 | for (let i = 0; i < 100; i++) { 18 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 19 | ;(await promiseRequest(request('/remote-port'))).body 20 | } 21 | }) 22 | 23 | it(`doesn't leak handlers, with keep alive`, async () => { 24 | const request = getIt([baseUrl, keepAlive()]) 25 | 26 | for (let i = 0; i < 100; i++) { 27 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 28 | ;(await promiseRequest(request('/remote-port'))).body 29 | } 30 | }) 31 | 32 | it(`doesn't leak handlers with many concurrent requests`, async () => { 33 | const request = getIt([baseUrl, keepAlive()]) 34 | 35 | const promises = [] 36 | for (let i = 0; i < 1000; i++) { 37 | promises.push(promiseRequest(request('/delay'))) 38 | } 39 | await Promise.all(promises) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/stream.test.ts: -------------------------------------------------------------------------------- 1 | import {environment, getIt} from 'get-it' 2 | import {getUri} from 'get-uri' 3 | import {Readable} from 'stream' 4 | import {describe, expect, it} from 'vitest' 5 | 6 | import {concat} from '../src/request/node/simpleConcat' 7 | import {baseUrl, baseUrlPrefix, debugRequest, expectRequest, expectRequestBody} from './helpers' 8 | 9 | describe.runIf(environment === 'node')('streams', {timeout: 15000}, () => { 10 | it('should be able to send a stream to a remote endpoint', async () => { 11 | const body = 'Just some plain text for you to consume' 12 | const request = getIt([baseUrl, debugRequest]) 13 | const req = request({url: '/echo', body: Readable.from(body)}) 14 | await expectRequestBody(req).resolves.toEqual(body) 15 | }) 16 | 17 | it('should be able to pipe one request stream into the other', () => 18 | getUri(`${baseUrlPrefix}/plain-text`).then(async (stream) => { 19 | const expected = 'Just some plain text for you to consume' 20 | const request = getIt([baseUrl, debugRequest]) 21 | const req = request({url: '/echo', body: stream}) 22 | await expectRequestBody(req).resolves.toEqual(expected) 23 | })) 24 | 25 | it('does not retry failed requests when using streams', async () => { 26 | const body = 'Just some plain text for you to consume' 27 | const request = getIt([baseUrl, debugRequest]) 28 | const req = request({url: '/fail?n=3', body: Readable.from(body)}) 29 | await expectRequest(req).rejects.toThrow(Error) 30 | }) 31 | 32 | it('can get a response stream', async () => 33 | new Promise((resolve) => { 34 | const request = getIt([baseUrl, debugRequest]) 35 | const req = request({url: '/drip', stream: true}) 36 | req.response.subscribe((res: any) => { 37 | expect(res.body).to.have.property('pipe') 38 | expect(res.body.pipe).to.be.a('function') 39 | concat(res.body, (err: any, body: any) => { 40 | expect(err).to.eq(null) 41 | expect(body.toString('utf8')).to.eq('chunkchunkchunkchunkchunkchunkchunkchunkchunk') 42 | resolve(undefined) 43 | }) 44 | }) 45 | })) 46 | }) 47 | -------------------------------------------------------------------------------- /test/timeouts.test.ts: -------------------------------------------------------------------------------- 1 | import {finished} from 'node:stream/promises' 2 | 3 | import {adapter, environment, getIt} from 'get-it' 4 | import {keepAlive} from 'get-it/middleware' 5 | import {describe, expect, it} from 'vitest' 6 | 7 | import {baseUrl, debugRequest, promiseRequest} from './helpers' 8 | 9 | const testTimeout = 250 10 | const testTimeoutThreshold = testTimeout * 0.95 11 | 12 | describe('timeouts', {timeout: 10000}, () => { 13 | // @TODO make the this test work in happy-dom 14 | it.skipIf(adapter === 'xhr' && environment === 'browser')( 15 | 'should be able to set a "global" timeout', 16 | () => 17 | new Promise((resolve, reject) => { 18 | // To prevent the connection from being established use a non-routable IP 19 | // address. See https://tools.ietf.org/html/rfc5737#section-3 20 | const request = getIt([debugRequest]) 21 | const req = request({url: 'http://192.0.2.1/', timeout: testTimeout}) 22 | 23 | req.response.subscribe(() => reject(new Error('response channel should not be called'))) 24 | req.error.subscribe((err: any) => { 25 | expect(err.message).to.match(/timed out/i) 26 | resolve(undefined) 27 | }) 28 | }), 29 | ) 30 | 31 | it('should be able to set individual timeouts', () => 32 | new Promise((resolve, reject) => { 33 | const request = getIt([debugRequest]) 34 | const startTime = Date.now() 35 | const req = request({url: 'http://192.0.2.1/', timeout: {socket: testTimeout, connect: 450}}) 36 | 37 | req.response.subscribe(() => reject(new Error('response channel should not be called'))) 38 | req.error.subscribe(() => { 39 | expect(Date.now() - startTime).toBeGreaterThanOrEqual(testTimeoutThreshold) 40 | resolve(undefined) 41 | }) 42 | })) 43 | 44 | it.skipIf(adapter === 'xhr' && environment === 'browser').each([ 45 | [false, false, false], 46 | [false, true, false], 47 | [true, false, false], 48 | [true, true, false], 49 | 50 | // Gzip tests at the bottom 51 | [false, false, true], 52 | [false, true, true], 53 | [true, false, true], 54 | [true, true, true], 55 | ])( 56 | 'should be able to set socket timeout with {followingRedirects: %s, stream: %s, gzip: %s}', 57 | async (followRedirects, stream, gzip) => { 58 | const request = getIt([baseUrl, debugRequest]) 59 | const req = request({ 60 | url: gzip ? '/stall-after-initial-gzip' : '/stall-after-initial', 61 | timeout: {socket: 500, connect: testTimeout}, 62 | maxRedirects: followRedirects ? 3 : 0, 63 | stream, 64 | }) 65 | 66 | await expect(async () => { 67 | const res = await promiseRequest(req) 68 | if (stream) { 69 | // If we're in stream mode then we expect the error to appear here instead: 70 | await finished(res.body) 71 | } 72 | }).rejects.toThrowError(/Socket timed out on request to/) 73 | }, 74 | ) 75 | 76 | it.runIf(environment === 'node')( 77 | 'should reset the timeout when a connection is reused', 78 | async () => { 79 | // Keep-alive can only be reliably tested in Node.js. 80 | 81 | const request = getIt([baseUrl, debugRequest, keepAlive()]) 82 | 83 | // We do one request: 84 | const remotePort1 = ( 85 | await promiseRequest( 86 | request({url: '/remote-port', timeout: {socket: 500, connect: testTimeout}}), 87 | ) 88 | ).body 89 | 90 | // And now the other one should also succeed (after 6 seconds). 91 | await promiseRequest(request({url: '/stall-after-initial', timeout: false})) 92 | 93 | // And verify that keep-alive was actually used: 94 | const remotePort2 = (await promiseRequest(request({url: '/remote-port', timeout: false}))) 95 | .body 96 | expect(remotePort1).toBe(remotePort2) 97 | }, 98 | ) 99 | }) 100 | -------------------------------------------------------------------------------- /test/urlEncoded.test.ts: -------------------------------------------------------------------------------- 1 | import {environment, getIt} from 'get-it' 2 | import {jsonResponse, urlEncoded} from 'get-it/middleware' 3 | import {Readable} from 'stream' 4 | import {describe, it} from 'vitest' 5 | 6 | import {baseUrl, debugRequest, expectRequestBody} from './helpers' 7 | 8 | describe.runIf(typeof ArrayBuffer !== 'undefined')('urlEncoded middleware', () => { 9 | it('should be able to send urlencoded data to an endpoint and get JSON back', () => { 10 | const request = getIt([baseUrl, urlEncoded(), jsonResponse(), debugRequest]) 11 | const body: Record = { 12 | randomValue: Date.now(), 13 | someThing: 'spaces & commas - all sorts!', 14 | } 15 | const strBody = Object.keys(body).reduce( 16 | (acc, key) => Object.assign(acc, {[key]: `${body[key]}`}), 17 | {}, 18 | ) 19 | const req = request({url: '/urlencoded', body}) 20 | return expectRequestBody(req).resolves.toEqual(strBody) 21 | }) 22 | 23 | it('should be able to send PUT-requests with urlencoded bodies', () => { 24 | const request = getIt([baseUrl, urlEncoded(), jsonResponse(), debugRequest]) 25 | const req = request({url: '/urlencoded', method: 'PUT', body: {foo: 'bar'}}) 26 | return expectRequestBody(req).resolves.toEqual({foo: 'bar'}) 27 | }) 28 | 29 | it('should serialize arrays', () => { 30 | const request = getIt([baseUrl, urlEncoded(), jsonResponse(), debugRequest]) 31 | const body = {foo: ['foo', 'bar', 'baz']} 32 | const req = request({url: '/urlencoded', method: 'PUT', body}) 33 | return expectRequestBody(req).resolves.toEqual({ 34 | 'foo[0]': 'foo', 35 | 'foo[1]': 'bar', 36 | 'foo[2]': 'baz', 37 | }) 38 | }) 39 | 40 | it('should serialize complex objects', () => { 41 | const request = getIt([baseUrl, urlEncoded(), jsonResponse(), debugRequest]) 42 | const body = { 43 | str: 'val', 44 | num: 0, 45 | arr: [3, {prop: false}, 1, null, 6], 46 | obj: {prop1: null, prop2: ['elem']}, 47 | emoji: '😀', 48 | set: new Set([1, 'two']), 49 | } 50 | const req = request({url: '/urlencoded', method: 'PUT', body}) 51 | return expectRequestBody(req).resolves.toEqual({ 52 | 'arr[0]': '3', 53 | 'arr[1][prop]': 'false', 54 | 'arr[2]': '1', 55 | 'arr[3]': 'null', 56 | 'arr[4]': '6', 57 | 'num': '0', 58 | 'obj[prop1]': 'null', 59 | 'obj[prop2][0]': 'elem', 60 | 'str': 'val', 61 | 'emoji': '😀', 62 | 'set[0]': '1', 63 | 'set[1]': 'two', 64 | }) 65 | }) 66 | 67 | it.skipIf(typeof Buffer === 'undefined')('should not serialize buffers', () => { 68 | const request = getIt([baseUrl, urlEncoded(), jsonResponse(), debugRequest]) 69 | const body = Buffer.from('blåbærsyltetøy', 'utf8') 70 | const req = request({url: '/echo', method: 'PUT', body}) 71 | return expectRequestBody(req).resolves.toEqual('blåbærsyltetøy') 72 | }) 73 | 74 | it.runIf(environment === 'node')('should not serialize streams', () => { 75 | const request = getIt([baseUrl, urlEncoded(), jsonResponse(), debugRequest]) 76 | const body = Readable.from('unicorn') 77 | const req = request({url: '/echo', method: 'PUT', body}) 78 | return expectRequestBody(req).resolves.toEqual('unicorn') 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src", "./modules.d.ts"], 4 | "compilerOptions": { 5 | "noCheck": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src", "./modules.d.ts", "./test"], 4 | "exclude": ["./test-deno/test.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/pkg-utils/tsconfig/strictest.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "get-it": ["./src"], 6 | "get-it/middleware": ["./src/middleware"] 7 | }, 8 | 9 | "outDir": "./dist", 10 | "rootDir": ".", 11 | 12 | "noUncheckedIndexedAccess": false, 13 | "exactOptionalPropertyTypes": false, 14 | "useUnknownInCatchVariables": true, 15 | "erasableSyntaxOnly": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {configDefaults, defineConfig, type UserConfig} from 'vitest/config' 2 | 3 | import pkg from './package.json' 4 | 5 | export const sharedConfig = { 6 | // Ignore deno and esm 7 | exclude: [...configDefaults.exclude, 'test-deno/*', 'test-esm/*'], 8 | globalSetup: [ 9 | './test/helpers/globalSetup.http.ts', 10 | './test/helpers/globalSetup.https.ts', 11 | './test/helpers/globalSetup.proxy.http.ts', 12 | './test/helpers/globalSetup.proxy.https.ts', 13 | ], 14 | reporters: process.env.GITHUB_ACTIONS ? ['default', 'github-actions'] : 'default', 15 | alias: { 16 | 'get-it/middleware': new URL(pkg.exports['./middleware'].source, import.meta.url).pathname, 17 | 'get-it': new URL(pkg.exports['.'].source, import.meta.url).pathname, 18 | }, 19 | } satisfies UserConfig['test'] 20 | 21 | export default defineConfig({ 22 | test: sharedConfig, 23 | }) 24 | -------------------------------------------------------------------------------- /vitest.browser.config.ts: -------------------------------------------------------------------------------- 1 | // Simulates a browser environment until `@vitest/browser` is ready for production and 2 | // we can run the tests in a real browser 3 | 4 | import {defineConfig} from 'vitest/config' 5 | 6 | import pkg from './package.json' 7 | import {sharedConfig} from './vite.config' 8 | 9 | export default defineConfig({ 10 | test: { 11 | ...sharedConfig, 12 | alias: { 13 | 'get-it/middleware': new URL(pkg.exports['./middleware'].browser.source, import.meta.url) 14 | .pathname, 15 | 'get-it': new URL(pkg.exports['.'].browser.source, import.meta.url).pathname, 16 | }, 17 | }, 18 | resolve: { 19 | conditions: ['browser', 'module', 'import'], 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /vitest.edge.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config' 2 | 3 | import pkg from './package.json' 4 | import {sharedConfig} from './vite.config' 5 | 6 | export default defineConfig({ 7 | test: { 8 | ...sharedConfig, 9 | environment: 'edge-runtime', 10 | alias: { 11 | 'get-it/middleware': new URL(pkg.exports['./middleware'].browser.source, import.meta.url) 12 | .pathname, 13 | 'get-it': new URL(pkg.exports['.'].browser.source, import.meta.url).pathname, 14 | }, 15 | }, 16 | resolve: { 17 | // https://github.com/vercel/next.js/blob/95322649ffb2ad0d6423481faed188dd7b1f7ff2/packages/next/src/build/webpack-config.ts#L1079-L1084 18 | conditions: ['edge-light', 'worker', 'browser', 'module', 'import', 'node'], 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /vitest.react-server.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config' 2 | 3 | import pkg from './package.json' 4 | import {sharedConfig} from './vite.config' 5 | 6 | export default defineConfig({ 7 | test: { 8 | ...sharedConfig, 9 | alias: { 10 | 'get-it/middleware': new URL(pkg.exports['./middleware']['react-server'], import.meta.url) 11 | .pathname, 12 | 'get-it': new URL('./src/index.react-server.ts', import.meta.url).pathname, 13 | }, 14 | }, 15 | resolve: { 16 | conditions: ['react-server', 'node'], 17 | }, 18 | }) 19 | --------------------------------------------------------------------------------