2323import org .thingsboard .client .model .LoginResponse ;
2424
2525import java .io .InputStream ;
26+ import java .net .Authenticator ;
27+ import java .net .CookieHandler ;
28+ import java .net .ProxySelector ;
2629import java .net .URI ;
2730import java .net .http .HttpClient ;
2831import java .net .http .HttpRequest ;
2932import java .net .http .HttpResponse ;
33+ import java .time .Duration ;
3034import java .util .Base64 ;
3135import java .util .Map ;
36+ import java .util .concurrent .Executor ;
3237import java .util .concurrent .TimeUnit ;
3338import java .util .logging .Level ;
39+ import javax .net .ssl .SSLContext ;
40+ import javax .net .ssl .SSLParameters ;
3441
3542/**
3643 * High-level ThingsBoard REST API client with automatic authentication management.
4148 * <li>Automatic token refresh before expiry</li>
4249 * <li>Automatic re-login when the refresh token expires</li>
4350 * <li>Client-server clock skew compensation</li>
51+ * <li>Automatic retry on HTTP 429 (Too Many Requests) with exponential backoff</li>
4452 * </ul>
4553 *
4654 * <pre>{@code
5664 * .apiKey("your-api-key")
5765 * .build();
5866 *
67+ * // Tuning rate-limit retry behaviour (retry is enabled by default)
68+ * ThingsboardClient client = ThingsboardClient.builder()
69+ * .url("http://localhost:8080")
70+ * .credentials("tenant@thingsboard.org", "password")
71+ * .maxRetries(5)
72+ * .initialRetryDelayMs(500L)
73+ * .maxRetryDelayMs(60_000L)
74+ * .build();
75+ *
76+ * // Disable retry entirely
77+ * ThingsboardClient client = ThingsboardClient.builder()
78+ * .url("http://localhost:8080")
79+ * .credentials("tenant@thingsboard.org", "password")
80+ * .retryEnabled(false)
81+ * .build();
82+ *
5983 * // All generated API methods are available directly
6084 * Device device = client.getDeviceById(deviceId);
6185 * }</pre>
@@ -96,6 +120,10 @@ public static class Builder {
96120 private String username ;
97121 private String password ;
98122 private String apiKey ;
123+ private boolean retryEnabled = true ;
124+ private int maxRetries = 3 ;
125+ private long initialRetryDelayMs = 1000L ;
126+ private long maxRetryDelayMs = 30_000L ;
99127
100128 private Builder () {}
101129
@@ -115,21 +143,142 @@ public Builder apiKey(String apiKey) {
115143 return this ;
116144 }
117145
146+ /**
147+ * Enables or disables automatic retry on HTTP 429 responses.
148+ * Retry is enabled by default.
149+ */
150+ public Builder retryEnabled (boolean retryEnabled ) {
151+ this .retryEnabled = retryEnabled ;
152+ return this ;
153+ }
154+
155+ /**
156+ * Maximum number of retry attempts after an HTTP 429 response.
157+ * Default: 3.
158+ */
159+ public Builder maxRetries (int maxRetries ) {
160+ this .maxRetries = maxRetries ;
161+ return this ;
162+ }
163+
164+ /**
165+ * Initial backoff delay in milliseconds for the first retry.
166+ * Subsequent retries use exponential backoff with ±20% jitter.
167+ * Default: 1000 ms.
168+ */
169+ public Builder initialRetryDelayMs (long initialRetryDelayMs ) {
170+ this .initialRetryDelayMs = initialRetryDelayMs ;
171+ return this ;
172+ }
173+
174+ /**
175+ * Maximum backoff delay in milliseconds. The computed delay is capped at this value.
176+ * Default: 30 000 ms.
177+ */
178+ public Builder maxRetryDelayMs (long maxRetryDelayMs ) {
179+ this .maxRetryDelayMs = maxRetryDelayMs ;
180+ return this ;
181+ }
182+
118183 public ThingsboardClient build () throws ApiException {
119184 if (url == null ) {
120185 throw new IllegalArgumentException ("url is required" );
121186 }
122187 if (apiKey != null ) {
123- return new ThingsboardClient (new AuthManager (url , AuthType .API_KEY , apiKey ));
188+ AuthManager auth = new AuthManager (url , AuthType .API_KEY , apiKey );
189+ installRetryingClient (auth );
190+ return new ThingsboardClient (auth );
124191 }
125192 AuthManager auth = new AuthManager (url , AuthType .JWT , null );
193+ installRetryingClient (auth );
126194 ThingsboardClient client = new ThingsboardClient (auth );
127195 if (username != null ) {
128196 client .login (username , password );
129197 }
130198 return client ;
131199 }
132200
201+ private void installRetryingClient (AuthManager auth ) {
202+ if (!retryEnabled ) {
203+ return ;
204+ }
205+ // Replace the HttpClient already captured by AuthManager for raw auth calls
206+ RetryingHttpClient retrying = RetryingHttpClient .wrap (
207+ auth .httpClient , maxRetries , initialRetryDelayMs , maxRetryDelayMs );
208+ auth .httpClient = retrying ;
209+
210+ // Replace the ApiClient's builder so that ThingsboardApi (constructed next)
211+ // also gets a RetryingHttpClient when it calls apiClient.getHttpClient()
212+ HttpClient .Builder realBuilder = auth .apiClient .builder ;
213+ auth .apiClient .builder = new HttpClient .Builder () {
214+ @ Override
215+ public HttpClient build () {
216+ return RetryingHttpClient .wrap (
217+ realBuilder .build (), maxRetries , initialRetryDelayMs , maxRetryDelayMs );
218+ }
219+
220+ @ Override
221+ public HttpClient .Builder cookieHandler (CookieHandler cookieHandler ) {
222+ realBuilder .cookieHandler (cookieHandler );
223+ return this ;
224+ }
225+
226+ @ Override
227+ public HttpClient .Builder connectTimeout (Duration duration ) {
228+ realBuilder .connectTimeout (duration );
229+ return this ;
230+ }
231+
232+ @ Override
233+ public HttpClient .Builder sslContext (SSLContext sslContext ) {
234+ realBuilder .sslContext (sslContext );
235+ return this ;
236+ }
237+
238+ @ Override
239+ public HttpClient .Builder sslParameters (SSLParameters sslParameters ) {
240+ realBuilder .sslParameters (sslParameters );
241+ return this ;
242+ }
243+
244+ @ Override
245+ public HttpClient .Builder executor (Executor executor ) {
246+ realBuilder .executor (executor );
247+ return this ;
248+ }
249+
250+ @ Override
251+ public HttpClient .Builder followRedirects (HttpClient .Redirect policy ) {
252+ realBuilder .followRedirects (policy );
253+ return this ;
254+ }
255+
256+ @ Override
257+ public HttpClient .Builder version (HttpClient .Version version ) {
258+ realBuilder .version (version );
259+ return this ;
260+ }
261+
262+ @ Override
263+ public HttpClient .Builder proxy (ProxySelector proxySelector ) {
264+ realBuilder .proxy (proxySelector );
265+ return this ;
266+ }
267+
268+ @ Override
269+ public HttpClient .Builder authenticator (java .net .Authenticator authenticator ) {
270+ realBuilder .authenticator (authenticator );
271+ return this ;
272+ }
273+
274+ @ Override
275+ public HttpClient .Builder priority (int priority ) {
276+ realBuilder .priority (priority );
277+ return this ;
278+ }
279+ };
280+ }
281+
133282 }
134283
135284 /**
@@ -169,7 +318,7 @@ private record TokenInfo(String token, String refreshToken, long tokenExpTs,
169318
170319 final ApiClient apiClient ;
171320 private final AuthType authType ;
172- private final HttpClient httpClient ;
321+ HttpClient httpClient ; // package- private and non- final so Builder can replace with RetryingHttpClient
173322 private final ObjectMapper objectMapper ;
174323 private final String baseUrl ;
175324
0 commit comments