diff --git a/README.md b/README.md
index 1ca77ea333..40054c178c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
欢迎使用腾讯云开发者工具套件(SDK)3.0,SDK3.0是云 API3.0 平台的配套工具。
为方便 JAVA 开发者调试和接入腾讯云产品 API,这里向您介绍适用于 Java 的腾讯云开发工具包,并提供首次使用开发工具包的简单示例。让您快速获取腾讯云 Java SDK 并开始调用。
# 依赖环境
-1. 依赖环境:JDK 7 版本及以上。
+1. 依赖环境:JDK 8 版本及以上。
2. 从 腾讯云控制台 开通相应产品。
3. 获取 SecretID、SecretKey 以及调用地址(endpoint),endpoint 一般形式为\*.tencentcloudapi.com,如CVM 的调用地址为 cvm.tencentcloudapi.com,具体参考各产品说明。
@@ -546,6 +546,29 @@ cpf.getHttpProfile().setHttpClient(...);
请注意,从 3.0.x 版本升级到 3.1.x 版本有兼容性问题,对于 Integer 字段的使用修改为了 Long 类型,需要重新编译项目。
+## OkHttp 升级
+
+为规避 OkHttp 3.x 版本存在的安全风险,从版本 3.2.0 起,OkHttp 依赖由 3.12.13 升级至 4.12.0。
+
+### 回退至 OkHttp 3.12.13
+
+若升级后与项目存在冲突,可在应用 `pom.xml` 中显式声明以下依赖进行回退。依据 Maven “路径最近优先”的依赖仲裁规则,应用声明的版本将覆盖 SDK 传递引入的 4.12.0:
+
+```xml
+
+ com.squareup.okhttp3
+ okhttp
+ 3.12.13
+
+
+ com.squareup.okhttp3
+ logging-interceptor
+ 3.12.13
+
+```
+
+> **注意**:回退操作将重新引入旧版 OkHttp 已修复的安全风险,仅可用于故障应急与问题定位,问题排除后应尽快恢复使用 OkHttp 4。
+
## 证书问题
证书问题通常是客户端环境配置错误导致的。SDK 没有对证书进行操作,依赖的是 Java 运行环境本身的处理。出现证书问题后,可以使用`-Djavax.net.debug=ssl`开启详细日志辅助判断。
diff --git a/pom.xml b/pom.xml
index fb6ff19d3b..9d8a69c0e0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
com.squareup.okhttp3okhttp
- 3.12.13
+ 4.12.0com.google.code.gson
@@ -27,7 +27,7 @@
com.squareup.okhttp3logging-interceptor
- 3.12.13
+ 4.12.0org.apache.commons
@@ -63,8 +63,8 @@
maven-compiler-plugin2.3.2
- 1.7
- 1.7
+ 1.8
+ 1.8UTF-8
diff --git a/src/main/java/com/tencentcloudapi/common/AbstractClient.java b/src/main/java/com/tencentcloudapi/common/AbstractClient.java
index 3f2e544278..83ba77be6e 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.1.1516";
- 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 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 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 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;
- 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.1.1354";
+ 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 638ad0b71e..31d2cfb7eb 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 344d7a9ea4..e01b1678b0 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 f18560eddf..bfdfc1943f 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 0000000000..eb04f1a8b4
--- /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 9902706765..d72b2a5e0c 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 54af3dcd22..8c10cef954 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 0000000000..47a2e7091f
--- /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.