Skip to content

Commit 3e0fd77

Browse files
authored
Create EC2MetadataClientException for IMDS 4XX errors (#5947)
* Create EC2MetadataClientException for IMDS 400 errors * Creating EC2MetadataClientException for IMDS 4XX errors * Added Changelog * Additional changes for EC2MetadataClientException handling * Fixing checkstyle issues * Additional changes for EC2MetadataClientException - added clear description and some code refactoring * Additional EC2MetadataClientException improvements
1 parent ffc9ccc commit 3e0fd77

File tree

7 files changed

+210
-16
lines changed

7 files changed

+210
-16
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "EC2 Metadata Client",
4+
"contributor": "",
5+
"description": "Added new Ec2MetadataClientException extending SdkClientException for IMDS unsuccessful responses that captures HTTP status codes, headers, and raw response content for improved error handling. See [#5786](https://github.com/aws/aws-sdk-java-v2/issues/5786)"
6+
}

core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataAsyncClient.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import software.amazon.awssdk.annotations.Immutable;
2222
import software.amazon.awssdk.annotations.SdkPublicApi;
2323
import software.amazon.awssdk.annotations.ThreadSafe;
24+
import software.amazon.awssdk.core.exception.RetryableException;
2425
import software.amazon.awssdk.core.exception.SdkClientException;
2526
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
2627
import software.amazon.awssdk.imds.internal.DefaultEc2MetadataAsyncClient;
@@ -68,6 +69,11 @@ public interface Ec2MetadataAsyncClient extends SdkAutoCloseable {
6869
*
6970
* @param path Input path
7071
* @return A CompletableFuture that completes when the MetadataResponse is made available.
72+
* @throws Ec2MetadataClientException if the request returns a 4XX error response. The exception includes
73+
* the HTTP status code, headers, and error response body
74+
* @throws RetryableException if the request returns a 5XX error response and should be retried
75+
* @throws SdkClientException if the maximum number of retries is reached, if there's an IO error during
76+
* the request, or if the response is empty when success is expected
7177
*/
7278
CompletableFuture<Ec2MetadataResponse> get(String path);
7379

core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClient.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import software.amazon.awssdk.annotations.Immutable;
1919
import software.amazon.awssdk.annotations.SdkPublicApi;
2020
import software.amazon.awssdk.annotations.ThreadSafe;
21+
import software.amazon.awssdk.core.exception.RetryableException;
2122
import software.amazon.awssdk.core.exception.SdkClientException;
2223
import software.amazon.awssdk.http.SdkHttpClient;
2324
import software.amazon.awssdk.imds.internal.DefaultEc2MetadataClient;
@@ -66,6 +67,11 @@ public interface Ec2MetadataClient extends SdkAutoCloseable {
6667
*
6768
* @param path Input path
6869
* @return Instance metadata value as part of MetadataResponse Object
70+
* @throws Ec2MetadataClientException if the request returns a 4XX error response. The exception includes
71+
* the HTTP status code, headers, and error response body
72+
* @throws RetryableException if the request returns a 5XX error response and should be retried
73+
* @throws SdkClientException if the maximum number of retries is reached, if there's an IO error during
74+
* the request, or if the response is empty when success is expected
6975
*/
7076
Ec2MetadataResponse get(String path);
7177

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.imds;
17+
18+
19+
import software.amazon.awssdk.annotations.SdkPublicApi;
20+
import software.amazon.awssdk.core.SdkBytes;
21+
import software.amazon.awssdk.core.exception.SdkClientException;
22+
import software.amazon.awssdk.http.SdkHttpResponse;
23+
24+
/**
25+
* Extends {@link SdkClientException} for EC2 Instance Metadata Service (IMDS) non-successful
26+
* responses (4XX codes). Provides detailed error information through:
27+
* <p>
28+
* - HTTP status code via {@link #statusCode()} for specific error handling
29+
* - Raw response content via {@link #rawResponse()} containing the error response body
30+
* - HTTP headers via {@link #sdkHttpResponse()} providing additional error context from the response
31+
*/
32+
33+
@SdkPublicApi
34+
public final class Ec2MetadataClientException extends SdkClientException {
35+
36+
private final int statusCode;
37+
private final SdkBytes rawResponse;
38+
private final SdkHttpResponse sdkHttpResponse;
39+
40+
private Ec2MetadataClientException(BuilderImpl builder) {
41+
super(builder);
42+
this.statusCode = builder.statusCode;
43+
this.rawResponse = builder.rawResponse;
44+
this.sdkHttpResponse = builder.sdkHttpResponse;
45+
}
46+
47+
/**
48+
* @return The HTTP status code returned by the IMDS service.
49+
*/
50+
public int statusCode() {
51+
return statusCode;
52+
}
53+
54+
/**
55+
* Returns the response payload as bytes.
56+
*/
57+
public SdkBytes rawResponse() {
58+
return rawResponse;
59+
}
60+
61+
/**
62+
* Returns a map of HTTP headers associated with the error response.
63+
*/
64+
public SdkHttpResponse sdkHttpResponse() {
65+
return sdkHttpResponse;
66+
}
67+
68+
public static Builder builder() {
69+
return new BuilderImpl();
70+
}
71+
72+
public interface Builder extends SdkClientException.Builder {
73+
Builder statusCode(int statusCode);
74+
75+
Builder rawResponse(SdkBytes rawResponse);
76+
77+
Builder sdkHttpResponse(SdkHttpResponse sdkHttpResponse);
78+
79+
@Override
80+
Ec2MetadataClientException build();
81+
}
82+
83+
private static final class BuilderImpl extends SdkClientException.BuilderImpl implements Builder {
84+
private int statusCode;
85+
private SdkBytes rawResponse;
86+
private SdkHttpResponse sdkHttpResponse;
87+
88+
@Override
89+
public Builder statusCode(int statusCode) {
90+
this.statusCode = statusCode;
91+
return this;
92+
}
93+
94+
@Override
95+
public Builder rawResponse(SdkBytes rawResponse) {
96+
this.rawResponse = rawResponse;
97+
return this;
98+
}
99+
100+
@Override
101+
public Builder sdkHttpResponse(SdkHttpResponse sdkHttpResponse) {
102+
this.sdkHttpResponse = sdkHttpResponse;
103+
return this;
104+
}
105+
106+
@Override
107+
public Ec2MetadataClientException build() {
108+
return new Ec2MetadataClientException(this);
109+
}
110+
}
111+
}

core/imds/src/main/java/software/amazon/awssdk/imds/internal/AsyncHttpRequestHelper.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.concurrent.CompletableFuture;
2424
import java.util.function.Function;
2525
import software.amazon.awssdk.annotations.SdkInternalApi;
26+
import software.amazon.awssdk.core.SdkBytes;
2627
import software.amazon.awssdk.core.exception.RetryableException;
2728
import software.amazon.awssdk.core.exception.SdkClientException;
2829
import software.amazon.awssdk.core.http.HttpResponseHandler;
@@ -37,6 +38,7 @@
3738
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
3839
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
3940
import software.amazon.awssdk.http.async.SdkHttpContentPublisher;
41+
import software.amazon.awssdk.imds.Ec2MetadataClientException;
4042
import software.amazon.awssdk.utils.CompletableFutureUtils;
4143

4244
@SdkInternalApi
@@ -84,15 +86,22 @@ private static String handleResponse(SdkHttpFullResponse response, ExecutionAttr
8486
response.content().orElseThrow(() -> SdkClientException.create("Unexpected error: empty response content"));
8587
String responseContent = uncheckedInputStreamToUtf8(inputStream);
8688

87-
// non-retryable error
88-
if (statusCode.isOneOf(HttpStatusFamily.CLIENT_ERROR)) {
89-
throw SdkClientException.builder().message(responseContent).build();
90-
}
91-
9289
// retryable error
9390
if (statusCode.isOneOf(HttpStatusFamily.SERVER_ERROR)) {
9491
throw RetryableException.create(responseContent);
9592
}
93+
94+
// non-retryable error
95+
if (!statusCode.isOneOf(HttpStatusFamily.SUCCESSFUL)) {
96+
throw Ec2MetadataClientException.builder()
97+
.statusCode(response.statusCode())
98+
.sdkHttpResponse(response)
99+
.rawResponse(SdkBytes.fromUtf8String(responseContent))
100+
.message(String.format("Failed to send request to IMDS. "
101+
+ "Service returned %d error",
102+
response.statusCode()))
103+
.build();
104+
}
96105
return responseContent;
97106
}
98107

core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClient.java

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import software.amazon.awssdk.annotations.Immutable;
3131
import software.amazon.awssdk.annotations.SdkInternalApi;
3232
import software.amazon.awssdk.annotations.ThreadSafe;
33+
import software.amazon.awssdk.core.SdkBytes;
3334
import software.amazon.awssdk.core.exception.RetryableException;
3435
import software.amazon.awssdk.core.exception.SdkClientException;
3536
import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder;
@@ -40,6 +41,7 @@
4041
import software.amazon.awssdk.http.HttpStatusFamily;
4142
import software.amazon.awssdk.http.SdkHttpClient;
4243
import software.amazon.awssdk.imds.Ec2MetadataClient;
44+
import software.amazon.awssdk.imds.Ec2MetadataClientException;
4345
import software.amazon.awssdk.imds.Ec2MetadataResponse;
4446
import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy;
4547
import software.amazon.awssdk.imds.EndpointMode;
@@ -149,6 +151,19 @@ public Ec2MetadataResponse get(String path) {
149151
throw sdkClientExceptionBuilder.build();
150152
}
151153

154+
private void handleUnsuccessfulResponse(int statusCode, Optional<AbortableInputStream> responseBody,
155+
HttpExecuteResponse response, Supplier<String> errorMessageSupplier) {
156+
String responseContent = responseBody.map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8)
157+
.orElse("");
158+
159+
throw Ec2MetadataClientException.builder()
160+
.statusCode(statusCode)
161+
.sdkHttpResponse(response.httpResponse())
162+
.rawResponse(SdkBytes.fromUtf8String(responseContent))
163+
.message(errorMessageSupplier.get())
164+
.build();
165+
}
166+
152167
private Ec2MetadataResponse sendRequest(String path, String token) throws IOException {
153168

154169
HttpExecuteRequest httpExecuteRequest =
@@ -170,12 +185,9 @@ private Ec2MetadataResponse sendRequest(String path, String token) throws IOExce
170185
}
171186

172187
if (!HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SUCCESSFUL)) {
173-
responseBody.map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8)
174-
.ifPresent(str -> log.debug(() -> "Metadata request response body: " + str));
175-
throw SdkClientException
176-
.builder()
177-
.message(String.format("The requested metadata at path '%s' returned Http code %s", path, statusCode))
178-
.build();
188+
handleUnsuccessfulResponse(statusCode, responseBody, response,
189+
() -> String.format("The requested metadata at path '%s' returned Http code %s", path, statusCode)
190+
);
179191
}
180192

181193
AbortableInputStream abortableInputStream = responseBody.orElseThrow(
@@ -219,11 +231,9 @@ private Token getToken() {
219231
}
220232

221233
if (!HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SUCCESSFUL)) {
222-
response.responseBody().map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8)
223-
.ifPresent(body -> log.debug(() -> "Token request response body: " + body));
224-
throw SdkClientException.builder()
225-
.message("Could not retrieve token, " + statusCode + " error occurred.")
226-
.build();
234+
handleUnsuccessfulResponse(statusCode, response.responseBody(), response,
235+
() -> String.format("Could not retrieve token, %d error occurred", statusCode)
236+
);
227237
}
228238

229239
String ttl = response.httpResponse()

core/imds/src/test/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClientTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import software.amazon.awssdk.core.exception.SdkClientException;
5151
import software.amazon.awssdk.core.retry.backoff.FixedDelayBackoffStrategy;
5252
import software.amazon.awssdk.imds.Ec2MetadataClientBuilder;
53+
import software.amazon.awssdk.imds.Ec2MetadataClientException;
5354
import software.amazon.awssdk.imds.Ec2MetadataResponse;
5455
import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy;
5556
import software.amazon.awssdk.imds.EndpointMode;
@@ -356,4 +357,49 @@ void get_successOnFirstTry_shouldNotRetryAndSucceed_whenConnectionTakesMoreThanO
356357
.withHeader(TOKEN_HEADER, equalTo("some-token")));
357358
});
358359
}
360+
361+
@Test
362+
void clientError_throwsEc2MetadataClientExceptionWithFullResponse() {
363+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH))
364+
.willReturn(aResponse()
365+
.withBody("token")
366+
.withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
367+
368+
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE))
369+
.willReturn(aResponse()
370+
.withStatus(400)
371+
.withHeader("x-custom-header", "custom-value")
372+
.withBody("Bad Request")));
373+
374+
failureAssertions(
375+
AMI_ID_RESOURCE,
376+
Ec2MetadataClientException.class,
377+
exception -> {
378+
assertThat(exception.statusCode()).isEqualTo(400);
379+
assertThat(exception.sdkHttpResponse().firstMatchingHeader("x-custom-header"))
380+
.hasValue("custom-value");
381+
assertThat(exception.rawResponse().asUtf8String()).isEqualTo("Bad Request");
382+
}
383+
);
384+
}
385+
386+
@Test
387+
void tokenRequest_clientError_throwsEc2MetadataClientExceptionWithFullResponse() {
388+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH))
389+
.willReturn(aResponse()
390+
.withStatus(403)
391+
.withHeader("x-error-type", "AccessDenied")
392+
.withBody("Forbidden")));
393+
394+
failureAssertions(
395+
AMI_ID_RESOURCE,
396+
Ec2MetadataClientException.class,
397+
exception -> {
398+
assertThat(exception.statusCode()).isEqualTo(403);
399+
assertThat(exception.sdkHttpResponse().firstMatchingHeader("x-error-type"))
400+
.hasValue("AccessDenied");
401+
assertThat(exception.rawResponse().asUtf8String()).isEqualTo("Forbidden");
402+
}
403+
);
404+
}
359405
}

0 commit comments

Comments
 (0)