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