@@ -104,6 +104,26 @@ export function formatCheckResult (checkResult: any) {
104
104
] )
105
105
}
106
106
}
107
+ } if ( checkResult . checkType === 'TCP' ) {
108
+ if ( checkResult . checkRunData ?. requestError ) {
109
+ result . push ( [
110
+ formatSectionTitle ( 'Request Error' ) ,
111
+ checkResult . checkRunData . requestError ,
112
+ ] )
113
+ } else {
114
+ if ( checkResult . checkRunData ?. response ?. error ) {
115
+ result . push ( [
116
+ formatSectionTitle ( 'Connection Error' ) ,
117
+ formatConnectionError ( checkResult . checkRunData ?. response ?. error ) ,
118
+ ] )
119
+ }
120
+ if ( checkResult . checkRunData ?. assertions ?. length ) {
121
+ result . push ( [
122
+ formatSectionTitle ( 'Assertions' ) ,
123
+ formatAssertions ( checkResult . checkRunData . assertions ) ,
124
+ ] )
125
+ }
126
+ }
107
127
}
108
128
if ( checkResult . logs ?. length ) {
109
129
result . push ( [
@@ -132,6 +152,7 @@ const assertionSources: any = {
132
152
HEADERS : 'headers' ,
133
153
TEXT_BODY : 'text body' ,
134
154
RESPONSE_TIME : 'response time' ,
155
+ RESPONSE_DATA : 'response data' ,
135
156
}
136
157
137
158
const assertionComparisons : any = {
@@ -216,6 +237,146 @@ function formatHttpResponse (response: any) {
216
237
] . filter ( Boolean ) . join ( '\n' )
217
238
}
218
239
240
+ // IPv4 lookup for a non-existing hostname:
241
+ //
242
+ // {
243
+ // "code": "ENOTFOUND",
244
+ // "syscall": "queryA",
245
+ // "hostname": "does-not-exist.checklyhq.com"
246
+ // }
247
+ //
248
+ // IPv6 lookup for a non-existing hostname:
249
+ //
250
+ // {
251
+ // "code": "ENOTFOUND",
252
+ // "syscall": "queryAaaa",
253
+ // "hostname": "does-not-exist.checklyhq.com"
254
+ // }
255
+ interface DNSLookupFailureError {
256
+ code : 'ENOTFOUND'
257
+ syscall : string
258
+ hostname : string
259
+ }
260
+
261
+ function isDNSLookupFailureError ( error : any ) : error is DNSLookupFailureError {
262
+ return error . code === 'ENOTFOUND' &&
263
+ typeof error . syscall === 'string' &&
264
+ typeof error . hostname === 'string'
265
+ }
266
+
267
+ // Connection attempt to a port that isn't open:
268
+ //
269
+ // {
270
+ // "errno": -111,
271
+ // "code": "ECONNREFUSED",
272
+ // "syscall": "connect",
273
+ // "address": "127.0.0.1",
274
+ // "port": 22
275
+ // }
276
+ //
277
+ interface ConnectionRefusedError {
278
+ code : 'ECONNREFUSED'
279
+ errno ?: number
280
+ syscall : string
281
+ address : string
282
+ port : number
283
+ }
284
+
285
+ function isConnectionRefusedError ( error : any ) : error is ConnectionRefusedError {
286
+ return error . code === 'ECONNREFUSED' &&
287
+ typeof error . syscall === 'string' &&
288
+ typeof error . address === 'string' &&
289
+ typeof error . port === 'number' &&
290
+ typeof ( error . errno ?? 0 ) === 'number'
291
+ }
292
+
293
+ // Connection kept open after data exchange and it timed out:
294
+ //
295
+ // {
296
+ // "code": "SOCKET_TIMEOUT",
297
+ // "address": "api.checklyhq.com",
298
+ // "port": 9999
299
+ // }
300
+ interface SocketTimeoutError {
301
+ code : 'SOCKET_TIMEOUT'
302
+ address : string
303
+ port : number
304
+ }
305
+
306
+ function isSocketTimeoutError ( error : any ) : error is SocketTimeoutError {
307
+ return error . code === 'SOCKET_TIMEOUT' &&
308
+ typeof error . address === 'string' &&
309
+ typeof error . port === 'number'
310
+ }
311
+
312
+ // Invalid IP address (e.g. IPv4-only hostname when IPFamily is IPv6)
313
+ //
314
+ // {
315
+ // "code": "ERR_INVALID_IP_ADDRESS",
316
+ // }
317
+ interface InvalidIPAddressError {
318
+ code : 'ERR_INVALID_IP_ADDRESS'
319
+ }
320
+
321
+ function isInvalidIPAddressError ( error : any ) : error is InvalidIPAddressError {
322
+ return error . code === 'ERR_INVALID_IP_ADDRESS'
323
+ }
324
+
325
+ function formatConnectionError ( error : any ) {
326
+ if ( isDNSLookupFailureError ( error ) ) {
327
+ const message = [
328
+ logSymbols . error ,
329
+ `DNS lookup for "${ error . hostname } " failed` ,
330
+ `(syscall: ${ error . syscall } )` ,
331
+ ] . join ( ' ' )
332
+ return chalk . red ( message )
333
+ }
334
+
335
+ if ( isConnectionRefusedError ( error ) ) {
336
+ const message = [
337
+ logSymbols . error ,
338
+ `Connection to "${ error . address } :${ error . port } " was refused` ,
339
+ `(syscall: ${ error . syscall } , errno: ${ error . errno ?? '<None>' } )` ,
340
+ ] . join ( ' ' )
341
+ return chalk . red ( message )
342
+ }
343
+
344
+ if ( isSocketTimeoutError ( error ) ) {
345
+ const message = [
346
+ logSymbols . error ,
347
+ `Connection to "${ error . address } :${ error . port } " timed out (perhaps connection was never closed)` ,
348
+ ] . join ( ' ' )
349
+ return chalk . red ( message )
350
+ }
351
+
352
+ if ( isInvalidIPAddressError ( error ) ) {
353
+ const message = [
354
+ logSymbols . error ,
355
+ 'Invalid IP address (perhaps hostname and IP family do not match)' ,
356
+ ] . join ( ' ' )
357
+ return chalk . red ( message )
358
+ }
359
+
360
+ // Some other error we don't have detection for.
361
+ if ( error . code !== undefined ) {
362
+ const { code, ...extra } = error
363
+ const detailsString = JSON . stringify ( extra )
364
+ const message = [
365
+ logSymbols . error ,
366
+ `${ code } (details: ${ detailsString } )` ,
367
+ ] . join ( ' ' )
368
+ return chalk . red ( message )
369
+ }
370
+
371
+ // If we don't even have a code, give up and output the whole thing.
372
+ const detailsString = JSON . stringify ( error )
373
+ const message = [
374
+ logSymbols . error ,
375
+ `Error (details: ${ detailsString } )` ,
376
+ ] . join ( ' ' )
377
+ return chalk . red ( message )
378
+ }
379
+
219
380
function formatLogs ( logs : Array < { level : string , msg : string , time : number } > ) {
220
381
return logs . flatMap ( ( { level, msg, time } ) => {
221
382
const timestamp = DateTime . fromMillis ( time ) . toLocaleString ( DateTime . TIME_24_WITH_SECONDS )
0 commit comments