@naturalcycles/nodejs-lib
Version:
Standard library for Node.js
139 lines (122 loc) • 4.13 kB
text/typescript
import {
HttpErrorResponse,
_isHttpErrorResponse,
_jsonParseIfPossible,
_since,
} from '@naturalcycles/js-lib'
import got, { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Got, HTTPError } from 'got'
import { inspectAny } from '..'
import { dimGrey, grey, red, yellow } from '../colors'
import {
GetGotOptions,
GotAfterResponseHookOptions,
GotBeforeRequestHookOptions,
GotErrorHookOptions,
GotRequestContext,
} from './got.model'
/**
* Returns instance of Got with "reasonable defaults":
*
* 1. Error handler hook that prints helpful errors.
* 2. Hooks that log start/end of request (optional, false by default).
*/
export function getGot(opt: GetGotOptions = {}): Got {
return got.extend({
hooks: {
beforeError: [gotErrorHook(opt)],
beforeRequest: [gotBeforeRequestHook(opt)],
afterResponse: [gotAfterResponseHook(opt)],
},
})
}
/**
* Without this hook (default behaviour):
*
* HTTPError: Response code 422 (Unprocessable Entity)
* at EventEmitter.<anonymous> (.../node_modules/got/dist/source/as-promise.js:118:31)
* at processTicksAndRejections (internal/process/task_queues.js:97:5) {
* name: 'HTTPError'
*
*
* With this hook:
*
* HTTPError 422 GET http://a.com/err?q=1 in 8 ms
* {
* message: 'Reference already exists',
* documentation_url: 'https://developer.github.com/v3/git/refs/#create-a-reference'
* }
*
* Features:
* 1. Includes original method and URL (including e.g searchParams) in the error message.
* 2. Includes response.body in the error message (limited length).
* 3. Auto-detects and parses JSON response body (limited length).
* 4. Includes time spent (gotBeforeRequestHook must also be enabled).
*/
export function gotErrorHook(opt: GotErrorHookOptions = {}): BeforeErrorHook {
const { maxResponseLength = 10000 } = opt
return err => {
if (err instanceof HTTPError) {
const { statusCode } = err.response
const { method, url, context } = err.options
const { started } = context as GotRequestContext
// Auto-detect and prettify JSON response (if any)
let body = _jsonParseIfPossible(err.response.body)
// Detect HttpErrorResponse
if (_isHttpErrorResponse(body)) {
body = (body as HttpErrorResponse).error
}
body = inspectAny(body, {
maxLen: maxResponseLength,
colors: false,
})
err.message = [
[statusCode, method, url, started && `in ${_since(started)}`].filter(Boolean).join(' '),
body,
]
.filter(Boolean)
.join('\n')
}
return err
}
}
export function gotBeforeRequestHook(opt: GotBeforeRequestHookOptions = {}): BeforeRequestHook {
return options => {
options.context = {
...options.context,
started: Date.now(),
} as GotRequestContext
if (opt.logStart) {
console.log([dimGrey(' >>'), dimGrey(options.method), grey(options.url)].join(' '))
}
}
}
export function gotAfterResponseHook(opt: GotAfterResponseHookOptions = {}): AfterResponseHook {
return resp => {
const success = resp.statusCode >= 200 && resp.statusCode < 400
if (opt.logFinished) {
const { started } = resp.request.options.context as GotRequestContext
console.log(
[
dimGrey(' <<'),
coloredHttpCode(resp.statusCode),
dimGrey(resp.request.options.method),
grey(resp.request.options.url),
started && dimGrey('in ' + _since(started)),
]
.filter(Boolean)
.join(' '),
)
// console.log(`afterResp! ${resp.request.options.method} ${resp.url}`, { context: resp.request.options.context })
}
// Error responses are not logged, cause they're included in Error message already
if (opt.logResponse && success) {
console.log(inspectAny(_jsonParseIfPossible(resp.body), { maxLen: opt.maxResponseLength }))
}
return resp
}
}
function coloredHttpCode(statusCode: number): string {
if (statusCode < 400) return dimGrey(statusCode) // default
if (statusCode < 500) return yellow(statusCode)
return red(statusCode)
}