UNPKG

5.45 kBPlain TextView Raw
1import { _isHttpErrorResponse, _jsonParseIfPossible, _since } from '@naturalcycles/js-lib'
2import got, { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Got, HTTPError } from 'got'
3import { URL } from 'url'
4import { inspectAny } from '..'
5import { dimGrey, grey, red, yellow } from '../colors'
6import { GetGotOptions, GotRequestContext } from './got.model'
7
8/**
9 * Returns instance of Got with "reasonable defaults":
10 *
11 * 1. Error handler hook that prints helpful errors.
12 * 2. Hooks that log start/end of request (optional, false by default).
13 * 3. Reasonable defaults(tm), e.g non-infinite Timeout
14 */
15export function getGot(opt: GetGotOptions = {}): Got {
16 return got.extend({
17 // Most-important is to set to anything non-empty (so, requests don't "hang" by default).
18 // Should be long enough to handle for slow responses from scaled cloud APIs in times of spikes
19 // Ideally should be LESS than default Request timeout in backend-lib (so, it has a chance to error
20 // before server times out with 503).
21 timeout: 90_000,
22 ...opt,
23 hooks: {
24 ...opt.hooks,
25 beforeError: [
26 ...(opt.hooks?.beforeError || []),
27 // User hooks go BEFORE
28 gotErrorHook(opt),
29 ],
30 beforeRequest: [
31 gotBeforeRequestHook(opt),
32 // User hooks go AFTER
33 ...(opt.hooks?.beforeRequest || []),
34 ],
35 afterResponse: [
36 ...(opt.hooks?.afterResponse || []),
37 // User hooks go BEFORE
38 gotAfterResponseHook(opt),
39 ],
40 },
41 })
42}
43
44/**
45 * Without this hook (default behaviour):
46 *
47 * HTTPError: Response code 422 (Unprocessable Entity)
48 * at EventEmitter.<anonymous> (.../node_modules/got/dist/source/as-promise.js:118:31)
49 * at processTicksAndRejections (internal/process/task_queues.js:97:5) {
50 * name: 'HTTPError'
51 *
52 *
53 * With this hook:
54 *
55 * HTTPError 422 GET http://a.com/err?q=1 in 8 ms
56 * {
57 * message: 'Reference already exists',
58 * documentation_url: 'https://developer.github.com/v3/git/refs/#create-a-reference'
59 * }
60 *
61 * Features:
62 * 1. Includes original method and URL (including e.g searchParams) in the error message.
63 * 2. Includes response.body in the error message (limited length).
64 * 3. Auto-detects and parses JSON response body (limited length).
65 * 4. Includes time spent (gotBeforeRequestHook must also be enabled).
66 * UPD: excluded now to allow automatic Sentry error grouping
67 *
68 * todo (try): Return error as familiar/convenient js-lib's HttpError (not got's HTTPError)
69 */
70function gotErrorHook(opt: GetGotOptions = {}): BeforeErrorHook {
71 const { maxResponseLength = 10000 } = opt
72
73 return err => {
74 if (err instanceof HTTPError) {
75 const { statusCode } = err.response
76 const { method, url, prefixUrl } = err.options
77 const shortUrl = getShortUrl(opt, url, prefixUrl)
78 // const { started } = context as GotRequestContext
79
80 // Auto-detect and prettify JSON response (if any)
81 let body = _jsonParseIfPossible(err.response.body)
82
83 // Detect HttpErrorResponse
84 if (_isHttpErrorResponse(body)) {
85 body = body.error
86 }
87
88 body = inspectAny(body, {
89 maxLen: maxResponseLength,
90 colors: false,
91 })
92
93 // timings are not part of err.message to allow automatic error grouping in Sentry
94 err.message = [[statusCode, method, shortUrl].filter(Boolean).join(' '), body]
95 .filter(Boolean)
96 .join('\n')
97 }
98
99 return err
100 }
101}
102
103function gotBeforeRequestHook(opt: GetGotOptions): BeforeRequestHook {
104 return options => {
105 options.context = {
106 ...options.context,
107 started: Date.now(),
108 } as GotRequestContext
109
110 if (opt.logStart) {
111 const shortUrl = getShortUrl(opt, options.url, options.prefixUrl)
112 console.log([dimGrey(' >>'), dimGrey(options.method), grey(shortUrl)].join(' '))
113 }
114 }
115}
116
117function gotAfterResponseHook(opt: GetGotOptions = {}): AfterResponseHook {
118 return resp => {
119 const success = resp.statusCode >= 200 && resp.statusCode < 400
120
121 if (opt.logFinished) {
122 const { started } = resp.request.options.context as GotRequestContext
123 const { url, prefixUrl, method } = resp.request.options
124 const shortUrl = getShortUrl(opt, url, prefixUrl)
125
126 console.log(
127 [
128 dimGrey(' <<'),
129 coloredHttpCode(resp.statusCode),
130 dimGrey(method),
131 grey(shortUrl),
132 started && dimGrey('in ' + _since(started)),
133 ]
134 .filter(Boolean)
135 .join(' '),
136 )
137 // console.log(`afterResp! ${resp.request.options.method} ${resp.url}`, { context: resp.request.options.context })
138 }
139
140 // Error responses are not logged, cause they're included in Error message already
141 if (opt.logResponse && success) {
142 console.log(inspectAny(_jsonParseIfPossible(resp.body), { maxLen: opt.maxResponseLength }))
143 }
144
145 return resp
146 }
147}
148
149function coloredHttpCode(statusCode: number): string {
150 if (statusCode < 400) return dimGrey(statusCode) // default
151 if (statusCode < 500) return yellow(statusCode)
152 return red(statusCode)
153}
154
155function getShortUrl(opt: GetGotOptions, url: URL, prefixUrl?: string): string {
156 let shortUrl = url.toString()
157
158 if (opt.logWithSearchParams === false) {
159 shortUrl = shortUrl.split('?')[0]!
160 }
161
162 if (opt.logWithPrefixUrl === false && prefixUrl && shortUrl.startsWith(prefixUrl)) {
163 shortUrl = shortUrl.slice(prefixUrl.length)
164 }
165
166 return shortUrl
167}