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 modifyException(Context.FailedExecution context, ExecutionAttributes executionAttributes)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 */ getGlobalRegionErrorDetails(ExecutionAttributes executionAttributes)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 */ clientRegion(ExecutionAttributes executionAttributes)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 */ globalPartitionsForService(ExecutionAttributes executionAttributes)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 hasCause(Throwable thrown, Class<? extends Throwable> cause)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