├── .github └── workflows │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .prettierignore ├── README.md ├── browser ├── .npmignore ├── LICENSE ├── karma.conf.js ├── package.json ├── src │ └── index.ts ├── test │ └── common.ts ├── tsconfig.build.json └── tsconfig.json ├── codecov.yml ├── common ├── package.json ├── src │ ├── index.ts │ └── types.ts └── tsconfig.json ├── configs ├── .prettierrc.json ├── release-it.json ├── rollup.config.ts ├── tsconfig.json ├── tsconfig.typedoc.json └── typedoc.json ├── node ├── .npmignore ├── package.json ├── rollup.config.ts ├── src │ └── index.ts ├── test │ └── common.ts ├── tsconfig.build.json └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── test ├── headers.ts ├── helpers.ts ├── index.ts ├── query.ts ├── responseType.ts ├── send.ts ├── serialize.ts ├── stream.ts └── support ├── data.json └── server.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | paths-ignore: 5 | - '**.md' 6 | 7 | jobs: 8 | build: 9 | name: build 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - name: Checkout repo 13 | uses: actions/checkout@v2 14 | 15 | - name: Install pnpm 16 | run: sudo npm install --global pnpm 17 | 18 | - name: Cache dependencies 19 | uses: actions/cache@v1 20 | id: cache-pnpm-store 21 | with: 22 | path: ~/.pnpm-store 23 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} 24 | restore-keys: | 25 | ${{ runner.os }}-pnpm-store- 26 | 27 | - name: Install dependencies 28 | run: pnpm recursive install 29 | 30 | - name: Build project 31 | run: pnpm recursive run build 32 | 33 | - name: Run tests 34 | run: pnpm recursive test --workspace-concurrency 1 35 | 36 | #- name: Upload coverage 37 | #uses: codecov/codecov-action@v1 38 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | push: 4 | paths: 5 | - 'browser/src/**' 6 | - 'node/src/**' 7 | - 'common/src/**' 8 | - 'README.md' 9 | - '.github/workflows/docs.yml' 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build: 15 | name: build 16 | runs-on: ubuntu-18.04 17 | steps: 18 | - name: Checkout repo 19 | uses: actions/checkout@v2 20 | 21 | - name: Install pnpm 22 | run: sudo npm install --global pnpm 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v1 26 | id: cache-pnpm-store 27 | with: 28 | path: ~/.pnpm-store 29 | key: docs-pnpm-store-${{ hashFiles('package.json') }} 30 | restore-keys: | 31 | docs-pnpm-store- 32 | 33 | - name: Install dependencies 34 | run: pnpm recursive install 35 | 36 | - name: Build packages 37 | run: pnpm recursive run build 38 | 39 | - name: Build documentation 40 | run: pnpm run docs 41 | 42 | - name: Deploy 43 | uses: peaceiris/actions-gh-pages@v3 44 | with: 45 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 46 | publish_dir: ./docs 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .nyc_output/ 4 | coverage/ 5 | docs/ 6 | 7 | test/support/db.json 8 | 9 | *.log 10 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm exec -- commithelper check --file $1 --fix 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm exec -- lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && npm exec -- commithelper prompt --file $1 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | coverage/ 3 | node_modules/ 4 | .nyc_output/ 5 | docs/ 6 | 7 | pnpm-lock.yaml 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minireq 2 | 3 | ![build](https://github.com/jvanbruegge/minireq/workflows/Continous%20Integration/badge.svg) ![docs](https://github.com/jvanbruegge/minireq/workflows/Documentation/badge.svg) [![codecov](https://codecov.io/gh/jvanbruegge/minireq/branch/master/graph/badge.svg)](https://codecov.io/gh/jvanbruegge/minireq) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 4 | 5 | A minimal request library built on XMLHTTPRequest for the browser, and for nodejs with the same API. 6 | 7 | Documentation on [Github Pages](https://jvanbruegge.github.io/minireq/) 8 | 9 | ## Why not fetch, axios or superagent 10 | 11 | `fetch` is too bare bones and also does not support features like progress indication. `axios` and `superagent` are neither minimal nor are they written with ES modules with makes them awkward to bundle. 12 | 13 | Also I want a request library with better types than currently available. 14 | 15 | ## Example 16 | 17 | ```ts 18 | import { makeRequest } from '@minireq/browser'; 19 | // If you are using nodejs, you can use 20 | // import { makeRequest } from '@minireq/node'; 21 | 22 | const request = makeRequest(); 23 | 24 | const { promise, abort } = request({ 25 | method: 'GET', 26 | url: '/api/users', 27 | }); 28 | 29 | // Abort on user click 30 | document.querySelector('button.abortRequest').addEventListener('click', () => { 31 | abort(); 32 | }); 33 | 34 | promise.then(({ status, data }) => { 35 | if (status === 200) { 36 | console.log(data.name); 37 | } 38 | }); 39 | ``` 40 | 41 | Making a post request, with a timeout on 500ms 42 | 43 | ```ts 44 | import { makeRequest } from '@minireq/browser'; 45 | 46 | const request = makeRequest(); 47 | 48 | const { promise } = request({ 49 | method: 'POST', 50 | url: '/api/users', 51 | send: { 52 | name: 'Peter', 53 | age: 50, 54 | children: [], 55 | }, 56 | timeout: 500, 57 | }); 58 | 59 | promise.then(({ status, data }) => { 60 | if (status === 201) { 61 | console.log(data.id); 62 | } 63 | }); 64 | ``` 65 | 66 | Using a custom content type 67 | 68 | ```ts 69 | import { makeRequest, defaultSerializers } from '@minireq/browser'; 70 | 71 | const serializer = { 72 | parse: (data: string) => data.split('\n').map(x => JSON.parse(x)), 73 | convert: (data: any) => { 74 | if (!Array.isArray(data)) { 75 | return [JSON.stringify(data)]; 76 | } else { 77 | return data.map(x => JSON.stringify(x)).join('\n'); 78 | } 79 | }, 80 | }; 81 | 82 | const { request } = makeRequest({ 83 | ...defaultSerializers, 84 | 'application/ndjson': serializer, 85 | }); 86 | 87 | const { promise, abort } = request({ 88 | method: 'GET', 89 | url: '/api/users', 90 | accept: 'application/ndjson', 91 | }); 92 | 93 | const { status, data } = await promise; 94 | 95 | if (status === 200) { 96 | console.log(data.length); 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /browser/.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | test/ 3 | .nyc_output/ 4 | -------------------------------------------------------------------------------- /browser/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jan van Brügge 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 deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | 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 all 13 | 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 FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /browser/karma.conf.js: -------------------------------------------------------------------------------- 1 | const debug = process.env.DEBUG === '1'; 2 | 3 | module.exports = config => { 4 | config.set({ 5 | basePath: '..', 6 | 7 | frameworks: ['mocha', 'karma-typescript'], 8 | 9 | files: [ 10 | { pattern: 'browser/src/**/*.ts' }, 11 | { pattern: '{.,browser}/test/**.ts' }, 12 | ], 13 | 14 | plugins: [ 15 | 'karma-mocha', 16 | 'karma-firefox-launcher', 17 | 'karma-chrome-launcher', 18 | 'karma-typescript', 19 | ], 20 | 21 | preprocessors: { 22 | '**/*.ts': ['karma-typescript'], 23 | }, 24 | 25 | reporters: ['dots', 'karma-typescript'], 26 | 27 | browsers: debug ? ['Chrome'] : ['FirefoxHeadless', 'ChromeHeadless'], 28 | concurrency: 1, 29 | 30 | karmaTypescriptConfig: { 31 | reports: { 32 | text: null, 33 | html: 'coverage', 34 | json: { directory: 'coverage', filename: 'coverage.json' }, 35 | }, 36 | coverageOptions: { 37 | exclude: /^test\//, 38 | }, 39 | }, 40 | 41 | singleRun: !debug, 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@minireq/browser", 3 | "version": "2.0.0", 4 | "description": "A minimal request library for the browser", 5 | "main": "build/bundle.cjs.js", 6 | "module": "build/bundle.esm.js", 7 | "typings": "build/index.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "build": "tsc --project tsconfig.build.json && pnpm run build:esm && pnpm run build:cjs", 11 | "build:esm": "cross-env BUNDLE_FORMAT=esm rollup -c ../configs/rollup.config.ts", 12 | "build:cjs": "cross-env BUNDLE_FORMAT=cjs rollup -c ../configs/rollup.config.ts", 13 | "test": "start-server-and-test start-server 3000 test:normal", 14 | "debug": "cross-env DEBUG=1 pnpm test", 15 | "test:normal": "karma start", 16 | "start-server": "ts-node ../test/support/server.ts", 17 | "prepublishOnly": "pnpm run build && pnpm test", 18 | "release": "release-it --config ../configs/release-it.json --git.commitMessage='chore(release): @minireq/browser v${version}'", 19 | "format": "prettier --write './{src,test}/**/*.ts'" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "author": "Jan van Brügge", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@types/assert": "^1.5.2", 28 | "@types/cors": "^2.8.7", 29 | "@types/express": "^4.17.8", 30 | "@types/mocha": "^8.0.3", 31 | "@types/node": "^14.11.2", 32 | "assert": "^2.0.0", 33 | "cors": "^2.8.5", 34 | "express": "^4.17.1", 35 | "karma": "^5.2.3", 36 | "karma-chrome-launcher": "^3.1.0", 37 | "karma-cli": "^2.0.0", 38 | "karma-firefox-launcher": "^1.3.0", 39 | "karma-mocha": "^2.0.1", 40 | "karma-typescript": "^5.2.0", 41 | "mocha": "^8.1.3", 42 | "nyc": "^15.1.0", 43 | "prettier": "^2.1.2", 44 | "rollup": "^2.28.2", 45 | "start-server-and-test": "^1.11.5", 46 | "ts-node": "^9.0.0", 47 | "typescript": "^4.0.3" 48 | }, 49 | "dependencies": { 50 | "@minireq/common": "workspace:^2.0.0" 51 | }, 52 | "prettier": "../configs/.prettierrc.json" 53 | } 54 | -------------------------------------------------------------------------------- /browser/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | METHOD, 3 | Serializer, 4 | RequestOpts, 5 | RequestOptions, 6 | ResponseType, 7 | Result, 8 | ResultMapping, 9 | makeQueryString, 10 | defaultSerializers, 11 | defaults, 12 | } from '@minireq/common'; 13 | 14 | export { 15 | GET, 16 | HEAD, 17 | POST, 18 | PUT, 19 | DELETE, 20 | CONNECT, 21 | OPTIONS, 22 | TRACE, 23 | PATCH, 24 | METHOD, 25 | ResponseType, 26 | ResultMapping, 27 | RequestFn, 28 | RequestFunction, 29 | RequestOptions, 30 | Progress, 31 | RequestOpts, 32 | Response, 33 | Result, 34 | Serializer, 35 | } from '@minireq/common'; 36 | 37 | export function makeRequest( 38 | serializers: Record = defaultSerializers, 39 | defaultOptions: Partial = {} 40 | ): ( 41 | options: RequestOpts 42 | ) => Result[Type]> { 43 | return function request( 44 | options: RequestOpts 45 | ): Result[Type]> { 46 | const opts = { ...defaults, ...defaultOptions, ...options }; 47 | const url = opts.url + makeQueryString(opts.query); 48 | 49 | // Because fuck JavaScript Promises and their garbage error handing regarding throw 50 | let resolve: any; 51 | let reject: any; 52 | 53 | const request = new XMLHttpRequest(); 54 | 55 | const abort = () => { 56 | request.abort(); 57 | }; 58 | 59 | if (opts.timeout) request.timeout = opts.timeout; 60 | if (opts.onTimeout) { 61 | request.addEventListener('timeout', opts.onTimeout); 62 | } 63 | 64 | request.addEventListener('load', () => { 65 | let response: any = request.response; 66 | if (opts.responseType === 'parsed') { 67 | const mimeType = request 68 | .getResponseHeader('Content-Type') 69 | ?.split(';')[0]; 70 | if (mimeType && serializers[mimeType]?.parse) { 71 | response = serializers[mimeType].parse!(response); 72 | } 73 | } 74 | 75 | resolve({ 76 | status: request.status, 77 | data: response, 78 | }); 79 | }); 80 | request.addEventListener('error', reject); 81 | 82 | if (opts.progress) { 83 | request.onprogress = opts.progress; 84 | } 85 | if (opts.uploadProgress) { 86 | request.upload.onprogress = opts.uploadProgress; 87 | } 88 | 89 | request.open(opts.method, url, true); 90 | 91 | request.responseType = 92 | opts.responseType === 'binary' ? 'arraybuffer' : 'text'; 93 | 94 | if (opts.headers) { 95 | for (const key in opts.headers) { 96 | request.setRequestHeader(key, opts.headers[key]); 97 | } 98 | } 99 | if (opts.contentType) { 100 | request.setRequestHeader('Content-Type', opts.contentType); 101 | } 102 | if (opts.accept) { 103 | request.setRequestHeader('Accept', opts.accept); 104 | } 105 | 106 | if (opts.auth) { 107 | request.setRequestHeader( 108 | 'Authorization', 109 | `Basic ${btoa(opts.auth.user + ':' + opts.auth.password)}` 110 | ); 111 | } 112 | 113 | if (opts.send) { 114 | if ( 115 | typeof opts.send === 'string' || 116 | opts.send instanceof Blob || 117 | opts.send instanceof ArrayBuffer || 118 | opts.send instanceof Int8Array || 119 | opts.send instanceof Uint8Array || 120 | opts.send instanceof Uint8ClampedArray || 121 | opts.send instanceof Int16Array || 122 | opts.send instanceof Uint16Array || 123 | opts.send instanceof Int32Array || 124 | opts.send instanceof Uint32Array || 125 | opts.send instanceof Float32Array || 126 | opts.send instanceof Float64Array || 127 | opts.send instanceof DataView || 128 | opts.send instanceof FormData || 129 | opts.send instanceof URLSearchParams 130 | ) { 131 | request.send(opts.send); 132 | } else if (serializers[opts.contentType!]?.convert) { 133 | request.send( 134 | serializers[opts.contentType!].convert!(opts.send) 135 | ); 136 | } else { 137 | throw new Error( 138 | `Could not find a serializer for content type ${opts.contentType}` 139 | ); 140 | } 141 | } else { 142 | request.send(); 143 | } 144 | 145 | return { 146 | promise: new Promise((res, rej) => { 147 | resolve = res; 148 | reject = rej; 149 | }), 150 | abort, 151 | }; 152 | }; 153 | } 154 | -------------------------------------------------------------------------------- /browser/test/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makeSimpleTests, 3 | makeHeaderTests, 4 | makeQueryTests, 5 | makeResponseTypeTests, 6 | makeSendTests, 7 | makeSerializationTests, 8 | makeStreamTests, 9 | } from '../../test/index'; 10 | import { makeRequest } from '../src/index'; 11 | 12 | const request = makeRequest(); 13 | 14 | makeSimpleTests(request); 15 | makeHeaderTests(request); 16 | makeQueryTests(request); 17 | makeResponseTypeTests(request); 18 | makeSendTests(request); 19 | makeSerializationTests(makeRequest); 20 | makeStreamTests(request); 21 | -------------------------------------------------------------------------------- /browser/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "build" 5 | }, 6 | "include": [], 7 | "files": ["./src/index.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../configs/tsconfig.json", 3 | "include": ["./src/**/*", "./test/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@minireq/common", 3 | "version": "2.0.0", 4 | "description": "A minimal request library for the browser - types only", 5 | "main": "build/bundle.cjs.js", 6 | "module": "build/bundle.esm.js", 7 | "types": "build/index.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "build": "tsc && pnpm run build:esm && pnpm run build:cjs", 11 | "build:esm": "cross-env BUNDLE_FORMAT=esm rollup -c ../configs/rollup.config.ts", 12 | "build:cjs": "cross-env BUNDLE_FORMAT=cjs rollup -c ../configs/rollup.config.ts", 13 | "release": "release-it --config ../configs/release-it.json --git.commitMessage='chore(release): @minireq/common v${version}'", 14 | "prepublishOnly": "pnpm run build" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "author": "Jan van Brügge", 20 | "license": "MIT", 21 | "prettier": "../configs/.prettierrc.json" 22 | } 23 | -------------------------------------------------------------------------------- /common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | import type { RequestOptions } from './types'; 3 | 4 | export const defaultSerializers = { 5 | 'application/json': { parse: JSON.parse, convert: JSON.stringify }, 6 | }; 7 | 8 | export const defaults: Partial = { 9 | contentType: 'application/json', 10 | responseType: 'parsed', 11 | accept: '*/*', 12 | }; 13 | 14 | export function makeQueryString( 15 | query: Record | string | undefined 16 | ): string { 17 | if (!query) { 18 | return ''; 19 | } 20 | if (typeof query === 'string') { 21 | if (query.charAt(0) === '?') { 22 | return query; 23 | } else { 24 | return '?' + query; 25 | } 26 | } 27 | 28 | let str = '?'; 29 | 30 | for (const key of Object.keys(query)) { 31 | str += 32 | encodeURIComponent(key) + 33 | '=' + 34 | encodeURIComponent(query[key]) + 35 | '&'; 36 | } 37 | 38 | if (str === '?') { 39 | throw new Error('An empty object is not valid as query parameter'); 40 | } 41 | 42 | return str; 43 | } 44 | -------------------------------------------------------------------------------- /common/src/types.ts: -------------------------------------------------------------------------------- 1 | export type GET = 'GET'; 2 | export type HEAD = 'HEAD'; 3 | export type POST = 'POST'; 4 | export type PUT = 'PUT'; 5 | export type DELETE = 'DELETE'; 6 | export type CONNECT = 'CONNECT'; 7 | export type OPTIONS = 'OPTIONS'; 8 | export type TRACE = 'OPTIONS'; 9 | export type PATCH = 'PATCH'; 10 | 11 | export type METHOD = 12 | | GET 13 | | HEAD 14 | | POST 15 | | PUT 16 | | DELETE 17 | | CONNECT 18 | | OPTIONS 19 | | TRACE 20 | | PATCH; 21 | 22 | export type ResponseType = 'raw_text' | 'binary' | 'parsed'; 23 | 24 | export type ResultMapping = { 25 | binary: ArrayBuffer; 26 | raw_text: string; 27 | parsed: T; 28 | }; 29 | 30 | export type RequestFn = RequestFunction; 31 | 32 | export type RequestFunction = ( 33 | options: RequestOpts 34 | ) => Result[Type]>; 35 | 36 | export type RequestOptions = RequestOpts; 37 | 38 | export type Progress = { 39 | lengthComputable: boolean; 40 | loaded: number; 41 | total: number; 42 | }; 43 | 44 | export type RequestOpts = { 45 | method: Method; 46 | url: string; 47 | headers?: { 48 | [name: string]: string; 49 | }; 50 | /** 51 | * Either a premade query string or an object of key: value pairs 52 | */ 53 | query?: string | Record; 54 | /** 55 | * If this is an object, the contentType will be used to serialize it 56 | */ 57 | send?: 58 | | string 59 | | Blob 60 | | Record 61 | | BufferSource 62 | | FormData 63 | | URLSearchParams 64 | | ReadableStream; 65 | accept?: string; 66 | /** 67 | * @default 'application/json' 68 | */ 69 | contentType?: string; 70 | /** 71 | * Credentials for HTTP basic auth 72 | */ 73 | auth?: { 74 | user: string; 75 | password: string; 76 | }; 77 | attach?: { 78 | [field: string]: Blob | File; 79 | }; 80 | /** 81 | * A callback for listening to **download** progress events 82 | */ 83 | progress?: (x: Progress) => void; 84 | /** 85 | * A callback for listening to **upload** progress events 86 | */ 87 | uploadProgress?: (x: Progress) => void; 88 | agent?: { 89 | key: string; 90 | cert: string; 91 | }; 92 | /** 93 | * Usually 'parsed' or 'arraybuffer' for binary data 94 | * @default 'parsed' 95 | */ 96 | responseType?: Type; 97 | /** 98 | * Timeout in milliseconds 99 | */ 100 | timeout?: number; 101 | onTimeout?: (x: Progress) => void; 102 | }; 103 | 104 | export type Response = { 105 | data: T; 106 | status: number; 107 | }; 108 | 109 | export type Result = { 110 | promise: Promise>; 111 | abort: () => void; 112 | }; 113 | 114 | export type Serializer = { 115 | parse?: (data: string) => any; 116 | convert?: (obj: any) => string; 117 | }; 118 | -------------------------------------------------------------------------------- /common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../configs/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "build" 5 | }, 6 | "include": ["./src/**.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /configs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "avoid", 4 | "tabWidth": 4, 5 | "overrides": [ 6 | { 7 | "files": "*.json", 8 | "options": { 9 | "tabWidth": 2 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /configs/release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | }, 5 | "plugins": { 6 | "@release-it/conventional-changelog": { 7 | "preset": "angular", 8 | "infile": "CHANGELOG.md" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /configs/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | 3 | const pkg = JSON.parse(readFileSync('./package.json', { encoding: 'utf-8' })); 4 | 5 | const format = process.env.BUNDLE_FORMAT || 'esm'; 6 | 7 | export default { 8 | input: 'build/index.js', 9 | ...(pkg.dependencies 10 | ? { external: Object.keys(pkg.dependencies) } 11 | : undefined), 12 | output: { 13 | file: `build/bundle.${format}.js`, 14 | format, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /configs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": false, 4 | "sourceMap": true, 5 | "declaration": true, 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "target": "es6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /configs/tsconfig.typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["../browser/src/*", "../node/src/*", "../common/src/*"] 4 | } 5 | -------------------------------------------------------------------------------- /configs/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minireq", 3 | "out": "docs", 4 | "theme": "default", 5 | "tsconfig": "configs/tsconfig.typedoc.json", 6 | "entryPoints": ["browser/src", "node/src", "common/src"] 7 | } 8 | -------------------------------------------------------------------------------- /node/.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | test/ 3 | .nyc_output/ 4 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@minireq/node", 3 | "version": "2.0.0", 4 | "description": "A minimal request library for nodejs", 5 | "main": "build/bundle.cjs.js", 6 | "module": "build/bundle.esm.js", 7 | "types": "build/index.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "build": "tsc --project tsconfig.build.json && pnpm run build:cjs && pnpm run build:esm", 11 | "build:esm": "cross-env BUNDLE_FORMAT=esm rollup -c rollup.config.ts", 12 | "build:cjs": "cross-env BUNDLE_FORMAT=cjs rollup -c rollup.config.ts", 13 | "test": "start-server-and-test start-server 3000 test:normal", 14 | "test:normal": "nyc --reporter text --reporter json --reporter html pnpm run test:no_cover", 15 | "test:no_cover": "ts-mocha test/**/*.ts", 16 | "start-server": "ts-node ../test/support/server.ts", 17 | "release": "release-it --config ../configs/release-it.json --git.commitMessage='chore(release): @minireq/node v${version}'", 18 | "prepublishOnly": "pnpm run build && pnpm test" 19 | }, 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "author": "Jan van Brügge", 24 | "license": "MIT", 25 | "dependencies": { 26 | "@minireq/common": "workspace:^2.0.0" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^14.11.2", 30 | "mocha": "^8.1.3", 31 | "nyc": "^15.1.0", 32 | "start-server-and-test": "^1.11.5", 33 | "ts-mocha": "^7.0.0", 34 | "ts-node": "^9.0.0" 35 | }, 36 | "prettier": "../configs/.prettierrc.json" 37 | } 38 | -------------------------------------------------------------------------------- /node/rollup.config.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import config from '../configs/rollup.config.ts'; 3 | 4 | export default { 5 | ...config, 6 | external: [...config.external, 'http', 'https'], 7 | }; 8 | -------------------------------------------------------------------------------- /node/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | METHOD, 3 | Serializer, 4 | RequestOpts, 5 | RequestOptions, 6 | ResponseType, 7 | Result, 8 | ResultMapping, 9 | Progress, 10 | makeQueryString, 11 | defaultSerializers, 12 | defaults, 13 | } from '@minireq/common'; 14 | import * as http from 'http'; 15 | import * as https from 'https'; 16 | 17 | export { 18 | GET, 19 | HEAD, 20 | POST, 21 | PUT, 22 | DELETE, 23 | CONNECT, 24 | OPTIONS, 25 | TRACE, 26 | PATCH, 27 | METHOD, 28 | ResponseType, 29 | ResultMapping, 30 | RequestFn, 31 | RequestFunction, 32 | RequestOptions, 33 | Progress, 34 | RequestOpts, 35 | Response, 36 | Result, 37 | Serializer, 38 | } from '@minireq/common'; 39 | 40 | export function makeRequest( 41 | serializers: Record = defaultSerializers, 42 | defaultOptions: Partial = {} 43 | ): ( 44 | options: RequestOpts 45 | ) => Result[Type]> { 46 | return function request( 47 | options: RequestOpts 48 | ): Result[Type]> { 49 | const opts = { ...defaults, ...defaultOptions, ...options }; 50 | const url = opts.url + makeQueryString(opts.query); 51 | 52 | const h = /^http:\/\//.test(opts.url) ? http : https; 53 | 54 | if (opts.uploadProgress) { 55 | throw new Error( 56 | 'Node.js does not support reporting upload progress' 57 | ); 58 | } 59 | 60 | const headers = { 61 | ...opts.headers, 62 | } as Record; 63 | 64 | if (opts.contentType) { 65 | headers['Content-Type'] = opts.contentType; 66 | } 67 | if (opts.accept) { 68 | headers['Accept'] = opts.accept; 69 | } 70 | 71 | if (opts.auth) { 72 | const base64 = Buffer.from( 73 | opts.auth.user + ':' + opts.auth.password 74 | ).toString('base64'); 75 | headers.Authorization = `Basic ${base64}`; 76 | } 77 | 78 | let data: any = ''; 79 | const req = h.request(url, { 80 | method: opts.method, 81 | timeout: opts.timeout, 82 | headers, 83 | }); 84 | 85 | let id: any = undefined; 86 | if (opts.timeout) { 87 | id = setTimeout(() => { 88 | req.abort(); 89 | if (opts.onTimeout) { 90 | opts.onTimeout(makeProgress(data, undefined)); 91 | } 92 | }, opts.timeout); 93 | } 94 | 95 | const promise = new Promise((resolve, reject) => { 96 | req.on('response', res => { 97 | if (opts.responseType !== 'binary') { 98 | res.setEncoding('utf-8'); 99 | } 100 | res.on('data', chunk => { 101 | if (typeof chunk === 'string') { 102 | data += chunk; 103 | } else { 104 | if (data === '') { 105 | data = []; 106 | } else { 107 | data.push(chunk); 108 | } 109 | } 110 | if (opts.progress) { 111 | opts.progress( 112 | makeProgress(data, res.headers['content-length']) 113 | ); 114 | } 115 | }); 116 | 117 | res.on('end', () => { 118 | if (id) clearTimeout(id); 119 | 120 | let response: any = data; 121 | if (Array.isArray(data)) { 122 | const buffer = Buffer.concat(data); 123 | response = buffer.buffer.slice( 124 | buffer.byteOffset, 125 | buffer.byteOffset + buffer.byteLength 126 | ); 127 | } 128 | 129 | if ( 130 | typeof response === 'string' && 131 | opts.responseType === 'parsed' 132 | ) { 133 | const mimeType = (res.headers[ 134 | 'content-type' 135 | ] as string)?.split(';')[0]; 136 | 137 | if (mimeType && serializers[mimeType]?.parse) { 138 | response = serializers[mimeType].parse!(response); 139 | } 140 | } 141 | 142 | resolve({ 143 | status: res.statusCode, 144 | data: response, 145 | }); 146 | }); 147 | }); 148 | req.on('error', err => { 149 | if (!req.aborted) { 150 | reject(err); 151 | } 152 | }); 153 | }); 154 | 155 | if (opts.send) { 156 | if ( 157 | typeof opts.send === 'string' || 158 | opts.send instanceof ArrayBuffer || 159 | opts.send instanceof ArrayBuffer || 160 | opts.send instanceof Int8Array || 161 | opts.send instanceof Uint8Array || 162 | opts.send instanceof Uint8ClampedArray || 163 | opts.send instanceof Int16Array || 164 | opts.send instanceof Uint16Array || 165 | opts.send instanceof Int32Array || 166 | opts.send instanceof Uint32Array || 167 | opts.send instanceof Float32Array || 168 | opts.send instanceof Float64Array || 169 | opts.send instanceof DataView || 170 | opts.send instanceof URLSearchParams 171 | ) { 172 | req.write(opts.send); 173 | } else if (serializers[opts.contentType!]?.convert) { 174 | req.write(serializers[opts.contentType!].convert!(opts.send)); 175 | } else { 176 | req.abort(); 177 | throw new Error( 178 | `Could not find a serializer for content type ${opts.contentType}` 179 | ); 180 | } 181 | } 182 | req.end(); 183 | 184 | return { 185 | promise, 186 | abort: () => req.abort(), 187 | }; 188 | }; 189 | } 190 | 191 | function makeProgress( 192 | data: string | Buffer[], 193 | contentLength: string | undefined 194 | ): Progress { 195 | const lengthComputable = contentLength !== undefined; 196 | 197 | const loaded = 198 | typeof data === 'string' 199 | ? Buffer.byteLength(data) 200 | : data.reduce((acc, curr) => acc + curr.length, 0); 201 | 202 | let total = 0; 203 | if (lengthComputable) { 204 | total = parseInt(contentLength!); 205 | } 206 | return { 207 | lengthComputable, 208 | loaded, 209 | total, 210 | }; 211 | } 212 | -------------------------------------------------------------------------------- /node/test/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makeSimpleTests, 3 | makeHeaderTests, 4 | makeQueryTests, 5 | makeResponseTypeTests, 6 | makeSendTests, 7 | makeSerializationTests, 8 | makeStreamTests, 9 | } from '../../test/index'; 10 | 11 | import { makeRequest } from '../src/index'; 12 | 13 | const request = makeRequest(); 14 | 15 | makeSimpleTests(request); 16 | makeHeaderTests(request); 17 | makeQueryTests(request); 18 | makeResponseTypeTests(request); 19 | makeSendTests(request); 20 | makeSerializationTests(makeRequest); 21 | makeStreamTests(request); 22 | -------------------------------------------------------------------------------- /node/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es6" 5 | }, 6 | "include": [], 7 | "files": ["./src/index.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../configs/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "module": "commonjs" 6 | }, 7 | "include": ["./src/**/*", "./test/**/*", "../test/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "MIT", 4 | "sideEffects": false, 5 | "scripts": { 6 | "format": "prettier --write .", 7 | "docs": "rm -r docs; typedoc --options configs/typedoc.json && touch docs/.nojekyll", 8 | "prepare": "husky install" 9 | }, 10 | "lint-staged": { 11 | "*.(js|ts|json|yaml)": "prettier --write" 12 | }, 13 | "commithelper": { 14 | "scopes": [ 15 | "browser", 16 | "node", 17 | "common" 18 | ], 19 | "scopeOverrides": { 20 | "chore": [ 21 | "tools", 22 | "refactor", 23 | "release", 24 | "test", 25 | "deps", 26 | "docs" 27 | ] 28 | } 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/jvanbruegge/minireq.git" 33 | }, 34 | "keywords": [], 35 | "author": "", 36 | "bugs": { 37 | "url": "https://github.com/jvanbruegge/minireq/issues" 38 | }, 39 | "homepage": "https://github.com/jvanbruegge/minireq#readme", 40 | "devDependencies": { 41 | "@minireq/common": "workspace:^2.0.0", 42 | "@release-it/conventional-changelog": "^2.0.1", 43 | "@types/cors": "^2.8.7", 44 | "@types/express": "^4.17.8", 45 | "@types/mocha": "^8.0.3", 46 | "@types/node": "^14.11.2", 47 | "commithelper": "^1.1.1", 48 | "cors": "^2.8.5", 49 | "cross-env": "^7.0.2", 50 | "express": "^4.17.1", 51 | "husky": "^5.2.0", 52 | "lint-staged": "^10.5.4", 53 | "prettier": "^2.1.2", 54 | "release-it": "^14.5.0", 55 | "rollup": "^2.28.2", 56 | "typedoc": "^0.20.34", 57 | "typescript": "^4.0.3" 58 | }, 59 | "prettier": "./configs/.prettierrc.json" 60 | } 61 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - browser 3 | - common 4 | - node 5 | -------------------------------------------------------------------------------- /test/headers.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { url } from './helpers'; 3 | 4 | import { RequestFn } from '@minireq/common'; 5 | 6 | export function makeHeaderTests(request: RequestFn) { 7 | describe('header tests', () => { 8 | it('should be able to send headers on a request', () => { 9 | const echo = 'This is my message'; 10 | 11 | const { promise } = request({ 12 | method: 'GET', 13 | url: url('/headers'), 14 | headers: { 15 | 'X-Auth-Token': 'superSecretToken', 16 | 'X-Echo': echo, 17 | }, 18 | }); 19 | 20 | return promise.then(({ status, data }) => { 21 | assert.strictEqual(status, 200); 22 | assert.strictEqual(data, echo); 23 | }); 24 | }); 25 | 26 | it('should be able to use authentication', () => { 27 | const { promise } = request({ 28 | method: 'GET', 29 | url: url('/secret'), 30 | auth: { 31 | user: 'admin', 32 | password: 'admin', 33 | }, 34 | }); 35 | 36 | return promise.then(({ status }) => { 37 | assert.strictEqual(status, 200); 38 | }); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | export const url = (path: string) => 'http://localhost:3000' + path; 2 | 3 | export const db = require('./support/data.json'); 4 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { db, url } from './helpers'; 3 | 4 | import { RequestFn } from '@minireq/common'; 5 | 6 | export { makeHeaderTests } from './headers'; 7 | export { makeQueryTests } from './query'; 8 | export { makeResponseTypeTests } from './responseType'; 9 | export { makeSendTests } from './send'; 10 | export { makeSerializationTests } from './serialize'; 11 | export { makeStreamTests } from './stream'; 12 | 13 | export function makeSimpleTests(request: RequestFn) { 14 | describe('simple tests', () => { 15 | it('should be able to make a simple GET request', () => { 16 | const { promise } = request({ 17 | method: 'GET', 18 | url: url('/users'), 19 | }); 20 | 21 | return promise.then(({ status, data }) => { 22 | assert.strictEqual(status, 200); 23 | assert.deepStrictEqual(data, db.users); 24 | }); 25 | }); 26 | 27 | it('should allow to make a simple POST request', () => { 28 | const user = { 29 | name: 'Ingo', 30 | age: 9, 31 | children: [], 32 | }; 33 | 34 | const { promise } = request({ 35 | method: 'POST', 36 | url: url('/users'), 37 | send: user, 38 | }); 39 | 40 | return promise.then(({ status, data }) => { 41 | assert.strictEqual(status, 201); 42 | assert.deepStrictEqual(data, { ...user, id: 4 }); 43 | }); 44 | }); 45 | 46 | it('should have a clean slate again', () => { 47 | const { promise } = request({ 48 | method: 'GET', 49 | url: url('/users'), 50 | }); 51 | 52 | return promise.then(({ status, data }) => { 53 | assert.strictEqual(status, 200); 54 | assert.deepStrictEqual(data, db.users); 55 | }); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/query.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { db, url } from './helpers'; 3 | 4 | import { RequestFn } from '@minireq/common'; 5 | 6 | export function makeQueryTests(request: RequestFn) { 7 | describe('queryString', () => { 8 | it('should accept a premade queryString with ?', () => { 9 | const { promise } = request({ 10 | method: 'GET', 11 | url: url('/users'), 12 | query: '?id=1', 13 | }); 14 | 15 | return promise.then(({ status, data }) => { 16 | assert.strictEqual(status, 200); 17 | assert.deepStrictEqual(data, [db.users[0]]); 18 | }); 19 | }); 20 | 21 | it('should accept a premade queryString without ?', () => { 22 | const { promise } = request({ 23 | method: 'GET', 24 | url: url('/users'), 25 | query: 'age=40', 26 | }); 27 | 28 | return promise.then(({ status, data }) => { 29 | assert.strictEqual(status, 200); 30 | assert.deepStrictEqual(data, [db.users[0]]); 31 | }); 32 | }); 33 | 34 | it('should throw when passed an empty object as queryString', () => { 35 | assert.throws(() => 36 | request({ 37 | method: 'GET', 38 | url: url('/users'), 39 | query: {}, 40 | }) 41 | ); 42 | }); 43 | 44 | it('should be able to create a queryString from an object', () => { 45 | const { promise } = request({ 46 | method: 'GET', 47 | url: url('/users'), 48 | query: { 49 | name: 'Agnes', 50 | age: 18, 51 | }, 52 | }); 53 | 54 | return promise.then(({ status, data }) => { 55 | assert.strictEqual(status, 200); 56 | assert.deepStrictEqual(data, [db.users[1]]); 57 | }); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/responseType.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { url } from './helpers'; 3 | 4 | import { RequestFn } from '@minireq/common'; 5 | 6 | export function makeResponseTypeTests(request: RequestFn) { 7 | describe('responseType tests', () => {}); 8 | } 9 | -------------------------------------------------------------------------------- /test/send.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { url } from './helpers'; 3 | 4 | import { RequestFn } from '@minireq/common'; 5 | 6 | export function makeSendTests(request: RequestFn) { 7 | describe('send tests', () => { 8 | it('should send strings without serializing first', () => { 9 | const str = JSON.stringify({ 10 | name: 'Hans', 11 | age: 99, 12 | children: [], 13 | }); 14 | 15 | const { promise } = request({ 16 | method: 'POST', 17 | url: url('/users'), 18 | send: str, 19 | }); 20 | 21 | return promise.then(({ status, data }) => { 22 | assert.strictEqual(status, 201); 23 | assert.deepStrictEqual(data, { ...JSON.parse(str), id: 4 }); 24 | }); 25 | }); 26 | 27 | it('should throw error if passed an object without serializer', () => { 28 | assert.throws(() => 29 | request({ 30 | method: 'POST', 31 | url: url('/users'), 32 | send: { 33 | foo: 'bar', 34 | }, 35 | contentType: 'application/xml', 36 | }) 37 | ); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/serialize.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { url, db } from './helpers'; 3 | import { defaultSerializers } from '@minireq/common'; 4 | 5 | export function makeSerializationTests(makeRequest: any) { 6 | describe('serialization', () => { 7 | it('should be able to handle a serialize-only serializer', () => { 8 | const serializer = { 9 | convert: JSON.stringify, 10 | }; 11 | 12 | const request = makeRequest({ 13 | 'application/json': serializer, 14 | }); 15 | 16 | const { promise } = request({ 17 | method: 'GET', 18 | url: url('/users'), 19 | }); 20 | 21 | const newUser = { 22 | name: 'Hans', 23 | age: 65, 24 | children: [], 25 | }; 26 | 27 | return promise 28 | .then(({ status, data }) => { 29 | assert.strictEqual(status, 200); 30 | assert.strictEqual(data, JSON.stringify(db.users)); 31 | 32 | return request({ 33 | method: 'POST', 34 | url: url('/users'), 35 | send: newUser, 36 | }).promise; 37 | }) 38 | .then(({ status, data }) => { 39 | assert.strictEqual(status, 201); 40 | assert.deepStrictEqual( 41 | data, 42 | JSON.stringify({ ...newUser, id: 4 }) 43 | ); 44 | }); 45 | }); 46 | 47 | it('should be able to handle a deserialize-only serializer', () => { 48 | const serializer = { 49 | parse: JSON.parse, 50 | }; 51 | 52 | const request = makeRequest({ 53 | 'application/json': serializer, 54 | }); 55 | 56 | const { promise } = request({ 57 | method: 'GET', 58 | url: url('/users'), 59 | }); 60 | 61 | const newUser = { 62 | name: 'Hans', 63 | age: 65, 64 | children: [], 65 | }; 66 | 67 | return promise.then(({ status, data }) => { 68 | assert.strictEqual(status, 200); 69 | assert.deepStrictEqual(data, db.users); 70 | 71 | assert.throws(() => 72 | request({ 73 | method: 'POST', 74 | url: url('/users'), 75 | send: newUser, 76 | }) 77 | ); 78 | }); 79 | }); 80 | 81 | it('should allow to add a serializer for a new mime type', () => { 82 | let usedNdJson = false; 83 | 84 | const serializers = { 85 | ...defaultSerializers, 86 | 'application/ndjson': { 87 | parse: (s: string) => { 88 | usedNdJson = true; 89 | return s.split('\n').map(x => JSON.parse(x)); 90 | }, 91 | }, 92 | }; 93 | 94 | const request = makeRequest(serializers, { 95 | accept: 'application/ndjson', 96 | }); 97 | 98 | const { promise } = request({ 99 | method: 'GET', 100 | url: url('/users'), 101 | }); 102 | 103 | return promise.then(({ status, data }) => { 104 | assert.strictEqual(status, 200); 105 | assert.deepStrictEqual(data, db.users); 106 | assert.strictEqual(usedNdJson, true); 107 | }); 108 | }); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /test/stream.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { url } from './helpers'; 3 | 4 | import { RequestFn } from '@minireq/common'; 5 | 6 | export function makeStreamTests(request: RequestFn) { 7 | describe('abort tests', () => { 8 | let response = ''; 9 | for (let i = 1; i <= 10; i++) { 10 | response += `Chunk ${i}\n`; 11 | } 12 | 13 | it('should be able to receive streaming responses', () => { 14 | const { promise } = request({ 15 | method: 'GET', 16 | url: url('/streaming'), 17 | }); 18 | 19 | return promise.then(({ status, data }) => { 20 | assert.strictEqual(status, 200); 21 | assert.strictEqual(data, response); 22 | }); 23 | }); 24 | 25 | it('should not send data after aborting request', done => { 26 | const { promise, abort } = request({ 27 | method: 'GET', 28 | url: url('/streaming'), 29 | }); 30 | 31 | setTimeout(abort, 25); 32 | 33 | promise.then(() => done('should not deliver data')); 34 | 35 | setTimeout(done, 110); 36 | }); 37 | 38 | it('should allow to monitor progress', () => { 39 | let progressed = false; 40 | 41 | const { promise } = request({ 42 | method: 'GET', 43 | url: url('/streaming'), 44 | progress: ev => { 45 | progressed = ev.loaded > 0; 46 | }, 47 | }); 48 | 49 | return promise.then(({ status, data }) => { 50 | assert.strictEqual(status, 200); 51 | assert.strictEqual(progressed, true); 52 | assert.strictEqual(data, response); 53 | }); 54 | }); 55 | 56 | // This fails in chrome, because for some reason no progress events are sent 57 | it('should provide progress when aborting later', done => { 58 | let progressed = false; 59 | 60 | if ( 61 | typeof navigator !== 'undefined' && 62 | navigator.userAgent.indexOf('Chrome') != -1 63 | ) { 64 | done(); 65 | return; 66 | } 67 | 68 | const { promise, abort } = request({ 69 | method: 'GET', 70 | url: url('/streaming'), 71 | progress: ev => { 72 | progressed = ev.loaded > 0; 73 | }, 74 | }); 75 | 76 | promise.then(() => done('should not deliver data')); 77 | 78 | setTimeout(abort, 60); 79 | 80 | setTimeout(() => { 81 | assert.strictEqual(progressed, true); 82 | done(); 83 | }, 120); 84 | }); 85 | 86 | it('should allow to specify a timeout on the request', done => { 87 | const { promise } = request({ 88 | method: 'GET', 89 | url: url('/streaming'), 90 | timeout: 60, 91 | onTimeout: () => done(), 92 | }); 93 | 94 | promise.then(() => done('should not deliver data')); 95 | }); 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /test/support/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 1, 5 | "name": "Peter", 6 | "age": 40, 7 | "children": ["Agnes", "Herbert"] 8 | }, 9 | { "id": 2, "name": "Agnes", "age": 18, "children": [] }, 10 | { "id": 3, "name": "Herbert", "age": 20, "children": [] } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/support/server.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as cors from 'cors'; 3 | 4 | const app = express(); 5 | const port = 3000; 6 | 7 | const data = require('./data.json'); 8 | 9 | app.use(express.json()); 10 | app.use(cors()); 11 | 12 | app.get('/', (_: any, res: any) => { 13 | res.sendStatus(200); 14 | }); 15 | 16 | app.get('/users', (req: any, res: any) => { 17 | let users = data.users; 18 | 19 | if (req.query.id) { 20 | users = users.filter((x: any) => x.id == req.query.id); 21 | } 22 | if (req.query.age) { 23 | users = users.filter((x: any) => x.age == req.query.age); 24 | } 25 | if (req.query.name) { 26 | users = users.filter((x: any) => x.name == req.query.name); 27 | } 28 | 29 | if ( 30 | req.header('Accept') === 'application/json' || 31 | req.header('Accept') === '*/*' 32 | ) { 33 | res.json(users); 34 | } else if (req.header('Accept') === 'application/ndjson') { 35 | res.set('Content-Type', 'application/ndjson'); 36 | res.status(200).send( 37 | users.map((x: any) => JSON.stringify(x)).join('\n') 38 | ); 39 | } else res.sendStatus(415); 40 | }); 41 | 42 | app.get('/document/html', (_: any, res: any) => { 43 | res.send( 44 | '
' 45 | ); 46 | }); 47 | 48 | app.get('/streaming', (_: any, res: any) => { 49 | let i = 1; 50 | const id = setInterval(() => { 51 | if (i > 10) { 52 | res.end(); 53 | clearInterval(id); 54 | } else { 55 | res.write(`Chunk ${i++}\n`); 56 | } 57 | }, 10); 58 | }); 59 | 60 | app.get('/headers', (req: any, res: any) => { 61 | if (req.header('X-Auth-Token') === 'superSecretToken') { 62 | res.set('Content-Type', 'text/plain'); 63 | res.status(200).send(req.header('X-Echo')); 64 | } else { 65 | res.sendStatus(401); 66 | } 67 | }); 68 | 69 | app.get('/secret', (req: any, res: any) => { 70 | if (req.header('Authorization') === 'Basic YWRtaW46YWRtaW4=') { 71 | res.sendStatus(200); 72 | } else res.sendStatus(401); 73 | }); 74 | 75 | app.post('/users', (req: any, res: any) => { 76 | res.status(201).json({ 77 | ...req.body, 78 | id: 4, 79 | }); 80 | }); 81 | 82 | app.listen(port, () => console.log(`Support server listening on port ${port}`)); 83 | --------------------------------------------------------------------------------