@@ -13,6 +13,8 @@ import { MetadataServiceOptions } from "./MetadataServiceOptions";
1313export class MetadataService {
1414 private disableFetchToken : boolean ;
1515 private config : Promise < MetadataServiceOptions > ;
16+ private retries : number ;
17+ private backoffFn : ( numFailures : number ) => void ;
1618
1719 /**
1820 * Creates a new MetadataService object with a given set of options.
@@ -30,68 +32,123 @@ export class MetadataService {
3032 } ;
3133 } ) ( ) ;
3234 this . disableFetchToken = options ?. disableFetchToken || false ;
35+ this . retries = options ?. retries ?? 3 ;
36+ this . backoffFn = this . createBackoffFunction ( options ?. backoff ) ;
3337 }
3438
35- async request ( path : string , options : { method ?: string ; headers ?: Record < string , string > } ) : Promise < string > {
36- const { endpoint, ec2MetadataV1Disabled, httpOptions } = await this . config ;
37- const handler = new NodeHttpHandler ( {
38- requestTimeout : httpOptions ?. timeout ,
39- throwOnRequestTimeout : true ,
40- connectionTimeout : httpOptions ?. timeout ,
41- } ) ;
42- const endpointUrl = new URL ( endpoint ! ) ;
43- const headers = options . headers || { } ;
44- /**
45- * If IMDSv1 is disabled and disableFetchToken is true, throw an error
46- * Refer: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
47- */
48- if ( this . disableFetchToken && ec2MetadataV1Disabled ) {
49- throw new Error ( "IMDSv1 is disabled and fetching token is disabled, cannot make the request." ) ;
39+ private createBackoffFunction ( backoff ?: number | ( ( numFailures : number ) => void ) ) : ( numFailures : number ) => void {
40+ if ( typeof backoff === "function" ) {
41+ return backoff ;
5042 }
51- /**
52- * Make request with token if disableFetchToken is not true (IMDSv2).
53- * Note that making the request call with token will result in an additional request to fetch the token.
54- */
55- if ( ! this . disableFetchToken ) {
43+ if ( typeof backoff === "number" ) {
44+ return ( ) => this . sleep ( backoff * 1000 ) ;
45+ }
46+ // Default exponential backoff
47+ return ( numFailures : number ) => this . sleep ( Math . pow ( 1.2 , numFailures ) * 1000 ) ;
48+ }
49+
50+ private sleep ( ms : number ) : Promise < void > {
51+ return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
52+ }
53+
54+ private async retryWithBackoff < T > ( operation : ( ) => Promise < T > ) : Promise < T > {
55+ let lastError : Error ;
56+
57+ for ( let attempt = 0 ; attempt <= this . retries ; attempt ++ ) {
5658 try {
57- headers [ "x-aws-ec2-metadata-token" ] = await this . fetchMetadataToken ( ) ;
58- } catch ( err ) {
59- if ( ec2MetadataV1Disabled ) {
60- // If IMDSv1 is disabled and token fetch fails (IMDSv2 fails), rethrow the error
61- throw err ;
59+ return await operation ( ) ;
60+ } catch ( error ) {
61+ lastError = error as Error ;
62+
63+ // Don't retry on final attempt
64+ if ( attempt === this . retries ) {
65+ break ;
6266 }
63- // If token fetch fails and IMDSv1 is not disabled, proceed without token (IMDSv1 fallback)
67+
68+ if ( this . shouldNotRetry ( error as any ) ) {
69+ throw error ;
70+ }
71+
72+ await this . backoffFn ( attempt ) ;
6473 }
65- } // else, IMDSv1 fallback mode
66- const request = new HttpRequest ( {
67- method : options . method || "GET" , // Default to GET if no method is specified
68- headers : headers ,
69- hostname : endpointUrl . hostname ,
70- path : endpointUrl . pathname + path ,
71- protocol : endpointUrl . protocol ,
72- port : endpointUrl . port ? parseInt ( endpointUrl . port ) : undefined ,
73- } ) ;
74- try {
75- const { response } = await handler . handle ( request , { } as HttpHandlerOptions ) ;
76- if ( response . statusCode === 200 && response . body ) {
77- // handle response.body as stream
78- return sdkStreamMixin ( response . body ) . transformToString ( ) ;
79- } else {
80- throw Object . assign ( new Error ( `Request failed with status code ${ response . statusCode } ` ) , {
81- $metadata : { httpStatusCode : response . statusCode } ,
82- } ) ;
74+ }
75+
76+ throw lastError ! ;
77+ }
78+
79+ private shouldNotRetry ( error : any ) : boolean {
80+ // 400/403 errors for token fetch MUST NOT be retried
81+ // 404 errors for metadata fetch MUST NOT be retried
82+ const statusCode = error . statusCode || error . $metadata ?. httpStatusCode ;
83+ return statusCode === 400 || statusCode === 403 || statusCode === 404 ;
84+ }
85+
86+ async request ( path : string , options : { method ?: string ; headers ?: Record < string , string > } ) : Promise < string > {
87+ return this . retryWithBackoff ( async ( ) => {
88+ const { endpoint, ec2MetadataV1Disabled, httpOptions } = await this . config ;
89+ const handler = new NodeHttpHandler ( {
90+ requestTimeout : httpOptions ?. timeout ,
91+ throwOnRequestTimeout : true ,
92+ connectionTimeout : httpOptions ?. timeout ,
93+ } ) ;
94+ const endpointUrl = new URL ( endpoint ! ) ;
95+ const headers = options . headers || { } ;
96+ /**
97+ * If IMDSv1 is disabled and disableFetchToken is true, throw an error
98+ * Refer: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
99+ */
100+ if ( this . disableFetchToken && ec2MetadataV1Disabled ) {
101+ throw new Error ( "IMDSv1 is disabled and fetching token is disabled, cannot make the request." ) ;
83102 }
84- } catch ( error ) {
85- const wrappedError = new Error ( `Error making request to the metadata service: ${ error } ` ) ;
86- const { $metadata } = error as any ;
87- if ( $metadata ?. httpStatusCode !== undefined ) {
88- Object . assign ( wrappedError , { $metadata } ) ;
103+ /**
104+ * Make request with token if disableFetchToken is not true (IMDSv2).
105+ * Note that making the request call with token will result in an additional request to fetch the token.
106+ */
107+ if ( ! this . disableFetchToken ) {
108+ try {
109+ headers [ "x-aws-ec2-metadata-token" ] = await this . fetchMetadataTokenInternal ( ) ;
110+ } catch ( err ) {
111+ if ( ec2MetadataV1Disabled ) {
112+ // If IMDSv1 is disabled and token fetch fails (IMDSv2 fails), rethrow the error
113+ throw err ;
114+ }
115+ // If token fetch fails and IMDSv1 is not disabled, proceed without token (IMDSv1 fallback)
116+ }
117+ } // else, IMDSv1 fallback mode
118+ const request = new HttpRequest ( {
119+ method : options . method || "GET" , // Default to GET if no method is specified
120+ headers : headers ,
121+ hostname : endpointUrl . hostname ,
122+ path : endpointUrl . pathname + path ,
123+ protocol : endpointUrl . protocol ,
124+ port : endpointUrl . port ? parseInt ( endpointUrl . port ) : undefined ,
125+ } ) ;
126+ try {
127+ const { response } = await handler . handle ( request , { } as HttpHandlerOptions ) ;
128+ if ( response . statusCode === 200 && response . body ) {
129+ // handle response.body as stream
130+ return sdkStreamMixin ( response . body ) . transformToString ( ) ;
131+ } else {
132+ throw Object . assign ( new Error ( `Request failed with status code ${ response . statusCode } ` ) , {
133+ $metadata : { httpStatusCode : response . statusCode } ,
134+ } ) ;
135+ }
136+ } catch ( error ) {
137+ const wrappedError = new Error ( `Error making request to the metadata service: ${ error } ` ) ;
138+ const { $metadata } = error as any ;
139+ if ( $metadata ?. httpStatusCode !== undefined ) {
140+ Object . assign ( wrappedError , { $metadata } ) ;
141+ }
142+ throw wrappedError ;
89143 }
90- throw wrappedError ;
91- }
144+ } ) ;
92145 }
93146
94147 async fetchMetadataToken ( ) : Promise < string > {
148+ return this . retryWithBackoff ( ( ) => this . fetchMetadataTokenInternal ( ) ) ;
149+ }
150+
151+ private async fetchMetadataTokenInternal ( ) : Promise < string > {
95152 /**
96153 * Refer: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-v2-how-it-works.html
97154 */
@@ -124,11 +181,14 @@ export class MetadataService {
124181 return bodyString ;
125182 } else {
126183 throw Object . assign ( new Error ( `Failed to fetch metadata token with status code ${ response . statusCode } ` ) , {
127- statusCode : response . statusCode ,
184+ $metadata : { httpStatusCode : response . statusCode } ,
128185 } ) ;
129186 }
130187 } catch ( error ) {
131- if ( error . message === "TimeoutError" || [ 403 , 404 , 405 ] . includes ( error . statusCode ) ) {
188+ if (
189+ error . message === "TimeoutError" ||
190+ [ 403 , 404 , 405 ] . includes ( ( error as any ) . statusCode || ( error as any ) . $metadata ?. httpStatusCode )
191+ ) {
132192 this . disableFetchToken = true ; // as per JSv2 and fromInstanceMetadata implementations
133193 throw new Error ( `Error fetching metadata token: ${ error } . [disableFetchToken] is now set to true.` ) ;
134194 }
0 commit comments