1 | const hash = require('object-hash');
|
2 | const fetchAPI = require('@adobe/helix-fetch');
|
3 | const { Lock } = require('./lock');
|
4 |
|
5 | const context = process.env.HELIX_FETCH_FORCE_HTTP1
|
6 | ? fetchAPI.context({
|
7 | httpProtocols: ['http1'],
|
8 | httpsProtocols: ['http1'],
|
9 | })
|
10 | : fetchAPI;
|
11 | const { fetch } = context;
|
12 |
|
13 | class FastlyError extends Error {
|
14 | constructor(response, text) {
|
15 | try {
|
16 | const body = JSON.parse(text);
|
17 | super(body.detail || body.msg || text);
|
18 | this.data = body;
|
19 | this.code = body.msg;
|
20 | } catch {
|
21 |
|
22 | super(text);
|
23 | }
|
24 |
|
25 | this.status = response.status;
|
26 | this.name = 'FastlyError';
|
27 | }
|
28 | }
|
29 |
|
30 | function memo(fn) {
|
31 | const keyfn = (...args) => hash(args[0]);
|
32 | const cache = new Map();
|
33 | return (...fnargs) => {
|
34 | const key = keyfn(...fnargs);
|
35 | if (cache.has(key)) {
|
36 | return cache.get(key);
|
37 | }
|
38 | const prom = fn(...fnargs);
|
39 | return Promise.resolve(prom).then((res) => {
|
40 | cache.set(key, res);
|
41 | return res;
|
42 | }).catch((e) => {
|
43 | throw e;
|
44 | });
|
45 | };
|
46 | }
|
47 |
|
48 | function repeatError(error) {
|
49 | if (error.name === 'FastlyError') {
|
50 | return false;
|
51 | }
|
52 | return error.name === 'Error';
|
53 | }
|
54 |
|
55 | function repeatResponse({ status }) {
|
56 | if (status === 429) {
|
57 | return true;
|
58 | }
|
59 | if (status > 500 && status < 505) {
|
60 | return true;
|
61 | }
|
62 | return false;
|
63 | }
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | function repeat(responseOrError) {
|
72 | if (responseOrError instanceof Error) {
|
73 | return repeatError(responseOrError);
|
74 | }
|
75 | return responseOrError.status ? repeatResponse(responseOrError) : false;
|
76 | }
|
77 |
|
78 | function create({ baseURL, timeout, headers }) {
|
79 | const responselog = [];
|
80 | |
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | function makereq(method, memoize = false, retries = 0) {
|
92 | const myreq = function req(path, body, config) {
|
93 | const myheaders = Object.assign(headers,
|
94 | config && config.headers ? config.headers : {});
|
95 |
|
96 | const options = {
|
97 | method,
|
98 | headers: myheaders,
|
99 | cache: 'no-store',
|
100 | };
|
101 |
|
102 | const uri = `${baseURL}${path}`;
|
103 |
|
104 | if (timeout) {
|
105 | options.signal = context.timeoutSignal(timeout);
|
106 | }
|
107 |
|
108 |
|
109 | const contentType = myheaders['content-type']
|
110 | || (method === 'patch' ? 'application/json' : 'application/x-www-form-urlencoded');
|
111 | if (contentType === 'application/x-www-form-urlencoded') {
|
112 |
|
113 | options.body = new URLSearchParams(Object.entries(body || {})).toString();
|
114 | } else {
|
115 |
|
116 | options.json = body;
|
117 | }
|
118 | options.headers['Content-Type'] = contentType;
|
119 | const start = Date.now();
|
120 |
|
121 | const reqfn = (attempt) => fetch(uri, options).then((response) => {
|
122 | const end = Date.now();
|
123 | responselog.push({ 'request-duration': end - start, headers: response.headers });
|
124 |
|
125 | if (!response.ok) {
|
126 | if (attempt < retries && repeat(response)) {
|
127 | return response.text().then(() => reqfn(attempt + 1));
|
128 | }
|
129 | return response.text().then((text) => { throw new FastlyError(response, text); });
|
130 | }
|
131 |
|
132 | return response.text().then((text) => {
|
133 | let data = text;
|
134 | try {
|
135 | data = JSON.parse(text);
|
136 | return {
|
137 | status: response.status,
|
138 | statusText: response.statusText,
|
139 | headers: response.headers,
|
140 | config: options,
|
141 | data,
|
142 | };
|
143 | } catch {
|
144 | return {
|
145 | status: response.status,
|
146 | statusText: response.statusText,
|
147 | headers: response.headers,
|
148 | config: options,
|
149 | data,
|
150 | };
|
151 | }
|
152 | });
|
153 | }).catch((reason) => {
|
154 | if (attempt < retries && repeat(reason)) {
|
155 | return reqfn(attempt + 1);
|
156 | }
|
157 | throw reason;
|
158 | });
|
159 | return reqfn(0);
|
160 | };
|
161 | return memoize ? memo(myreq) : myreq;
|
162 | }
|
163 |
|
164 | const lock = new Lock();
|
165 |
|
166 | |
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 | function protect(fn) {
|
174 | return async (...args) => {
|
175 | await lock.acquire();
|
176 | try {
|
177 | return fn(...args);
|
178 | } finally {
|
179 | lock.release();
|
180 | }
|
181 | };
|
182 | }
|
183 |
|
184 | const client = {
|
185 |
|
186 |
|
187 |
|
188 | post: makereq('post'),
|
189 | get: makereq('get', true, 2),
|
190 | // put: protect(makereq('put')),
|
191 | put: makereq('put'),
|
192 | // patch: protect(makereq('patch')),
|
193 | patch: makereq('patch'),
|
194 | // delete: protect(makereq('delete')),
|
195 | delete: makereq('delete'),
|
196 | monitor: {
|
197 | get count() {
|
198 | return responselog.length;
|
199 | },
|
200 |
|
201 | get remaining() {
|
202 | return responselog
|
203 | .map((o) => o.headers)
|
204 | .filter((hdrs) => typeof hdrs.get('fastly-ratelimit-remaining') !== 'undefined')
|
205 | .map((hdrs) => hdrs.get('fastly-ratelimit-remaining'))
|
206 | .map((remaining) => Number.parseInt(remaining, 10))
|
207 | .pop();
|
208 | },
|
209 |
|
210 | get edgedurations() {
|
211 | return responselog
|
212 | .map((o) => o.headers)
|
213 | .filter((hdrs) => typeof hdrs.get('x-timer') !== 'undefined')
|
214 | .map((hdrs) => hdrs.get('x-timer'))
|
215 | .map((timer) => timer.split(',').pop())
|
216 | .map((ve) => ve.substring(2))
|
217 | .map((ve) => Number.parseInt(ve, 10));
|
218 | },
|
219 |
|
220 | get durations() {
|
221 | return responselog
|
222 | .filter((hdrs) => typeof hdrs['request-duration'] !== 'undefined')
|
223 | .map((hdrs) => hdrs['request-duration'])
|
224 | .map((ve) => Number.parseInt(ve, 10));
|
225 | },
|
226 |
|
227 | get stats() {
|
228 | const retval = {
|
229 | count: this.count,
|
230 | remaining: this.remaining,
|
231 | minduration: Math.min(...this.durations),
|
232 | maxduration: Math.max(...this.durations),
|
233 | meanduration: Math.round(this.durations.reduce((a, b) => a + b, 0)
|
234 | / this.durations.length),
|
235 | minedgeduration: Math.min(...this.edgedurations),
|
236 | maxedgeduration: Math.max(...this.edgedurations),
|
237 | meanedgeduration: Math.round(this.edgedurations.reduce((a, b) => a + b, 0)
|
238 | / this.edgedurations.length),
|
239 | };
|
240 | return retval;
|
241 | },
|
242 | },
|
243 | };
|
244 |
|
245 | client.get.fresh = makereq('get');
|
246 | return client;
|
247 | }
|
248 |
|
249 | module.exports = { create, FastlyError };
|