UNPKG

6.49 kBJavaScriptView Raw
1// @flow
2
3import request from 'request'
4import _ from 'lodash'
5
6import { asyncRetry, sleep } from './asyncUtils'
7import { ExtendedError } from './errorUtils'
8
9const promisifiedRequest = async (options: Object) => await new Promise((resp, rej) => request(options, (err, data) => err ? rej(err) : resp(data)))
10
11const DEFAULT_REQ_HEADERS = {}
12const DEFAULT_REQ_REJECT_UNAUTHORIZED = true
13
14export type HttpReqResponse = {|
15 bodyObj: ?Object,
16 body: string,
17 statusCode: number,
18 triesExecuted: number,
19|}
20
21export type HttpReqOptions = {
22 url?: ?string,
23 method?: string,
24 timeout?: ?number,
25 headers?: { [id: string]: string },
26 body?: ?string | Object,
27 maxRetries?: number,
28 expoBackoffMs?: ?number,
29 asyncErrorHandler?: ?(Object) => Promise<void>,
30 rejectUnauthorized?: boolean,
31 trackingName?: ?string,
32 parseJsonResponse?: ?boolean,
33}
34
35export async function httpReq(url: string, options: HttpReqOptions = {}): Promise<HttpReqResponse> {
36 let body: ?string = null
37 try {
38 options = options || {}
39 options.url = url
40 options.method = options.method ? options.method.toUpperCase() : 'GET'
41 options.timeout = typeof options.timeout !== 'undefined' ? options.timeout : 5000
42 options.headers = options.headers || DEFAULT_REQ_HEADERS
43 options.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body)
44 options.rejectUnauthorized = typeof options.rejectUnauthorized !== 'undefined' ? options.rejectUnauthorized : DEFAULT_REQ_REJECT_UNAUTHORIZED
45
46 options.maxRetries = options.maxRetries || 0
47 options.expoBackoffMs = options.expoBackoffMs || 100
48
49 return await asyncRetry(async (triesExecuted) => {
50 body = null
51 const resp = await promisifiedRequest(options)
52 if (options.parseJsonResponse === true) {
53 try {
54 body = resp.body
55 resp.bodyObj = JSON.parse(resp.body)
56 resp.triesExecuted = triesExecuted
57 } catch (err) {
58 // NOTE: don't change this string without searching the code for it
59 throw new ExtendedError('Failed to parse HTTP response as JSON', { url: url, bodyStart: resp.body.substr(0, 1000) })
60 }
61 }
62 return resp
63 }, {
64 opName: `${options.url}`,
65 maxRetries: options.maxRetries,
66 expoBackoffMs: options.expoBackoffMs,
67 asyncErrorHandler: options.asyncErrorHandler,
68 })
69 } catch (err) {
70 throw new ExtendedError('HTTP request failed', { url, err, body })
71 }
72}
73
74
75type ResultHandlerTrigger = {|
76 onError: RegExp,
77|} | {|
78 onStatus: number | RegExp,
79|} | {||}
80type ResultHandlerAction = {|
81 retry: number,
82 nextBackoffMs?: ?number,
83|} | {|
84 throw: Error,
85|} | {|
86 return: HttpReqResponse,
87|}
88type ResultHandler = {|
89 ...ResultHandlerTrigger,
90 ...ResultHandlerAction,
91|}
92
93export type HttpReqOptions2 = {
94 url?: ?string,
95 method?: string,
96 timeout?: ?number,
97 headers?: { [id: string]: string },
98 body?: ?string | Object,
99 maxRetries?: number,
100 rejectUnauthorized?: boolean,
101 trackingName?: ?string,
102 parseJsonResponse?: ?boolean,
103 resultHandlers?: ?Array<ResultHandler>
104}
105
106export async function httpReq2(url: string, options: HttpReqOptions2 = {}): Promise<HttpReqResponse> {
107 let body: ?string = null
108 try {
109 options = options || {}
110 options.url = url
111 options.method = options.method ? options.method.toUpperCase() : 'GET'
112 options.timeout = typeof options.timeout !== 'undefined' ? options.timeout : 5000
113 options.headers = options.headers || DEFAULT_REQ_HEADERS
114 options.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body)
115 options.rejectUnauthorized = typeof options.rejectUnauthorized !== 'undefined' ? options.rejectUnauthorized : DEFAULT_REQ_REJECT_UNAUTHORIZED
116 options.resultHandlers = options.resultHandlers ? _.clone(options.resultHandlers) : options.resultHandlers
117
118 let triesLeft = 1
119 let triesExecuted = 0
120
121 let resp: ?HttpReqResponse = null
122 let error: ?Error = null
123
124 while (triesLeft > 0) {
125
126 try {
127 triesExecuted++
128 triesLeft--
129 resp = await promisifiedRequest(options)
130 } catch (err) {
131 error = err
132 }
133
134 if (options.resultHandlers) {
135 for (const resultHandler of options.resultHandlers) {
136 if (
137 (
138 resultHandler.onStatus == null &&
139 resultHandler.onError == null
140 ) || (
141 resp &&
142 typeof resultHandler.onStatus === 'number' &&
143 resultHandler.onStatus === resp.statusCode
144 ) || (
145 resp &&
146 resultHandler.onStatus &&
147 typeof resultHandler.onStatus.test === 'function' &&
148 // $FlowIgnore
149 resultHandler.onStatus.test(resp.statusCode.toString())
150 ) || (
151 error &&
152 resultHandler.onError &&
153 typeof resultHandler.onError.test === 'function' &&
154 resultHandler.onError.test(error.message)
155 )
156 ) {
157 if (resultHandler.retry != null) {
158 if (resultHandler.retry > 0) {
159 triesLeft++
160 resultHandler.retry--
161 // $FlowIgnore
162 resultHandler.nextBackoffMs = resultHandler.nextBackoffMs == null ? 100 : resultHandler.nextBackoffMs
163 await sleep(resultHandler.nextBackoffMs)
164 // $FlowIgnore
165 resultHandler.nextBackoffMs *= 1.5
166
167 error = undefined
168 resp = undefined
169 }
170 } else if (resultHandler.throw != null) {
171 throw resultHandler.throw
172 } else if (resultHandler.return != null) {
173 return resultHandler.return
174 } else {
175 throw new ExtendedError('Invalid result handler in httpReq', resultHandler)
176 }
177 }
178 }
179 }
180 }
181
182 if (resp) {
183 if (options.parseJsonResponse === true) {
184 try {
185 body = resp.body
186 resp.bodyObj = JSON.parse(resp.body)
187 resp.triesExecuted = triesExecuted
188 } catch (err) {
189 // NOTE: don't change this string without searching the code for it
190 throw new ExtendedError('Failed to parse HTTP response as JSON', { url: url, bodyStart: resp.body.substr(0, 1000) })
191 }
192 }
193 return resp
194 }
195
196 throw error
197
198 } catch (err) {
199 throw new ExtendedError('HTTP request failed', { url, err, body })
200 }
201}
\No newline at end of file