diff --git a/src/main/java/com/aliyun/oss/ClientConfiguration.java b/src/main/java/com/aliyun/oss/ClientConfiguration.java index d2d4a44d..45b66d12 100644 --- a/src/main/java/com/aliyun/oss/ClientConfiguration.java +++ b/src/main/java/com/aliyun/oss/ClientConfiguration.java @@ -127,6 +127,8 @@ public class ClientConfiguration { private boolean tracerEnabled = false; + private boolean enableAutoCorrectClockSkew = false; + public ClientConfiguration() { super(); AppendDefaultExcludeList(this.cnameExcludeList); @@ -991,4 +993,28 @@ public boolean isTracerEnabled() { public void setTracerEnabled(boolean enabled) { this.tracerEnabled = enabled; } + + /** + * Checks whether the automatic clock skew correction feature is enabled. + * + * @return + * {@code true} if the client is configured to automatically adjust its clock + * based on server time when a significant time discrepancy is detected; + * {@code false} otherwise. + */ + public boolean isEnableAutoCorrectClockSkew() { + return enableAutoCorrectClockSkew; + } + + /** + * Enables or disables the automatic clock skew correction feature. + * + * @param enableAutoCorrectClockSkew + * {@code true} to enable automatic clock adjustment (client will correct time offsets + * detected during requests based on server time); + * {@code false} to disable this feature. + */ + public void setEnableAutoCorrectClockSkew(boolean enableAutoCorrectClockSkew) { + this.enableAutoCorrectClockSkew = enableAutoCorrectClockSkew; + } } diff --git a/src/main/java/com/aliyun/oss/OSSException.java b/src/main/java/com/aliyun/oss/OSSException.java index 7d345a7c..a8470793 100644 --- a/src/main/java/com/aliyun/oss/OSSException.java +++ b/src/main/java/com/aliyun/oss/OSSException.java @@ -19,6 +19,8 @@ package com.aliyun.oss; +import java.util.Map; + /** * The OSSException is thrown upon error when accessing OSS. */ @@ -70,6 +72,15 @@ public OSSException(String errorMessage, String errorCode, String requestId, Str this.method = method; } + public OSSException(String errorMessage, String errorCode, String requestId, String hostId, String header, + String resourceType, String method, String rawResponseError, Throwable cause, String ec, + Map headers, Map errorFields) { + super(errorMessage, errorCode, requestId, hostId, rawResponseError, cause, ec, headers, errorFields); + this.resourceType = resourceType; + this.header = header; + this.method = method; + } + public String getResourceType() { return resourceType; } diff --git a/src/main/java/com/aliyun/oss/ServiceException.java b/src/main/java/com/aliyun/oss/ServiceException.java index 6ec28310..76dc0c3f 100644 --- a/src/main/java/com/aliyun/oss/ServiceException.java +++ b/src/main/java/com/aliyun/oss/ServiceException.java @@ -19,6 +19,15 @@ package com.aliyun.oss; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.input.SAXBuilder; +import java.io.StringReader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + /** *

* This is the base exception class to represent any expected or unexpected OSS @@ -61,6 +70,9 @@ public class ServiceException extends RuntimeException { private String rawResponseError; + private Map headers = new TreeMap(String.CASE_INSENSITIVE_ORDER); + private Map errorFields = new TreeMap(String.CASE_INSENSITIVE_ORDER); + /** * Creates a default instance. */ @@ -187,6 +199,18 @@ public ServiceException(String errorMessage, String errorCode, String requestId, this.ec = ec; } + public ServiceException(String errorMessage, String errorCode, String requestId, String hostId, + String rawResponseError, Throwable cause, String ec, Map headers, Map errorFields) { + this(errorMessage, cause); + this.errorCode = errorCode; + this.requestId = requestId; + this.hostId = hostId; + this.rawResponseError = rawResponseError; + this.ec = ec; + this.headers = headers; + this.errorFields = errorFields; + } + /** * Gets error message. * @@ -258,6 +282,22 @@ private String formatRawResponseError() { return String.format("\n[ResponseError]:\n%s", this.rawResponseError); } + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public Map getErrorFields() { + return errorFields; + } + + public void setErrorFields(Map errorFields) { + this.errorFields = errorFields; + } + @Override public String getMessage() { String msg = getErrorMessage() + "\n[ErrorCode]: " + getErrorCode() + "\n[RequestId]: " + getRequestId() diff --git a/src/main/java/com/aliyun/oss/common/comm/ServiceClient.java b/src/main/java/com/aliyun/oss/common/comm/ServiceClient.java index 4a683c2e..c5a309d8 100644 --- a/src/main/java/com/aliyun/oss/common/comm/ServiceClient.java +++ b/src/main/java/com/aliyun/oss/common/comm/ServiceClient.java @@ -28,8 +28,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.util.Date; import java.util.List; +import com.aliyun.oss.common.utils.DateUtil; import org.apache.http.HttpMessage; import com.aliyun.oss.ClientConfiguration; @@ -161,6 +163,17 @@ private ResponseMessage sendRequestImpl(RequestMessage request, ExecutionContext logException("[Server]Unable to execute HTTP request: ", sex, request.getOriginalRequest().isLogEnabled()); + if (response != null && response.getStatusCode() == 403) { + if ("RequestTimeTooSkewed".equals(sex.getErrorCode()) && this.config.isEnableAutoCorrectClockSkew()) { + try { + Date serverTime = DateUtil.parseIso8601Date(sex.getErrorFields().get("ServerTime").toString()); + this.config.setTickOffset(serverTime.getTime()); + } catch (Exception e) { + throw new ClientException(e.getMessage(), e); + } + } + } + // Notice that the response should not be closed in the // finally block because if the request is successful, // the response should be returned to the callers. diff --git a/src/main/java/com/aliyun/oss/common/utils/ExceptionFactory.java b/src/main/java/com/aliyun/oss/common/utils/ExceptionFactory.java index ebb496ad..7e139a00 100644 --- a/src/main/java/com/aliyun/oss/common/utils/ExceptionFactory.java +++ b/src/main/java/com/aliyun/oss/common/utils/ExceptionFactory.java @@ -101,7 +101,8 @@ public static OSSException createOSSException(OSSErrorResult errorResult) { public static OSSException createOSSException(OSSErrorResult errorResult, String rawResponseError) { return new OSSException(errorResult.Message, errorResult.Code, errorResult.RequestId, errorResult.HostId, - errorResult.Header, errorResult.ResourceType, errorResult.Method, rawResponseError, null, errorResult.EC); + errorResult.Header, errorResult.ResourceType, errorResult.Method, rawResponseError, null, errorResult.EC, + errorResult.Headers, errorResult.ErrorFields); } public static OSSException createOSSException(String requestId, String errorCode, String message) { diff --git a/src/main/java/com/aliyun/oss/internal/OSSErrorResponseHandler.java b/src/main/java/com/aliyun/oss/internal/OSSErrorResponseHandler.java index fc39ace1..13de76c6 100644 --- a/src/main/java/com/aliyun/oss/internal/OSSErrorResponseHandler.java +++ b/src/main/java/com/aliyun/oss/internal/OSSErrorResponseHandler.java @@ -58,7 +58,7 @@ public void handle(ResponseMessage response) throws OSSException, ClientExceptio try { if (response.getHeaders().containsKey(OSSHeaders.OSS_ERROR)) { byte[] data = BinaryUtil.fromBase64String(response.getHeaders().get(OSSHeaders.OSS_ERROR)); - result = errorResponseParser.parseErrorResponse(new ByteArrayInputStream(data)); + result = errorResponseParser.parseErrorResponse(new ByteArrayInputStream(data), response.getHeaders()); if (result.Code == null) { result = null; } diff --git a/src/main/java/com/aliyun/oss/internal/ResponseParsers.java b/src/main/java/com/aliyun/oss/internal/ResponseParsers.java index 4892738c..9d538e04 100644 --- a/src/main/java/com/aliyun/oss/internal/ResponseParsers.java +++ b/src/main/java/com/aliyun/oss/internal/ResponseParsers.java @@ -178,13 +178,13 @@ public static final class ErrorResponseParser implements ResponseParser headers) throws ResponseParseException { OSSErrorResult ossErrorResult = new OSSErrorResult(); if (inputStream == null) { return ossErrorResult; @@ -199,6 +199,12 @@ OSSErrorResult parseErrorResponse(InputStream inputStream) throws ResponseParseE ossErrorResult.Method = root.getChildText("Method"); ossErrorResult.Header = root.getChildText("Header"); ossErrorResult.EC = root.getChildText("EC"); + + Map map = new TreeMap(); + parseElement(root, map); + + ossErrorResult.Headers = headers; + ossErrorResult.ErrorFields = map; return ossErrorResult; } catch (JDOMParseException e) { throw new ResponseParseException(e.getPartialDocument() + ": " + e.getMessage(), e); @@ -206,6 +212,19 @@ OSSErrorResult parseErrorResponse(InputStream inputStream) throws ResponseParseE throw new ResponseParseException(e.getMessage(), e); } } + + private static void parseElement(Element element, Map map) { + List children = element.getChildren(); + for (Element child : children) { + if (child.getChildren().isEmpty()) { + map.put(child.getName(), child.getTextTrim()); + } else { + Map subMap = new TreeMap<>(); + parseElement(child, subMap); + map.put(child.getName(), subMap); + } + } + } } public static void setResultParameter(GenericResult result, ResponseMessage response){ diff --git a/src/main/java/com/aliyun/oss/internal/model/OSSErrorResult.java b/src/main/java/com/aliyun/oss/internal/model/OSSErrorResult.java index 36da6152..7fecf49e 100644 --- a/src/main/java/com/aliyun/oss/internal/model/OSSErrorResult.java +++ b/src/main/java/com/aliyun/oss/internal/model/OSSErrorResult.java @@ -21,6 +21,7 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; @XmlRootElement(name = "Error") public class OSSErrorResult { @@ -48,4 +49,9 @@ public class OSSErrorResult { @XmlElement(name = "EC") public String EC; + @XmlElement(name = "Headers") + public Map Headers; + + @XmlElement(name = "ErrorFields") + public Map ErrorFields; } \ No newline at end of file diff --git a/src/test/java/com/aliyun/oss/common/parser/ResponseParsersTest.java b/src/test/java/com/aliyun/oss/common/parser/ResponseParsersTest.java index 79a7834f..2684c0bb 100644 --- a/src/test/java/com/aliyun/oss/common/parser/ResponseParsersTest.java +++ b/src/test/java/com/aliyun/oss/common/parser/ResponseParsersTest.java @@ -5580,6 +5580,125 @@ public void testOSSErrorResponseHandler() { } } + @Test + public void testParseErrorResponseWithMap() { + InputStream instream = null; + String respBody; + + respBody = "" + + "\n" + + " RequestTimeTooSkewed\n" + + " The difference between the request time and the current time is too large.\n" + + " 5CAC0CF8DE0170*****\n" + + " versioning-get.oss-cn-hangzhou.aliyunc*****\n" + + " 900000\n" + + " 2025-04-10T11:00:57.000Z\n" + + " 2025-04-10T10:00:57.000Z\n" + + " 0002-00000504\n" + + " https://api.aliyun.com/troubleshoot?q=0002-00000504\n" + + ""; + + try { + instream = new ByteArrayInputStream(respBody.getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + Assert.fail("UnsupportedEncodingException"); + } + + OSSErrorResult result = null; + try { + ResponseMessage response = new ResponseMessage(null); + response.setContent(instream); + ResponseParsers.ErrorResponseParser parser = new ResponseParsers.ErrorResponseParser(); + result = parser.parse(response); + } catch (ResponseParseException e) { + Assert.fail("parse delete directory response body fail!"); + } + + Assert.assertEquals("RequestTimeTooSkewed", result.Code); + Assert.assertEquals("The difference between the request time and the current time is too large.", result.Message); + Assert.assertEquals("5CAC0CF8DE0170*****", result.RequestId); + Assert.assertEquals("versioning-get.oss-cn-hangzhou.aliyunc*****", result.HostId); + Assert.assertEquals("0002-00000504", result.EC); + Assert.assertEquals("2025-04-10T11:00:57.000Z", result.ErrorFields.get("RequestTime").toString()); + Assert.assertEquals("2025-04-10T10:00:57.000Z", result.ErrorFields.get("ServerTime").toString()); + + + respBody = "" + + "\n" + + " RequestTimeTooSkewed\n" + + " The difference between the request time and the current time is too large.\n" + + " 5CAC0CF8DE0170*****\n" + + " versioning-get.oss-cn-hangzhou.aliyunc*****\n" + + " 900000\n" + + " \n" + + " 2025-04-10T11:00:57.000Z\n" + + " 2025-04-10T10:00:57.000Z\n" + + " \n" + + " 0002-00000504\n" + + " https://api.aliyun.com/troubleshoot?q=0002-00000504\n" + + ""; + + try { + instream = new ByteArrayInputStream(respBody.getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + Assert.fail("UnsupportedEncodingException"); + } + + try { + ResponseMessage response = new ResponseMessage(null); + response.setContent(instream); + ResponseParsers.ErrorResponseParser parser = new ResponseParsers.ErrorResponseParser(); + result = parser.parse(response); + } catch (ResponseParseException e) { + Assert.fail("parse delete directory response body fail!"); + } + + Assert.assertEquals("RequestTimeTooSkewed", result.Code); + Assert.assertEquals("The difference between the request time and the current time is too large.", result.Message); + Assert.assertEquals("5CAC0CF8DE0170*****", result.RequestId); + Assert.assertEquals("versioning-get.oss-cn-hangzhou.aliyunc*****", result.HostId); + Assert.assertEquals("0002-00000504", result.EC); + + + Object value = result.ErrorFields.get("Detail"); + if (value instanceof Map) { + Map nestedMap = (Map) value; + Assert.assertEquals("2025-04-10T11:00:57.000Z", nestedMap.get("RequestTime").toString()); + Assert.assertEquals("2025-04-10T10:00:57.000Z", nestedMap.get("ServerTime").toString()); + } + + + respBody = "" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "

\n" + + " \n" + + " \n" + + ""; + + try { + instream = new ByteArrayInputStream(respBody.getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + Assert.fail("UnsupportedEncodingException"); + } + + try { + ResponseMessage response = new ResponseMessage(null); + response.setContent(instream); + ResponseParsers.ErrorResponseParser parser = new ResponseParsers.ErrorResponseParser(); + result = parser.parse(response); + Assert.assertTrue(true); + } catch (Exception e) { + e.printStackTrace(); + Assert.assertTrue(false); + } + } + @Test public void testGetBucketCallbackPolicyParser() { diff --git a/src/test/java/com/aliyun/oss/integrationtests/ErrorTest.java b/src/test/java/com/aliyun/oss/integrationtests/ErrorTest.java index 0911fb24..76d2dd03 100644 --- a/src/test/java/com/aliyun/oss/integrationtests/ErrorTest.java +++ b/src/test/java/com/aliyun/oss/integrationtests/ErrorTest.java @@ -9,6 +9,7 @@ import com.aliyun.oss.model.*; import junit.framework.Assert; import org.junit.Test; +import java.util.Date; public class ErrorTest extends TestBase { @@ -55,8 +56,100 @@ public void testErr() throws Exception { } catch (OSSException e) { Assert.assertEquals(e.getEC(), "0002-00000227"); Assert.assertEquals(e.getErrorCode(), "InvalidArgument"); - Assert.assertEquals(e.getErrorMessage(), "Authorization header is invalid."); + Assert.assertEquals(e.getErrorMessage(), "Invalid signing product in Authorization header."); e.printStackTrace(); } } + + @Test + public void testRequestTimeTooSkewedErrWithV1(){ + ClientBuilderConfiguration conf = new ClientBuilderConfiguration(); + conf.setSignatureVersion(SignVersion.V1); + + // disable auto correct clock skew + OSS ossClient = OSSClientBuilder.create() + .endpoint(TestConfig.OSS_TEST_ENDPOINT) + .credentialsProvider(new DefaultCredentialProvider(TestConfig.OSS_TEST_ACCESS_KEY_ID, TestConfig.OSS_TEST_ACCESS_KEY_SECRET)) + .clientConfiguration(conf) + .region(TestConfig.OSS_TEST_REGION) + .build(); + + AccessControlList result = ossClient.getBucketAcl(bucketName); + Assert.assertEquals(result.getCannedACL(), CannedAccessControlList.Private); + + // enable auto correct clock skew + conf.setEnableAutoCorrectClockSkew(true); + conf.setTickOffset(new Date().getTime() + 3600 * 1000); + ossClient = OSSClientBuilder.create() + .endpoint(TestConfig.OSS_TEST_ENDPOINT) + .credentialsProvider(new DefaultCredentialProvider(TestConfig.OSS_TEST_ACCESS_KEY_ID, TestConfig.OSS_TEST_ACCESS_KEY_SECRET)) + .clientConfiguration(conf) + .region(TestConfig.OSS_TEST_REGION) + .build(); + + try { + ossClient.getBucketAcl(bucketName); + Assert.fail("should not be here"); + } catch (OSSException e) { + Assert.assertEquals(e.getEC(), "0002-00000504"); + Assert.assertEquals(e.getErrorCode(), "RequestTimeTooSkewed"); + Assert.assertEquals(e.getErrorMessage(), "The difference between the request time and the current time is too large."); + Assert.assertEquals("0002-00000504", e.getHeaders().get("x-oss-ec")); + Assert.assertNotNull(e.getHeaders().get("Date")); + Assert.assertNotNull(e.getHeaders().get("Content-Type")); + Assert.assertNotNull(e.getHeaders().get("x-oss-request-id")); + Assert.assertNotNull(e.getErrorFields().get("RequestTime").toString()); + Assert.assertNotNull(e.getErrorFields().get("ServerTime").toString()); + e.printStackTrace(); + + result = ossClient.getBucketAcl(bucketName); + Assert.assertEquals(result.getCannedACL(), CannedAccessControlList.Private); + } + } + + @Test + public void testRequestTimeTooSkewedErrWithV4() { + ClientBuilderConfiguration conf = new ClientBuilderConfiguration(); + conf.setSignatureVersion(SignVersion.V4); + + // disable auto correct clock skew + OSS ossClient = OSSClientBuilder.create() + .endpoint(TestConfig.OSS_TEST_ENDPOINT) + .credentialsProvider(new DefaultCredentialProvider(TestConfig.OSS_TEST_ACCESS_KEY_ID, TestConfig.OSS_TEST_ACCESS_KEY_SECRET)) + .clientConfiguration(conf) + .region(TestConfig.OSS_TEST_REGION) + .build(); + + AccessControlList result = ossClient.getBucketAcl(bucketName); + Assert.assertEquals(result.getCannedACL(), CannedAccessControlList.Private); + + // enable auto correct clock skew + conf.setEnableAutoCorrectClockSkew(true); + conf.setTickOffset(new Date().getTime() + 3600 * 1000); + ossClient = OSSClientBuilder.create() + .endpoint(TestConfig.OSS_TEST_ENDPOINT) + .credentialsProvider(new DefaultCredentialProvider(TestConfig.OSS_TEST_ACCESS_KEY_ID, TestConfig.OSS_TEST_ACCESS_KEY_SECRET)) + .clientConfiguration(conf) + .region(TestConfig.OSS_TEST_REGION) + .build(); + + try { + ossClient.getBucketAcl(bucketName); + Assert.fail("should not be here"); + } catch (OSSException e) { + Assert.assertEquals(e.getEC(), "0002-00000504"); + Assert.assertEquals(e.getErrorCode(), "RequestTimeTooSkewed"); + Assert.assertEquals(e.getErrorMessage(), "The difference between the request time and the current time is too large."); + Assert.assertEquals("0002-00000504", e.getHeaders().get("x-oss-ec")); + Assert.assertNotNull(e.getHeaders().get("Date")); + Assert.assertNotNull(e.getHeaders().get("Content-Type")); + Assert.assertNotNull(e.getHeaders().get("x-oss-request-id")); + Assert.assertNotNull(e.getErrorFields().get("RequestTime").toString()); + Assert.assertNotNull(e.getErrorFields().get("ServerTime").toString()); + e.printStackTrace(); + + result = ossClient.getBucketAcl(bucketName); + Assert.assertEquals(result.getCannedACL(), CannedAccessControlList.Private); + } + } }