Skip to content

Commit

Permalink
WIP introduce more specific error codes
Browse files Browse the repository at this point in the history
  • Loading branch information
oleiade committed Jul 29, 2024
1 parent 2119312 commit 49551b9
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 87 deletions.
16 changes: 16 additions & 0 deletions examples/s3-test-errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AWSConfig, S3Client } from '../dist/s3.js'

const awsConfig = new AWSConfig({
region: __ENV.AWS_REGION,
accessKeyId: __ENV.AWS_ACCESS_KEY_ID,
secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY,
sessionToken: __ENV.AWS_SESSION_TOKEN,
})

export default async function () {
const s3 = new S3Client(awsConfig)

const bucket = 'test-js-'
const objects = await s3.listObjects(bucket)
console.log(objects)
}
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,
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
111 changes: 111 additions & 0 deletions src/internal/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,114 @@ export class AWSError extends Error {
}
}
}

export class NetworkError<N extends NetworkErrorName, K extends ErrorKind> extends Error {
code: K;
name: N;

constructor(name: N, code: K) {
super(ErrorMessages[code] || 'An unknown error occurred')
this.name = name
this.code = code
}
}

export class GeneralError extends NetworkError<'GeneralError', GeneralErrorKind> {
constructor(code: GeneralErrorKind) {
super('GeneralError', code)
}
}

export class DNSError extends NetworkError<'DNSError', DNSErrorKind> {
constructor(code: DNSErrorKind) {
super('DNSError', code)
}
}

export class TCPError extends NetworkError<'TCPError', TCPErrorKind> {
constructor(code: TCPErrorKind) {
super('TCPError', code)
}
}

export class TLSError extends NetworkError<'TLSError', TLSErrorKind> {
constructor(code: TLSErrorKind) {
super('TLSError', code)
}
}

export class HTTP2Error extends NetworkError<'HTTP2Error', HTTP2ErrorKind> {
constructor(code: HTTP2ErrorKind) {
super('HTTP2Error', code)
}
}


type NetworkErrorName = 'GeneralError' | 'DNSError' | 'TCPError' | 'TLSError' | 'HTTP2Error'

type ErrorKind =
GeneralErrorKind |
DNSErrorKind |
TCPErrorKind |
TLSErrorKind |
HTTP2ErrorKind

export enum GeneralErrorKind {
GenericError = 1000,
NonTCPNetworkError = 1010,
InvalidURL = 1020,
HTTPRequestTimeout = 1050,
}

export enum DNSErrorKind {
GenericDNSError = 1100,
NoIPFound = 1101,
BlacklistedIP = 1110,
BlacklistedHostname = 1111,
}

export enum TCPErrorKind {
GenericTCPError = 1200,
BrokenPipeOnWrite = 1201,
UnknownTCPError = 1202,
GeneralTCPDialError = 1210,
DialTimeoutError = 1211,
DialConnectionRefused = 1212,
DialUnknownError = 1213,
ResetByPeer = 1220,
}

export enum TLSErrorKind {
GeneralTLSError = 1300,
UnknownAuthority = 1310,
CertificateHostnameMismatch = 1311,
}

export enum HTTP2ErrorKind {
GenericHTTP2Error = 1600,
GeneralHTTP2GoAwayError = 1610,
}

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
25 changes: 11 additions & 14 deletions src/internal/kinesis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,23 +281,18 @@ export class KinesisClient extends AWSClient {
headers: signedRequest.headers,
})

this._handle_error(action, res)
this.handleError(res, action)
return res
}

/**
* If the response is an error, throw an error
*
* @param {string} operation - The name of the operation that was called.
* @param response - RefinedResponse<ResponseType | undefined>
* @returns The response is being returned.
*/
_handle_error(operation: string, 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 @@ -311,16 +306,18 @@ export class KinesisClient extends AWSClient {
}

// Otherwise throw a standard service error
throw new KinesisServiceError(errorMessage, error.__type as string, operation)
throw new KinesisServiceError(errorMessage, error.__type as string, operation || 'Unknown')
}

if (errorCode === 1500) {
throw new KinesisServiceError(
'An error occured on the server side',
'InternalServiceError',
operation
operation || 'Unknown'
)
}

return true
}
}

Expand Down
19 changes: 11 additions & 8 deletions src/internal/kms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class KMSClient extends AWSClient {
const res = await http.asyncRequest(this.method, signedRequest.url, signedRequest.body, {
headers: signedRequest.headers,
})
this._handle_error(KMSOperation.ListKeys, res)
this.handleError(res, KMSOperation.ListKeys)

const json: JSONArray = res.json('Keys') as JSONArray
return json.map((k) => KMSKey.fromJSON(k as JSONObject))
Expand Down Expand Up @@ -114,17 +114,18 @@ export class KMSClient extends AWSClient {
const res = await http.asyncRequest(this.method, signedRequest.url, signedRequest.body, {
headers: signedRequest.headers,
})
this._handle_error(KMSOperation.GenerateDataKey, res)
this.handleError(res, KMSOperation.GenerateDataKey)

return KMSDataKey.fromJSON(res.json() as JSONObject)
}

_handle_error(operation: KMSOperation, 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 @@ -138,16 +139,18 @@ export class KMSClient extends AWSClient {
}

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

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

return true
}
}

Expand Down
Loading

0 comments on commit 49551b9

Please sign in to comment.