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.awscore.interceptor;
17
18 import java.net.UnknownHostException;
19 import java.util.List;
20 import java.util.Optional;
21 import java.util.stream.Collectors;
22 import software.amazon.awssdk.annotations.SdkInternalApi;
23 import software.amazon.awssdk.awscore.AwsExecutionAttribute;
24 import software.amazon.awssdk.core.exception.SdkClientException;
25 import software.amazon.awssdk.core.interceptor.Context;
26 import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
27 import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
28 import software.amazon.awssdk.regions.PartitionMetadata;
29 import software.amazon.awssdk.regions.Region;
30 import software.amazon.awssdk.regions.RegionMetadata;
31 import software.amazon.awssdk.regions.ServiceMetadata;
32 import software.amazon.awssdk.regions.ServicePartitionMetadata;
33
34 /**
35  * This interceptor will monitor for {@link UnknownHostException}s and provide the customer with additional information they can
36  * use to debug or fix the problem.
37  */

38 @SdkInternalApi
39 public final class HelpfulUnknownHostExceptionInterceptor implements ExecutionInterceptor {
40     @Override
41     public Throwable modifyException(Context.FailedExecution context, ExecutionAttributes executionAttributes) {
42         if (!hasCause(context.exception(), UnknownHostException.class)) {
43             return context.exception();
44         }
45
46         StringBuilder error = new StringBuilder();
47         error.append("Received an UnknownHostException when attempting to interact with a service. See cause for the "
48                      + "exact endpoint that is failing to resolve. ");
49
50         Optional<String> globalRegionErrorDetails = getGlobalRegionErrorDetails(executionAttributes);
51
52         if (globalRegionErrorDetails.isPresent()) {
53             error.append(globalRegionErrorDetails.get());
54         } else {
55             error.append("If this is happening on an endpoint that previously worked, there may be a network connectivity "
56                          + "issue or your DNS cache could be storing endpoints for too long.");
57         }
58
59         return SdkClientException.builder().message(error.toString()).cause(context.exception()).build();
60     }
61
62     /**
63      * If the customer is interacting with a global service (one with a single endpoint/region for an entire partition), this
64      * will return error details that can instruct the customer on how to configure their client for success.
65      */

66     private Optional<String> getGlobalRegionErrorDetails(ExecutionAttributes executionAttributes) {
67         Region clientRegion = clientRegion(executionAttributes);
68         if (clientRegion.isGlobalRegion()) {
69             return Optional.empty();
70         }
71
72         List<ServicePartitionMetadata> globalPartitionsForService = globalPartitionsForService(executionAttributes);
73         if (globalPartitionsForService.isEmpty()) {
74             return Optional.empty();
75         }
76
77         String clientPartition = Optional.ofNullable(clientRegion.metadata())
78                                          .map(RegionMetadata::partition)
79                                          .map(PartitionMetadata::id)
80                                          .orElse(null);
81
82         Optional<Region> globalRegionForClientRegion =
83             globalPartitionsForService.stream()
84                                       .filter(p -> p.partition().id().equals(clientPartition))
85                                       .findAny()
86                                       .flatMap(ServicePartitionMetadata::globalRegion);
87
88         if (!globalRegionForClientRegion.isPresent()) {
89             String globalRegionsForThisService = globalPartitionsForService.stream()
90                                                                            .map(ServicePartitionMetadata::globalRegion)
91                                                                            .filter(Optional::isPresent)
92                                                                            .map(Optional::get)
93                                                                            .filter(Region::isGlobalRegion)
94                                                                            .map(Region::id)
95                                                                            .collect(Collectors.joining("/"));
96
97             return Optional.of("This specific service may be a global service, in which case you should configure a global "
98                                + "region like " + globalRegionsForThisService + " on the client.");
99         }
100
101         Region globalRegion = globalRegionForClientRegion.get();
102
103         return Optional.of("This specific service is global in the same partition as the region configured on this client ("
104                            + clientRegion + "). If this is the first time you're trying to talk to this service in this region, "
105                            + "you should try configuring the global region on your client, instead: " + globalRegion);
106     }
107
108     /**
109      * Retrieve the region configured on the client.
110      */

111     private Region clientRegion(ExecutionAttributes executionAttributes) {
112         return executionAttributes.getAttribute(AwsExecutionAttribute.AWS_REGION);
113     }
114
115     /**
116      * Retrieve all global partitions for the AWS service that we're interacting with.
117      */

118     private List<ServicePartitionMetadata> globalPartitionsForService(ExecutionAttributes executionAttributes) {
119         return ServiceMetadata.of(executionAttributes.getAttribute(AwsExecutionAttribute.ENDPOINT_PREFIX))
120                               .servicePartitions()
121                               .stream()
122                               .filter(sp -> sp.globalRegion().isPresent())
123                               .collect(Collectors.toList());
124     }
125
126     private boolean hasCause(Throwable thrown, Class<? extends Throwable> cause) {
127         if (thrown == null) {
128             return false;
129         }
130
131         if (cause.isAssignableFrom(thrown.getClass())) {
132             return true;
133         }
134
135         return hasCause(thrown.getCause(), cause);
136     }
137 }
138