Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce more specific error kinds #108

Merged
merged 5 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"@babel/preset-env": "^7.25.0",
"@babel/preset-typescript": "^7.24.7",
"@types/k6": "^0.52.0",
"@types/uuid": "^10.0.0",
"@types/webpack": "^5.28.5",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
Expand Down
73 changes: 73 additions & 0 deletions src/internal/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { RefinedResponse, ResponseType } from 'k6/http'

import { AWSConfig } from './config'
import { Endpoint } from './endpoint'
import { HTTPHeaders } from './http'
import {
// AWSError,
oleiade marked this conversation as resolved.
Show resolved Hide resolved
GeneralErrorKind,
DNSErrorKind,
TCPErrorKind,
TLSErrorKind,
HTTP2ErrorKind,
GeneralError,
DNSError,
TCPError,
TLSError,
HTTP2Error,
} from './error'

/**
* Class allowing to build requests targeting AWS APIs
Expand Down Expand Up @@ -60,6 +75,64 @@ export class AWSClient {
public set endpoint(endpoint: Endpoint) {
this._endpoint = endpoint
}

/**
* Handles the k6 http response potential errors produced when making a
* request to an AWS service.
*
* Importantly, this method only handles errors that emerge from the k6 http client itself, and
* won't handle AWS specific errors. To handle AWS specific errors, client classes are
* expected to implement their own error handling logic by overriding this method.
*
* @param response {RefinedResponse<ResponseType | undefined>} the response received by the k6 http client
* @param operation {string | undefined } the name of the operation that was attempted when the error occurred
* @param {boolean} returns true if an error was handled, false otherwise
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected handleError(response: RefinedResponse<ResponseType | undefined>, operation?: string): boolean {
const status: number = response.status
const errorCode: number = response.error_code
const errorMessage: string = response.error

// We consider codes 200-299 as success.
//
// We do not consider 3xx as success as some services such as S3 can use
// 301 to indicate a bucket not found
if (status >= 200 && status < 300 && errorMessage == '' && errorCode === 0) {
return false
}

switch (errorCode) {
case GeneralErrorKind.GenericError:
case GeneralErrorKind.NonTCPNetworkError:
case GeneralErrorKind.InvalidURL:
case GeneralErrorKind.HTTPRequestTimeout:
throw new GeneralError(errorCode);
case DNSErrorKind.GenericDNSError:
case DNSErrorKind.NoIPFound:
case DNSErrorKind.BlacklistedIP:
case DNSErrorKind.BlacklistedHostname:
throw new DNSError(errorCode);
case TCPErrorKind.GenericTCPError:
case TCPErrorKind.BrokenPipeOnWrite:
case TCPErrorKind.UnknownTCPError:
case TCPErrorKind.GeneralTCPDialError:
case TCPErrorKind.DialTimeoutError:
case TCPErrorKind.DialConnectionRefused:
case TCPErrorKind.DialUnknownError:
case TCPErrorKind.ResetByPeer:
throw new TCPError(errorCode);
case TLSErrorKind.GeneralTLSError:
case TLSErrorKind.UnknownAuthority:
case TLSErrorKind.CertificateHostnameMismatch:
throw new TLSError(errorCode);
case HTTP2ErrorKind.GenericHTTP2Error:
case HTTP2ErrorKind.GeneralHTTP2GoAwayError:
throw new HTTP2Error(errorCode);
}

return true
}
}

/**
Expand Down
211 changes: 211 additions & 0 deletions src/internal/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,19 @@ export class AWSError extends Error {
* Parse an AWSError from an XML document
*
* @param {string} xmlDocument - Serialized XML document to parse the error from
* @returns {AWSError} - The parsed AWSError object
*/
static parseXML(xmlDocument: string): AWSError {
const doc = parseHTML(xmlDocument)
return new AWSError(doc.find('Message').text(), doc.find('Code').text())
}

/**
* Parse an AWSError from a Response object
*
* @param {Response} response - The Response object to parse the error from
* @returns {AWSError} - The parsed AWSError object
*/
static parse(response: Response): AWSError {
if (response.headers['Content-Type'] === 'application/json') {
const error = (response.json() as JSONObject) || {}
Expand All @@ -53,3 +60,207 @@ export class AWSError extends Error {
}
}
}

/**
* Base class for network errors as produced by k6.
*
* Based on the network error handling in k6, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*
* @typeparam N - The name of the network error
* @typeparam K - The kind of the network error
*/
export class NetworkError<N extends NetworkErrorName, K extends ErrorKind> extends Error {
code: K
name: N

/**
* Create a NetworkError
*
* @param {N} name - The name of the network error
* @param {K} code - The kind of the network error
*/
constructor(name: N, code: K) {
super(ErrorMessages[code] || 'An unknown error occurred')
this.name = name
this.code = code
}
}

/**
* Represents a general network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export class GeneralError extends NetworkError<'GeneralError', GeneralErrorKind> {
/**
* Create a GeneralError
*
* @param {GeneralErrorKind} code - The kind of the general error
*/
constructor(code: GeneralErrorKind) {
super('GeneralError', code)
}
}

/**
* Represents a DNS-related network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export class DNSError extends NetworkError<'DNSError', DNSErrorKind> {
/**
* Create a DNSError
*
* @param {DNSErrorKind} code - The kind of the DNS error
*/
constructor(code: DNSErrorKind) {
super('DNSError', code)
}
}

/**
* Represents a TCP-related network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export class TCPError extends NetworkError<'TCPError', TCPErrorKind> {
/**
* Create a TCPError
*
* @param {TCPErrorKind} code - The kind of the TCP error
*/
constructor(code: TCPErrorKind) {
super('TCPError', code)
}
}

/**
* Represents a TLS-related network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export class TLSError extends NetworkError<'TLSError', TLSErrorKind> {
/**
* Create a TLSError
*
* @param {TLSErrorKind} code - The kind of the TLS error
*/
constructor(code: TLSErrorKind) {
super('TLSError', code)
}
}

/**
* Represents an HTTP/2-related network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export class HTTP2Error extends NetworkError<'HTTP2Error', HTTP2ErrorKind> {
/**
* Create an HTTP2Error
*
* @param {HTTP2ErrorKind} code - The kind of the HTTP/2 error
*/
constructor(code: HTTP2ErrorKind) {
super('HTTP2Error', code)
}
}

/**
* Represents the name of a network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
type NetworkErrorName = 'GeneralError' | 'DNSError' | 'TCPError' | 'TLSError' | 'HTTP2Error'

/**
* Represents the kind of a network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
type ErrorKind = GeneralErrorKind | DNSErrorKind | TCPErrorKind | TLSErrorKind | HTTP2ErrorKind

/**
* Represents the kind of a general network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export enum GeneralErrorKind {
GenericError = 1000,
NonTCPNetworkError = 1010,
InvalidURL = 1020,
HTTPRequestTimeout = 1050,
}

/**
* Represents the kind of a DNS-related network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export enum DNSErrorKind {
GenericDNSError = 1100,
NoIPFound = 1101,
BlacklistedIP = 1110,
BlacklistedHostname = 1111,
}

/**
* Represents the kind of a TCP-related network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export enum TCPErrorKind {
GenericTCPError = 1200,
BrokenPipeOnWrite = 1201,
UnknownTCPError = 1202,
GeneralTCPDialError = 1210,
DialTimeoutError = 1211,
DialConnectionRefused = 1212,
DialUnknownError = 1213,
ResetByPeer = 1220,
}

/**
* Represents the kind of a TLS-related network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export enum TLSErrorKind {
GeneralTLSError = 1300,
UnknownAuthority = 1310,
CertificateHostnameMismatch = 1311,
}

/**
* Represents the kind of an HTTP/2-related network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
export enum HTTP2ErrorKind {
GenericHTTP2Error = 1600,
GeneralHTTP2GoAwayError = 1610,
}

/**
* Error messages for each kind of network error, as described in:
* https://grafana.com/docs/k6/latest/javascript-api/error-codes/
*/
const ErrorMessages: { [key in ErrorKind]: string } = {
[GeneralErrorKind.GenericError]: 'A generic error that isn’t any of the ones listed below',
[GeneralErrorKind.NonTCPNetworkError]:
'A non-TCP network error - this is a placeholder and there is no error currently known to trigger it',
[GeneralErrorKind.InvalidURL]: 'An invalid URL was specified',
[GeneralErrorKind.HTTPRequestTimeout]: 'The HTTP request has timed out',
[DNSErrorKind.GenericDNSError]: 'A generic DNS error that isn’t any of the ones listed below',
[DNSErrorKind.NoIPFound]: 'No IP for the provided host was found',
[DNSErrorKind.BlacklistedIP]:
'Blacklisted IP was resolved or a connection to such was tried to be established',
[DNSErrorKind.BlacklistedHostname]: 'Blacklisted hostname using The Block Hostnames option',
[TCPErrorKind.GenericTCPError]: 'A generic TCP error that isn’t any of the ones listed below',
[TCPErrorKind.BrokenPipeOnWrite]:
'A “broken pipe” on write - the other side has likely closed the connection',
[TCPErrorKind.UnknownTCPError]:
'An unknown TCP error - We got an error that we don’t recognize but it is from the operating system and has errno set on it. The message in error includes the operation(write,read) and the errno, the OS, and the original message of the error',
[TCPErrorKind.GeneralTCPDialError]: 'General TCP dial error',
[TCPErrorKind.DialTimeoutError]: 'Dial timeout error - the timeout for the dial was reached',
[TCPErrorKind.DialConnectionRefused]:
'Dial connection refused - the connection was refused by the other party on dial',
[TCPErrorKind.DialUnknownError]: 'Dial unknown error',
[TCPErrorKind.ResetByPeer]:
'Reset by peer - the connection was reset by the other party, most likely a server',
[TLSErrorKind.GeneralTLSError]: 'General TLS error',
[TLSErrorKind.UnknownAuthority]: 'Unknown authority - the certificate issuer is unknown',
[TLSErrorKind.CertificateHostnameMismatch]: 'The certificate doesn’t match the hostname',
[HTTP2ErrorKind.GenericHTTP2Error]:
'A generic HTTP/2 error that isn’t any of the ones listed below',
[HTTP2ErrorKind.GeneralHTTP2GoAwayError]: 'A general HTTP/2 GoAway error',
}
21 changes: 11 additions & 10 deletions src/internal/event-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,17 @@ export class EventBridgeClient extends AWSClient {
const res = await http.asyncRequest(this.method, signedRequest.url, signedRequest.body, {
headers: signedRequest.headers,
})
this._handle_error(EventBridgeOperation.PutEvents, res)
this.handleError(res, EventBridgeOperation.PutEvents)
}

_handle_error(
operation: EventBridgeOperation,
response: RefinedResponse<ResponseType | undefined>
) {
const errorCode = response.error_code
if (errorCode === 0) {
return

protected handleError(response: RefinedResponse<ResponseType | undefined>, operation?: string): boolean {
const errored = super.handleError(response, operation);
if (!errored) {
return false
}

const errorCode = response.error_code
const error = response.json() as JSONObject
if (errorCode >= 1400 && errorCode <= 1499) {
// In the event of certain errors, the message is not set.
Expand All @@ -95,16 +94,18 @@ export class EventBridgeClient extends AWSClient {
}

// Otherwise throw a standard service error
throw new EventBridgeServiceError(errorMessage, error.__type as string, operation)
throw new EventBridgeServiceError(errorMessage, error.__type as string, operation as EventBridgeOperation)
}

if (errorCode === 1500) {
throw new EventBridgeServiceError(
'An error occured on the server side',
'InternalServiceError',
operation
operation as EventBridgeOperation
)
}

return true
}
}

Expand Down
Loading
Loading