@ouroboros/body 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -21,6 +21,8 @@ const METHODS = {
21
21
  read: 'GET',
22
22
  update: 'PUT'
23
23
  };
24
+ // 30 second timeout
25
+ const REQUEST_TIMEOUT = 30000;
24
26
  /**
25
27
  * Body
26
28
  *
@@ -80,6 +82,11 @@ class Body {
80
82
  * @param data The data associated with the request
81
83
  */
82
84
  request(action, service, noun, data) {
85
+ // If no error handler was assigned
86
+ if (!this.error) {
87
+ throw new Error('Must assign an error handler via body.on(\'error\', handler)' +
88
+ ' or body.onError(handler)');
89
+ }
83
90
  // Generate the URL for the request
84
91
  let url = `https://${this._domain}/${service}/${noun}`;
85
92
  // Init the response and json variables
@@ -98,24 +105,31 @@ class Body {
98
105
  }
99
106
  // Set this
100
107
  const _ = this;
101
- // Handles an error based on whether an error handler is set
102
- function handleError(message) {
103
- if (_.error) {
104
- _.error(message, { action, data, url });
105
- }
106
- else {
107
- throw new Error(message);
108
- }
109
- }
110
108
  // Create a new Promise and return it
111
109
  return new Promise((resolve, reject) => {
110
+ // Handle errors the same way every time
111
+ function handleError(reason) {
112
+ // Generate the error message
113
+ const err = `${METHODS[action]} ${url} ${reason}`;
114
+ // Call the error handler
115
+ _.error(err, { action, data, url });
116
+ // Return reject path
117
+ return reject({ code: 0, msg: err });
118
+ }
119
+ // Create an abort controller for timeouts
120
+ const controller = new AbortController();
112
121
  // Init the fetch init
113
122
  const fetchInit = {
114
- method: METHODS[action],
115
123
  headers: {
116
124
  'Content-Type': 'application/json; charset=utf-8'
117
- }
125
+ },
126
+ method: METHODS[action],
127
+ signal: controller.signal
118
128
  };
129
+ // Start a timer so we timeout if the request doesn't return
130
+ const timer = setTimeout(() => {
131
+ controller.abort();
132
+ }, REQUEST_TIMEOUT);
119
133
  // If we have a session token, add it as the Authorization header
120
134
  if (_.token) {
121
135
  fetchInit.headers.Authorization = _.token;
@@ -135,12 +149,46 @@ class Body {
135
149
  // If the Content-Type is missing or invalid
136
150
  const ct = response.headers.get('Content-Type');
137
151
  if (!ct || ct !== 'application/json; charset=utf-8') {
138
- const err = `${METHODS[action]} ${url} returned invalid Content-Type: ${ct}`;
139
- handleError(err);
140
- return reject({ code: 0, msg: err });
152
+ return handleError(`returned invalid Content-Type: "${ct}"`);
141
153
  }
142
154
  // Return the JSON
143
- return response.json();
155
+ return response.json().then(result => {
156
+ // If there's no result, return an error + reject
157
+ if (!result) {
158
+ return handleError('returned: empty JSON');
159
+ }
160
+ // Set res
161
+ res = result;
162
+ // Init the structure to pass to resolve
163
+ const oResult = {};
164
+ // If we got an error
165
+ if ('error' in result && result.error) {
166
+ // If we don't have an onErrorCode callback, or we
167
+ // do and calling it returns false
168
+ if (!_.errorCode ||
169
+ _.errorCode(result.error, { action, data, res, url }) === false) {
170
+ // If it wasn't handled, add it to the result
171
+ oResult.error = structuredClone(result.error);
172
+ }
173
+ }
174
+ // If we got a warning
175
+ if ('warning' in result && result.warning) {
176
+ // If we don't have an onWarning callback, or we do
177
+ // and calling it returns false
178
+ if (!_.warning ||
179
+ _.warning(result.warning, { action, data, res, url }) === false) {
180
+ // If it wasn't handled, add it to the result
181
+ oResult.warning = structuredClone(result.warning);
182
+ }
183
+ }
184
+ // If we got data
185
+ if ('data' in result) {
186
+ // Add it
187
+ oResult.data = structuredClone(result.data);
188
+ }
189
+ // Resolve
190
+ return resolve(oResult);
191
+ });
144
192
  }
145
193
  // If it's 401
146
194
  else if (response.status === 401) {
@@ -149,53 +197,15 @@ class Body {
149
197
  _.noSession();
150
198
  }
151
199
  // Reject the request
152
- return reject({ code: 0, msg: `${METHODS[action]} ${url} return 401 NOT AUTHORIZED` });
200
+ return handleError('returned: 401 NOT AUTHORIZED');
153
201
  }
154
202
  // Else, invalid status
155
- else {
156
- const err = `${METHODS[action]} ${url} returned invalid status: ${response.status}`;
157
- handleError(err);
158
- return reject({ code: 0, msg: err });
159
- }
160
- }).then(result => {
161
- // If there's no result, do nothing
162
- if (!result) {
163
- return;
164
- }
165
- // Set res
166
- res = result;
167
- // Init the structure to pass to resolve
168
- const oResult = {};
169
- // If we got an error
170
- if ('error' in result && result.error) {
171
- // If we don't have an onErrorCode callback, or we do and
172
- // calling it returns false
173
- if (!_.errorCode ||
174
- _.errorCode(result.error, { action, data, res, url }) === false) {
175
- // If it wasn't handled, add it to the result
176
- oResult.error = structuredClone(result.error);
177
- }
178
- }
179
- // If we got a warning and we have an onWarning callback
180
- if ('warning' in result && result.warning && _.warning) {
181
- // If we don't have an onWarning callback, or we do and
182
- // calling it returns false
183
- if (!_.warning ||
184
- _.warning(result.warning, { action, data, res, url }) === false) {
185
- // If it wasn't handled, add it to the result
186
- oResult.warning = structuredClone(result.warning);
187
- }
188
- }
189
- // If we got data
190
- if ('data' in result) {
191
- // Add it
192
- oResult.data = structuredClone(result.data);
193
- }
194
- // Resolve
195
- return resolve(oResult);
203
+ return handleError(`returned: invalid status ${response.status}`);
196
204
  }).catch(reason => {
197
- handleError(`${METHODS[action]} ${url} failed because of "${reason}"`);
205
+ return handleError(`failed with: "${String(reason)}"`);
198
206
  }).finally(() => {
207
+ // Clear the timeout timer
208
+ clearTimeout(timer);
199
209
  // If we have a requested callback
200
210
  if (this.requested) {
201
211
  this.requested({ action, data, res, url });
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouroboros/body",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Classes to communicate with Ouroboros Coding Inc. body services",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
package/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @ouroboros/body releases
2
2
 
3
+ ## 1.3.1
4
+ - Fixed issues with requests potentially not resolving promises on errors.
5
+ - Added 30 second timeout on requests so they don't hang forever.
6
+
3
7
  ## 1.3.0
4
8
  - Stopped splitting data and error into resolve and reject as this made it harder to identify and solve real communication problems. From now on all requests that are returned successfully, regardless of the result, are returned in full to resolve. This means all current `.then()` calls on requests must handle the full responseStruct result instead of just the `.data`. Similarly, any code currently running in the reject based on completed, but error filled, requests must be moved into the first argument of `.then()`.
5
9
 
package/src/index.ts CHANGED
@@ -63,6 +63,9 @@ const METHODS = {
63
63
  update: 'PUT'
64
64
  }
65
65
 
66
+ // 30 second timeout
67
+ const REQUEST_TIMEOUT = 30000;
68
+
66
69
  /**
67
70
  * Body
68
71
  *
@@ -140,6 +143,14 @@ class Body {
140
143
  data: any
141
144
  ): Promise<any> {
142
145
 
146
+ // If no error handler was assigned
147
+ if(!this.error) {
148
+ throw new Error(
149
+ 'Must assign an error handler via body.on(\'error\', handler)' +
150
+ ' or body.onError(handler)'
151
+ )
152
+ }
153
+
143
154
  // Generate the URL for the request
144
155
  let url = `https://${this._domain}/${service}/${noun}`;
145
156
 
@@ -162,26 +173,39 @@ class Body {
162
173
  // Set this
163
174
  const _ = this;
164
175
 
165
- // Handles an error based on whether an error handler is set
166
- function handleError(message: string): void {
167
- if(_.error) {
168
- _.error(message, { action, data, url });
169
- } else {
170
- throw new Error(message);
171
- }
172
- }
173
-
174
176
  // Create a new Promise and return it
175
177
  return new Promise((resolve: responseResolve, reject) => {
176
178
 
179
+ // Handle errors the same way every time
180
+ function handleError(reason: string): void {
181
+
182
+ // Generate the error message
183
+ const err: string = `${METHODS[action]} ${url} ${reason}`;
184
+
185
+ // Call the error handler
186
+ (_.error as onError)(err, { action, data, url });
187
+
188
+ // Return reject path
189
+ return reject({ code: 0, msg: err });
190
+ }
191
+
192
+ // Create an abort controller for timeouts
193
+ const controller = new AbortController()
194
+
177
195
  // Init the fetch init
178
196
  const fetchInit: any = {
179
- method: METHODS[action],
180
197
  headers: {
181
198
  'Content-Type': 'application/json; charset=utf-8'
182
- }
199
+ },
200
+ method: METHODS[action],
201
+ signal: controller.signal
183
202
  }
184
203
 
204
+ // Start a timer so we timeout if the request doesn't return
205
+ const timer = setTimeout(() => {
206
+ controller.abort();
207
+ }, REQUEST_TIMEOUT);
208
+
185
209
  // If we have a session token, add it as the Authorization header
186
210
  if(_.token) {
187
211
  fetchInit.headers.Authorization = _.token;
@@ -206,13 +230,67 @@ class Body {
206
230
  // If the Content-Type is missing or invalid
207
231
  const ct = response.headers.get('Content-Type');
208
232
  if(!ct || ct !== 'application/json; charset=utf-8') {
209
- const err = `${METHODS[action]} ${url} returned invalid Content-Type: ${ct}`
210
- handleError(err);
211
- return reject({ code: 0, msg: err });
233
+ return handleError(`returned invalid Content-Type: "${ct}"`);
212
234
  }
213
235
 
214
236
  // Return the JSON
215
- return response.json();
237
+ return response.json().then(result => {
238
+
239
+ // If there's no result, return an error + reject
240
+ if(!result) {
241
+ return handleError('returned: empty JSON');
242
+ }
243
+
244
+ // Set res
245
+ res = result;
246
+
247
+ // Init the structure to pass to resolve
248
+ const oResult: responseStruct = { };
249
+
250
+ // If we got an error
251
+ if('error' in result && result.error) {
252
+
253
+ // If we don't have an onErrorCode callback, or we
254
+ // do and calling it returns false
255
+ if(!_.errorCode ||
256
+ _.errorCode(
257
+ result.error as responseErrorStruct,
258
+ { action, data, res, url }
259
+ ) === false
260
+ ) {
261
+ // If it wasn't handled, add it to the result
262
+ oResult.error = structuredClone(result.error);
263
+ }
264
+ }
265
+
266
+ // If we got a warning
267
+ if('warning' in result && result.warning) {
268
+
269
+ // If we don't have an onWarning callback, or we do
270
+ // and calling it returns false
271
+ if(!_.warning ||
272
+ _.warning(
273
+ result.warning,
274
+ { action, data, res, url }
275
+ ) === false
276
+ ) {
277
+ // If it wasn't handled, add it to the result
278
+ oResult.warning = structuredClone(
279
+ result.warning
280
+ );
281
+ }
282
+ }
283
+
284
+ // If we got data
285
+ if('data' in result) {
286
+
287
+ // Add it
288
+ oResult.data = structuredClone(result.data);
289
+ }
290
+
291
+ // Resolve
292
+ return resolve(oResult);
293
+ });
216
294
  }
217
295
 
218
296
  // If it's 401
@@ -224,77 +302,19 @@ class Body {
224
302
  }
225
303
 
226
304
  // Reject the request
227
- return reject({ code: 0, msg: `${METHODS[action]} ${url} return 401 NOT AUTHORIZED` });
228
- }
229
- // Else, invalid status
230
- else {
231
- const err = `${METHODS[action]} ${url} returned invalid status: ${response.status}`;
232
- handleError(err);
233
- return reject({ code: 0, msg: err });
234
- }
235
-
236
- }).then(result => {
237
-
238
- // If there's no result, do nothing
239
- if(!result) {
240
- return;
305
+ return handleError('returned: 401 NOT AUTHORIZED');
241
306
  }
242
307
 
243
- // Set res
244
- res = result;
245
-
246
- // Init the structure to pass to resolve
247
- const oResult: responseStruct = { };
248
-
249
- // If we got an error
250
- if('error' in result && result.error) {
251
-
252
- // If we don't have an onErrorCode callback, or we do and
253
- // calling it returns false
254
- if(!_.errorCode ||
255
- _.errorCode(
256
- result.error as responseErrorStruct,
257
- { action, data, res, url }
258
- ) === false
259
- ) {
260
- // If it wasn't handled, add it to the result
261
- oResult.error = structuredClone(result.error);
262
- }
263
- }
264
-
265
- // If we got a warning and we have an onWarning callback
266
- if('warning' in result && result.warning && _.warning) {
267
-
268
- // If we don't have an onWarning callback, or we do and
269
- // calling it returns false
270
- if(!_.warning ||
271
- _.warning(
272
- result.warning,
273
- { action, data, res, url }
274
- ) === false
275
- ) {
276
- // If it wasn't handled, add it to the result
277
- oResult.warning = structuredClone(result.warning);
278
- }
279
- }
280
-
281
- // If we got data
282
- if('data' in result) {
283
-
284
- // Add it
285
- oResult.data = structuredClone(result.data);
286
- }
287
-
288
- // Resolve
289
- return resolve(oResult);
308
+ // Else, invalid status
309
+ return handleError(`returned: invalid status ${response.status}`);
290
310
 
291
311
  }).catch(reason => {
292
- handleError(
293
- `${METHODS[action]} ${url} failed because of "${reason}"`
294
- )
295
-
312
+ return handleError(`failed with: "${String(reason)}"`);
296
313
  }).finally(() => {
297
314
 
315
+ // Clear the timeout timer
316
+ clearTimeout(timer);
317
+
298
318
  // If we have a requested callback
299
319
  if(this.requested) {
300
320
  this.requested({ action, data, res, url });