@@ -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,125 @@ 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 as per SEP
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+ // Check if error should not be retried
69+ if ( this . shouldNotRetry ( error as any ) ) {
70+ throw error ;
71+ }
72+
73+ // Apply backoff before retry
74+ await this . backoffFn ( attempt ) ;
6475 }
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- } ) ;
76+ }
77+
78+ throw lastError ! ;
79+ }
80+
81+ private shouldNotRetry ( error : any ) : boolean {
82+ // 400/403 errors for token fetch MUST NOT be retried
83+ // 404 errors for metadata fetch MUST NOT be retried
84+ const statusCode = error . statusCode || error . $metadata ?. httpStatusCode ;
85+ return statusCode === 400 || statusCode === 403 || statusCode === 404 ;
86+ }
87+
88+ async request ( path : string , options : { method ?: string ; headers ?: Record < string , string > } ) : Promise < string > {
89+ return this . retryWithBackoff ( async ( ) => {
90+ const { endpoint, ec2MetadataV1Disabled, httpOptions } = await this . config ;
91+ const handler = new NodeHttpHandler ( {
92+ requestTimeout : httpOptions ?. timeout ,
93+ throwOnRequestTimeout : true ,
94+ connectionTimeout : httpOptions ?. timeout ,
95+ } ) ;
96+ const endpointUrl = new URL ( endpoint ! ) ;
97+ const headers = options . headers || { } ;
98+ /**
99+ * If IMDSv1 is disabled and disableFetchToken is true, throw an error
100+ * Refer: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
101+ */
102+ if ( this . disableFetchToken && ec2MetadataV1Disabled ) {
103+ throw new Error ( "IMDSv1 is disabled and fetching token is disabled, cannot make the request." ) ;
83104 }
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 } ) ;
105+ /**
106+ * Make request with token if disableFetchToken is not true (IMDSv2).
107+ * Note that making the request call with token will result in an additional request to fetch the token.
108+ */
109+ if ( ! this . disableFetchToken ) {
110+ try {
111+ headers [ "x-aws-ec2-metadata-token" ] = await this . fetchMetadataTokenInternal ( ) ;
112+ } catch ( err ) {
113+ if ( ec2MetadataV1Disabled ) {
114+ // If IMDSv1 is disabled and token fetch fails (IMDSv2 fails), rethrow the error
115+ throw err ;
116+ }
117+ // If token fetch fails and IMDSv1 is not disabled, proceed without token (IMDSv1 fallback)
118+ }
119+ } // else, IMDSv1 fallback mode
120+ const request = new HttpRequest ( {
121+ method : options . method || "GET" , // Default to GET if no method is specified
122+ headers : headers ,
123+ hostname : endpointUrl . hostname ,
124+ path : endpointUrl . pathname + path ,
125+ protocol : endpointUrl . protocol ,
126+ port : endpointUrl . port ? parseInt ( endpointUrl . port ) : undefined ,
127+ } ) ;
128+ try {
129+ const { response } = await handler . handle ( request , { } as HttpHandlerOptions ) ;
130+ if ( response . statusCode === 200 && response . body ) {
131+ // handle response.body as stream
132+ return sdkStreamMixin ( response . body ) . transformToString ( ) ;
133+ } else {
134+ throw Object . assign ( new Error ( `Request failed with status code ${ response . statusCode } ` ) , {
135+ $metadata : { httpStatusCode : response . statusCode } ,
136+ } ) ;
137+ }
138+ } catch ( error ) {
139+ const wrappedError = new Error ( `Error making request to the metadata service: ${ error } ` ) ;
140+ const { $metadata } = error as any ;
141+ if ( $metadata ?. httpStatusCode !== undefined ) {
142+ Object . assign ( wrappedError , { $metadata } ) ;
143+ }
144+ throw wrappedError ;
89145 }
90- throw wrappedError ;
91- }
146+ } ) ;
92147 }
93148
94149 async fetchMetadataToken ( ) : Promise < string > {
150+ return this . retryWithBackoff ( ( ) => this . fetchMetadataTokenInternal ( ) ) ;
151+ }
152+
153+ private async fetchMetadataTokenInternal ( ) : Promise < string > {
95154 /**
96155 * Refer: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-v2-how-it-works.html
97156 */
@@ -124,11 +183,14 @@ export class MetadataService {
124183 return bodyString ;
125184 } else {
126185 throw Object . assign ( new Error ( `Failed to fetch metadata token with status code ${ response . statusCode } ` ) , {
127- statusCode : response . statusCode ,
186+ $metadata : { httpStatusCode : response . statusCode } ,
128187 } ) ;
129188 }
130189 } catch ( error ) {
131- if ( error . message === "TimeoutError" || [ 403 , 404 , 405 ] . includes ( error . statusCode ) ) {
190+ if (
191+ error . message === "TimeoutError" ||
192+ [ 403 , 404 , 405 ] . includes ( ( error as any ) . statusCode || ( error as any ) . $metadata ?. httpStatusCode )
193+ ) {
132194 this . disableFetchToken = true ; // as per JSv2 and fromInstanceMetadata implementations
133195 throw new Error ( `Error fetching metadata token: ${ error } . [disableFetchToken] is now set to true.` ) ;
134196 }
0 commit comments