ErrorTranslation.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.fs.s3a.impl;
import java.io.IOException;
import java.lang.reflect.Constructor;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.core.exception.SdkException;
import org.apache.hadoop.classification.VisibleForTesting;
import org.apache.hadoop.fs.s3a.HttpChannelEOFException;
import org.apache.hadoop.fs.PathIOException;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.apache.hadoop.fs.s3a.impl.InternalConstants.SC_404_NOT_FOUND;
/**
* Translate from AWS SDK-wrapped exceptions into IOExceptions with
* as much information as possible.
* The core of the translation logic is in S3AUtils, in
* {@code translateException} and nearby; that has grown to be
* a large a complex piece of logic, as it ties in with retry/recovery
* policies, throttling, etc.
*
* This class is where future expansion of that code should go so that we have
* an isolated place for all the changes..
* The existing code las been left in S3AUtils it is to avoid cherry-picking
* problems on backports.
*/
public final class ErrorTranslation {
/**
* OpenSSL stream closed error: {@value}.
* See HADOOP-19027.
*/
public static final String OPENSSL_STREAM_CLOSED = "WFOPENSSL0035";
/**
* Classname of unshaded Http Client exception: {@value}.
*/
private static final String RAW_NO_HTTP_RESPONSE_EXCEPTION =
"org.apache.http.NoHttpResponseException";
/**
* Classname of shaded Http Client exception: {@value}.
*/
private static final String SHADED_NO_HTTP_RESPONSE_EXCEPTION =
"software.amazon.awssdk.thirdparty.org.apache.http.NoHttpResponseException";
/**
* S3 encryption client exception class name: {@value}.
*/
private static final String S3_ENCRYPTION_CLIENT_EXCEPTION =
"software.amazon.encryption.s3.S3EncryptionClientException";
/**
* Private constructor for utility class.
*/
private ErrorTranslation() {
}
/**
* Does this exception indicate that the AWS Bucket was unknown.
* @param e exception.
* @return true if the status code and error code mean that the
* remote bucket is unknown.
*/
public static boolean isUnknownBucket(AwsServiceException e) {
return e.statusCode() == SC_404_NOT_FOUND
&& AwsErrorCodes.E_NO_SUCH_BUCKET.equals(e.awsErrorDetails().errorCode());
}
/**
* Does this exception indicate that a reference to an object
* returned a 404. Unknown bucket errors do not match this
* predicate.
* @param e exception.
* @return true if the status code and error code mean that the
* HEAD request returned 404 but the bucket was there.
*/
public static boolean isObjectNotFound(AwsServiceException e) {
return e.statusCode() == SC_404_NOT_FOUND && !isUnknownBucket(e);
}
/**
* Tail recursive extraction of the innermost throwable.
* @param thrown next thrown in chain.
* @param outer outermost.
* @return the last non-null throwable in the chain.
*/
private static Throwable getInnermostThrowable(Throwable thrown, Throwable outer) {
if (thrown == null) {
return outer;
}
return getInnermostThrowable(thrown.getCause(), thrown);
}
/**
* Attempts to extract the underlying SdkException from an S3 encryption client exception.
*
* <p>This method is designed to handle exceptions that may be wrapped within
* S3EncryptionClientExceptions. It performs the following steps:
* <ol>
* <li>Checks if the input exception is null.</li>
* <li>Verifies if the exception contains the S3EncryptionClientException signature.</li>
* <li>Examines the cause chain to find the most relevant SdkException.</li>
* </ol>
*
* <p>The method aims to unwrap nested exceptions to provide more meaningful
* error information, particularly in the context of S3 encryption operations.
*
* @param exception The SdkException to analyze. This may be a wrapper exception
* containing a more specific underlying cause.
* @return The extracted SdkException if found within the exception chain,
* or the original exception if no relevant nested exception is found.
* Returns null if the input exception is null.
*
* @see SdkException
* @see AwsServiceException
*/
public static SdkException maybeProcessEncryptionClientException(
SdkException exception) {
if (exception == null) {
return null;
}
// check if the exception contains S3EncryptionClientException
if (!exception.toString().contains(S3_ENCRYPTION_CLIENT_EXCEPTION)) {
return exception;
}
Throwable cause = exception.getCause();
if (!(cause instanceof SdkException)) {
return exception;
}
// get the actual sdk exception.
SdkException sdkCause = (SdkException) cause;
if (sdkCause.getCause() instanceof AwsServiceException) {
return (SdkException) sdkCause.getCause();
}
return sdkCause;
}
/**
* Translate an exception if it or its inner exception is an
* IOException.
* This also contains the logic to extract an AWS HTTP channel exception,
* which may or may not be an IOE, depending on the underlying SSL implementation
* in use.
* If an IOException cannot be extracted, null is returned.
* @param path path of operation.
* @param thrown exception
* @param message message generated by the caller.
* @return a translated exception or null.
*/
public static IOException maybeExtractIOException(
String path,
Throwable thrown,
String message) {
if (thrown == null) {
return null;
}
// walk down the chain of exceptions to find the innermost.
Throwable cause = getInnermostThrowable(thrown.getCause(), thrown);
// see if this is an http channel exception
HttpChannelEOFException channelException =
maybeExtractChannelException(path, message, cause);
if (channelException != null) {
return channelException;
}
// not a channel exception, not an IOE.
if (!(cause instanceof IOException)) {
return null;
}
// the cause can be extracted to an IOE.
// rather than just return it, we try to preserve the stack trace
// of the outer exception.
// as a new instance is created through reflection, the
// class of the returned instance will be that of the innermost,
// unless no suitable constructor is available.
final IOException ioe = (IOException) cause;
return wrapWithInnerIOE(path, message, thrown, ioe);
}
/**
* Given an outer and an inner exception, create a new IOE
* of the inner type, with the outer exception as the cause.
* The message is derived from both.
* This only works if the inner exception has a constructor which
* takes a string; if not a PathIOException is created.
* <p>
* See {@code NetUtils}.
* @param <T> type of inner exception.
* @param path path of the failure.
* @param message message generated by the caller.
* @param outer outermost exception.
* @param inner inner exception.
* @return the new exception.
*/
@SuppressWarnings("unchecked")
private static <T extends IOException> IOException wrapWithInnerIOE(
String path,
String message,
Throwable outer,
T inner) {
String msg = (isNotEmpty(message) ? (message + ":"
+ " ") : "")
+ outer.toString() + ": " + inner.getMessage();
Class<? extends Throwable> clazz = inner.getClass();
try {
Constructor<? extends Throwable> ctor = clazz.getConstructor(String.class);
Throwable t = ctor.newInstance(msg);
return (T) (t.initCause(outer));
} catch (Throwable e) {
return new PathIOException(path, msg, outer);
}
}
/**
* Extract an AWS HTTP channel exception if the inner exception is considered
* an HttpClient {@code NoHttpResponseException} or an OpenSSL channel exception.
* This is based on string matching, which is inelegant and brittle.
* @param path path of the failure.
* @param message message generated by the caller.
* @param thrown inner exception.
* @return the new exception.
*/
@VisibleForTesting
public static HttpChannelEOFException maybeExtractChannelException(
String path,
String message,
Throwable thrown) {
final String classname = thrown.getClass().getName();
if (thrown instanceof IOException
&& (classname.equals(RAW_NO_HTTP_RESPONSE_EXCEPTION)
|| classname.equals(SHADED_NO_HTTP_RESPONSE_EXCEPTION))) {
// shaded or unshaded http client exception class
return new HttpChannelEOFException(path, message, thrown);
}
// there's ambiguity about what exception class this is
// so rather than use its type, we look for an OpenSSL string in the message
if (thrown.getMessage().contains(OPENSSL_STREAM_CLOSED)) {
return new HttpChannelEOFException(path, message, thrown);
}
return null;
}
/**
* AWS error codes explicitly recognized and processes specially;
* kept in their own class for isolation.
*/
public static final class AwsErrorCodes {
/**
* The AWS S3 error code used to recognize when a 404 means the bucket is
* unknown.
*/
public static final String E_NO_SUCH_BUCKET = "NoSuchBucket";
/** private constructor. */
private AwsErrorCodes() {
}
}
}