├── .editorconfig ├── .gitignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── source └── index.ts ├── tests └── test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | node_modules 3 | package-lock.json 4 | dist 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '16' 4 | - '14' 5 | after_success: npm run coveralls 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Szymon Marczak 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http-timer 2 | > Timings for HTTP requests 3 | 4 | [![Build Status](https://travis-ci.org/szmarczak/http-timer.svg?branch=master)](https://travis-ci.org/szmarczak/http-timer) 5 | [![Coverage Status](https://coveralls.io/repos/github/szmarczak/http-timer/badge.svg?branch=master)](https://coveralls.io/github/szmarczak/http-timer?branch=master) 6 | [![install size](https://packagephobia.now.sh/badge?p=@szmarczak/http-timer)](https://packagephobia.now.sh/result?p=@szmarczak/http-timer) 7 | 8 | Inspired by the [`request` package](https://github.com/request/request). 9 | 10 | ## Installation 11 | 12 | NPM: 13 | 14 | > `npm install @szmarczak/http-timer` 15 | 16 | Yarn: 17 | 18 | > `yarn add @szmarczak/http-timer` 19 | 20 | ## Usage 21 | **Note:** 22 | > - The measured events resemble Node.js events, not the kernel ones. 23 | > - Sending a chunk greater than [`highWaterMark`](https://nodejs.org/api/stream.html#stream_new_stream_writable_options) will result in invalid `upload` and `response` timings. You can avoid this by splitting the payload into smaller chunks. 24 | 25 | ```js 26 | import https from 'https'; 27 | import timer from '@szmarczak/http-timer'; 28 | 29 | const request = https.get('https://httpbin.org/anything'); 30 | timer(request); 31 | 32 | request.once('response', response => { 33 | response.resume(); 34 | response.once('end', () => { 35 | console.log(response.timings); // You can use `request.timings` as well 36 | }); 37 | }); 38 | 39 | // { 40 | // start: 1572712180361, 41 | // socket: 1572712180362, 42 | // lookup: 1572712180415, 43 | // connect: 1572712180571, 44 | // upload: 1572712180884, 45 | // response: 1572712181037, 46 | // end: 1572712181039, 47 | // error: undefined, 48 | // abort: undefined, 49 | // phases: { 50 | // wait: 1, 51 | // dns: 53, 52 | // tcp: 156, 53 | // request: 313, 54 | // firstByte: 153, 55 | // download: 2, 56 | // total: 678 57 | // } 58 | // } 59 | ``` 60 | 61 | ## API 62 | 63 | ### timer(request) 64 | 65 | Returns: `Object` 66 | 67 | **Note**: The time is a `number` representing the milliseconds elapsed since the UNIX epoch. 68 | 69 | - `start` - Time when the request started. 70 | - `socket` - Time when a socket was assigned to the request. 71 | - `lookup` - Time when the DNS lookup finished. 72 | - `connect` - Time when the socket successfully connected. 73 | - `secureConnect` - Time when the socket securely connected. 74 | - `upload` - Time when the request finished uploading. 75 | - `response` - Time when the request fired `response` event. 76 | - `end` - Time when the response fired `end` event. 77 | - `error` - Time when the request fired `error` event. 78 | - `abort` - Time when the request fired `abort` event. 79 | - `phases` 80 | - `wait` - `timings.socket - timings.start` 81 | - `dns` - `timings.lookup - timings.socket` 82 | - `tcp` - `timings.connect - timings.lookup` 83 | - `tls` - `timings.secureConnect - timings.connect` 84 | - `request` - `timings.upload - (timings.secureConnect || timings.connect)` 85 | - `firstByte` - `timings.response - timings.upload` 86 | - `download` - `timings.end - timings.response` 87 | - `total` - `(timings.end || timings.error || timings.abort) - timings.start` 88 | 89 | If something has not been measured yet, it will be `undefined`. 90 | 91 | ## License 92 | 93 | MIT 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@szmarczak/http-timer", 3 | "version": "5.0.1", 4 | "description": "Timings for HTTP requests", 5 | "type": "module", 6 | "exports": "./dist/source/index.js", 7 | "engines": { 8 | "node": ">=14.16" 9 | }, 10 | "scripts": { 11 | "test": "xo && ava", 12 | "build": "del-cli dist && tsc", 13 | "prepare": "npm run build", 14 | "coveralls": "exit 0 && nyc report --reporter=text-lcov | coveralls" 15 | }, 16 | "files": [ 17 | "dist/source" 18 | ], 19 | "keywords": [ 20 | "http", 21 | "https", 22 | "http2", 23 | "timer", 24 | "timings", 25 | "performance", 26 | "measure" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/szmarczak/http-timer.git" 31 | }, 32 | "author": "Szymon Marczak", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/szmarczak/http-timer/issues" 36 | }, 37 | "homepage": "https://github.com/szmarczak/http-timer#readme", 38 | "dependencies": { 39 | "defer-to-connect": "^2.0.1" 40 | }, 41 | "devDependencies": { 42 | "@ava/typescript": "^2.0.0", 43 | "@sindresorhus/tsconfig": "^1.0.2", 44 | "@types/node": "^16.7.0", 45 | "ava": "^3.15.0", 46 | "coveralls": "^3.1.1", 47 | "del-cli": "^4.0.1", 48 | "http2-wrapper": "^2.1.4", 49 | "nyc": "^15.1.0", 50 | "p-event": "^4.2.0", 51 | "ts-node": "^10.2.1", 52 | "typescript": "^4.3.5", 53 | "xo": "^0.44.0" 54 | }, 55 | "types": "dist/source", 56 | "nyc": { 57 | "extension": [ 58 | ".ts" 59 | ], 60 | "exclude": [ 61 | "**/tests/**" 62 | ] 63 | }, 64 | "xo": { 65 | "rules": { 66 | "@typescript-eslint/no-non-null-assertion": "off", 67 | "@typescript-eslint/no-unsafe-assignment": "off", 68 | "@typescript-eslint/no-unsafe-member-access": "off", 69 | "@typescript-eslint/no-unsafe-call": "off", 70 | "unicorn/prefer-node-protocol": "off" 71 | } 72 | }, 73 | "ava": { 74 | "files": [ 75 | "tests/*" 76 | ], 77 | "timeout": "1m", 78 | "nonSemVerExperiments": { 79 | "nextGenConfig": true, 80 | "configurableModuleFormat": true 81 | }, 82 | "extensions": { 83 | "ts": "module" 84 | }, 85 | "nodeArguments": [ 86 | "--loader=ts-node/esm" 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | import {errorMonitor} from 'events'; 2 | import {types} from 'util'; 3 | import type {EventEmitter} from 'events'; 4 | import type {Socket} from 'net'; 5 | import type {ClientRequest, IncomingMessage} from 'http'; 6 | import deferToConnect from 'defer-to-connect'; 7 | 8 | export interface Timings { 9 | start: number; 10 | socket?: number; 11 | lookup?: number; 12 | connect?: number; 13 | secureConnect?: number; 14 | upload?: number; 15 | response?: number; 16 | end?: number; 17 | error?: number; 18 | abort?: number; 19 | phases: { 20 | wait?: number; 21 | dns?: number; 22 | tcp?: number; 23 | tls?: number; 24 | request?: number; 25 | firstByte?: number; 26 | download?: number; 27 | total?: number; 28 | }; 29 | } 30 | 31 | export interface ClientRequestWithTimings extends ClientRequest { 32 | timings?: Timings; 33 | } 34 | 35 | export interface IncomingMessageWithTimings extends IncomingMessage { 36 | timings?: Timings; 37 | } 38 | 39 | const timer = (request: ClientRequestWithTimings): Timings => { 40 | if (request.timings) { 41 | return request.timings; 42 | } 43 | 44 | const timings: Timings = { 45 | start: Date.now(), 46 | socket: undefined, 47 | lookup: undefined, 48 | connect: undefined, 49 | secureConnect: undefined, 50 | upload: undefined, 51 | response: undefined, 52 | end: undefined, 53 | error: undefined, 54 | abort: undefined, 55 | phases: { 56 | wait: undefined, 57 | dns: undefined, 58 | tcp: undefined, 59 | tls: undefined, 60 | request: undefined, 61 | firstByte: undefined, 62 | download: undefined, 63 | total: undefined, 64 | }, 65 | }; 66 | 67 | request.timings = timings; 68 | 69 | const handleError = (origin: EventEmitter): void => { 70 | origin.once(errorMonitor, () => { 71 | timings.error = Date.now(); 72 | timings.phases.total = timings.error - timings.start; 73 | }); 74 | }; 75 | 76 | handleError(request); 77 | 78 | const onAbort = (): void => { 79 | timings.abort = Date.now(); 80 | timings.phases.total = timings.abort - timings.start; 81 | }; 82 | 83 | request.prependOnceListener('abort', onAbort); 84 | 85 | const onSocket = (socket: Socket): void => { 86 | timings.socket = Date.now(); 87 | timings.phases.wait = timings.socket - timings.start; 88 | 89 | if (types.isProxy(socket)) { 90 | return; 91 | } 92 | 93 | const lookupListener = (): void => { 94 | timings.lookup = Date.now(); 95 | timings.phases.dns = timings.lookup - timings.socket!; 96 | }; 97 | 98 | socket.prependOnceListener('lookup', lookupListener); 99 | 100 | deferToConnect(socket, { 101 | connect: () => { 102 | timings.connect = Date.now(); 103 | 104 | if (timings.lookup === undefined) { 105 | socket.removeListener('lookup', lookupListener); 106 | timings.lookup = timings.connect; 107 | timings.phases.dns = timings.lookup - timings.socket!; 108 | } 109 | 110 | timings.phases.tcp = timings.connect - timings.lookup; 111 | }, 112 | secureConnect: () => { 113 | timings.secureConnect = Date.now(); 114 | timings.phases.tls = timings.secureConnect - timings.connect!; 115 | }, 116 | }); 117 | }; 118 | 119 | if (request.socket) { 120 | onSocket(request.socket); 121 | } else { 122 | request.prependOnceListener('socket', onSocket); 123 | } 124 | 125 | const onUpload = (): void => { 126 | timings.upload = Date.now(); 127 | timings.phases.request = timings.upload - (timings.secureConnect ?? timings.connect!); 128 | }; 129 | 130 | if (request.writableFinished) { 131 | onUpload(); 132 | } else { 133 | request.prependOnceListener('finish', onUpload); 134 | } 135 | 136 | request.prependOnceListener('response', (response: IncomingMessageWithTimings): void => { 137 | timings.response = Date.now(); 138 | timings.phases.firstByte = timings.response - timings.upload!; 139 | 140 | response.timings = timings; 141 | 142 | handleError(response); 143 | 144 | response.prependOnceListener('end', () => { 145 | request.off('abort', onAbort); 146 | response.off('aborted', onAbort); 147 | 148 | if (timings.phases.total) { 149 | // Aborted or errored 150 | return; 151 | } 152 | 153 | timings.end = Date.now(); 154 | timings.phases.download = timings.end - timings.response!; 155 | timings.phases.total = timings.end - timings.start; 156 | }); 157 | 158 | response.prependOnceListener('aborted', onAbort); 159 | }); 160 | 161 | return timings; 162 | }; 163 | 164 | export default timer; 165 | -------------------------------------------------------------------------------- /tests/test.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {URL} from 'node:url'; 3 | import {EventEmitter} from 'node:events'; 4 | import http, {ClientRequest, IncomingMessage} from 'node:http'; 5 | import https from 'node:https'; 6 | import {AddressInfo} from 'node:net'; 7 | import util from 'node:util'; 8 | import pEvent from 'p-event'; 9 | import test from 'ava'; 10 | import http2 from 'http2-wrapper'; 11 | import timer, {Timings, ClientRequestWithTimings, IncomingMessageWithTimings} from '../source/index.js'; 12 | 13 | let server: http.Server & { 14 | url?: string; 15 | listenAsync?: any; 16 | closeAsync?: any; 17 | }; 18 | 19 | test.before('setup', async () => { 20 | server = http.createServer((_request, response) => { 21 | response.write('o'); 22 | 23 | setTimeout(() => { 24 | response.end('k'); 25 | }, 200); 26 | }); 27 | 28 | server.listenAsync = util.promisify(server.listen.bind(server)); 29 | server.closeAsync = util.promisify(server.close.bind(server)); 30 | 31 | await server.listenAsync(); 32 | server.url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; 33 | }); 34 | 35 | test.after('cleanup', async () => { 36 | await server.closeAsync(); 37 | }); 38 | 39 | const nodejsMajorVersion = Number(process.versions.node.split('.')[0]); 40 | 41 | const error = 'Simple error'; 42 | 43 | const makeRequest = (url = 'https://httpbin.org/anything', options: http.RequestOptions = {}): {request: ClientRequest; timings: Timings} => { 44 | const {protocol} = new URL(url); 45 | const fn = protocol === 'http:' ? http : https; 46 | 47 | const request = fn.get(url, {agent: false, ...options}); 48 | const timings = timer(request); 49 | 50 | return {request, timings}; 51 | }; 52 | 53 | test('by default everything is set to undefined', t => { 54 | const {timings} = makeRequest(); 55 | 56 | t.is(typeof timings, 'object'); 57 | t.is(typeof timings.start, 'number'); 58 | t.is(timings.socket, undefined); 59 | t.is(timings.lookup, undefined); 60 | t.is(timings.connect, undefined); 61 | t.is(timings.secureConnect, undefined); 62 | t.is(timings.response, undefined); 63 | t.is(timings.end, undefined); 64 | t.is(timings.error, undefined); 65 | t.is(timings.abort, undefined); 66 | 67 | t.deepEqual(timings.phases, { 68 | wait: undefined, 69 | dns: undefined, 70 | tcp: undefined, 71 | tls: undefined, 72 | request: undefined, 73 | firstByte: undefined, 74 | download: undefined, 75 | total: undefined, 76 | }); 77 | }); 78 | 79 | test('timings (socket reuse)', async t => { 80 | const agent = new http.Agent({keepAlive: true}); 81 | 82 | { 83 | const {request} = makeRequest(server.url, {agent}); 84 | const response: IncomingMessage = await pEvent(request, 'response'); 85 | response.resume(); 86 | await pEvent(response, 'end'); 87 | } 88 | 89 | { 90 | const {request} = makeRequest(server.url, {agent}); 91 | await pEvent(request, 'socket'); 92 | const timings = timer(request); 93 | 94 | const response: IncomingMessage = await pEvent(request, 'response'); 95 | response.resume(); 96 | await pEvent(response, 'end'); 97 | 98 | t.true(timings.phases.wait! <= 1); 99 | t.true(timings.phases.dns! <= 1); 100 | t.true(timings.phases.tcp! <= 1); 101 | t.true(timings.phases.request! <= 1); 102 | } 103 | 104 | agent.destroy(); 105 | 106 | t.pass(); 107 | }); 108 | 109 | test('timings', async t => { 110 | const {request, timings} = makeRequest(); 111 | const response: IncomingMessage = await pEvent(request, 'response'); 112 | response.resume(); 113 | await pEvent(response, 'end'); 114 | 115 | t.is(typeof timings, 'object'); 116 | t.is(typeof timings.start, 'number'); 117 | t.is(typeof timings.socket, 'number'); 118 | t.is(typeof timings.lookup, 'number'); 119 | t.is(typeof timings.connect, 'number'); 120 | t.is(typeof timings.secureConnect, 'number'); 121 | t.is(typeof timings.upload, 'number'); 122 | t.is(typeof timings.response, 'number'); 123 | t.is(typeof timings.end, 'number'); 124 | }); 125 | 126 | test('phases', async t => { 127 | const {request, timings} = makeRequest(); 128 | const response: IncomingMessage = await pEvent(request, 'response'); 129 | response.resume(); 130 | await pEvent(response, 'end'); 131 | 132 | t.is(typeof timings.phases, 'object'); 133 | t.is(typeof timings.phases.wait, 'number'); 134 | t.is(typeof timings.phases.dns, 'number'); 135 | t.is(typeof timings.phases.tcp, 'number'); 136 | t.is(typeof timings.phases.tls, 'number'); 137 | t.is(typeof timings.phases.firstByte, 'number'); 138 | t.is(typeof timings.phases.download, 'number'); 139 | t.is(typeof timings.phases.total, 'number'); 140 | 141 | t.is(timings.phases.wait, timings.socket! - timings.start); 142 | t.is(timings.phases.dns, timings.lookup! - timings.socket!); 143 | t.is(timings.phases.tcp, timings.connect! - timings.lookup!); 144 | t.is(timings.phases.tls, timings.secureConnect! - timings.connect!); 145 | t.is(timings.phases.request, timings.upload! - timings.secureConnect!); 146 | t.is(timings.phases.firstByte, timings.response! - timings.upload!); 147 | t.is(timings.phases.download, timings.end! - timings.response!); 148 | t.is(timings.phases.total, timings.end! - timings.start); 149 | }); 150 | 151 | test('no memory leak (`lookup` event)', async t => { 152 | const {request} = makeRequest(); 153 | 154 | await pEvent(request, 'finish'); 155 | 156 | t.is(request.socket!.listenerCount('lookup'), 0); 157 | }); 158 | 159 | test('sets `total` on request error', async t => { 160 | const request = http.get(server.url!, { 161 | timeout: 1, 162 | }); 163 | request.on('timeout', () => { 164 | request.abort(); 165 | }); 166 | 167 | const timings = timer(request); 168 | 169 | const error: Error = await pEvent(request, 'error'); 170 | t.is(error.message, 'socket hang up'); 171 | 172 | t.is(typeof timings.error, 'number'); 173 | t.is(timings.phases.total, timings.error! - timings.start); 174 | }); 175 | 176 | test('sets `total` on response error', async t => { 177 | const request = http.get(server.url!, (response: IncomingMessage) => { 178 | setImmediate(() => { 179 | response.emit('error', new Error(error)); 180 | }); 181 | }); 182 | const timings = timer(request); 183 | 184 | const response: IncomingMessage = await pEvent(request, 'response'); 185 | const error_: Error = await pEvent(response, 'error'); 186 | 187 | t.is(error_.message, error); 188 | t.is(typeof timings.error, 'number'); 189 | t.is(timings.phases.total, timings.error! - timings.start); 190 | }); 191 | 192 | test.cb('sets `total` on abort', t => { 193 | const request = http.get(server.url!); 194 | request.abort(); 195 | 196 | const timings = timer(request); 197 | 198 | process.nextTick(() => { 199 | t.is(typeof timings.abort, 'number'); 200 | t.is(timings.phases.total, timings.abort! - timings.start); 201 | t.falsy((request as any).res); 202 | 203 | t.end(); 204 | }); 205 | }); 206 | 207 | test.cb('sets `total` on abort - after `response` event', t => { 208 | const request = http.get(server.url!); 209 | const timings = timer(request); 210 | 211 | request.once('response', response => { 212 | request.abort(); 213 | 214 | if (nodejsMajorVersion >= 13) { 215 | process.nextTick(() => { 216 | t.is(typeof timings.abort, 'number'); 217 | t.is(timings.phases.total, timings.abort! - timings.start); 218 | t.truthy((request as any).res); 219 | 220 | t.end(); 221 | }); 222 | } else { 223 | response.once('end', () => { 224 | t.is(typeof timings.abort, 'number'); 225 | t.is(timings.phases.total, timings.end! - timings.start); 226 | t.truthy((request as any).res); 227 | 228 | t.end(); 229 | }); 230 | } 231 | }); 232 | }); 233 | 234 | test('doesn\'t throw when someone used `.prependOnceListener()`', t => { 235 | const emitter = new EventEmitter(); 236 | timer(emitter as ClientRequest); 237 | // eslint-disable-next-line @typescript-eslint/no-empty-function 238 | emitter.prependOnceListener('error', () => {}); 239 | 240 | t.notThrows(() => emitter.emit('error', new Error(error))); 241 | }); 242 | 243 | test('sensible timings', async t => { 244 | const {timings, request} = makeRequest('https://google.com'); 245 | const now = Date.now(); 246 | 247 | const response: IncomingMessage = await pEvent(request, 'response'); 248 | response.resume(); 249 | await pEvent(response, 'end'); 250 | 251 | t.true(timings.socket! >= now); 252 | t.true(timings.lookup! >= now); 253 | t.true(timings.connect! >= now); 254 | t.true(timings.secureConnect! >= now); 255 | t.true(timings.response! >= now); 256 | t.true(timings.end! >= now); 257 | t.is(timings.error, undefined); 258 | t.is(timings.abort, undefined); 259 | t.true(timings.phases.wait! < 1000); 260 | t.true(timings.phases.dns! < 1000); 261 | t.true(timings.phases.tcp! < 1000); 262 | t.true(timings.phases.tls! < 1000); 263 | t.true(timings.phases.request! < 1000); 264 | t.true(timings.phases.firstByte! < 1000); 265 | t.true(timings.phases.download! < 1000); 266 | t.true(timings.phases.total! < 1000); 267 | }); 268 | 269 | test('prepends once listeners', async t => { 270 | const request = https.get('https://google.com'); 271 | 272 | const promise = new Promise(resolve => { 273 | request.once('response', () => { 274 | t.true(typeof timings.response === 'number'); 275 | 276 | resolve(); 277 | }); 278 | 279 | const timings = timer(request); 280 | }); 281 | 282 | await promise; 283 | request.abort(); 284 | }); 285 | 286 | test('`tls` phase for https requests', async t => { 287 | const {request, timings} = makeRequest('https://google.com'); 288 | 289 | const response: IncomingMessage = await pEvent(request, 'response'); 290 | response.resume(); 291 | await pEvent(response, 'end'); 292 | 293 | t.is(typeof timings.secureConnect, 'number'); 294 | t.is(typeof timings.phases.tls, 'number'); 295 | }); 296 | 297 | test('no `tls` phase for http requests', async t => { 298 | const {request, timings} = makeRequest(server.url); 299 | 300 | const response: IncomingMessage = await pEvent(request, 'response'); 301 | response.resume(); 302 | await pEvent(response, 'end'); 303 | 304 | t.is(timings.secureConnect, undefined); 305 | t.is(timings.phases.tls, undefined); 306 | }); 307 | 308 | test('timings are accessible via `request.timings`', t => { 309 | const {request, timings} = makeRequest('https://google.com'); 310 | request.abort(); 311 | 312 | const typedRequest = request as ClientRequestWithTimings; 313 | 314 | t.is(typedRequest.timings, timings); 315 | }); 316 | 317 | test('timings are accessible via `response.timings`', async t => { 318 | const {request, timings} = makeRequest('https://google.com'); 319 | 320 | const response: IncomingMessage = await pEvent(request, 'response'); 321 | const typedResponse = response as IncomingMessageWithTimings; 322 | 323 | t.is(typedResponse.timings, timings); 324 | 325 | response.resume(); 326 | await pEvent(response, 'end'); 327 | }); 328 | 329 | test('can extend `http.IncomingMessage`', t => { 330 | interface Response extends IncomingMessage { 331 | timings: boolean; 332 | } 333 | 334 | 0 as unknown as Response; 335 | 336 | t.pass(); 337 | }); 338 | 339 | test('singleton', t => { 340 | const {request, timings} = makeRequest('https://google.com'); 341 | request.abort(); 342 | 343 | const secondTimings = timer(request); 344 | 345 | const typedRequest = request as ClientRequestWithTimings; 346 | 347 | t.is(typedRequest.timings, timings); 348 | t.is(timings, secondTimings); 349 | }); 350 | 351 | test('sets `abort` on response.destroy()', async t => { 352 | const {request, timings} = makeRequest(server.url); 353 | const response = await pEvent(request, 'response'); 354 | response.destroy(); 355 | 356 | await pEvent(request, 'close'); 357 | t.is(typeof timings.abort, 'number'); 358 | }); 359 | 360 | test.cb('works on already writableFinished request', t => { 361 | const request = http.get(server.url!); 362 | request.end(() => { 363 | const timings = timer(request); 364 | 365 | t.is(typeof timings.upload, 'number'); 366 | t.is(typeof timings.phases.request, 'number'); 367 | 368 | t.end(); 369 | }); 370 | }); 371 | 372 | const version = process.versions.node.split('.').map(v => Number(v)) as [number, number, number]; 373 | if (version[0] >= 16) { 374 | test('skips proxy-wrapped sockets', async t => { 375 | const request = await http2.auto('https://httpbin.org/anything'); 376 | const timings = timer(request); 377 | 378 | request.end(); 379 | 380 | const response: IncomingMessage = await pEvent(request, 'response'); 381 | response.resume(); 382 | 383 | t.is(request.socket!.listenerCount('lookup'), 0); 384 | 385 | await pEvent(response, 'end'); 386 | 387 | t.is(typeof timings, 'object'); 388 | t.is(typeof timings.start, 'number'); 389 | t.is(typeof timings.socket, 'number'); 390 | t.is(timings.lookup, undefined); 391 | t.is(timings.connect, undefined); 392 | t.is(timings.secureConnect, undefined); 393 | t.is(typeof timings.response, 'number'); 394 | t.is(typeof timings.end, 'number'); 395 | t.is(timings.error, undefined); 396 | t.is(timings.abort, undefined); 397 | }); 398 | } 399 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2020", // Node.js 14 6 | "lib": [ 7 | "es2020" 8 | ] 9 | }, 10 | "include": [ 11 | "source", 12 | "tests" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------