This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new cfe3cd9f28 Add configurable HTTP retry mechanism to REST transform
(#6440)
cfe3cd9f28 is described below
commit cfe3cd9f28ac2594b0581e8094293a68336513a2
Author: lance <[email protected]>
AuthorDate: Mon Feb 2 20:41:46 2026 +0800
Add configurable HTTP retry mechanism to REST transform (#6440)
Signed-off-by: lance <[email protected]>
---
.../apache/hop/pipeline/transforms/rest/Rest.java | 211 ++++++++++++++++-----
.../hop/pipeline/transforms/rest/RestDialog.java | 194 +++++++++++++++++++
.../hop/pipeline/transforms/rest/RestMeta.java | 39 ++++
.../pipeline/transforms/rest/common/RestConst.java | 44 +++++
.../rest/messages/messages_en_US.properties | 7 +
.../rest/messages/messages_zh_CN.properties | 7 +
6 files changed, 459 insertions(+), 43 deletions(-)
diff --git
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
index f96d450613..24dfd0ae05 100644
---
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
+++
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
@@ -40,6 +40,8 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Supplier;
import javax.net.ssl.SSLContext;
import org.apache.commons.lang.StringUtils;
import org.apache.hop.core.Const;
@@ -247,48 +249,24 @@ public class Rest extends BaseTransform<RestMeta,
RestData> {
logDebug(BaseMessages.getString(PKG, "Rest.Log.BodyValue",
entityString));
}
}
- try {
- switch (data.method) {
- case RestMeta.HTTP_METHOD_GET -> response =
invocationBuilder.get(Response.class);
- case RestMeta.HTTP_METHOD_POST -> {
- if (null != contentType) {
- response = invocationBuilder.post(Entity.entity(entityString,
contentType));
- } else {
- response = invocationBuilder.post(Entity.entity(entityString,
data.mediaType));
- }
- }
- case RestMeta.HTTP_METHOD_PUT -> {
- if (null != contentType) {
- response = invocationBuilder.put(Entity.entity(entityString,
contentType));
- } else {
- response = invocationBuilder.put(Entity.entity(entityString,
data.mediaType));
- }
- }
- case RestMeta.HTTP_METHOD_DELETE -> {
- Invocation invocation =
- invocationBuilder.build("DELETE", Entity.entity(entityString,
data.mediaType));
- response = invocation.invoke();
- }
- case RestMeta.HTTP_METHOD_HEAD -> response =
invocationBuilder.head();
- case RestMeta.HTTP_METHOD_OPTIONS -> response =
invocationBuilder.options();
- case RestMeta.HTTP_METHOD_PATCH -> {
- if (null != contentType) {
- response =
- invocationBuilder.method(
- RestMeta.HTTP_METHOD_PATCH, Entity.entity(entityString,
contentType));
- } else {
- response =
- invocationBuilder.method(
- RestMeta.HTTP_METHOD_PATCH, Entity.entity(entityString,
data.mediaType));
- }
- }
- default ->
- throw new HopException(
- BaseMessages.getString(PKG, "Rest.Error.UnknownMethod",
data.method));
- }
- } catch (Exception e) {
- throw new HopException("Request could not be processed", e);
- }
+
+ // execute first call
+ final Invocation.Builder finalInvocationBuilder = invocationBuilder;
+ final String finalEntityString = entityString;
+ final String finalContentType = contentType;
+
+ // add retry
+ response =
+ executeWithRetry(
+ () -> {
+ try {
+ return executeRequest(
+ finalInvocationBuilder, finalEntityString,
finalContentType);
+ } catch (HopException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
if (response != null) {
response.bufferEntity();
}
@@ -418,6 +396,154 @@ public class Rest extends BaseTransform<RestMeta,
RestData> {
return newRow;
}
+ private Response executeWithRetry(Supplier<Response> requestSupplier) throws
HopException {
+ int maxRetries = meta.getRetryTimes();
+ long baseDelay = meta.getRetryDelayMs();
+ List<String> retryMethods = meta.getRetryMethods();
+ Exception lastException = null;
+
+ if (retryMethods == null || retryMethods.isEmpty() ||
!retryMethods.contains(data.method)) {
+ return requestSupplier.get();
+ }
+
+ for (int attempt = 0; attempt <= maxRetries; attempt++) {
+ Response response = null;
+ try {
+ response = requestSupplier.get();
+ int status = response.getStatus();
+
+ if (!shouldRetry(String.valueOf(status))) {
+ return response;
+ }
+
+ logRetry(attempt, maxRetries, status);
+ if (attempt >= maxRetries) {
+ throw new HopException("Request failed after retries, status: " +
status);
+ }
+ } catch (Exception e) {
+ lastException = e;
+ if (attempt == maxRetries) {
+ throw new HopException("Request failed after retries", e);
+ }
+ }
+
+ if (response != null) {
+ response.close();
+ }
+ sleepBeforeRetry(attempt, baseDelay);
+ }
+
+ throw new HopException("Request failed after retries", lastException);
+ }
+
+ /**
+ * Sleeps for a computed backoff duration before performing the next retry.
+ *
+ * @param attempt the current retry attempt, starting from 0
+ * @param delay the base delay in milliseconds
+ */
+ private void sleepBeforeRetry(int attempt, long delay) {
+ try {
+ Thread.sleep(computeRetryDelay(attempt, delay));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Determines whether the current request should be retried.
+ *
+ * @param status the HTTP response status code as a string
+ * @return {@code true} if the request should be retried, {@code false}
otherwise
+ */
+ private boolean shouldRetry(String status) {
+ return meta.getRetryStatusCodes().contains(status);
+ }
+
+ /** Logs retry attempt information at detailed log level. */
+ private void logRetry(int attempt, int maxRetries, int status) {
+ if (isDetailed()) {
+ logDetailed(
+ "Retry rest {0}/{1}, status: {2}, method: {3}",
+ attempt + 1, maxRetries, status, meta.getMethod());
+ }
+ }
+
+ /**
+ * Computes the retry delay using an exponential backoff strategy with equal
jitter.
+ *
+ * <pre>
+ * delay = 100
+ * attempt = 0 → delay -> 50 ~ 149 ms
+ * attempt = 1 → delay * 2 -> 100 ~ 199 ms
+ * attempt = 2 → delay * 4 -> 200 ~ 299 ms
+ * </pre>
+ *
+ * @param attempt the current retry attempt, starting from 0
+ * @param delay the base delay in milliseconds
+ * @return the computed retry delay in milliseconds
+ */
+ private long computeRetryDelay(int attempt, long delay) {
+ long maxDelay = 30_000;
+
+ long expDelay = delay * (1L << attempt);
+ long capped = Math.min(expDelay, maxDelay);
+
+ long jitter = ThreadLocalRandom.current().nextLong(delay);
+ return capped / 2 + jitter;
+ }
+
+ private Response executeRequest(
+ Invocation.Builder invocationBuilder, String entityString, String
contentType)
+ throws HopException {
+ try {
+ switch (data.method) {
+ case RestMeta.HTTP_METHOD_GET -> {
+ return invocationBuilder.get(Response.class);
+ }
+ case RestMeta.HTTP_METHOD_POST -> {
+ if (null != contentType) {
+ return invocationBuilder.post(Entity.entity(entityString,
contentType));
+ } else {
+ return invocationBuilder.post(Entity.entity(entityString,
data.mediaType));
+ }
+ }
+ case RestMeta.HTTP_METHOD_PUT -> {
+ if (null != contentType) {
+ return invocationBuilder.put(Entity.entity(entityString,
contentType));
+ } else {
+ return invocationBuilder.put(Entity.entity(entityString,
data.mediaType));
+ }
+ }
+ case RestMeta.HTTP_METHOD_DELETE -> {
+ Invocation invocation =
+ invocationBuilder.build("DELETE", Entity.entity(entityString,
data.mediaType));
+ return invocation.invoke();
+ }
+ case RestMeta.HTTP_METHOD_HEAD -> {
+ return invocationBuilder.head();
+ }
+ case RestMeta.HTTP_METHOD_OPTIONS -> {
+ return invocationBuilder.options();
+ }
+ case RestMeta.HTTP_METHOD_PATCH -> {
+ if (null != contentType) {
+ return invocationBuilder.method(
+ RestMeta.HTTP_METHOD_PATCH, Entity.entity(entityString,
contentType));
+ } else {
+ return invocationBuilder.method(
+ RestMeta.HTTP_METHOD_PATCH, Entity.entity(entityString,
data.mediaType));
+ }
+ }
+ default ->
+ throw new HopException(
+ BaseMessages.getString(PKG, "Rest.Error.UnknownMethod",
data.method));
+ }
+ } catch (Exception e) {
+ throw new HopException("Request could not be processed", e);
+ }
+ }
+
private void setConfig() throws HopException {
if (data.config == null) {
// Use ApacheHttpClient for supporting proxy authentication.
@@ -731,7 +857,6 @@ public class Rest extends BaseTransform<RestMeta, RestData>
{
@Override
public void dispose() {
-
data.config = null;
data.headerNames = null;
data.indexOfHeaderFields = null;
diff --git
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestDialog.java
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestDialog.java
index b60dc3cd53..a21f8eb37d 100644
---
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestDialog.java
+++
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestDialog.java
@@ -29,9 +29,11 @@ import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.metadata.rest.RestConnection;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.rest.common.RestConst;
import org.apache.hop.pipeline.transforms.rest.fields.HeaderField;
import org.apache.hop.pipeline.transforms.rest.fields.MatrixParameterField;
import org.apache.hop.pipeline.transforms.rest.fields.ParameterField;
+import org.apache.hop.ui.core.FormDataBuilder;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.BaseDialog;
import org.apache.hop.ui.core.dialog.ErrorDialog;
@@ -55,6 +57,7 @@ import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
@@ -150,6 +153,13 @@ public class RestDialog extends BaseTransformDialog {
private MetaSelectionLine wSelectionLine;
+ /** retry */
+ private TextVar wRetryTimes;
+
+ private TextVar wRetryDelay;
+ private TableView wStatusCodes;
+ private TableView wMethods;
+
public RestDialog(
Shell parent, IVariables variables, RestMeta transformMeta, PipelineMeta
pipelineMeta) {
super(parent, variables, transformMeta, pipelineMeta);
@@ -425,6 +435,9 @@ public class RestDialog extends BaseTransformDialog {
setupMatrixParamTabContent(lsMod, margin, wMatrixParametersTab,
wMatrixParametersComp);
+ // retry
+ addRetryTabItem(wTabFolder, lsMod);
+
FormData fdTabFolder = new FormData();
fdTabFolder.left = new FormAttachment(0, 0);
fdTabFolder.top = new FormAttachment(wTransformName, margin);
@@ -455,6 +468,117 @@ public class RestDialog extends BaseTransformDialog {
return transformName;
}
+ private void addRetryTabItem(CTabFolder wTabFolder, ModifyListener lsMod) {
+ CTabItem retryTab = new CTabItem(wTabFolder, SWT.NONE);
+ retryTab.setFont(GuiResource.getInstance().getFontDefault());
+ retryTab.setText(BaseMessages.getString(PKG,
"RestDialog.Tab.Retry.Title"));
+
+ Composite wRetryComp = new Composite(wTabFolder, SWT.NONE);
+ PropsUi.setLook(wRetryComp);
+ FormLayout pl = new FormLayout();
+ pl.marginWidth = PropsUi.getFormMargin();
+ pl.marginHeight = PropsUi.getFormMargin();
+ wRetryComp.setLayout(pl);
+
+ // Retry Times
+ Label wlRetryTimes = new Label(wRetryComp, SWT.RIGHT);
+ wlRetryTimes.setText(BaseMessages.getString(PKG,
"RestDialog.Tab.Retry.Times"));
+ PropsUi.setLook(wlRetryTimes);
+ wlRetryTimes.setLayoutData(FormDataBuilder.builder().top(0,
5).left().width(300).build());
+
+ wRetryTimes = new TextVar(variables, wRetryComp, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ PropsUi.setLook(wRetryTimes);
+ wRetryTimes.setLayoutData(
+ FormDataBuilder.builder().top(0, 5).left(wlRetryTimes, 5).right(100,
0).build());
+
+ // Retry Delay (ms)
+ Label wlRetryDelay = new Label(wRetryComp, SWT.RIGHT);
+ wlRetryDelay.setText(BaseMessages.getString(PKG,
"RestDialog.Tab.Retry.Delay"));
+ PropsUi.setLook(wlRetryDelay);
+ wlRetryDelay.setLayoutData(
+ FormDataBuilder.builder().top(wlRetryTimes,
5).left().width(300).build());
+
+ wRetryDelay = new TextVar(variables, wRetryComp, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ PropsUi.setLook(wRetryDelay);
+ wRetryDelay.setLayoutData(
+ FormDataBuilder.builder().top(wRetryTimes, 5).left(wlRetryDelay,
5).right(100, 0).build());
+
+ // Status Codes table (left)
+ retryResponseCodeLeft(wRetryComp, wlRetryDelay, lsMod);
+ // HTTP Methods table (right)
+ retryRequestMethodRight(wRetryComp, wRetryDelay, lsMod);
+
+ wRetryComp.setLayoutData(
+ FormDataBuilder.builder().top().left().right(100, 0).bottom(100,
0).build());
+ wRetryComp.layout();
+ retryTab.setControl(wRetryComp);
+ }
+
+ private void retryResponseCodeLeft(Composite wRetryComp, Control top,
ModifyListener lsMod) {
+ // Status Codes table (left)
+ Label wlStatusCodes = new Label(wRetryComp, SWT.LEFT);
+ wlStatusCodes.setText(BaseMessages.getString(PKG,
"RestDialog.Tab.Retry.Http.Status"));
+ PropsUi.setLook(wlStatusCodes);
+ wlStatusCodes.setLayoutData(FormDataBuilder.builder().top(top, 30).left(0,
0).build());
+
+ String colName = BaseMessages.getString(PKG,
"RestDialog.Tab.Retry.Http.Status.Header");
+ ColumnInfo[] headers =
+ new ColumnInfo[] {new ColumnInfo(colName, ColumnInfo.COLUMN_TYPE_TEXT,
false)};
+
+ wStatusCodes =
+ new TableView(
+ variables,
+ wRetryComp,
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI,
+ headers,
+ 0,
+ false,
+ lsMod,
+ props);
+
+ PropsUi.setLook(wStatusCodes);
+ wStatusCodes.setLayoutData(
+ FormDataBuilder.builder()
+ .top(wlStatusCodes, PropsUi.getMargin())
+ .left(0, 0)
+ .right(50, -PropsUi.getMargin() / 2)
+ .bottom(100, 0)
+ .build());
+ }
+
+ private void retryRequestMethodRight(Composite wRetryComp, Control top,
ModifyListener lsMod) {
+ // HTTP Methods table (right)
+ Label wlMethods = new Label(wRetryComp, SWT.LEFT);
+ wlMethods.setText(BaseMessages.getString(PKG,
"RestDialog.Tab.Retry.Http.Method"));
+ PropsUi.setLook(wlMethods);
+ wlMethods.setLayoutData(
+ FormDataBuilder.builder().top(top, 30).left(50, PropsUi.getMargin() /
2).build());
+
+ String colName = BaseMessages.getString(PKG,
"RestDialog.Tab.Retry.Http.Method.Header");
+ ColumnInfo[] headers =
+ new ColumnInfo[] {new ColumnInfo(colName, ColumnInfo.COLUMN_TYPE_TEXT,
false)};
+
+ wMethods =
+ new TableView(
+ variables,
+ wRetryComp,
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI,
+ headers,
+ 0,
+ false,
+ lsMod,
+ props);
+
+ PropsUi.setLook(wMethods);
+ wMethods.setLayoutData(
+ FormDataBuilder.builder()
+ .top(wlMethods, PropsUi.getMargin())
+ .left(50, PropsUi.getMargin() / 2)
+ .right(100, 0)
+ .bottom(100, 0)
+ .build());
+ }
+
private void setupMatrixParamTabContent(
ModifyListener lsMod,
int margin,
@@ -1460,10 +1584,46 @@ public class RestDialog extends BaseTransformDialog {
wFields.setRowNums();
wFields.optWidth(true);
+ // get retry data
+ getDataForRetry();
+
wTransformName.selectAll();
wTransformName.setFocus();
}
+ private void getDataForRetry() {
+ if (input.getRetryTimes() != null) {
+ wRetryTimes.setText(input.getRetryTimes().toString());
+ } else {
+ wRetryTimes.setText(RestConst.DEFAULT_RETRY_TIMES + Const.EMPTY_STRING);
+ }
+
+ if (input.getRetryDelayMs() != null) {
+ wRetryDelay.setText(input.getRetryDelayMs().toString());
+ } else {
+ wRetryDelay.setText(RestConst.DEFAULT_RETRY_DELAY_MS +
Const.EMPTY_STRING);
+ }
+
+ // http response code
+ List<String> codes = input.getRetryStatusCodes();
+ if (codes != null && !codes.isEmpty()) {
+ wStatusCodes.table.removeAll();
+ for (String code : codes) {
+ TableItem item = new TableItem(wStatusCodes.table, SWT.NONE);
+ item.setText(1, code);
+ }
+ }
+
+ // http request methods
+ List<String> methods = input.getRetryMethods();
+ if (methods != null && !methods.isEmpty()) {
+ wMethods.table.removeAll();
+ for (String method : methods) {
+ wMethods.add(method);
+ }
+ }
+ }
+
private void cancel() {
transformName = null;
input.setChanged(changed);
@@ -1538,9 +1698,43 @@ public class RestDialog extends BaseTransformDialog {
input.setApplicationType(wApplicationType.getText());
transformName = wTransformName.getText(); // return value
+ okForRetry();
dispose();
}
+ private void okForRetry() {
+ // retry times
+ if (!Utils.isEmpty(wRetryTimes.getText())) {
+ int times = Integer.parseInt(wRetryTimes.getText());
+ input.setRetryTimes(Math.max(times, 0));
+ } else {
+ input.setRetryTimes(RestConst.DEFAULT_RETRY_TIMES);
+ }
+
+ // retry delay
+ if (!Utils.isEmpty(wRetryDelay.getText())) {
+ long delay = Long.parseLong(wRetryDelay.getText());
+ input.setRetryDelayMs(Math.max(delay, RestConst.DEFAULT_RETRY_DELAY_MS /
2));
+ } else {
+ input.setRetryDelayMs(RestConst.DEFAULT_RETRY_DELAY_MS);
+ }
+
+ input.getRetryStatusCodes().clear();
+ for (int i = 0; i < wStatusCodes.nrNonEmpty(); i++) {
+ TableItem item = wStatusCodes.getNonEmpty(i);
+ input.getRetryStatusCodes().add(item.getText(1));
+ }
+
+ input.getRetryMethods().clear();
+ for (int i = 0; i < wMethods.nrNonEmpty(); i++) {
+ TableItem item = wMethods.getNonEmpty(i);
+ String text = item.getText(1);
+ if (!Utils.isEmpty(text)) {
+ input.getRetryMethods().add(text.toUpperCase());
+ }
+ }
+ }
+
private void getParametersFields(TableView tView) {
try {
IRowMeta r = pipelineMeta.getPrevTransformFields(variables,
transformName);
diff --git
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestMeta.java
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestMeta.java
index 4ac1f9155f..886288992b 100644
---
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestMeta.java
+++
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestMeta.java
@@ -36,6 +36,7 @@ import org.apache.hop.metadata.api.IHopMetadataProvider;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.BaseTransformMeta;
import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.rest.common.RestConst;
import org.apache.hop.pipeline.transforms.rest.fields.HeaderField;
import org.apache.hop.pipeline.transforms.rest.fields.MatrixParameterField;
import org.apache.hop.pipeline.transforms.rest.fields.ParameterField;
@@ -185,12 +186,44 @@ public class RestMeta extends BaseTransformMeta<Rest,
RestData> {
@HopMetadataProperty(key = "result", injectionKey = "RESULT")
private ResultField resultField;
+ /**
+ * retry config retryTimes=0 retryDelayMs=500ms retryStatusCode=[429, 500,
502, 503, 504]
+ * retryMethods=[post,get,delete,put,head,option,patch]
+ */
+ /*--------------------------------------------------------------------
+ | retry config(retryTimes=0,retryDelayMs=500ms)
+ | retryStatusCode=[429, 500, 502, 503, 504]
+ | retryMethods=[get,delete,put]
+ --------------------------------------------------------------------- */
+ @HopMetadataProperty(key = "retryTimes", injectionKey = "RETRY_TIMES")
+ private Integer retryTimes;
+
+ @HopMetadataProperty(key = "retryDelayMs", injectionKey = "RETRY_DELAY_MS")
+ private Long retryDelayMs;
+
+ @HopMetadataProperty(
+ key = "retryStatusCode",
+ injectionKey = "RETRY_STATUS_CODE",
+ groupKey = "retryStatusCodes",
+ injectionGroupKey = "RETRY_STATUS_CODES")
+ private List<String> retryStatusCodes;
+
+ @HopMetadataProperty(
+ key = "retryMethod",
+ injectionKey = "RETRY_METHOD",
+ groupKey = "retryMethods",
+ injectionGroupKey = "RETRY_METHODS")
+ private List<String> retryMethods;
+
public RestMeta() {
super(); // allocate BaseTransformMeta
headerFields = new ArrayList<>();
parameterFields = new ArrayList<>();
matrixParameterFields = new ArrayList<>();
resultField = new ResultField();
+
+ this.retryStatusCodes = new ArrayList<>();
+ this.retryMethods = new ArrayList<>();
}
@Override
@@ -216,6 +249,12 @@ public class RestMeta extends BaseTransformMeta<Rest,
RestData> {
this.applicationType = APPLICATION_TYPE_TEXT_PLAIN;
this.readTimeout = String.valueOf(DEFAULT_READ_TIMEOUT);
this.connectionTimeout = String.valueOf(DEFAULT_CONNECTION_TIMEOUT);
+
+ // retry config.
+ this.retryTimes = RestConst.DEFAULT_RETRY_TIMES;
+ this.retryDelayMs = RestConst.DEFAULT_RETRY_DELAY_MS;
+ this.retryStatusCodes.addAll(RestConst.retryStatusCodes());
+ this.retryMethods.addAll(RestConst.retryMethods());
}
@Override
diff --git
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/common/RestConst.java
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/common/RestConst.java
new file mode 100644
index 0000000000..8b2e346e41
--- /dev/null
+++
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/common/RestConst.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.rest.common;
+
+import java.util.Set;
+import lombok.experimental.UtilityClass;
+import org.apache.hop.pipeline.transforms.rest.RestMeta;
+
+/** rest const */
+@UtilityClass
+public class RestConst {
+ public static final int DEFAULT_RETRY_TIMES = 0;
+ public static final long DEFAULT_RETRY_DELAY_MS = 200L;
+
+ /**
+ * By default, the HTTP status codes that should be retried include 429,
500, 502, 503, and 504.
+ */
+ public static Set<String> retryStatusCodes() {
+ return Set.of("429", "500", "502", "503", "504");
+ }
+
+ /**
+ * By default, the HTTP methods that should be retried include GET, PUT, and
DELETE, as these
+ * methods are considered idempotent and safe to retry.
+ */
+ public static Set<String> retryMethods() {
+ return Set.of(RestMeta.HTTP_METHOD_GET, RestMeta.HTTP_METHOD_DELETE,
RestMeta.HTTP_METHOD_PUT);
+ }
+}
diff --git
a/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_en_US.properties
b/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_en_US.properties
index dbc2df37a1..545f466c85 100644
---
a/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_en_US.properties
+++
b/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_en_US.properties
@@ -111,3 +111,10 @@ RestMeta.CheckResult.UrlMissing=URL is missing\!
RestMeta.CheckResult.UrlOk=URL is specified.
RestMeta.Exception.UnableToReadTransformMeta=Unable to read transform
information from XML
RestMeta.keyword=rest
+RestDialog.Tab.Retry.Title=Retry Option
+RestDialog.Tab.Retry.Times=Retry times
+RestDialog.Tab.Retry.Delay=Retry delay(ms)
+RestDialog.Tab.Retry.Http.Status=Retry HTTP status codes
+RestDialog.Tab.Retry.Http.Status.Header=Status Code
+RestDialog.Tab.Retry.Http.Method=Retry HTTP methods
+RestDialog.Tab.Retry.Http.Method.Header=Method
\ No newline at end of file
diff --git
a/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_zh_CN.properties
b/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_zh_CN.properties
index 75646a3ef4..5c1f83fa57 100644
---
a/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_zh_CN.properties
+++
b/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_zh_CN.properties
@@ -106,3 +106,10 @@ RestMeta.CheckResult.UrlMissing=\u7F3A\u5931 URL\!
RestMeta.CheckResult.UrlOk=\u5DF2\u6307\u5B9A URL
RestMeta.Exception.UnableToReadTransformMeta=\u65E0\u6CD5\u4ECE XML
\u7247\u65AD\u52A0\u8F7D\u8BE5 Transform \u4FE1\u606F
RestMeta.keyword=rest
+RestDialog.Tab.Retry.Title=\u91CD\u8BD5\u8BBE\u7F6E
+RestDialog.Tab.Retry.Times=\u6700\u5927\u91CD\u8BD5\u6B21\u6570
+RestDialog.Tab.Retry.Delay=\u91CD\u8BD5\u5EF6\u8FDF(ms)
+RestDialog.Tab.Retry.Http.Status=\u91CD\u8BD5http\u54CD\u5E94\u7801
+RestDialog.Tab.Retry.Http.Status.Header=\u54CD\u5E94\u7801
+RestDialog.Tab.Retry.Http.Method=\u91CD\u8BD5http\u65B9\u6CD5
+RestDialog.Tab.Retry.Http.Method.Header=\u65B9\u6CD5\u540D\u79F0