diff --git a/README.md b/README.md
index 84c13e9b2..409b0e672 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Welcome to Tencent Cloud Software Development Kit (SDK), a companion tool for th
Tencent Cloud SDK for Java helps Java developers debug and use TencentCloud APIs with ease. This document describes Tencent Cloud SDK for Java and how to quickly use it with code examples provided.
# Dependent Environment
-1. Dependent environment: JDK 7 or higher.
+1. Dependent environment: JDK 8 or higher.
2. Activate your product in the Tencent Cloud Console.
3. Get the `SecretID`, `SecretKey`, and endpoint. The general format of endpoint is `\*.intl.tencentcloudapi.com`. For example, the endpoint of CVM is `cvm.intl.tencentcloudapi.com`. For more information, please see the documentation of the specified product.
@@ -197,6 +197,29 @@ request.setHeader(header);
```
Example: [CustomHttpClient.java](examples/common/CustomHttpClient.java).
+# OkHttp Upgrade
+
+To mitigate security risks in OkHttp 3.x, starting from version 3.2.0, the OkHttp dependency is upgraded from 3.12.13 to 4.12.0.
+
+### Roll back to OkHttp 3.12.13
+
+If the upgrade conflicts with your project, you can explicitly declare the following dependencies in your application's `pom.xml` to roll back. Per Maven's "nearest definition" dependency mediation rule, the version declared by the application will override the 4.12.0 transitively introduced by the SDK:
+
+```xml
+
+ com.squareup.okhttp3
+ okhttp
+ 3.12.13
+
+
+ com.squareup.okhttp3
+ logging-interceptor
+ 3.12.13
+
+```
+
+> **Note**: Rolling back will reintroduce the security risks fixed in newer OkHttp versions. Use it only for emergency troubleshooting and issue diagnosis, and restore OkHttp 4 as soon as the issue is resolved.
+
# Other Issues
## Certificate Problems
diff --git a/pom.xml b/pom.xml
index 9b708e1b8..b2f8dc31d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
jartencentcloud-sdk-java-intl-enhttps://cloud.tencent.com/
- Tencent Cloud API SDK for Java
+ Tencent Cloud Open API SDK for Javacommons-logging
@@ -17,7 +17,7 @@
com.squareup.okhttp3okhttp
- 3.12.13
+ 4.12.0com.google.code.gson
@@ -27,12 +27,12 @@
com.squareup.okhttp3logging-interceptor
- 3.12.13
+ 4.12.0
- org.ini4j
- ini4j
- 0.5.4
+ org.apache.commons
+ commons-configuration2
+ 2.12.0junit
@@ -40,11 +40,6 @@
4.13.1test
-
- com.tencentcloudapi
- tencentcloud-sdk-java-common
- 3.1.924
- UTF-8
@@ -68,8 +63,8 @@
maven-compiler-plugin2.3.2
- 7
- 7
+ 1.8
+ 1.8UTF-8
@@ -79,7 +74,7 @@
2.3.2
-
+
@@ -154,3 +149,4 @@
+
diff --git a/src/main/java/com/tencentcloudapi/common/AbstractClient.java b/src/main/java/com/tencentcloudapi/common/AbstractClient.java
index 6f5392475..f7c9677a9 100644
--- a/src/main/java/com/tencentcloudapi/common/AbstractClient.java
+++ b/src/main/java/com/tencentcloudapi/common/AbstractClient.java
@@ -1,1104 +1,1232 @@
-/*
- * Copyright (c) 2018 Tencent. All Rights Reserved.
- *
- * Licensed 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 com.tencentcloudapi.common;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonSyntaxException;
-import com.google.gson.reflect.TypeToken;
-import com.tencentcloudapi.common.exception.TencentCloudSDKException;
-import com.tencentcloudapi.common.http.HttpConnection;
-import com.tencentcloudapi.common.profile.ClientProfile;
-import com.tencentcloudapi.common.profile.HttpProfile;
-import okhttp3.*;
-import okhttp3.Headers.Builder;
-
-import javax.crypto.Mac;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.X509TrustManager;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Type;
-import java.net.InetSocketAddress;
-import java.net.Proxy;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.security.SecureRandom;
-import java.sql.Date;
-import java.text.SimpleDateFormat;
-import java.util.*;
-
-/**
- * AbstractClient provides the basic functionalities for interacting with Tencent Cloud services.
- * It handles request signing, sending, and response processing.
- */
-public abstract class AbstractClient {
-
- public static final int HTTP_RSP_OK = 200;
- public static final String SDK_VERSION = "SDK_JAVA_3.0.1403";
- public Gson gson;
-
- // User's security credentials (SecretId, SecretKey, Token).
- private Credential credential;
-
- // Client configuration (e.g., timeout, endpoint).
- private ClientProfile profile;
-
- // API endpoint URL.
- private String endpoint;
-
- // Service name (e.g., "cvm").
- private String service;
-
- // Region (e.g., "ap-guangzhou").
- private String region;
-
- // API request path (usually "/").
- private String path;
-
- // SDK version string.
- private String sdkVersion;
-
- // API version string.
- private String apiVersion;
-
- // Logger for debugging and information.
- private TCLog log;
-
- // Handles HTTP connections.
- private HttpConnection httpConnection;
-
- // Circuit breaker for handling region failures.
- private CircuitBreaker regionBreaker;
-
- /**
- * Constructor for AbstractClient with default client profile.
- *
- * @param endpoint API endpoint URL.
- * @param version API version.
- * @param credential User credentials.
- * @param region Region.
- */
- public AbstractClient(String endpoint, String version, Credential credential, String region) {
- this(endpoint, version, credential, region, new ClientProfile());
- }
-
- /**
- * Constructor for AbstractClient with a custom client profile.
- *
- * @param endpoint API endpoint URL.
- * @param version API version.
- * @param credential User credentials.
- * @param region Region.
- * @param profile Client configuration profile.
- */
- public AbstractClient(
- String endpoint,
- String version,
- Credential credential,
- String region,
- ClientProfile profile) {
- this.credential = credential;
- this.profile = profile;
- this.endpoint = endpoint;
- this.service = endpoint.split("\\.")[0];
- this.region = region;
- this.path = "/";
- this.sdkVersion = AbstractClient.SDK_VERSION;
- this.apiVersion = version;
- this.gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
- this.log = new TCLog(getClass().getName(), profile.isDebug());
- this.httpConnection = new HttpConnection(
- this.profile.getHttpProfile().getConnTimeout(),
- this.profile.getHttpProfile().getReadTimeout(),
- this.profile.getHttpProfile().getWriteTimeout()
- );
- this.httpConnection.addInterceptors(this.log);
- this.trySetProxy(this.httpConnection);
- this.trySetSSLSocketFactory(this.httpConnection);
- this.trySetRegionBreaker();
- this.trySetHostnameVerifier(this.httpConnection);
- this.trySetHttpClient();
- warmup();
- }
-
- /**
- * Gets the region.
- *
- * @return The region.
- */
- public String getRegion() {
- return this.region;
- }
-
- /**
- * Sets the region.
- *
- * @param region The region to set.
- */
- public void setRegion(String region) {
- this.region = region;
- }
-
- /**
- * Gets the client profile.
- *
- * @return The client profile.
- */
- public ClientProfile getClientProfile() {
- return this.profile;
- }
-
- /**
- * Sets the client profile.
- *
- * @param profile The client profile to set.
- */
- public void setClientProfile(ClientProfile profile) {
- this.profile = profile;
- }
-
- /**
- * Gets the credential.
- *
- * @return The credential.
- */
- public Credential getCredential() {
- return this.credential;
- }
-
- /**
- * Sets the credential.
- *
- * @param credential The credential to set.
- */
- public void setCredential(Credential credential) {
- this.credential = credential;
- }
-
- /**
- * Calls an API action with JSON payload using the TC3-HMAC-SHA256 signature.
- * Ignores the request method and signature method defined in the profile.
- *
- * @param action Name of the API action.
- * @param jsonPayload JSON string containing the request parameters.
- * @return Raw response from the API.
- * @throws TencentCloudSDKException If an error occurs during the API call.
- */
- public String call(String action, String jsonPayload) throws TencentCloudSDKException {
- Credential credSnapshot = this.credential.getSnapshot();
- HashMap headers = this.getHeaders(credSnapshot);
- headers.put("X-TC-Action", action);
- headers.put("Content-Type", "application/json; charset=utf-8");
- byte[] requestPayload = jsonPayload.getBytes(StandardCharsets.UTF_8);
- String authorization = this.getAuthorization(headers, requestPayload, credSnapshot);
- headers.put("Authorization", authorization);
- String url = this.profile.getHttpProfile().getProtocol() + this.getEndpoint() + this.path;
- return this.getResponseBody(url, headers, requestPayload);
- }
-
- /**
- * Calls an API action with binary payload using the TC3-HMAC-SHA256 signature.
- * Ignores the request method and signature method defined in the profile.
- *
- * @param action Name of the API action.
- * @param headers HTTP headers to include in the request.
- * @param body Binary payload (octet-stream).
- * @return Raw response from the API.
- * @throws TencentCloudSDKException If an error occurs during the API call.
- */
- public String callOctetStream(String action, HashMap headers, byte[] body)
- throws TencentCloudSDKException {
- Credential credSnapshot = this.credential.getSnapshot();
- headers.putAll(this.getHeaders(credSnapshot));
- headers.put("X-TC-Action", action);
- headers.put("Content-Type", "application/octet-stream; charset=utf-8");
- String authorization = this.getAuthorization(headers, body, credSnapshot);
- headers.put("Authorization", authorization);
- String url = this.profile.getHttpProfile().getProtocol() + this.getEndpoint() + this.path;
- return this.getResponseBody(url, headers, body);
- }
-
- /**
- * Generates common HTTP headers for Tencent Cloud API requests.
- *
- * @return A HashMap containing the headers.
- */
- private HashMap getHeaders(Credential credSnapshot) {
- HashMap headers = new HashMap();
- String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
- headers.put("X-TC-Timestamp", timestamp);
- headers.put("X-TC-Version", this.apiVersion);
- headers.put("X-TC-Region", this.getRegion());
- headers.put("X-TC-RequestClient", SDK_VERSION);
- headers.put("Host", this.getEndpoint());
- String token = credSnapshot.getToken();
- if (token != null && !token.isEmpty()) {
- headers.put("X-TC-Token", token);
- }
- if (this.profile.isUnsignedPayload()) {
- headers.put("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD");
- }
- if (null != this.profile.getLanguage()) {
- headers.put("X-TC-Language", this.profile.getLanguage().getValue());
- }
- return headers;
- }
-
- /**
- * Generates the authorization header for TC3-HMAC-SHA256 signature.
- *
- * @param headers HTTP headers.
- * @param body Request payload.
- * @return The authorization header string.
- * @throws TencentCloudSDKException If an error occurs during signature generation.
- */
- private String getAuthorization(HashMap headers, byte[] body, Credential credSnapshot)
- throws TencentCloudSDKException {
- String endpoint = this.getEndpoint();
- // always use post tc3-hmac-sha256 signature process
- // okhttp always set charset even we don't specify it,
- // to ensure signature be correct, we have to set it here as well.
- String contentType = headers.get("Content-Type");
- byte[] requestPayload = body;
- String canonicalUri = "/";
- String canonicalQueryString = "";
- String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n";
- String signedHeaders = "content-type;host";
-
- String hashedRequestPayload = "";
- if (this.profile.isUnsignedPayload()) {
- hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8));
- } else {
- hashedRequestPayload = Sign.sha256Hex(requestPayload);
- }
- String canonicalRequest =
- HttpProfile.REQ_POST
- + "\n"
- + canonicalUri
- + "\n"
- + canonicalQueryString
- + "\n"
- + canonicalHeaders
- + "\n"
- + signedHeaders
- + "\n"
- + hashedRequestPayload;
-
- String timestamp = headers.get("X-TC-Timestamp");
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
- sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
- String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
- String service = endpoint.split("\\.")[0];
- String credentialScope = date + "/" + service + "/" + "tc3_request";
- String hashedCanonicalRequest =
- Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
- String stringToSign =
- "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
-
- String secretId = credSnapshot.getSecretId();
- String secretKey = credSnapshot.getSecretKey();
- byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
- byte[] secretService = Sign.hmac256(secretDate, service);
- byte[] secretSigning = Sign.hmac256(secretService, "tc3_request");
- String signature =
- DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase();
- return "TC3-HMAC-SHA256 "
- + "Credential="
- + secretId
- + "/"
- + credentialScope
- + ", "
- + "SignedHeaders="
- + signedHeaders
- + ", "
- + "Signature="
- + signature;
- }
-
- /**
- * Sends the HTTP request and retrieves the response body.
- *
- * @param url The request URL.
- * @param headers HTTP headers.
- * @param body Request payload.
- * @return The response body as a string.
- * @throws TencentCloudSDKException If an error occurs during the request or response processing.
- */
- private String getResponseBody(String url, HashMap headers, byte[] body)
- throws TencentCloudSDKException {
- Builder hb = new Builder();
- for (String key : headers.keySet()) {
- hb.add(key, headers.get(key));
- }
- Response resp = null;
- try {
- resp = this.httpConnection.postRequest(url, body, hb.build());
- } catch (IOException e) {
- throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage(), e);
- }
- if (resp.code() != AbstractClient.HTTP_RSP_OK) {
- String msg = "response code is " + resp.code() + ", not 200";
- log.info(msg);
- throw new TencentCloudSDKException(msg, "", "ServerSideError");
- }
- String respbody = null;
- try {
- respbody = resp.body().string();
- } catch (IOException e) {
- String msg =
- "Cannot transfer response body to string, because Content-Length is too large, or Content-Length " +
- "and stream length disagree.";
- log.info(msg);
- throw new TencentCloudSDKException(msg, e);
- }
- JsonResponseModel errResp = null;
- try {
- Type errType = new TypeToken>() {
- }.getType();
- errResp = gson.fromJson(respbody, errType);
- } catch (JsonSyntaxException e) {
- String msg = "json is not a valid representation for an object of type";
- log.info(msg);
- throw new TencentCloudSDKException(msg, e);
- }
- if (errResp.response.error != null) {
- throw new TencentCloudSDKException(
- errResp.response.error.message, errResp.response.requestId, errResp.response.error.code);
- }
- return respbody;
- }
-
- private void trySetProxy(HttpConnection conn) {
- String host = this.profile.getHttpProfile().getProxyHost();
- int port = this.profile.getHttpProfile().getProxyPort();
-
- if (host == null || host.isEmpty()) {
- return;
- }
- Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port));
- conn.setProxy(proxy);
-
- final String username = this.profile.getHttpProfile().getProxyUsername();
- final String password = this.profile.getHttpProfile().getProxyPassword();
- if (username == null || username.isEmpty()) {
- return;
- }
- conn.setProxyAuthenticator(
- new Authenticator() {
- @Override
- public Request authenticate(Route route, Response response) throws IOException {
- String credential = Credentials.basic(username, password);
- return response
- .request()
- .newBuilder()
- .header("Proxy-Authorization", credential)
- .build();
- }
- });
- }
-
- private void trySetSSLSocketFactory(HttpConnection conn) {
- SSLSocketFactory sslSocketFactory = this.profile.getHttpProfile().getSslSocketFactory();
- X509TrustManager trustManager = this.profile.getHttpProfile().getX509TrustManager();
- if (sslSocketFactory != null) {
- if (trustManager != null) {
- this.httpConnection.setSSLSocketFactory(sslSocketFactory, trustManager);
- } else {
- this.httpConnection.setSSLSocketFactory(sslSocketFactory);
- }
- }
- }
-
- private void trySetHostnameVerifier(HttpConnection conn) {
- HostnameVerifier hostnameVerifier = this.profile.getHttpProfile().getHostnameVerifier();
- if (hostnameVerifier != null) {
- this.httpConnection.setHostnameVerifier(hostnameVerifier);
- }
- }
-
- private void trySetRegionBreaker() {
- String ep = profile.getBackupEndpoint();
- if (ep != null && !ep.isEmpty()) {
- this.regionBreaker = new CircuitBreaker();
- }
- }
-
- private void trySetHttpClient() {
- Object httpClient = profile.getHttpProfile().getHttpClient();
- if (httpClient != null) {
- this.httpConnection.setHttpClient(httpClient);
- }
- }
-
- /**
- * Executes an API request and returns the raw string response.
- * Handles circuit breaking for region failover.
- *
- * @param request The request object containing API parameters.
- * @param actionName The name of the API action to be called.
- * @return The raw string response from the API.
- * @throws TencentCloudSDKException If an error occurs during the API call.
- */
- protected String internalRequest(AbstractModel request, String actionName)
- throws TencentCloudSDKException {
-
- CircuitBreaker.Token breakerToken = null;
- // Attempt to acquire a token from the circuit breaker.
- // If the circuit is open, use the backup endpoint.
- if (regionBreaker != null) {
- breakerToken = regionBreaker.allow();
- if (!breakerToken.allowed) {
- endpoint = service + "." + profile.getBackupEndpoint();
- }
- }
-
- Response okRsp;
- try {
- // Execute the raw API request.
- okRsp = internalRequestRaw(request, actionName);
- } catch (IOException e) {
- // Network failure: report to circuit breaker and throw exception.
- if (breakerToken != null) {
- breakerToken.report(false);
- }
- throw new TencentCloudSDKException("", e);
- }
-
- String strResp;
- try {
- // Extract the response body as a string.
- strResp = okRsp.body().string();
- } catch (IOException e) {
- String msg = "Cannot transfer response body to string, because Content-Length is too large, or " +
- "Content-Length and stream length disagree.";
- log.info(msg);
- throw new TencentCloudSDKException(msg, e);
- }
-
- JsonResponseModel errResp;
- try {
- // Deserialize the response to check for errors.
- Type errType = new TypeToken>() {
- }.getType();
- errResp = gson.fromJson(strResp, errType);
- } catch (JsonSyntaxException e) {
- // Invalid JSON response: log and throw exception.
- String msg = "json is not a valid representation for an object of type";
- log.info(msg);
- throw new TencentCloudSDKException(msg, e);
- }
-
- // Check for API errors in the response.
- if (errResp.response.error != null) {
- if (breakerToken != null) {
- // Report the success/failure of the request to the circuit breaker.
- JsonResponseErrModel error = errResp.response;
- // Consider a region "OK" if we get a valid requestId and no InternalError.
- boolean regionOk = error.requestId != null
- && !error.requestId.isEmpty()
- && error.error.code != null
- && !error.error.code.equals("InternalError");
- breakerToken.report(regionOk);
- }
- throw new TencentCloudSDKException(
- errResp.response.error.message,
- errResp.response.requestId,
- errResp.response.error.code);
- }
-
- return strResp;
- }
-
- /**
- * Executes an API request and returns the deserialized response object.
- * Handles circuit breaking for region failover.
- *
- * @param request The request object containing API parameters.
- * @param actionName The name of the API action to be called.
- * @param typeOfT The class of the response object to deserialize to.
- * @param The type of the response object.
- * @return The deserialized response object.
- * @throws TencentCloudSDKException If an error occurs during the API call.
- */
- protected T internalRequest(AbstractModel request, String actionName, Class typeOfT)
- throws TencentCloudSDKException {
- CircuitBreaker.Token breakerToken = null;
- // Attempt to acquire a token from the circuit breaker.
- // If the circuit is open, use the backup endpoint.
- if (regionBreaker != null) {
- breakerToken = regionBreaker.allow();
- if (!breakerToken.allowed) {
- endpoint = service + "." + profile.getBackupEndpoint();
- }
- }
-
- try {
- Response resp = internalRequestRaw(request, actionName);
- if (Objects.equals(resp.header("Content-Type"), "text/event-stream")) {
- return processResponseSSE(resp, typeOfT, breakerToken);
- }
- return processResponseJson(resp, typeOfT, breakerToken);
- } catch (IOException e) {
- // Network failure: report to circuit breaker and throw exception.
- if (breakerToken != null) {
- breakerToken.report(false);
- }
- throw new TencentCloudSDKException("", e);
- }
- }
-
- /**
- * Processes a Server-Sent Events (SSE) response.
- *
- * @param resp The raw HTTP response.
- * @param typeOfT The class of the response model.
- * @param breakerToken The circuit breaker token.
- * @param The type of the response model.
- * @return The SSE response model.
- * @throws TencentCloudSDKException If an error occurs during processing.
- */
- protected T processResponseSSE(Response resp, Class typeOfT, CircuitBreaker.Token breakerToken) throws TencentCloudSDKException {
- SSEResponseModel responseModel;
- try {
- // Create a new instance of the response model.
- responseModel = (SSEResponseModel) typeOfT.newInstance();
- } catch (InstantiationException | IllegalAccessException e) {
- throw new TencentCloudSDKException("", e);
- }
- // Set request ID and circuit breaker token in the response model.
- responseModel.setRequestId(resp.header("X-TC-RequestId"));
- responseModel.setToken(breakerToken);
- responseModel.setResponse(resp);
- return (T) responseModel;
- }
-
- /**
- * Processes a JSON response.
- *
- * @param resp The raw HTTP response.
- * @param typeOfT The class of the response object to deserialize to.
- * @param breakerToken The circuit breaker token.
- * @param The type of the response object.
- * @return The deserialized response object.
- * @throws TencentCloudSDKException If an error occurs during processing.
- */
- protected T processResponseJson(Response resp, Class typeOfT, CircuitBreaker.Token breakerToken) throws TencentCloudSDKException {
- String body;
- try {
- body = resp.body().string();
- } catch (IOException e) {
- String msg = "Cannot transfer response body to string, because Content-Length is too large, or " +
- "Content-Length and stream length disagree.";
- log.info(msg);
- throw new TencentCloudSDKException(msg, e);
- }
-
- JsonResponseModel errResp;
- try {
- Type errType = new TypeToken>() {
- }.getType();
- errResp = gson.fromJson(body, errType);
- } catch (JsonSyntaxException e) {
- String msg = "json is not a valid representation for an object of type";
- log.info(msg);
- throw new TencentCloudSDKException(msg, e);
- }
-
- // Check for API errors in the response.
- if (errResp.response.error != null) {
- if (breakerToken != null) {
- // Report the success/failure of the request to the circuit breaker.
- JsonResponseErrModel error = errResp.response;
- // Consider a region "OK" if we get a valid requestId and no InternalError.
- boolean regionOk = error.requestId != null
- && !error.requestId.isEmpty()
- && error.error.code != null
- && !error.error.code.equals("InternalError");
- breakerToken.report(regionOk);
- }
- throw new TencentCloudSDKException(
- errResp.response.error.message,
- errResp.response.requestId,
- errResp.response.error.code);
- }
-
- // Deserialize the successful response into the desired object type.
- Type type = TypeToken.getParameterized(JsonResponseModel.class, typeOfT).getType();
- return ((JsonResponseModel) gson.fromJson(body, type)).response;
- }
-
- /**
- * Executes the raw API request and returns the HTTP Response object.
- *
- * @param request The request object containing API parameters.
- * @param actionName The name of the API action to be called.
- * @return The raw HTTP Response object.
- * @throws TencentCloudSDKException If an error occurs during the API call.
- * @throws IOException If an I/O error occurs.
- */
- protected Response internalRequestRaw(AbstractModel request, String actionName)
- throws TencentCloudSDKException, IOException {
- Response okRsp = null;
- String endpoint = this.getEndpoint();
- String[] binaryParams = request.getBinaryParams();
- String sm = this.profile.getSignMethod();
- String reqMethod = this.profile.getHttpProfile().getReqMethod();
-
- // currently, customized params only can be supported via post json tc3-hmac-sha256
- HashMap customizedParams = request.any();
- if (customizedParams.size() > 0) {
- if (binaryParams.length > 0) {
- throw new TencentCloudSDKException(
- "WrongUsage: Cannot post multipart with customized parameters.");
- }
- if (sm.equals(ClientProfile.SIGN_SHA1) || sm.equals(ClientProfile.SIGN_SHA256)) {
- throw new TencentCloudSDKException(
- "WrongUsage: Cannot use HmacSHA1 or HmacSHA256 with customized parameters.");
- }
- if (reqMethod.equals(HttpProfile.REQ_GET)) {
- throw new TencentCloudSDKException(
- "WrongUsage: Cannot use get method with customized parameters.");
- }
- }
-
-
- if (binaryParams.length > 0 || sm.equals(ClientProfile.SIGN_TC3_256)) {
- okRsp = doRequestWithTC3(endpoint, request, actionName);
- } else if (sm.equals(ClientProfile.SIGN_SHA1) || sm.equals(ClientProfile.SIGN_SHA256)) {
- okRsp = doRequest(endpoint, request, actionName);
- } else {
- throw new TencentCloudSDKException(
- "Signature method " + sm + " is invalid or not supported yet.");
- }
-
- // Check the HTTP response code.
- if (okRsp.code() != AbstractClient.HTTP_RSP_OK) {
- String msg = "response code is " + okRsp.code() + ", not 200";
- log.info(msg);
- throw new TencentCloudSDKException(msg, "", "ServerSideError");
- }
- return okRsp;
- }
-
- /**
- * Executes an API request using the older signature methods (HmacSHA1 or HmacSHA256).
- *
- * @param endpoint The API endpoint.
- * @param request The request object.
- * @param action The API action name.
- * @return The HTTP Response object.
- * @throws TencentCloudSDKException If an error occurs.
- * @throws IOException If an I/O error occurs.
- */
- private Response doRequest(String endpoint, AbstractModel request, String action)
- throws TencentCloudSDKException, IOException {
- HashMap param = new HashMap();
- request.toMap(param, "");
- String strParam = this.formatRequestData(action, param);
- String reqMethod = this.profile.getHttpProfile().getReqMethod();
- String protocol = this.profile.getHttpProfile().getProtocol();
- String url = protocol + endpoint + this.path;
- String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint();
- if (null != apigwEndpoint) {
- url = protocol + apigwEndpoint;
- }
- if (reqMethod.equals(HttpProfile.REQ_GET)) {
- return this.httpConnection.getRequest(url + "?" + strParam);
- } else if (reqMethod.equals(HttpProfile.REQ_POST)) {
- return this.httpConnection.postRequest(url, strParam);
- } else {
- throw new TencentCloudSDKException("Method only support (GET, POST)");
- }
- }
-
- /**
- * Executes an API request using the TC3-HMAC-SHA256 signature method.
- *
- * @param endpoint The API endpoint.
- * @param request The request object.
- * @param action The API action name.
- * @return The HTTP Response object.
- * @throws TencentCloudSDKException If an error occurs.
- * @throws IOException If an I/O error occurs.
- */
- private Response doRequestWithTC3(String endpoint, AbstractModel request, String action)
- throws TencentCloudSDKException, IOException {
- Credential credSnapshot = this.credential.getSnapshot();
- String httpRequestMethod = this.profile.getHttpProfile().getReqMethod();
- if (httpRequestMethod == null) {
- throw new TencentCloudSDKException(
- "Request method should not be null, can only be GET or POST");
- }
- String contentType = "application/x-www-form-urlencoded";
- byte[] requestPayload = "".getBytes(StandardCharsets.UTF_8);
- HashMap params = new HashMap();
- request.toMap(params, "");
- String[] binaryParams = request.getBinaryParams();
- if (binaryParams.length > 0) {
- httpRequestMethod = HttpProfile.REQ_POST;
- String boundary = UUID.randomUUID().toString();
- // okhttp always set charset even we don't specify it,
- // to ensure signature be correct, we have to set it here as well.
- contentType = "multipart/form-data; charset=utf-8" + "; boundary=" + boundary;
- try {
- requestPayload = getMultipartPayload(request, boundary);
- } catch (Exception e) {
- throw new TencentCloudSDKException("Failed to generate multipart.", e);
- }
- } else if (httpRequestMethod.equals(HttpProfile.REQ_POST)) {
- requestPayload = AbstractModel.toJsonString(request).getBytes(StandardCharsets.UTF_8);
- // okhttp always set charset even we don't specify it,
- // to ensure signature be correct, we have to set it here as well.
- contentType = "application/json; charset=utf-8";
- }
- // Construct the canonical request for signature calculation.
- String canonicalUri = "/";
- String canonicalQueryString = this.getCanonicalQueryString(params, httpRequestMethod);
- String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n";
- String signedHeaders = "content-type;host";
-
- String hashedRequestPayload = "";
- if (this.profile.isUnsignedPayload()) {
- hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8));
- } else {
- hashedRequestPayload = Sign.sha256Hex(requestPayload);
- }
- String canonicalRequest =
- httpRequestMethod
- + "\n"
- + canonicalUri
- + "\n"
- + canonicalQueryString
- + "\n"
- + canonicalHeaders
- + "\n"
- + signedHeaders
- + "\n"
- + hashedRequestPayload;
-
- String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
- sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
- String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
- String service = endpoint.split("\\.")[0];
- String credentialScope = date + "/" + service + "/" + "tc3_request";
- String hashedCanonicalRequest =
- Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
- String stringToSign =
- "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
- boolean skipSign = request.getSkipSign();
- String authorization = "";
- if (skipSign) {
- authorization = "SKIP";
- } else {
- String secretId = credSnapshot.getSecretId();
- String secretKey = credSnapshot.getSecretKey();
- byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
- byte[] secretService = Sign.hmac256(secretDate, service);
- byte[] secretSigning = Sign.hmac256(secretService, "tc3_request");
- String signature =
- DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase();
- authorization =
- "TC3-HMAC-SHA256 "
- + "Credential="
- + secretId
- + "/"
- + credentialScope
- + ", "
- + "SignedHeaders="
- + signedHeaders
- + ", "
- + "Signature="
- + signature;
- }
- Builder hb = new Builder();
- hb.add("Content-Type", contentType)
- .add("Host", endpoint)
- .add("Authorization", authorization)
- .add("X-TC-Action", action)
- .add("X-TC-Timestamp", timestamp)
- .add("X-TC-Version", this.apiVersion)
- .add("X-TC-RequestClient", SDK_VERSION);
- if (null != request.GetHeader()) {
- for (Map.Entry entry : request.GetHeader().entrySet()) {
- hb.add(entry.getKey(), entry.getValue());
- }
- }
- if (null != this.getRegion()) {
- hb.add("X-TC-Region", this.getRegion());
- }
- String token = credSnapshot.getToken();
- if (token != null && !token.isEmpty()) {
- hb.add("X-TC-Token", token);
- }
- if (this.profile.isUnsignedPayload()) {
- hb.add("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD");
- }
- if (null != this.profile.getLanguage()) {
- hb.add("X-TC-Language", this.profile.getLanguage().getValue());
- }
-
- String protocol = this.profile.getHttpProfile().getProtocol();
- String url = protocol + endpoint + this.path;
- String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint();
- if (null != apigwEndpoint) {
- url = protocol + apigwEndpoint;
- hb.set("Host", apigwEndpoint);
- }
- Headers headers = hb.build();
- if (httpRequestMethod.equals(HttpProfile.REQ_GET)) {
- return this.httpConnection.getRequest(url + "?" + canonicalQueryString, headers);
- } else if (httpRequestMethod.equals(HttpProfile.REQ_POST)) {
- return this.httpConnection.postRequest(url, requestPayload, headers);
- } else {
- throw new TencentCloudSDKException("Method only support GET, POST");
- }
- }
-
- /**
- * Constructs the multipart payload for file uploads.
- *
- * @param request The request object containing file parameters.
- * @param boundary The boundary string to separate parts of the multipart data.
- * @return The byte array representing the multipart payload.
- * @throws Exception If an error occurs during payload construction.
- */
- private byte[] getMultipartPayload(AbstractModel request, String boundary) throws Exception {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- String[] binaryParams = request.getBinaryParams();
- // Iterate through each parameter in the multipart request.
- for (Map.Entry entry : request.getMultipartRequestParams().entrySet()) {
- baos.write("--".getBytes(StandardCharsets.UTF_8));
- baos.write(boundary.getBytes(StandardCharsets.UTF_8));
- baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
- baos.write("Content-Disposition: form-data; name=\"".getBytes(StandardCharsets.UTF_8));
- baos.write(entry.getKey().getBytes(StandardCharsets.UTF_8));
- if (Arrays.asList(binaryParams).contains(entry.getKey())) {
- baos.write("\"; filename=\"".getBytes(StandardCharsets.UTF_8));
- baos.write(entry.getKey().getBytes(StandardCharsets.UTF_8));
- baos.write("\"\r\n".getBytes(StandardCharsets.UTF_8));
- } else {
- baos.write("\"\r\n".getBytes(StandardCharsets.UTF_8));
- }
- baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
- baos.write(entry.getValue());
- baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
- }
- // Write the closing boundary if there's any data.
- if (baos.size() != 0) {
- baos.write("--".getBytes(StandardCharsets.UTF_8));
- baos.write(boundary.getBytes(StandardCharsets.UTF_8));
- baos.write("--\r\n".getBytes(StandardCharsets.UTF_8));
- }
- byte[] bytes = baos.toByteArray();
- baos.close();
- return bytes;
- }
-
- /**
- * Generates the canonical query string for GET requests.
- *
- * @param params The map of request parameters.
- * @param method The HTTP method (should be GET).
- * @return The canonical query string.
- * @throws TencentCloudSDKException If UTF-8 encoding is not supported.
- */
- private String getCanonicalQueryString(HashMap params, String method)
- throws TencentCloudSDKException {
- // POST requests don't have a query string in the signature.
- if (method != null && method.equals(HttpProfile.REQ_POST)) {
- return "";
- }
- StringBuilder queryString = new StringBuilder("");
- // Iterate through each parameter and build the query string.
- for (Map.Entry entry : params.entrySet()) {
- String v;
- try {
- v = URLEncoder.encode(entry.getValue(), "UTF8");
- } catch (UnsupportedEncodingException e) {
- throw new TencentCloudSDKException("UTF8 is not supported.", e);
- }
- queryString.append("&").append(entry.getKey()).append("=").append(v);
- }
- // Remove the leading '&' if the query string is not empty.
- if (queryString.length() == 0) {
- return "";
- } else {
- return queryString.toString().substring(1);
- }
- }
-
- /**
- * Formats the request data for signing (older signature methods).
- *
- * @param action The API action name.
- * @param param The map of request parameters.
- * @return The formatted string for signing.
- * @throws TencentCloudSDKException If UTF-8 encoding is not supported.
- */
- private String formatRequestData(String action, Map param)
- throws TencentCloudSDKException {
- Credential credSnapshot = this.credential.getSnapshot();
- String secretId = credSnapshot.getSecretId();
- String secretKey = credSnapshot.getSecretKey();
- String token = credSnapshot.getToken();
- param.put("Action", action);
- param.put("RequestClient", this.sdkVersion);
- param.put("Nonce", String.valueOf(Math.abs(new SecureRandom().nextInt())));
- param.put("Timestamp", String.valueOf(System.currentTimeMillis() / 1000));
- param.put("Version", this.apiVersion);
-
- // Add SecretId, Region, SignatureMethod, and Token if available.
- if (secretId != null && (!secretId.isEmpty())) {
- param.put("SecretId", secretId);
- }
-
- if (this.region != null && (!this.region.isEmpty())) {
- param.put("Region", this.region);
- }
-
- if (this.profile.getSignMethod() != null && (!this.profile.getSignMethod().isEmpty())) {
- param.put("SignatureMethod", this.profile.getSignMethod());
- }
-
- if (token != null && (!token.isEmpty())) {
- param.put("Token", token);
- }
-
- if (null != this.profile.getLanguage()) {
- param.put("Language", this.profile.getLanguage().getValue());
- }
-
- String endpoint = this.getEndpoint();
-
- // Generate the string to be signed.
- String sigInParam =
- Sign.makeSignPlainText(
- new TreeMap(param),
- this.profile.getHttpProfile().getReqMethod(),
- endpoint,
- this.path);
- // Generate the signature.
- String sigOutParam =
- Sign.sign(secretKey, sigInParam, this.profile.getSignMethod());
-
- String strParam = "";
- try {
- // URL-encode each parameter and construct the query string.
- for (Map.Entry entry : param.entrySet()) {
- strParam +=
- (URLEncoder.encode(entry.getKey(), "utf-8")
- + "="
- + URLEncoder.encode(entry.getValue(), "utf-8")
- + "&");
- }
- strParam += ("Signature=" + URLEncoder.encode(sigOutParam, "utf-8"));
- } catch (UnsupportedEncodingException e) {
- throw new TencentCloudSDKException("", e);
- }
- return strParam;
- }
-
- /**
- * Performs initializations to avoid performance costs in the first request.
- */
- private void warmup() {
- try {
- // Initialize Mac instances (used for signature calculation).
- // First invoke costs around 250 ms.
- Mac.getInstance("HmacSHA1");
- Mac.getInstance("HmacSHA256");
- // Initialize SSLContext (used for HTTPS connections).
- // First invoke costs around 150 ms.
- SSLContext sslContext = SSLContext.getInstance("TLS");
- sslContext.init(null, null, null);
- } catch (Exception e) {
- // Ignore but print the stack trace to the console for debugging.
- e.printStackTrace();
- }
- }
-
- /**
- * Gets the API endpoint.
- *
- * @return The API endpoint URL.
- */
- private String getEndpoint() {
- // Use the endpoint from the profile if it's set, otherwise construct it from service and domain.
- if (null != this.profile.getHttpProfile().getEndpoint()) {
- return this.profile.getHttpProfile().getEndpoint();
- } else {
- // protected abstract String getService();
- // use this.getService() from overrided subclass will be better
- return this.service + "." + this.profile.getHttpProfile().getRootDomain();
- }
- }
-
- /**
- * 请注意购买类接口谨慎调用,可能导致多次购买
- * 仅幂等接口推荐使用
- *
- * @param req
- * @param retryTimes
- * @throws TencentCloudSDKException
- */
- public Object retry(AbstractModel req, int retryTimes) throws TencentCloudSDKException {
- if (retryTimes < 0 || retryTimes > 10) {
- throw new TencentCloudSDKException("The number of retryTimes supported is 0 to 10.", "", "ClientSideError");
- }
- Class cls = this.getClass();
- String methodName = req.getClass().getSimpleName().replace("Request", "");
- Method method;
- try {
- method = cls.getMethod(methodName, req.getClass());
- } catch (NoSuchMethodException e) {
- throw new TencentCloudSDKException("ClientSideError", e);
- }
- do {
- try {
- return method.invoke(this, req);
- } catch (IllegalAccessException e) {
- throw new TencentCloudSDKException("ClientSideError", e);
- } catch (InvocationTargetException e) {
- if (retryTimes == 0) {
- throw (TencentCloudSDKException) e.getTargetException();
- }
- }
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- throw new TencentCloudSDKException("ClientSideError", e);
- }
- } while (--retryTimes >= 0);
- return null;
- }
-
- public CircuitBreaker getRegionBreaker() {
- return regionBreaker;
- }
-
- public void setRegionBreaker(CircuitBreaker regionBreaker) {
- this.regionBreaker = regionBreaker;
- }
-}
+/*
+ * Copyright (c) 2018 Tencent. All Rights Reserved.
+ *
+ * Licensed 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 com.tencentcloudapi.common;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.common.http.HttpConnection;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import okhttp3.*;
+import okhttp3.Headers.Builder;
+
+import javax.crypto.Mac;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * AbstractClient provides the basic functionalities for interacting with Tencent Cloud services.
+ * It handles request signing, sending, and response processing.
+ */
+public abstract class AbstractClient {
+
+ public static final int HTTP_RSP_OK = 200;
+ public static final String SDK_VERSION = "SDK_JAVA_3.0.1403";
+ public Gson gson;
+
+ // User's security credentials (SecretId, SecretKey, Token).
+ private Credential credential;
+
+ // Client configuration (e.g., timeout, endpoint).
+ private ClientProfile profile;
+
+ // API endpoint URL.
+ private String endpoint;
+
+ // Service name (e.g., "cvm").
+ private String service;
+
+ // Region (e.g., "ap-guangzhou").
+ private String region;
+
+ // API request path (usually "/").
+ private String path;
+
+ // SDK version string.
+ private String sdkVersion;
+
+ // API version string.
+ private String apiVersion;
+
+ // Logger for debugging and information.
+ private TCLog log;
+
+ // Handles HTTP connections.
+ private HttpConnection httpConnection;
+
+ /**
+ * Constructor for AbstractClient with default client profile.
+ *
+ * @param endpoint API endpoint URL.
+ * @param version API version.
+ * @param credential User credentials.
+ * @param region Region.
+ */
+ public AbstractClient(String endpoint, String version, Credential credential, String region) {
+ this(endpoint, version, credential, region, new ClientProfile());
+ }
+
+ /**
+ * Constructor for AbstractClient with a custom client profile.
+ *
+ * @param endpoint API endpoint URL.
+ * @param version API version.
+ * @param credential User credentials.
+ * @param region Region.
+ * @param profile Client configuration profile.
+ */
+ public AbstractClient(
+ String endpoint,
+ String version,
+ Credential credential,
+ String region,
+ ClientProfile profile) {
+ this.credential = credential;
+ this.profile = profile;
+ this.endpoint = endpoint;
+ this.service = endpoint.split("\\.")[0];
+ this.region = region;
+ this.path = "/";
+ this.sdkVersion = AbstractClient.SDK_VERSION;
+ this.apiVersion = version;
+ this.gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+ this.log = new TCLog(getClass().getName(), profile.isDebug());
+ this.httpConnection = new HttpConnection(
+ this.profile.getHttpProfile().getConnTimeout(),
+ this.profile.getHttpProfile().getReadTimeout(),
+ this.profile.getHttpProfile().getWriteTimeout()
+ );
+ this.httpConnection.addInterceptors(this.log);
+ this.trySetProxy(this.httpConnection);
+ this.trySetSSLSocketFactory(this.httpConnection);
+ if (this.profile.isEnableDomainFailover()) {
+ this.httpConnection.addInterceptors(new EndpointFailoverInterceptor(this));
+ }
+ this.trySetHostnameVerifier(this.httpConnection);
+ this.trySetHttpClient();
+ warmup();
+ }
+
+ /**
+ * Gets the region.
+ *
+ * @return The region.
+ */
+ public String getRegion() {
+ return this.region;
+ }
+
+ /**
+ * Sets the region.
+ *
+ * @param region The region to set.
+ */
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ /**
+ * Gets the client profile.
+ *
+ * @return The client profile.
+ */
+ public ClientProfile getClientProfile() {
+ return this.profile;
+ }
+
+ /**
+ * Sets the client profile.
+ *
+ * @param profile The client profile to set.
+ */
+ public void setClientProfile(ClientProfile profile) {
+ this.profile = profile;
+ }
+
+ /**
+ * Gets the credential.
+ *
+ * @return The credential.
+ */
+ public Credential getCredential() {
+ return this.credential;
+ }
+
+ /**
+ * Sets the credential.
+ *
+ * @param credential The credential to set.
+ */
+ public void setCredential(Credential credential) {
+ this.credential = credential;
+ }
+
+ /**
+ * Calls an API action with JSON payload using the TC3-HMAC-SHA256 signature.
+ * Ignores the request method and signature method defined in the profile.
+ *
+ * @param action Name of the API action.
+ * @param jsonPayload JSON string containing the request parameters.
+ * @return Raw response from the API.
+ * @throws TencentCloudSDKException If an error occurs during the API call.
+ */
+ public String call(String action, String jsonPayload) throws TencentCloudSDKException {
+ Credential credSnapshot = this.credential.getSnapshot();
+ byte[] requestPayload = jsonPayload.getBytes(StandardCharsets.UTF_8);
+ String endpoint = this.getEndpoint();
+ String protocol = this.profile.getHttpProfile().getProtocol();
+ String url = protocol + endpoint + this.path;
+ String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint();
+ if (null != apigwEndpoint) {
+ url = protocol + apigwEndpoint;
+ }
+ Headers.Builder callerHeaders = new Headers.Builder();
+ RequestBuilder rb = RequestBuilder.create()
+ .fromClient(this)
+ .withSignMethod(ClientProfile.SIGN_TC3_256)
+ .withURL(HttpUrl.parse(url))
+ .withHost(null != apigwEndpoint ? apigwEndpoint : endpoint)
+ .withMethod(HttpProfile.REQ_POST)
+ .withContentTypeJson()
+ .withPayload(requestPayload)
+ .withHeaders(callerHeaders.build())
+ .withAction(action)
+ .withVersion(this.apiVersion)
+ .withRegion(this.region)
+ .withRequestClient(SDK_VERSION);
+ Response resp;
+ try {
+ resp = this.httpConnection.doRequest(rb.build());
+ } catch (IOException e) {
+ throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage(), e);
+ }
+ return parseResponseBody(resp);
+ }
+
+ /**
+ * Calls an API action with binary payload using the TC3-HMAC-SHA256 signature.
+ * Ignores the request method and signature method defined in the profile.
+ *
+ * @param action Name of the API action.
+ * @param headers HTTP headers to include in the request.
+ * @param body Binary payload (octet-stream).
+ * @return Raw response from the API.
+ * @throws TencentCloudSDKException If an error occurs during the API call.
+ */
+ public String callOctetStream(String action, HashMap headers, byte[] body)
+ throws TencentCloudSDKException {
+ String endpoint = this.getEndpoint();
+ String protocol = this.profile.getHttpProfile().getProtocol();
+ String url = protocol + endpoint + this.path;
+ String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint();
+ if (null != apigwEndpoint) {
+ url = protocol + apigwEndpoint;
+ }
+ Headers.Builder callerHeaders = new Headers.Builder();
+ if (headers != null) {
+ for (Map.Entry entry : headers.entrySet()) {
+ callerHeaders.add(entry.getKey(), entry.getValue());
+ }
+ }
+ RequestBuilder rb = RequestBuilder.create()
+ .fromClient(this)
+ .withSignMethod(ClientProfile.SIGN_TC3_256)
+ .withURL(HttpUrl.parse(url))
+ .withHost(null != apigwEndpoint ? apigwEndpoint : endpoint)
+ .withMethod(HttpProfile.REQ_POST)
+ .withContentTypeOctetStream()
+ .withPayload(body)
+ .withHeaders(callerHeaders.build())
+ .withAction(action)
+ .withVersion(this.apiVersion)
+ .withRegion(this.region)
+ .withRequestClient(SDK_VERSION);
+ Response resp;
+ try {
+ resp = this.httpConnection.doRequest(rb.build());
+ } catch (IOException e) {
+ throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage(), e);
+ }
+ return parseResponseBody(resp);
+ }
+
+ /**
+ * Generates common HTTP headers for Tencent Cloud API requests.
+ *
+ * @param credSnapshot Atomic snapshot of the credential to read from.
+ * @return A HashMap containing the headers.
+ */
+ private HashMap getHeaders(Credential credSnapshot) {
+ HashMap headers = new HashMap();
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+ headers.put("X-TC-Timestamp", timestamp);
+ headers.put("X-TC-Version", this.apiVersion);
+ headers.put("X-TC-Region", this.getRegion());
+ headers.put("X-TC-RequestClient", SDK_VERSION);
+ headers.put("Host", this.getEndpoint());
+ String token = credSnapshot.getToken();
+ if (token != null && !token.isEmpty()) {
+ headers.put("X-TC-Token", token);
+ }
+ if (this.profile.isUnsignedPayload()) {
+ headers.put("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD");
+ }
+ if (null != this.profile.getLanguage()) {
+ headers.put("X-TC-Language", this.profile.getLanguage().getValue());
+ }
+ return headers;
+ }
+
+ /**
+ * Generates the authorization header for TC3-HMAC-SHA256 signature.
+ *
+ * @param headers HTTP headers.
+ * @param body Request payload.
+ * @return The authorization header string.
+ * @throws TencentCloudSDKException If an error occurs during signature generation.
+ */
+ private String getAuthorization(HashMap headers, byte[] body, Credential credSnapshot)
+ throws TencentCloudSDKException {
+ String endpoint = this.getEndpoint();
+ // always use post tc3-hmac-sha256 signature process
+ // okhttp always set charset even we don't specify it,
+ // to ensure signature be correct, we have to set it here as well.
+ String contentType = headers.get("Content-Type");
+ byte[] requestPayload = body;
+ String canonicalUri = "/";
+ String canonicalQueryString = "";
+ String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n";
+ String signedHeaders = "content-type;host";
+
+ String hashedRequestPayload = "";
+ if (this.profile.isUnsignedPayload()) {
+ hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8));
+ } else {
+ hashedRequestPayload = Sign.sha256Hex(requestPayload);
+ }
+ String canonicalRequest =
+ HttpProfile.REQ_POST
+ + "\n"
+ + canonicalUri
+ + "\n"
+ + canonicalQueryString
+ + "\n"
+ + canonicalHeaders
+ + "\n"
+ + signedHeaders
+ + "\n"
+ + hashedRequestPayload;
+
+ String timestamp = headers.get("X-TC-Timestamp");
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
+ String service = endpoint.split("\\.")[0];
+ String credentialScope = date + "/" + service + "/" + "tc3_request";
+ String hashedCanonicalRequest =
+ Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
+ String stringToSign =
+ "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
+
+ String secretId = credSnapshot.getSecretId();
+ String secretKey = credSnapshot.getSecretKey();
+ byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
+ byte[] secretService = Sign.hmac256(secretDate, service);
+ byte[] secretSigning = Sign.hmac256(secretService, "tc3_request");
+ String signature =
+ DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase();
+ return "TC3-HMAC-SHA256 "
+ + "Credential="
+ + secretId
+ + "/"
+ + credentialScope
+ + ", "
+ + "SignedHeaders="
+ + signedHeaders
+ + ", "
+ + "Signature="
+ + signature;
+ }
+
+ /**
+ * Sends the HTTP request and retrieves the response body.
+ *
+ * @param url The request URL.
+ * @param headers HTTP headers.
+ * @param body Request payload.
+ * @return The response body as a string.
+ * @throws TencentCloudSDKException If an error occurs during the request or response processing.
+ */
+ private String getResponseBody(String url, HashMap headers, byte[] body)
+ throws TencentCloudSDKException {
+ Builder hb = new Headers.Builder();
+ for (String key : headers.keySet()) {
+ hb.add(key, headers.get(key));
+ }
+ Response resp = null;
+ try {
+ resp = this.httpConnection.postRequest(url, body, hb.build());
+ } catch (IOException e) {
+ throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage(), e);
+ }
+ if (resp.code() != AbstractClient.HTTP_RSP_OK) {
+ String msg = "response code is " + resp.code() + ", not 200";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, "", "ServerSideError");
+ }
+ String respbody = null;
+ try {
+ respbody = resp.body().string();
+ } catch (IOException e) {
+ String msg =
+ "Cannot transfer response body to string, because Content-Length is too large, or Content-Length " +
+ "and stream length disagree.";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, e);
+ }
+ JsonResponseModel errResp = null;
+ try {
+ Type errType = new TypeToken>() {
+ }.getType();
+ errResp = gson.fromJson(respbody, errType);
+ } catch (JsonSyntaxException e) {
+ String msg = "json is not a valid representation for an object of type";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, e);
+ }
+ if (errResp.response.error != null) {
+ throw new TencentCloudSDKException(
+ errResp.response.error.message, errResp.response.requestId, errResp.response.error.code);
+ }
+ return respbody;
+ }
+
+ private String parseResponseBody(Response resp) throws TencentCloudSDKException {
+ if (resp.code() != AbstractClient.HTTP_RSP_OK) {
+ String msg = "response code is " + resp.code() + ", not 200";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, "", "ServerSideError");
+ }
+ String respbody = null;
+ try {
+ respbody = resp.body().string();
+ } catch (IOException e) {
+ String msg =
+ "Cannot transfer response body to string, because Content-Length is too large, or Content-Length " +
+ "and stream length disagree.";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, e);
+ }
+ JsonResponseModel errResp = null;
+ try {
+ Type errType = new TypeToken>() {
+ }.getType();
+ errResp = gson.fromJson(respbody, errType);
+ } catch (JsonSyntaxException e) {
+ String msg = "json is not a valid representation for an object of type";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, e);
+ }
+ if (errResp.response.error != null) {
+ throw new TencentCloudSDKException(
+ errResp.response.error.message, errResp.response.requestId, errResp.response.error.code);
+ }
+ return respbody;
+ }
+
+ private void trySetProxy(HttpConnection conn) {
+ String host = this.profile.getHttpProfile().getProxyHost();
+ int port = this.profile.getHttpProfile().getProxyPort();
+
+ if (host == null || host.isEmpty()) {
+ return;
+ }
+ Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port));
+ conn.setProxy(proxy);
+
+ final String username = this.profile.getHttpProfile().getProxyUsername();
+ String configuredPassword = this.profile.getHttpProfile().getProxyPassword();
+ final String password = configuredPassword == null ? "" : configuredPassword;
+ if (username == null || username.isEmpty()) {
+ return;
+ }
+ conn.setProxyAuthenticator(
+ new Authenticator() {
+ @Override
+ public Request authenticate(Route route, Response response) throws IOException {
+ String credential = Credentials.basic(username, password);
+ return response
+ .request()
+ .newBuilder()
+ .header("Proxy-Authorization", credential)
+ .build();
+ }
+ });
+ }
+
+ private void trySetSSLSocketFactory(HttpConnection conn) {
+ SSLSocketFactory sslSocketFactory = this.profile.getHttpProfile().getSslSocketFactory();
+ X509TrustManager trustManager = this.profile.getHttpProfile().getX509TrustManager();
+ if (sslSocketFactory != null) {
+ if (trustManager != null) {
+ this.httpConnection.setSSLSocketFactory(sslSocketFactory, trustManager);
+ } else {
+ this.httpConnection.setSSLSocketFactory(sslSocketFactory);
+ }
+ }
+ }
+
+ private void trySetHostnameVerifier(HttpConnection conn) {
+ HostnameVerifier hostnameVerifier = this.profile.getHttpProfile().getHostnameVerifier();
+ if (hostnameVerifier != null) {
+ this.httpConnection.setHostnameVerifier(hostnameVerifier);
+ }
+ }
+
+ private void trySetHttpClient() {
+ Object httpClient = profile.getHttpProfile().getHttpClient();
+ if (httpClient != null) {
+ this.httpConnection.setHttpClient(httpClient);
+ }
+ }
+
+ /**
+ * Executes an API request and returns the raw string response.
+ *
+ * @param request The request object containing API parameters.
+ * @param actionName The name of the API action to be called.
+ * @return The raw string response from the API.
+ * @throws TencentCloudSDKException If an error occurs during the API call.
+ */
+ protected String internalRequest(AbstractModel request, String actionName)
+ throws TencentCloudSDKException {
+
+ Response okRsp;
+ try {
+ okRsp = internalRequestRaw(request, actionName);
+ } catch (IOException e) {
+ throw new TencentCloudSDKException("", e);
+ }
+
+ String strResp;
+ try {
+ strResp = okRsp.body().string();
+ } catch (IOException e) {
+ String msg = "Cannot transfer response body to string, because Content-Length is too large, or " +
+ "Content-Length and stream length disagree.";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, e);
+ }
+
+ JsonResponseModel errResp;
+ try {
+ Type errType = new TypeToken>() {
+ }.getType();
+ errResp = gson.fromJson(strResp, errType);
+ } catch (JsonSyntaxException e) {
+ String msg = "json is not a valid representation for an object of type";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, e);
+ }
+
+ if (errResp.response.error != null) {
+ throw new TencentCloudSDKException(
+ errResp.response.error.message,
+ errResp.response.requestId,
+ errResp.response.error.code);
+ }
+
+ return strResp;
+ }
+
+ /**
+ * Executes an API request and returns the deserialized response object.
+ *
+ * @param request The request object containing API parameters.
+ * @param actionName The name of the API action to be called.
+ * @param typeOfT The class of the response object to deserialize to.
+ * @param The type of the response object.
+ * @return The deserialized response object.
+ * @throws TencentCloudSDKException If an error occurs during the API call.
+ */
+ protected T internalRequest(AbstractModel request, String actionName, Class typeOfT)
+ throws TencentCloudSDKException {
+ try {
+ Response resp = internalRequestRaw(request, actionName);
+ if (Objects.equals(resp.header("Content-Type"), "text/event-stream")) {
+ return processResponseSSE(resp, typeOfT);
+ }
+ return processResponseJson(resp, typeOfT);
+ } catch (IOException e) {
+ throw new TencentCloudSDKException("", e);
+ }
+ }
+
+ /**
+ * Processes a Server-Sent Events (SSE) response.
+ *
+ * @param resp The raw HTTP response.
+ * @param typeOfT The class of the response model.
+ * @param The type of the response model.
+ * @return The SSE response model.
+ * @throws TencentCloudSDKException If an error occurs during processing.
+ */
+ protected T processResponseSSE(Response resp, Class typeOfT) throws TencentCloudSDKException {
+ SSEResponseModel responseModel;
+ try {
+ responseModel = (SSEResponseModel) typeOfT.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new TencentCloudSDKException("", e);
+ }
+ responseModel.setRequestId(resp.header("X-TC-RequestId"));
+ responseModel.setResponse(resp);
+ return (T) responseModel;
+ }
+
+ /**
+ * Legacy three-arg overload. The {@code breakerToken} is ignored — region
+ * failover is now handled by {@link EndpointFailoverInterceptor} at the HTTP
+ * layer, not via a per-call CircuitBreaker token. Kept so subclasses or
+ * external callers compiled against earlier SDK versions still link.
+ *
+ * @deprecated Use {@link #processResponseSSE(Response, Class)} instead.
+ */
+ @Deprecated
+ protected T processResponseSSE(Response resp, Class typeOfT, CircuitBreaker.Token breakerToken)
+ throws TencentCloudSDKException {
+ return processResponseSSE(resp, typeOfT);
+ }
+
+ /**
+ * Processes a JSON response.
+ *
+ * @param resp The raw HTTP response.
+ * @param typeOfT The class of the response object to deserialize to.
+ * @param The type of the response object.
+ * @return The deserialized response object.
+ * @throws TencentCloudSDKException If an error occurs during processing.
+ */
+ protected T processResponseJson(Response resp, Class typeOfT) throws TencentCloudSDKException {
+ String body;
+ try {
+ body = resp.body().string();
+ } catch (IOException e) {
+ String msg = "Cannot transfer response body to string, because Content-Length is too large, or " +
+ "Content-Length and stream length disagree.";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, e);
+ }
+
+ JsonResponseModel errResp;
+ try {
+ Type errType = new TypeToken>() {
+ }.getType();
+ errResp = gson.fromJson(body, errType);
+ } catch (JsonSyntaxException e) {
+ String msg = "json is not a valid representation for an object of type";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, e);
+ }
+
+ if (errResp.response.error != null) {
+ throw new TencentCloudSDKException(
+ errResp.response.error.message,
+ errResp.response.requestId,
+ errResp.response.error.code);
+ }
+
+ Type type = TypeToken.getParameterized(JsonResponseModel.class, typeOfT).getType();
+ return ((JsonResponseModel) gson.fromJson(body, type)).response;
+ }
+
+ /**
+ * Legacy three-arg overload. The {@code breakerToken} is ignored — region
+ * failover is now handled by {@link EndpointFailoverInterceptor} at the HTTP
+ * layer, not via a per-call CircuitBreaker token. Kept so subclasses or
+ * external callers compiled against earlier SDK versions still link.
+ *
+ * @deprecated Use {@link #processResponseJson(Response, Class)} instead.
+ */
+ @Deprecated
+ protected T processResponseJson(Response resp, Class typeOfT, CircuitBreaker.Token breakerToken)
+ throws TencentCloudSDKException {
+ return processResponseJson(resp, typeOfT);
+ }
+
+ /**
+ * Executes the raw API request and returns the HTTP Response object.
+ *
+ * @param request The request object containing API parameters.
+ * @param actionName The name of the API action to be called.
+ * @return The raw HTTP Response object.
+ * @throws TencentCloudSDKException If an error occurs during the API call.
+ * @throws IOException If an I/O error occurs.
+ */
+ protected Response internalRequestRaw(AbstractModel request, String actionName)
+ throws TencentCloudSDKException, IOException {
+ Response okRsp = null;
+ String endpoint = this.getEndpoint();
+ String[] binaryParams = request.getBinaryParams();
+ String sm = this.profile.getSignMethod();
+ String reqMethod = this.profile.getHttpProfile().getReqMethod();
+
+ // currently, customized params only can be supported via post json tc3-hmac-sha256
+ HashMap customizedParams = request.any();
+ if (customizedParams.size() > 0) {
+ if (binaryParams.length > 0) {
+ throw new TencentCloudSDKException(
+ "WrongUsage: Cannot post multipart with customized parameters.");
+ }
+ if (sm.equals(ClientProfile.SIGN_SHA1) || sm.equals(ClientProfile.SIGN_SHA256)) {
+ throw new TencentCloudSDKException(
+ "WrongUsage: Cannot use HmacSHA1 or HmacSHA256 with customized parameters.");
+ }
+ if (reqMethod.equals(HttpProfile.REQ_GET)) {
+ throw new TencentCloudSDKException(
+ "WrongUsage: Cannot use get method with customized parameters.");
+ }
+ }
+
+
+ if (binaryParams.length > 0 || sm.equals(ClientProfile.SIGN_TC3_256)) {
+ okRsp = doRequestWithTC3(endpoint, request, actionName);
+ } else if (sm.equals(ClientProfile.SIGN_SHA1) || sm.equals(ClientProfile.SIGN_SHA256)) {
+ okRsp = doRequest(endpoint, request, actionName);
+ } else {
+ throw new TencentCloudSDKException(
+ "Signature method " + sm + " is invalid or not supported yet.");
+ }
+
+ // Check the HTTP response code.
+ if (okRsp.code() != AbstractClient.HTTP_RSP_OK) {
+ String msg = "response code is " + okRsp.code() + ", not 200";
+ log.info(msg);
+ throw new TencentCloudSDKException(msg, "", "ServerSideError");
+ }
+ return okRsp;
+ }
+
+ /**
+ * Executes an API request using the older signature methods (HmacSHA1 or HmacSHA256).
+ *
+ * @param endpoint The API endpoint.
+ * @param request The request object.
+ * @param action The API action name.
+ * @return The HTTP Response object.
+ * @throws TencentCloudSDKException If an error occurs.
+ * @throws IOException If an I/O error occurs.
+ */
+ private Response doRequest(String endpoint, AbstractModel request, String action)
+ throws TencentCloudSDKException, IOException {
+ String reqMethod = this.profile.getHttpProfile().getReqMethod();
+ String protocol = this.profile.getHttpProfile().getProtocol();
+ String url = protocol + endpoint + this.path;
+ String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint();
+ if (null != apigwEndpoint) {
+ url = protocol + apigwEndpoint;
+ }
+ Headers.Builder callerHeaders = new Headers.Builder();
+ if (null != request.GetHeader()) {
+ for (Map.Entry entry : request.GetHeader().entrySet()) {
+ callerHeaders.add(entry.getKey(), entry.getValue());
+ }
+ }
+ RequestBuilder rb = RequestBuilder.create()
+ .fromClient(this)
+ .withSignMethod(this.profile.getSignMethod())
+ .withURL(HttpUrl.parse(url))
+ .withHost(endpoint)
+ .withMethod(reqMethod)
+ .withContentTypeForm()
+ .withPayload(request)
+ .withHeaders(callerHeaders.build())
+ .withAction(action)
+ .withVersion(this.apiVersion)
+ .withRegion(this.region)
+ .withRequestClient(SDK_VERSION)
+ .withNonce(Math.abs(new SecureRandom().nextInt()));
+ return this.httpConnection.doRequest(rb.build());
+ }
+
+ /**
+ * Executes an API request using the TC3-HMAC-SHA256 signature method.
+ *
+ * @param endpoint The API endpoint.
+ * @param request The request object.
+ * @param action The API action name.
+ * @return The HTTP Response object.
+ * @throws TencentCloudSDKException If an error occurs.
+ * @throws IOException If an I/O error occurs.
+ */
+ private Response doRequestWithTC3(String endpoint, AbstractModel request, String action)
+ throws TencentCloudSDKException, IOException {
+ Credential credSnapshot = this.credential.getSnapshot();
+ String httpRequestMethod = this.profile.getHttpProfile().getReqMethod();
+ if (httpRequestMethod == null) {
+ throw new TencentCloudSDKException(
+ "Request method should not be null, can only be GET or POST");
+ }
+ String contentType = "application/x-www-form-urlencoded";
+ byte[] requestPayload = "".getBytes(StandardCharsets.UTF_8);
+ HashMap params = new HashMap();
+ request.toMap(params, "");
+ String[] binaryParams = request.getBinaryParams();
+ if (binaryParams.length > 0) {
+ httpRequestMethod = HttpProfile.REQ_POST;
+ String boundary = UUID.randomUUID().toString();
+ // okhttp always set charset even we don't specify it,
+ // to ensure signature be correct, we have to set it here as well.
+ contentType = "multipart/form-data; charset=utf-8" + "; boundary=" + boundary;
+ try {
+ requestPayload = getMultipartPayload(request, boundary);
+ } catch (Exception e) {
+ throw new TencentCloudSDKException("Failed to generate multipart.", e);
+ }
+ } else if (httpRequestMethod.equals(HttpProfile.REQ_POST)) {
+ requestPayload = AbstractModel.toJsonString(request).getBytes(StandardCharsets.UTF_8);
+ // okhttp always set charset even we don't specify it,
+ // to ensure signature be correct, we have to set it here as well.
+ contentType = "application/json; charset=utf-8";
+ }
+ if (binaryParams.length == 0) {
+ String protocol = this.profile.getHttpProfile().getProtocol();
+ String url = protocol + endpoint + this.path;
+ String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint();
+ if (null != apigwEndpoint) {
+ url = protocol + apigwEndpoint;
+ }
+ Headers.Builder callerHeaders = new Headers.Builder();
+ if (null != request.GetHeader()) {
+ for (Map.Entry entry : request.GetHeader().entrySet()) {
+ callerHeaders.add(entry.getKey(), entry.getValue());
+ }
+ }
+ RequestBuilder rb = RequestBuilder.create()
+ .fromClient(this)
+ .withSignMethod(request.getSkipSign() ? "SKIP" : ClientProfile.SIGN_TC3_256)
+ .withURL(HttpUrl.parse(url))
+ .withHost(endpoint)
+ .withMethod(httpRequestMethod)
+ .withContentType(contentType)
+ .withPayload(request)
+ .withHeaders(callerHeaders.build())
+ .withAction(action)
+ .withVersion(this.apiVersion)
+ .withRegion(this.region)
+ .withRequestClient(SDK_VERSION);
+ return this.httpConnection.doRequest(rb.build());
+ }
+ // Construct the canonical request for signature calculation.
+ String canonicalUri = "/";
+ String canonicalQueryString = this.getCanonicalQueryString(params, httpRequestMethod);
+ String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n";
+ String signedHeaders = "content-type;host";
+
+ String hashedRequestPayload = "";
+ if (this.profile.isUnsignedPayload()) {
+ hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8));
+ } else {
+ hashedRequestPayload = Sign.sha256Hex(requestPayload);
+ }
+ String canonicalRequest =
+ httpRequestMethod
+ + "\n"
+ + canonicalUri
+ + "\n"
+ + canonicalQueryString
+ + "\n"
+ + canonicalHeaders
+ + "\n"
+ + signedHeaders
+ + "\n"
+ + hashedRequestPayload;
+
+ String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
+ String service = endpoint.split("\\.")[0];
+ String credentialScope = date + "/" + service + "/" + "tc3_request";
+ String hashedCanonicalRequest =
+ Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
+ String stringToSign =
+ "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
+ boolean skipSign = request.getSkipSign();
+ String authorization = "";
+ if (skipSign) {
+ authorization = "SKIP";
+ } else {
+ String secretId = credSnapshot.getSecretId();
+ String secretKey = credSnapshot.getSecretKey();
+ byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
+ byte[] secretService = Sign.hmac256(secretDate, service);
+ byte[] secretSigning = Sign.hmac256(secretService, "tc3_request");
+ String signature =
+ DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase();
+ authorization =
+ "TC3-HMAC-SHA256 "
+ + "Credential="
+ + secretId
+ + "/"
+ + credentialScope
+ + ", "
+ + "SignedHeaders="
+ + signedHeaders
+ + ", "
+ + "Signature="
+ + signature;
+ }
+ Builder hb = new Headers.Builder();
+ hb.add("Content-Type", contentType)
+ .add("Host", endpoint)
+ .add("Authorization", authorization)
+ .add("X-TC-Action", action)
+ .add("X-TC-Timestamp", timestamp)
+ .add("X-TC-Version", this.apiVersion)
+ .add("X-TC-RequestClient", SDK_VERSION);
+ if (null != request.GetHeader()) {
+ for (Map.Entry entry : request.GetHeader().entrySet()) {
+ hb.add(entry.getKey(), entry.getValue());
+ }
+ }
+ if (null != this.getRegion()) {
+ hb.add("X-TC-Region", this.getRegion());
+ }
+ String token = credSnapshot.getToken();
+ if (token != null && !token.isEmpty()) {
+ hb.add("X-TC-Token", token);
+ }
+ if (this.profile.isUnsignedPayload()) {
+ hb.add("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD");
+ }
+ if (null != this.profile.getLanguage()) {
+ hb.add("X-TC-Language", this.profile.getLanguage().getValue());
+ }
+
+ String protocol = this.profile.getHttpProfile().getProtocol();
+ String url = protocol + endpoint + this.path;
+ String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint();
+ if (null != apigwEndpoint) {
+ url = protocol + apigwEndpoint;
+ }
+ // Tag the outgoing multipart request with a RequestBuilder so failover
+ // can re-sign for an alternate host. Multipart body is treated as
+ // octet-stream for re-signing purposes (raw bytes, content-type preserved
+ // via the Content-Type header).
+ Headers.Builder callerHeaders = new Headers.Builder();
+ if (null != request.GetHeader()) {
+ for (Map.Entry entry : request.GetHeader().entrySet()) {
+ callerHeaders.add(entry.getKey(), entry.getValue());
+ }
+ }
+ RequestBuilder rb = RequestBuilder.create()
+ .fromClient(this)
+ .withSignMethod(request.getSkipSign() ? "SKIP" : ClientProfile.SIGN_TC3_256)
+ .withURL(HttpUrl.parse(url))
+ .withHost(null != apigwEndpoint ? apigwEndpoint : endpoint)
+ .withMethod(httpRequestMethod)
+ .withContentTypeOctetStream()
+ .withPayload(requestPayload)
+ .withHeaders(callerHeaders.build())
+ .withAction(action)
+ .withVersion(this.apiVersion)
+ .withRegion(this.region)
+ .withRequestClient(SDK_VERSION);
+ Request.Builder overrideBuilder = rb.build().newBuilder()
+ .header("Content-Type", contentType);
+ return this.httpConnection.doRequest(overrideBuilder.build());
+ }
+
+ /**
+ * Constructs the multipart payload for file uploads.
+ *
+ * @param request The request object containing file parameters.
+ * @param boundary The boundary string to separate parts of the multipart data.
+ * @return The byte array representing the multipart payload.
+ * @throws Exception If an error occurs during payload construction.
+ */
+ private byte[] getMultipartPayload(AbstractModel request, String boundary) throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ String[] binaryParams = request.getBinaryParams();
+ // Iterate through each parameter in the multipart request.
+ for (Map.Entry entry : request.getMultipartRequestParams().entrySet()) {
+ baos.write("--".getBytes(StandardCharsets.UTF_8));
+ baos.write(boundary.getBytes(StandardCharsets.UTF_8));
+ baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
+ baos.write("Content-Disposition: form-data; name=\"".getBytes(StandardCharsets.UTF_8));
+ baos.write(entry.getKey().getBytes(StandardCharsets.UTF_8));
+ if (Arrays.asList(binaryParams).contains(entry.getKey())) {
+ baos.write("\"; filename=\"".getBytes(StandardCharsets.UTF_8));
+ baos.write(entry.getKey().getBytes(StandardCharsets.UTF_8));
+ baos.write("\"\r\n".getBytes(StandardCharsets.UTF_8));
+ } else {
+ baos.write("\"\r\n".getBytes(StandardCharsets.UTF_8));
+ }
+ baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
+ baos.write(entry.getValue());
+ baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
+ }
+ // Write the closing boundary if there's any data.
+ if (baos.size() != 0) {
+ baos.write("--".getBytes(StandardCharsets.UTF_8));
+ baos.write(boundary.getBytes(StandardCharsets.UTF_8));
+ baos.write("--\r\n".getBytes(StandardCharsets.UTF_8));
+ }
+ byte[] bytes = baos.toByteArray();
+ baos.close();
+ return bytes;
+ }
+
+ /**
+ * Generates the canonical query string for GET requests.
+ *
+ * @param params The map of request parameters.
+ * @param method The HTTP method (should be GET).
+ * @return The canonical query string.
+ * @throws TencentCloudSDKException If UTF-8 encoding is not supported.
+ */
+ private String getCanonicalQueryString(HashMap params, String method)
+ throws TencentCloudSDKException {
+ // POST requests don't have a query string in the signature.
+ if (method != null && method.equals(HttpProfile.REQ_POST)) {
+ return "";
+ }
+ StringBuilder queryString = new StringBuilder("");
+ // Iterate through each parameter and build the query string.
+ for (Map.Entry entry : params.entrySet()) {
+ String v;
+ try {
+ v = URLEncoder.encode(entry.getValue(), "UTF8");
+ } catch (UnsupportedEncodingException e) {
+ throw new TencentCloudSDKException("UTF8 is not supported.", e);
+ }
+ queryString.append("&").append(entry.getKey()).append("=").append(v);
+ }
+ // Remove the leading '&' if the query string is not empty.
+ if (queryString.length() == 0) {
+ return "";
+ } else {
+ return queryString.toString().substring(1);
+ }
+ }
+
+ /**
+ * Formats the request data for signing (older signature methods).
+ *
+ * @param action The API action name.
+ * @param param The map of request parameters.
+ * @return The formatted string for signing.
+ * @throws TencentCloudSDKException If UTF-8 encoding is not supported.
+ */
+ private String formatRequestData(String action, Map param)
+ throws TencentCloudSDKException {
+ Credential credSnapshot = this.credential.getSnapshot();
+ String secretId = credSnapshot.getSecretId();
+ String secretKey = credSnapshot.getSecretKey();
+ String token = credSnapshot.getToken();
+ param.put("Action", action);
+ param.put("RequestClient", this.sdkVersion);
+ param.put("Nonce", String.valueOf(Math.abs(new SecureRandom().nextInt())));
+ param.put("Timestamp", String.valueOf(System.currentTimeMillis() / 1000));
+ param.put("Version", this.apiVersion);
+
+ // Add SecretId, Region, SignatureMethod, and Token if available.
+ if (secretId != null && (!secretId.isEmpty())) {
+ param.put("SecretId", secretId);
+ }
+
+ if (this.region != null && (!this.region.isEmpty())) {
+ param.put("Region", this.region);
+ }
+
+ if (this.profile.getSignMethod() != null && (!this.profile.getSignMethod().isEmpty())) {
+ param.put("SignatureMethod", this.profile.getSignMethod());
+ }
+
+ if (token != null && (!token.isEmpty())) {
+ param.put("Token", token);
+ }
+
+ if (null != this.profile.getLanguage()) {
+ param.put("Language", this.profile.getLanguage().getValue());
+ }
+
+ String endpoint = this.getEndpoint();
+
+ // Generate the string to be signed.
+ String sigInParam =
+ Sign.makeSignPlainText(
+ new TreeMap(param),
+ this.profile.getHttpProfile().getReqMethod(),
+ endpoint,
+ this.path);
+ // Generate the signature.
+ String sigOutParam =
+ Sign.sign(secretKey, sigInParam, this.profile.getSignMethod());
+
+ String strParam = "";
+ try {
+ // URL-encode each parameter and construct the query string.
+ for (Map.Entry entry : param.entrySet()) {
+ strParam +=
+ (URLEncoder.encode(entry.getKey(), "utf-8")
+ + "="
+ + URLEncoder.encode(entry.getValue(), "utf-8")
+ + "&");
+ }
+ strParam += ("Signature=" + URLEncoder.encode(sigOutParam, "utf-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new TencentCloudSDKException("", e);
+ }
+ return strParam;
+ }
+
+ /**
+ * Performs initializations to avoid performance costs in the first request.
+ */
+ private void warmup() {
+ try {
+ // Initialize Mac instances (used for signature calculation).
+ // First invoke costs around 250 ms.
+ Mac.getInstance("HmacSHA1");
+ Mac.getInstance("HmacSHA256");
+ // Initialize SSLContext (used for HTTPS connections).
+ // First invoke costs around 150 ms.
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, null, null);
+ } catch (Exception e) {
+ // Ignore but print the stack trace to the console for debugging.
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Gets the API endpoint.
+ *
+ * @return The API endpoint URL.
+ */
+ private String getEndpoint() {
+ // Use the endpoint from the profile if it's set, otherwise construct it from service and domain.
+ if (null != this.profile.getHttpProfile().getEndpoint()) {
+ return this.profile.getHttpProfile().getEndpoint();
+ } else {
+ // protected abstract String getService();
+ // use this.getService() from overrided subclass will be better
+ return this.service + "." + this.profile.getHttpProfile().getRootDomain();
+ }
+ }
+
+ String getServiceNameForFailover() {
+ return this.service;
+ }
+
+ /**
+ * 请注意购买类接口谨慎调用,可能导致多次购买
+ * 仅幂等接口推荐使用
+ *
+ * @param req
+ * @param retryTimes
+ * @throws TencentCloudSDKException
+ */
+ public Object retry(AbstractModel req, int retryTimes) throws TencentCloudSDKException {
+ if (retryTimes < 0 || retryTimes > 10) {
+ throw new TencentCloudSDKException("The number of retryTimes supported is 0 to 10.", "", "ClientSideError");
+ }
+ Class cls = this.getClass();
+ String methodName = req.getClass().getSimpleName().replace("Request", "");
+ Method method;
+ try {
+ method = cls.getMethod(methodName, req.getClass());
+ } catch (NoSuchMethodException e) {
+ throw new TencentCloudSDKException("ClientSideError", e);
+ }
+ do {
+ try {
+ return method.invoke(this, req);
+ } catch (IllegalAccessException e) {
+ throw new TencentCloudSDKException("ClientSideError", e);
+ } catch (InvocationTargetException e) {
+ if (retryTimes == 0) {
+ throw (TencentCloudSDKException) e.getTargetException();
+ }
+ }
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ throw new TencentCloudSDKException("ClientSideError", e);
+ }
+ } while (--retryTimes >= 0);
+ return null;
+ }
+
+ /**
+ * Returns the circuit breaker previously set by
+ * {@link #setRegionBreaker(CircuitBreaker)}.
+ *
+ * @return The circuit breaker, or null if none was set.
+ */
+ public CircuitBreaker getRegionBreaker() {
+ EndpointFailoverInterceptor interceptor = getFailoverInterceptor();
+ return interceptor != null ? interceptor.getRegionBreaker() : null;
+ }
+
+ /**
+ * Sets the circuit breaker to use for endpoint failover. The breaker's
+ * settings (maxFailNum, maxFailPercentage, windowIntervalMs, timeoutMs)
+ * are copied and applied to all per-host breakers created by the
+ * {@link EndpointFailoverInterceptor}.
+ *
+ * @param regionBreaker The circuit breaker whose settings will be used for failover.
+ */
+ public void setRegionBreaker(CircuitBreaker regionBreaker) {
+ EndpointFailoverInterceptor interceptor = getFailoverInterceptor();
+ if (interceptor != null) {
+ interceptor.setRegionBreaker(regionBreaker);
+ }
+ }
+
+ private EndpointFailoverInterceptor getFailoverInterceptor() {
+ for (okhttp3.Interceptor i : this.httpConnection.getInterceptors()) {
+ if (i instanceof EndpointFailoverInterceptor) {
+ return (EndpointFailoverInterceptor) i;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/tencentcloudapi/common/AbstractModel.java b/src/main/java/com/tencentcloudapi/common/AbstractModel.java
index 638ad0b71..31d2cfb7e 100644
--- a/src/main/java/com/tencentcloudapi/common/AbstractModel.java
+++ b/src/main/java/com/tencentcloudapi/common/AbstractModel.java
@@ -1,274 +1,274 @@
-/*
- * Copyright (c) 2018 Tencent. All Rights Reserved.
- *
- * Licensed 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 com.tencentcloudapi.common;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-
-import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Abstract base class for all model objects in the Tencent Cloud SDK.
- *
This class provides utility methods for serializing to and deserializing from JSON,
- * managing customized parameters, and handling HTTP headers and requests.
- */
-public abstract class AbstractModel {
-
- /**
- * Stores HTTP headers for the request.
- */
- public Map header = new HashMap();
-
- /**
- * Flag to indicate whether to skip signing for this request. Defaults to false.
- */
- protected boolean skipSign = false;
-
- /**
- * Stores any custom parameters that are not part of the predefined model fields.
- * These can be added dynamically and should be serializable to JSON.
- */
- private HashMap customizedParams = new HashMap();
-
- /**
- * Convert an object of type O to its JSON string representation.
- *
- * @param obj The object to be serialized to JSON.
- * @param The type of the object.
- * @return A JSON string representation of the object.
- */
- public static String toJsonString(O obj) {
- return toJsonObject(obj).toString();
- }
-
- /**
- * Recursively generates a JSON object from the model object.
- * This method handles serialization for both custom parameters and regular fields.
- *
- * @param obj The object to be serialized.
- * @param The type of the object.
- * @return A JSON object representation of the model.
- */
- private static JsonObject toJsonObject(O obj) {
- Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
- JsonObject joall = new JsonObject();
-
- // Serialize customized parameters
- JsonObject joadd = gson.toJsonTree(obj.any()).getAsJsonObject();
- for (Map.Entry entry : joadd.entrySet()) {
- joall.add(entry.getKey(), entry.getValue());
- }
-
- // Serialize regular fields of the object
- JsonObject jopublic = gson.toJsonTree(obj).getAsJsonObject();
- for (Map.Entry entry : jopublic.entrySet()) {
- Object fo = null;
- try {
- // Access each field using reflection
- Field f = obj.getClass().getDeclaredField(entry.getKey());
- f.setAccessible(true);
- fo = f.get(obj);
- } catch (Exception e) {
- // This should never happen
- e.printStackTrace();
- }
- if (fo instanceof AbstractModel) {
- // If the field is an AbstractModel, recursively serialize it
- joall.add(entry.getKey(), toJsonObject((AbstractModel) fo));
- } else {
- // Otherwise, directly add the value to the JSON object
- joall.add(entry.getKey(), entry.getValue());
- }
- }
- return joall;
- }
-
- /**
- * Deserialize a JSON string into an object of a subclass of AbstractModel.
- *
- * @param json The JSON string to be deserialized.
- * @param cls The class of the target model type.
- * @param The type of the object to deserialize.
- * @return An object of type O.
- */
- public static O fromJsonString(String json, Class cls) {
- Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
- return gson.fromJson(json, cls);
- }
-
- /**
- * Abstract method to be implemented by subclasses.
- * Used to convert the model into a map for HTTP requests.
- *
- * @param map The map to populate with the model's parameters.
- * @param prefix A string prefix for the keys.
- */
- protected abstract void toMap(HashMap map, String prefix);
-
- /**
- * Mark which parameters are binary type (for multipart requests).
- *
- * @return An array of parameter names that are binary.
- */
- protected String[] getBinaryParams() {
- return new String[0];
- }
-
- /**
- * Return the multipart request parameters (if any).
- *
- * @return A map of multipart parameters.
- */
- protected HashMap getMultipartRequestParams() {
- return new HashMap();
- }
-
- /**
- * Helper method to set a simple parameter in the map.
- * Converts the parameter name and value to a string.
- *
- * @param map The map to put the parameter into.
- * @param key The key for the parameter.
- * @param value The value of the parameter.
- * @param The type of the value.
- */
- protected void setParamSimple(HashMap map, String key, V value) {
- if (value != null) {
- key = key.substring(0, 1).toUpperCase() + key.substring(1);
- key = key.replace("_", ".");
- map.put(key, String.valueOf(value));
- }
- }
-
- /**
- * Helper method to set an array of simple parameters in the map.
- *
- * @param map The map to put the parameters into.
- * @param prefix The prefix for each key.
- * @param array The array of values.
- * @param The type of the values in the array.
- */
- protected void setParamArraySimple(HashMap map, String prefix, V[] array) {
- if (array != null) {
- for (int i = 0; i < array.length; i++) {
- this.setParamSimple(map, prefix + i, array[i]);
- }
- }
- }
-
- /**
- * Helper method to set an object parameter in the map.
- * Recursively calls toMap() for complex model objects.
- *
- * @param map The map to put the parameter into.
- * @param prefix The prefix for the key.
- * @param obj The object to add to the map.
- * @param The type of the object.
- */
- protected void setParamObj(
- HashMap map, String prefix, V obj) {
- if (obj != null) {
- obj.toMap(map, prefix);
- }
- }
-
- /**
- * Helper method to set an array of object parameters in the map.
- *
- * @param map The map to put the parameters into.
- * @param prefix The prefix for each key.
- * @param array The array of object values.
- * @param The type of the objects in the array.
- */
- protected void setParamArrayObj(
- HashMap map, String prefix, V[] array) {
- if (array != null) {
- for (int i = 0; i < array.length; i++) {
- this.setParamObj(map, prefix + i + ".", array[i]);
- }
- }
- }
-
- /**
- * Set any custom key-value pair to this model.
- *
- * @param key The key to set.
- * @param value The value to set.
- */
- public void set(String key, Object value) {
- this.customizedParams.put(key, value);
- }
-
- /**
- * Get all custom key-value pairs from this model.
- *
- * @return A map of custom parameters.
- */
- public HashMap any() {
- return this.customizedParams;
- }
-
- /**
- * Get the flag indicating whether the sign should be skipped.
- *
- * @return True if the sign should be skipped; otherwise, false.
- */
- public boolean getSkipSign() {
- return skipSign;
- }
-
- /**
- * Set the flag indicating whether the sign should be skipped.
- *
- * @param skipSign True to skip the sign; false otherwise.
- */
- public void setSkipSign(boolean skipSign) {
- this.skipSign = skipSign;
- }
-
- /**
- * Get the HTTP request headers.
- *
- * @return A map of HTTP headers.
- */
- public Map GetHeader() {
- return header;
- }
-
- /**
- * Set the HTTP request headers.
- *
- * @param header A map of HTTP headers to set.
- */
- public void SetHeader(Map header) {
- this.header = header;
- }
-
- /**
- * Check if the model is a stream type.
- *
- * @return False, as this model is not a stream type.
- */
- public boolean isStream() {
- return false;
- }
-}
+/*
+ * Copyright (c) 2018 Tencent. All Rights Reserved.
+ *
+ * Licensed 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 com.tencentcloudapi.common;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Abstract base class for all model objects in the Tencent Cloud SDK.
+ *
This class provides utility methods for serializing to and deserializing from JSON,
+ * managing customized parameters, and handling HTTP headers and requests.
+ */
+public abstract class AbstractModel {
+
+ /**
+ * Stores HTTP headers for the request.
+ */
+ public Map header = new HashMap();
+
+ /**
+ * Flag to indicate whether to skip signing for this request. Defaults to false.
+ */
+ protected boolean skipSign = false;
+
+ /**
+ * Stores any custom parameters that are not part of the predefined model fields.
+ * These can be added dynamically and should be serializable to JSON.
+ */
+ private HashMap customizedParams = new HashMap();
+
+ /**
+ * Convert an object of type O to its JSON string representation.
+ *
+ * @param obj The object to be serialized to JSON.
+ * @param The type of the object.
+ * @return A JSON string representation of the object.
+ */
+ public static String toJsonString(O obj) {
+ return toJsonObject(obj).toString();
+ }
+
+ /**
+ * Recursively generates a JSON object from the model object.
+ * This method handles serialization for both custom parameters and regular fields.
+ *
+ * @param obj The object to be serialized.
+ * @param The type of the object.
+ * @return A JSON object representation of the model.
+ */
+ private static JsonObject toJsonObject(O obj) {
+ Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+ JsonObject joall = new JsonObject();
+
+ // Serialize customized parameters
+ JsonObject joadd = gson.toJsonTree(obj.any()).getAsJsonObject();
+ for (Map.Entry entry : joadd.entrySet()) {
+ joall.add(entry.getKey(), entry.getValue());
+ }
+
+ // Serialize regular fields of the object
+ JsonObject jopublic = gson.toJsonTree(obj).getAsJsonObject();
+ for (Map.Entry entry : jopublic.entrySet()) {
+ Object fo = null;
+ try {
+ // Access each field using reflection
+ Field f = obj.getClass().getDeclaredField(entry.getKey());
+ f.setAccessible(true);
+ fo = f.get(obj);
+ } catch (Exception e) {
+ // This should never happen
+ e.printStackTrace();
+ }
+ if (fo instanceof AbstractModel) {
+ // If the field is an AbstractModel, recursively serialize it
+ joall.add(entry.getKey(), toJsonObject((AbstractModel) fo));
+ } else {
+ // Otherwise, directly add the value to the JSON object
+ joall.add(entry.getKey(), entry.getValue());
+ }
+ }
+ return joall;
+ }
+
+ /**
+ * Deserialize a JSON string into an object of a subclass of AbstractModel.
+ *
+ * @param json The JSON string to be deserialized.
+ * @param cls The class of the target model type.
+ * @param The type of the object to deserialize.
+ * @return An object of type O.
+ */
+ public static O fromJsonString(String json, Class cls) {
+ Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+ return gson.fromJson(json, cls);
+ }
+
+ /**
+ * Abstract method to be implemented by subclasses.
+ * Used to convert the model into a map for HTTP requests.
+ *
+ * @param map The map to populate with the model's parameters.
+ * @param prefix A string prefix for the keys.
+ */
+ protected abstract void toMap(HashMap map, String prefix);
+
+ /**
+ * Mark which parameters are binary type (for multipart requests).
+ *
+ * @return An array of parameter names that are binary.
+ */
+ protected String[] getBinaryParams() {
+ return new String[0];
+ }
+
+ /**
+ * Return the multipart request parameters (if any).
+ *
+ * @return A map of multipart parameters.
+ */
+ protected HashMap getMultipartRequestParams() {
+ return new HashMap();
+ }
+
+ /**
+ * Helper method to set a simple parameter in the map.
+ * Converts the parameter name and value to a string.
+ *
+ * @param map The map to put the parameter into.
+ * @param key The key for the parameter.
+ * @param value The value of the parameter.
+ * @param The type of the value.
+ */
+ protected void setParamSimple(HashMap map, String key, V value) {
+ if (value != null) {
+ key = key.substring(0, 1).toUpperCase() + key.substring(1);
+ key = key.replace("_", ".");
+ map.put(key, String.valueOf(value));
+ }
+ }
+
+ /**
+ * Helper method to set an array of simple parameters in the map.
+ *
+ * @param map The map to put the parameters into.
+ * @param prefix The prefix for each key.
+ * @param array The array of values.
+ * @param The type of the values in the array.
+ */
+ protected void setParamArraySimple(HashMap map, String prefix, V[] array) {
+ if (array != null) {
+ for (int i = 0; i < array.length; i++) {
+ this.setParamSimple(map, prefix + i, array[i]);
+ }
+ }
+ }
+
+ /**
+ * Helper method to set an object parameter in the map.
+ * Recursively calls toMap() for complex model objects.
+ *
+ * @param map The map to put the parameter into.
+ * @param prefix The prefix for the key.
+ * @param obj The object to add to the map.
+ * @param The type of the object.
+ */
+ protected void setParamObj(
+ HashMap map, String prefix, V obj) {
+ if (obj != null) {
+ obj.toMap(map, prefix);
+ }
+ }
+
+ /**
+ * Helper method to set an array of object parameters in the map.
+ *
+ * @param map The map to put the parameters into.
+ * @param prefix The prefix for each key.
+ * @param array The array of object values.
+ * @param The type of the objects in the array.
+ */
+ protected void setParamArrayObj(
+ HashMap map, String prefix, V[] array) {
+ if (array != null) {
+ for (int i = 0; i < array.length; i++) {
+ this.setParamObj(map, prefix + i + ".", array[i]);
+ }
+ }
+ }
+
+ /**
+ * Set any custom key-value pair to this model.
+ *
+ * @param key The key to set.
+ * @param value The value to set.
+ */
+ public void set(String key, Object value) {
+ this.customizedParams.put(key, value);
+ }
+
+ /**
+ * Get all custom key-value pairs from this model.
+ *
+ * @return A map of custom parameters.
+ */
+ public HashMap any() {
+ return this.customizedParams;
+ }
+
+ /**
+ * Get the flag indicating whether the sign should be skipped.
+ *
+ * @return True if the sign should be skipped; otherwise, false.
+ */
+ public boolean getSkipSign() {
+ return skipSign;
+ }
+
+ /**
+ * Set the flag indicating whether the sign should be skipped.
+ *
+ * @param skipSign True to skip the sign; false otherwise.
+ */
+ public void setSkipSign(boolean skipSign) {
+ this.skipSign = skipSign;
+ }
+
+ /**
+ * Get the HTTP request headers.
+ *
+ * @return A map of HTTP headers.
+ */
+ public Map GetHeader() {
+ return header;
+ }
+
+ /**
+ * Set the HTTP request headers.
+ *
+ * @param header A map of HTTP headers to set.
+ */
+ public void SetHeader(Map header) {
+ this.header = header;
+ }
+
+ /**
+ * Check if the model is a stream type.
+ *
+ * @return False, as this model is not a stream type.
+ */
+ public boolean isStream() {
+ return false;
+ }
+}
diff --git a/src/main/java/com/tencentcloudapi/common/CircuitBreaker.java b/src/main/java/com/tencentcloudapi/common/CircuitBreaker.java
index 344d7a9ea..e01b1678b 100644
--- a/src/main/java/com/tencentcloudapi/common/CircuitBreaker.java
+++ b/src/main/java/com/tencentcloudapi/common/CircuitBreaker.java
@@ -80,6 +80,15 @@ public CircuitBreaker(Setting setting) {
this.setting = setting;
}
+ /**
+ * Returns the settings used by this circuit breaker.
+ *
+ * @return The Setting instance.
+ */
+ public Setting getSetting() {
+ return setting;
+ }
+
/**
* Attempt to allow a request based on the current state of the circuit breaker.
*
@@ -214,7 +223,7 @@ private void onFailure(State state, long now) {
all++;
failures++;
consecutiveSuccesses = 0;
- consecutiveFailures = 0;
+ consecutiveFailures++;
if (readyToOpen()) {
setState(State.Open, now);
}
diff --git a/src/main/java/com/tencentcloudapi/common/Credential.java b/src/main/java/com/tencentcloudapi/common/Credential.java
index f18560edd..bfdfc1943 100644
--- a/src/main/java/com/tencentcloudapi/common/Credential.java
+++ b/src/main/java/com/tencentcloudapi/common/Credential.java
@@ -1,196 +1,196 @@
-/*
- * Copyright (c) 2018 Tencent. All Rights Reserved.
- *
- * Licensed 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 com.tencentcloudapi.common;
-
-import com.tencentcloudapi.common.exception.TencentCloudSDKException;
-
-/**
- * Credential has many types in Tencent Cloud Access Management service.
- *
- *
We mainly use two of them:
- *
- *
Permanent credential: SecretId & SecretKey, can only be obtained from Tencent Cloud Management
- * Console, https://console.cloud.tencent.com/cam/capi.
- *
- *
Ephemeral credential, can be obtained from Security Token Service (STS), has three dimensions:
- * SecretId, SecretKey and Token. It will expire after a short time, hence you need to invoke STS
- * API to refresh it.
- *
- *
Thread-safety and atomicity. The triple (secretId, secretKey, token) is only guaranteed
- * to be read atomically via {@link #getSnapshot()}. The individual {@code getSecretId()},
- * {@code getSecretKey()}, {@code getToken()} getters do NOT guarantee that two consecutive calls
- * observe a consistent triple — a refresh triggered between them may yield an id/key/token
- * mismatch. Any code path that consumes more than one of the three fields together (e.g. request
- * signing) should use {@link #getSnapshot()}.
- */
-public class Credential {
- private String secretId;
- private String secretKey;
- private String token;
- private Updater updater;
-
- public Credential() {
- }
-
- public Credential(String secretId, String secretKey) {
- this(secretId, secretKey, "");
- }
-
- public Credential(String secretId, String secretKey, String token) {
- this.secretId = secretId;
- this.secretKey = secretKey;
- this.token = token;
- }
-
- public Credential(String secretId, String secretKey, String token, Updater updater) {
- this.secretId = secretId;
- this.secretKey = secretKey;
- this.token = token;
- this.updater = updater;
- }
-
- public Updater getUpdater() {
- return updater;
- }
-
- public void setUpdater(Updater updater) {
- this.updater = updater;
- }
-
- /**
- * Returns the secret id, triggering a refresh via the attached {@link Updater} if one is set.
- *
- *
Backward-compatibility note. This getter calls {@link #tryUpdate()} outside of any
- * synchronization block. Two consequences follow from this:
- *
- *
Concurrent threads can each enter {@code tryUpdate()} simultaneously, so the
- * {@link Updater} may be invoked concurrently; well-behaved Updater implementations must
- * guard against this themselves (e.g. via {@code needRefresh()} checks).
- *
An {@link Updater} that calls back into {@code getSecretId()}, {@code getSecretKey()},
- * or {@code getToken()} on the same credential will recurse infinitely. Use
- * {@link #setCredential} for writes inside {@code update()}, never the individual
- * getters.
- *
- * Both behaviours are retained for backward compatibility. For any code path that reads more
- * than one of the three fields together, use {@link #getSnapshot()} instead.
- */
- public String getSecretId() {
- tryUpdate();
- return this.secretId;
- }
-
- public void setSecretId(String secretId) {
- this.secretId = secretId;
- }
-
- /**
- * Returns the secret key, triggering a refresh via the attached {@link Updater} if one is set.
- *
- *
See {@link #getSecretId()} for the backward-compatibility caveats that apply to all three
- * individual getters. Prefer {@link #getSnapshot()} when reading multiple fields together.
- */
- public String getSecretKey() {
- tryUpdate();
- return this.secretKey;
- }
-
- public void setSecretKey(String secretKey) {
- this.secretKey = secretKey;
- }
-
- /**
- * Returns the session token, triggering a refresh via the attached {@link Updater} if one is
- * set.
- *
- *
See {@link #getSecretId()} for the backward-compatibility caveats that apply to all three
- * individual getters. Prefer {@link #getSnapshot()} when reading multiple fields together.
- */
- public String getToken() {
- tryUpdate();
- return this.token;
- }
-
- public void setToken(String token) {
- this.token = token;
- }
-
- /**
- * Atomically replaces the entire (secretId, secretKey, token) triple under the same monitor
- * used by {@link #getSnapshot()}.
- *
- *
Use this instead of three separate {@code setSecretId} / {@code setSecretKey} /
- * {@code setToken} calls whenever you need to publish a freshly-refreshed triple: concurrent
- * readers going through {@link #getSnapshot()} are guaranteed to observe either the old triple
- * or the new triple in its entirety, never a mix of the two. The individual setters do not
- * provide this guarantee and should only be used when no other thread is reading.
- *
- *
This method is the intended write-side companion to {@link #getSnapshot()}'s read-side
- * atomicity. {@link Updater} implementations that refresh the triple in place (rather than
- * constructing a new {@code Credential}) should call this method instead of the individual
- * setters.
- *
- * @param secretId the new secret id.
- * @param secretKey the new secret key.
- * @param token the new token, may be empty or {@code null} for permanent credentials.
- */
- public void setCredential(String secretId, String secretKey, String token) {
- synchronized (this) {
- this.secretId = secretId;
- this.secretKey = secretKey;
- this.token = token;
- }
- }
-
- /**
- * Returns a point-in-time, self-consistent copy of the (secretId, secretKey, token) triple.
- *
- *
This is the only thread-safe, atomic way to read the credential triple. The refresh hook
- * (if any) is invoked exactly once under a lock, and the three fields are then sampled together
- * into a new {@code Credential} that does not carry an {@link Updater}. Use this in any code
- * path that consumes more than one of the three fields together (e.g. request signing).
- *
- *
The returned object should be treated as read-only. It is a fresh instance and mutating it
- * via the setters has no effect on the source credential, but doing so will break the
- * consistency guarantee for the holder of the snapshot.
- *
- * @return a point-in-time copy of the credential triple, with no attached updater.
- */
- public Credential getSnapshot() {
- synchronized (this) {
- tryUpdate();
- return new Credential(secretId, secretKey, token);
- }
- }
-
- private void tryUpdate() {
- if (updater == null) {
- return;
- }
-
- try {
- updater.update(this);
- } catch (TencentCloudSDKException e) {
- // wrap as RuntimeException to keep API consistent
- throw new RuntimeException(e);
- }
- }
-
- public interface Updater {
- void update(Credential credential) throws TencentCloudSDKException;
- }
-}
+/*
+ * Copyright (c) 2018 THL A29 Limited, a Tencent company. All Rights Reserved.
+ *
+ * Licensed 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 com.tencentcloudapi.common;
+
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+
+/**
+ * Credential has many types in Tencent Cloud Access Management service.
+ *
+ *
We mainly use two of them:
+ *
+ *
Permanent credential: SecretId & SecretKey, can only be obtained from Tencent Cloud Management
+ * Console, https://console.cloud.tencent.com/cam/capi.
+ *
+ *
Ephemeral credential, can be obtained from Security Token Service (STS), has three dimensions:
+ * SecretId, SecretKey and Token. It will expire after a short time, hence you need to invoke STS
+ * API to refresh it.
+ *
+ *
Thread-safety and atomicity. The triple (secretId, secretKey, token) is only guaranteed
+ * to be read atomically via {@link #getSnapshot()}. The individual {@code getSecretId()},
+ * {@code getSecretKey()}, {@code getToken()} getters do NOT guarantee that two consecutive calls
+ * observe a consistent triple — a refresh triggered between them may yield an id/key/token
+ * mismatch. Any code path that consumes more than one of the three fields together (e.g. request
+ * signing) should use {@link #getSnapshot()}.
+ */
+public class Credential {
+ private String secretId;
+ private String secretKey;
+ private String token;
+ private Updater updater;
+
+ public Credential() {
+ }
+
+ public Credential(String secretId, String secretKey) {
+ this(secretId, secretKey, "");
+ }
+
+ public Credential(String secretId, String secretKey, String token) {
+ this.secretId = secretId;
+ this.secretKey = secretKey;
+ this.token = token;
+ }
+
+ public Credential(String secretId, String secretKey, String token, Updater updater) {
+ this.secretId = secretId;
+ this.secretKey = secretKey;
+ this.token = token;
+ this.updater = updater;
+ }
+
+ public Updater getUpdater() {
+ return updater;
+ }
+
+ public void setUpdater(Updater updater) {
+ this.updater = updater;
+ }
+
+ /**
+ * Returns the secret id, triggering a refresh via the attached {@link Updater} if one is set.
+ *
+ *
Backward-compatibility note. This getter calls {@link #tryUpdate()} outside of any
+ * synchronization block. Two consequences follow from this:
+ *
+ *
Concurrent threads can each enter {@code tryUpdate()} simultaneously, so the
+ * {@link Updater} may be invoked concurrently; well-behaved Updater implementations must
+ * guard against this themselves (e.g. via {@code needRefresh()} checks).
+ *
An {@link Updater} that calls back into {@code getSecretId()}, {@code getSecretKey()},
+ * or {@code getToken()} on the same credential will recurse infinitely. Use
+ * {@link #setCredential} for writes inside {@code update()}, never the individual
+ * getters.
+ *
+ * Both behaviours are retained for backward compatibility. For any code path that reads more
+ * than one of the three fields together, use {@link #getSnapshot()} instead.
+ */
+ public String getSecretId() {
+ tryUpdate();
+ return this.secretId;
+ }
+
+ public void setSecretId(String secretId) {
+ this.secretId = secretId;
+ }
+
+ /**
+ * Returns the secret key, triggering a refresh via the attached {@link Updater} if one is set.
+ *
+ *
See {@link #getSecretId()} for the backward-compatibility caveats that apply to all three
+ * individual getters. Prefer {@link #getSnapshot()} when reading multiple fields together.
+ */
+ public String getSecretKey() {
+ tryUpdate();
+ return this.secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ /**
+ * Returns the session token, triggering a refresh via the attached {@link Updater} if one is
+ * set.
+ *
+ *
See {@link #getSecretId()} for the backward-compatibility caveats that apply to all three
+ * individual getters. Prefer {@link #getSnapshot()} when reading multiple fields together.
+ */
+ public String getToken() {
+ tryUpdate();
+ return this.token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ /**
+ * Atomically replaces the entire (secretId, secretKey, token) triple under the same monitor
+ * used by {@link #getSnapshot()}.
+ *
+ *
Use this instead of three separate {@code setSecretId} / {@code setSecretKey} /
+ * {@code setToken} calls whenever you need to publish a freshly-refreshed triple: concurrent
+ * readers going through {@link #getSnapshot()} are guaranteed to observe either the old triple
+ * or the new triple in its entirety, never a mix of the two. The individual setters do not
+ * provide this guarantee and should only be used when no other thread is reading.
+ *
+ *
This method is the intended write-side companion to {@link #getSnapshot()}'s read-side
+ * atomicity. {@link Updater} implementations that refresh the triple in place (rather than
+ * constructing a new {@code Credential}) should call this method instead of the individual
+ * setters.
+ *
+ * @param secretId the new secret id.
+ * @param secretKey the new secret key.
+ * @param token the new token, may be empty or {@code null} for permanent credentials.
+ */
+ public void setCredential(String secretId, String secretKey, String token) {
+ synchronized (this) {
+ this.secretId = secretId;
+ this.secretKey = secretKey;
+ this.token = token;
+ }
+ }
+
+ /**
+ * Returns a point-in-time, self-consistent copy of the (secretId, secretKey, token) triple.
+ *
+ *
This is the only thread-safe, atomic way to read the credential triple. The refresh hook
+ * (if any) is invoked exactly once under a lock, and the three fields are then sampled together
+ * into a new {@code Credential} that does not carry an {@link Updater}. Use this in any code
+ * path that consumes more than one of the three fields together (e.g. request signing).
+ *
+ *
The returned object should be treated as read-only. It is a fresh instance and mutating it
+ * via the setters has no effect on the source credential, but doing so will break the
+ * consistency guarantee for the holder of the snapshot.
+ *
+ * @return a point-in-time copy of the credential triple, with no attached updater.
+ */
+ public Credential getSnapshot() {
+ synchronized (this) {
+ tryUpdate();
+ return new Credential(secretId, secretKey, token);
+ }
+ }
+
+ private void tryUpdate() {
+ if (updater == null) {
+ return;
+ }
+
+ try {
+ updater.update(this);
+ } catch (TencentCloudSDKException e) {
+ // wrap as RuntimeException to keep API consistent
+ throw new RuntimeException(e);
+ }
+ }
+
+ public interface Updater {
+ void update(Credential credential) throws TencentCloudSDKException;
+ }
+}
diff --git a/src/main/java/com/tencentcloudapi/common/EndpointFailoverInterceptor.java b/src/main/java/com/tencentcloudapi/common/EndpointFailoverInterceptor.java
new file mode 100644
index 000000000..eb04f1a8b
--- /dev/null
+++ b/src/main/java/com/tencentcloudapi/common/EndpointFailoverInterceptor.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2018 Tencent. All Rights Reserved.
+ *
+ * Licensed 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 com.tencentcloudapi.common;
+
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import okhttp3.*;
+
+import javax.net.ssl.SSLException;
+import java.io.IOException;
+import java.net.*;
+import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Selects a healthy API host via per-host circuit breakers.
+ * No same-request retry — API calls may be non-idempotent.
+ * All breakers open → fall through to origin host.
+ */
+class EndpointFailoverInterceptor implements Interceptor {
+
+ /** More-specific families first — matchFamily returns first hit. */
+ static final String[][] FAILOVER_DOMAIN_FAMILIES = {
+ {
+ "ai.tencentcloudapi.com",
+ "ai.tencentcloudapi.com.cn",
+ "ai.tencentcloudapi.cn",
+ },
+ {
+ "internal.tencentcloudapi.com",
+ "internal.tencentcloudapi.com.cn",
+ "internal.tencentcloudapi.cn",
+ },
+ {
+ "tencentcloudapi.com",
+ "tencentcloudapi.com.cn",
+ "tencentcloudapi.cn",
+ },
+ };
+
+ public static long BREAKER_TIMEOUT_MS = 60 * 1000;
+
+ private final AbstractClient client;
+ private final String backupEndpoint;
+ // Per AbstractClient instance.
+ private final ConcurrentHashMap breakers =
+ new ConcurrentHashMap();
+
+ /**
+ * Stores the region breaker set via {@link AbstractClient#setRegionBreaker}
+ * so its settings are applied to all per-host breakers created here.
+ */
+ private CircuitBreaker regionBreaker;
+
+ EndpointFailoverInterceptor(AbstractClient client) {
+ this.client = client;
+ String bp = client.getClientProfile().getBackupEndpoint();
+ this.backupEndpoint = (bp != null && !bp.isEmpty()) ? bp : null;
+ }
+
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Request request = chain.request();
+ String originHost = request.url().host();
+
+ // selectHost always returns a non-null Candidate.
+ Candidate c = selectHost(request);
+
+ try {
+ // rewriteFor returns the original request unchanged when c.host == originHost.
+ Request rewritten = rewriteFor(request, originHost, c.host);
+ Response raw = chain.proceed(rewritten);
+ Response validated = validateResponse(raw);
+ c.token.report(true);
+ return validated;
+ } catch (TencentCloudSDKException e) {
+ throw new IOException("Failed to re-sign request for failover: " + e.getMessage(), e);
+ } catch (IOException e) {
+ if (!shouldFailover(e)) {
+ throw e;
+ }
+ c.token.report(false);
+ throw e;
+ }
+ }
+
+ // --- Candidate selection ---
+
+ /**
+ * Returns the first candidate whose breaker is closed, or the origin host
+ * when all breakers are open.
+ */
+ Candidate selectHost(Request request) {
+ String urlHost = request.url().host();
+
+ // Fast path: check the origin host's breaker first. In the common case
+ // the origin is healthy, so we avoid allocating the candidate list entirely.
+ CircuitBreaker.Token originToken = breakerFor(urlHost).allow();
+ if (originToken.allowed) {
+ return new Candidate(urlHost, originToken);
+ }
+
+ // Origin host is tripped — build the failover candidates (excluding the
+ // origin, which we already checked) and walk them.
+ List candidates = buildCandidateHosts(urlHost);
+ for (int i = 0; i < candidates.size(); i++) {
+ String host = candidates.get(i);
+ CircuitBreaker.Token token = breakerFor(host).allow();
+ if (token.allowed) {
+ return new Candidate(host, token);
+ }
+ }
+ // All breakers open: fall back to origin host.
+ return new Candidate(urlHost, originToken);
+ }
+
+ /**
+ * Ordered failover candidates (excluding the origin host, which is checked
+ * first in {@link #selectHost}). Priority: backup endpoint, then TLD-family rotation.
+ */
+ private List buildCandidateHosts(String urlHost) {
+ String prefix = serviceOf(urlHost);
+ if (prefix == null || prefix.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ // 1. backup endpoint
+ if (backupEndpoint != null) {
+ return Collections.singletonList(prefix + "." + backupEndpoint);
+ }
+
+ // 2. Default TLD-family rotation
+ Match match = matchFamily(urlHost);
+ if (match == null) {
+ return Collections.emptyList();
+ }
+ List hosts = new ArrayList();
+ for (int i = 1; i < match.family.length; i++) {
+ int tldIdx = (match.tldIdx + i) % match.family.length;
+ hosts.add(prefix + "." + match.family[tldIdx]);
+ }
+ return hosts;
+ }
+
+ CircuitBreaker breakerFor(String host) {
+ CircuitBreaker existing = breakers.get(host);
+ if (existing != null) {
+ return existing;
+ }
+ CircuitBreaker created = newBreaker();
+ CircuitBreaker prev = breakers.putIfAbsent(host, created);
+ return prev != null ? prev : created;
+ }
+
+ /** Test hook. */
+ void putBreakerForTesting(String host, CircuitBreaker breaker) {
+ breakers.put(host, breaker);
+ }
+
+ private CircuitBreaker newBreaker() {
+ if (regionBreaker != null) {
+ return new CircuitBreaker(regionBreaker.getSetting());
+ }
+ CircuitBreaker.Setting s = new CircuitBreaker.Setting();
+ s.timeoutMs = BREAKER_TIMEOUT_MS;
+ return new CircuitBreaker(s);
+ }
+
+ /** Package-private: store the region breaker instance. */
+ void setRegionBreaker(CircuitBreaker regionBreaker) {
+ this.regionBreaker = regionBreaker;
+ }
+
+ /** Package-private: return the region breaker instance. */
+ CircuitBreaker getRegionBreaker() {
+ return regionBreaker;
+ }
+
+ // Per-candidate helpers.
+
+ private Request rewriteFor(Request request, String originHost, String targetHost)
+ throws TencentCloudSDKException, IOException {
+ if (originHost.equals(targetHost)) {
+ return request;
+ }
+ return RequestBuilder.from(request)
+ .fromClient(client)
+ .withUrlHost(targetHost)
+ .withUrlSchemeHttps()
+ .withHost(targetHost)
+ .build();
+ }
+
+ static final class Candidate {
+ final String host;
+ final CircuitBreaker.Token token;
+
+ Candidate(String host, CircuitBreaker.Token token) {
+ this.host = host;
+ this.token = token;
+ }
+ }
+
+ // --- Host classification & TLD family matching ---
+
+ static boolean isKnownTencentCloudHost(String host) {
+ return matchFamily(host) != null;
+ }
+
+ /** Returns null if host matches no family. */
+ static Match matchFamily(String host) {
+ if (host == null) {
+ return null;
+ }
+ for (int familyIdx = 0; familyIdx < FAILOVER_DOMAIN_FAMILIES.length; familyIdx++) {
+ Match m = tryMatchFamily(host, FAILOVER_DOMAIN_FAMILIES[familyIdx]);
+ if (m != null) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ private static Match tryMatchFamily(String host, String[] family) {
+ for (int tldIdx = 0; tldIdx < family.length; tldIdx++) {
+ String suffix = family[tldIdx];
+ if (!host.endsWith("." + suffix)) {
+ continue;
+ }
+ String prefix = host.substring(0, host.length() - suffix.length() - 1);
+ if (prefix.isEmpty() || hasEmptyLabel(prefix)) {
+ return null;
+ }
+ return new Match(family, prefix, tldIdx);
+ }
+ return null;
+ }
+
+ // "foo..bar" → malformed hostname.
+ private static boolean hasEmptyLabel(String prefix) {
+ if (prefix.startsWith(".") || prefix.endsWith(".")) {
+ return true;
+ }
+ for (int i = 0; i < prefix.length() - 1; i++) {
+ if (prefix.charAt(i) == '.' && prefix.charAt(i + 1) == '.') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static final class Match {
+ final String[] family;
+ final String prefix;
+ final int tldIdx;
+
+ Match(String[] family, String prefix, int tldIdx) {
+ this.family = family;
+ this.prefix = prefix;
+ this.tldIdx = tldIdx;
+ }
+ }
+
+ /** Test hook. */
+ static String hostWithTld(String originHost, int newTldIdx) {
+ Match m = matchFamily(originHost);
+ return serviceOf(originHost) + "." + m.family[newTldIdx];
+ }
+
+ private static String serviceOf(String host) {
+ int dot = host.indexOf('.');
+ return dot < 0 ? host : host.substring(0, dot);
+ }
+
+ // Failure classification.
+
+ // Host is unreachable or compromised — not a transient application error.
+ private static boolean shouldFailover(IOException e) {
+ return e instanceof UnknownHostException
+ || e instanceof SSLException
+ || e instanceof SocketException
+ || e instanceof SocketTimeoutException
+ || e instanceof UnhealthyResponseException;
+ }
+
+ // Wraps non-200 or invalid-JSON responses so validateResponse's caller
+ // records them as breaker failures alongside transport errors.
+ private static final class UnhealthyResponseException extends IOException {
+ UnhealthyResponseException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Validates response is healthy based solely on the HTTP status code.
+ * Body content is not inspected — business-level errors (HTTP 200 + JSON
+ * error body) are handled by the response parser, not by the failover
+ * breaker. Non-200 responses are recorded as breaker failures.
+ */
+ private static Response validateResponse(Response resp) throws IOException {
+ if (resp.code() != 200) {
+ String msg = "HTTP " + resp.code() + " " + resp.message();
+ resp.close();
+ throw new UnhealthyResponseException(msg);
+ }
+ return resp;
+ }
+}
diff --git a/src/main/java/com/tencentcloudapi/common/JsonResponseErrModel.java b/src/main/java/com/tencentcloudapi/common/JsonResponseErrModel.java
index 990270676..d72b2a5e0 100644
--- a/src/main/java/com/tencentcloudapi/common/JsonResponseErrModel.java
+++ b/src/main/java/com/tencentcloudapi/common/JsonResponseErrModel.java
@@ -1,41 +1,41 @@
-/*
- * Copyright (c) 2018 Tencent. All Rights Reserved.
- *
- * Licensed 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 com.tencentcloudapi.common;
-
-import com.google.gson.annotations.Expose;
-import com.google.gson.annotations.SerializedName;
-
-public class JsonResponseErrModel {
- @SerializedName("RequestId")
- @Expose
- public String requestId;
-
- @SerializedName("Error")
- @Expose
- public ErrorInfo error;
-
- class ErrorInfo {
- @SerializedName("Code")
- @Expose
- public String code;
-
- @Expose
- @SerializedName("Message")
- public String message;
- }
-}
+/*
+ * Copyright (c) 2018 Tencent. All Rights Reserved.
+ *
+ * Licensed 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 com.tencentcloudapi.common;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+public class JsonResponseErrModel {
+ @SerializedName("RequestId")
+ @Expose
+ public String requestId;
+
+ @SerializedName("Error")
+ @Expose
+ public ErrorInfo error;
+
+ class ErrorInfo {
+ @SerializedName("Code")
+ @Expose
+ public String code;
+
+ @Expose
+ @SerializedName("Message")
+ public String message;
+ }
+}
diff --git a/src/main/java/com/tencentcloudapi/common/JsonResponseModel.java b/src/main/java/com/tencentcloudapi/common/JsonResponseModel.java
index 54af3dcd2..8c10cef95 100644
--- a/src/main/java/com/tencentcloudapi/common/JsonResponseModel.java
+++ b/src/main/java/com/tencentcloudapi/common/JsonResponseModel.java
@@ -1,27 +1,27 @@
-/*
- * Copyright (c) 2018 Tencent. All Rights Reserved.
- *
- * Licensed 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 com.tencentcloudapi.common;
-
-import com.google.gson.annotations.Expose;
-import com.google.gson.annotations.SerializedName;
-
-public class JsonResponseModel {
- @SerializedName("Response")
- @Expose
- public T response;
-}
+/*
+ * Copyright (c) 2018 Tencent. All Rights Reserved.
+ *
+ * Licensed 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 com.tencentcloudapi.common;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+public class JsonResponseModel {
+ @SerializedName("Response")
+ @Expose
+ public T response;
+}
diff --git a/src/main/java/com/tencentcloudapi/common/RequestBuilder.java b/src/main/java/com/tencentcloudapi/common/RequestBuilder.java
new file mode 100644
index 000000000..47a2e7091
--- /dev/null
+++ b/src/main/java/com/tencentcloudapi/common/RequestBuilder.java
@@ -0,0 +1,715 @@
+/*
+ * Copyright (c) 2018 Tencent. All Rights Reserved.
+ *
+ * Licensed 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 com.tencentcloudapi.common;
+
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import okhttp3.*;
+import okio.Buffer;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Holds all parameters for one cloud API call and builds a signed OkHttp {@link Request}.
+ *
+ *
Mirrors the Go SDK requestBuilder: {@code url} is the single source of truth for
+ * transport, {@code host} signs the canonical Host header. Failover swaps both
+ * {@code url.host} and {@code host}, then rebuilds.