Skip to content

Commit 0784aba

Browse files
authored
Merge pull request #345 from lowcoder-org/rest_api_timeout
fix: honor timeout set for REST API calls
2 parents 1c11818 + 6025a80 commit 0784aba

File tree

13 files changed

+99
-42
lines changed

13 files changed

+99
-42
lines changed

deploy/docker/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Image can be configured by setting environment variables.
3737
| `ENCRYPTION_SALT` | Salt used for encrypting password | `lowcoder.org` |
3838
| `CORS_ALLOWED_DOMAINS` | CORS allowed domains | `*` |
3939
| `LOWCODER_MAX_REQUEST_SIZE` | Lowcoder max request size | `20m` |
40+
| `LOWCODER_MAX_QUERY_TIMEOUT` | Lowcoder max query timeout (in seconds) | `120` |
4041
| `LOWCODER_API_SERVICE_URL` | Lowcoder API service URL | `http://localhost:8080` |
4142
| `LOWCODER_NODE_SERVICE_URL` | Lowcoder Node service (js executor) URL | `http://localhost:6060` |
4243
| `DEFAULT_ORGS_PER_USER` | Default maximum organizations per user | `100` |
@@ -77,6 +78,8 @@ Image can be configured by setting environment variables.
7778
| `DEFAULT_ORG_GROUP_COUNT` | Default maximum groups per organization | `100` |
7879
| `DEFAULT_ORG_APP_COUNT` | Default maximum applications per organization | `1000` |
7980
| `DEFAULT_DEVELOPER_COUNT` | Default maximum developers | `100` |
81+
| `LOWCODER_MAX_QUERY_TIMEOUT` | Lowcoder max query timeout (in seconds) | `120` |
82+
| `LOWCODER_MAX_REQUEST_SIZE` | Lowcoder max request size | `20m` |
8083

8184

8285

@@ -122,6 +125,7 @@ Image can be configured by setting environment variables.
122125
| --------------------------------| --------------------------------------------------------------------| ------------------------------------------------------- |
123126
| `PUID` | ID of user running services. It will own all created logs and data. | `9001` |
124127
| `PGID` | ID of group of the user running services. | `9001` |
128+
| `LOWCODER_MAX_QUERY_TIMEOUT` | Lowcoder max query timeout (in seconds) | `120` |
125129
| `LOWCODER_MAX_REQUEST_SIZE` | Lowcoder max request size | `20m` |
126130
| `LOWCODER_API_SERVICE_URL` | Lowcoder API service URL | `http://localhost:8080` |
127131
| `LOWCODER_NODE_SERVICE_URL` | Lowcoder Node service (js executor) URL | `http://localhost:6060` |

deploy/docker/docker-compose-multi.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ services:
3636
MONGODB_URL: "mongodb://lowcoder:secret123@mongodb/lowcoder?authSource=admin"
3737
REDIS_URL: "redis://redis:6379"
3838
LOWCODER_NODE_SERVICE_URL: "http://lowcoder-node-service:6060"
39+
LOWCODER_MAX_QUERY_TIMEOUT: 120
3940
ENABLE_USER_SIGN_UP: "true"
4041
ENCRYPTION_PASSWORD: "lowcoder.org"
4142
ENCRYPTION_SALT: "lowcoder.org"
@@ -76,6 +77,7 @@ services:
7677
PUID: "9001"
7778
PGID: "9001"
7879
LOWCODER_MAX_REQUEST_SIZE: 20m
80+
LOWCODER_MAX_QUERY_TIMEOUT: 120
7981
LOWCODER_API_SERVICE_URL: "http://lowcoder-api-service:8080"
8082
LOWCODER_NODE_SERVICE_URL: "http://lowcoder-node-service:6060"
8183
restart: unless-stopped

deploy/docker/docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ services:
3838
LOWCODER_NODE_SERVICE_URL: "http://localhost:6060"
3939
# frontend parameters
4040
LOWCODER_MAX_REQUEST_SIZE: 20m
41+
LOWCODER_MAX_QUERY_TIMEOUT: 120
4142
volumes:
4243
- ./lowcoder-stacks:/lowcoder-stacks
4344
restart: unless-stopped

deploy/docker/frontend/01-update-nginx-conf.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ else
1818
ln -s /etc/nginx/nginx-http.conf /etc/nginx/nginx.conf
1919
fi;
2020

21+
sed -i "s@__LOWCODER_MAX_QUERY_TIMEOUT__@${LOWCODER_MAX_QUERY_TIMEOUT:=120}@" /etc/nginx/nginx.conf
2122
sed -i "s@__LOWCODER_MAX_REQUEST_SIZE__@${LOWCODER_MAX_REQUEST_SIZE:=20m}@" /etc/nginx/nginx.conf
2223
sed -i "s@__LOWCODER_API_SERVICE_URL__@${LOWCODER_API_SERVICE_URL:=http://localhost:8080}@" /etc/nginx/nginx.conf
2324
sed -i "s@__LOWCODER_NODE_SERVICE_URL__@${LOWCODER_NODE_SERVICE_URL:=http://localhost:6060}@" /etc/nginx/nginx.conf

deploy/docker/frontend/nginx-http.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ http {
3535
listen 3000 default_server;
3636
root /lowcoder/client;
3737

38+
proxy_connect_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
39+
proxy_send_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
40+
proxy_read_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
3841

3942
location / {
4043
try_files $uri /index.html;

deploy/docker/frontend/nginx-https.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ http {
3838
include /etc/nginx/ssl-certificate.conf;
3939
include /etc/nginx/ssl-params.conf;
4040

41+
proxy_connect_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
42+
proxy_send_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
43+
proxy_read_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
44+
4145
location / {
4246
try_files $uri /index.html;
4347

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/query/service/QueryExecutionService.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.lowcoder.domain.plugin.client.DatasourcePluginClient;
1717
import org.lowcoder.domain.plugin.service.DatasourceMetaInfoService;
1818
import org.lowcoder.domain.query.util.QueryTimeoutUtils;
19+
import org.lowcoder.sdk.config.CommonConfig;
1920
import org.lowcoder.sdk.exception.BizException;
2021
import org.lowcoder.sdk.exception.PluginException;
2122
import org.lowcoder.sdk.models.QueryExecutionResult;
@@ -40,10 +41,14 @@ public class QueryExecutionService {
4041
@Autowired
4142
private DatasourcePluginClient datasourcePluginClient;
4243

44+
@Autowired
45+
private CommonConfig common;
46+
4347
public Mono<QueryExecutionResult> executeQuery(Datasource datasource, Map<String, Object> queryConfig, Map<String, Object> requestParams,
4448
String timeoutStr, QueryVisitorContext queryVisitorContext) {
4549

46-
int timeoutMs = QueryTimeoutUtils.parseQueryTimeoutMs(timeoutStr, requestParams);
50+
int timeoutMs = QueryTimeoutUtils.parseQueryTimeoutMs(timeoutStr, requestParams, common.getMaxQueryTimeout());
51+
queryConfig.putIfAbsent("timeoutMs", timeoutMs);
4752

4853
return Mono.defer(() -> {
4954
if (datasourceMetaInfoService.isJsDatasourcePlugin(datasource.getType())) {

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/query/util/QueryTimeoutUtils.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@
1717
public final class QueryTimeoutUtils {
1818

1919
private static final int DEFAULT_QUERY_TIMEOUT_MILLIS = 10000;
20-
private static final int MAX_QUERY_TIMEOUT_SECONDS = 120;
2120

22-
public static int parseQueryTimeoutMs(String timeoutStr, Map<String, Object> paramMap) {
23-
return parseQueryTimeoutMs(renderMustacheString(timeoutStr, paramMap));
21+
public static int parseQueryTimeoutMs(String timeoutStr, Map<String, Object> paramMap, int maxQueryTimeout) {
22+
return parseQueryTimeoutMs(renderMustacheString(timeoutStr, paramMap), maxQueryTimeout);
2423
}
2524

2625
@VisibleForTesting
27-
public static int parseQueryTimeoutMs(String timeoutStr) {
26+
public static int parseQueryTimeoutMs(String timeoutStr, int maxQueryTimeout) {
2827
if (StringUtils.isBlank(timeoutStr)) {
2928
return DEFAULT_QUERY_TIMEOUT_MILLIS;
3029
}
@@ -44,10 +43,10 @@ public static int parseQueryTimeoutMs(String timeoutStr) {
4443
if (value < 0) {
4544
throw new PluginException(QUERY_ARGUMENT_ERROR, "INVALID_TIMEOUT_SETTING", timeoutStr);
4645
}
47-
46+
4847
int millis = convertToMs(value, unit);
49-
if (millis > Duration.ofSeconds(MAX_QUERY_TIMEOUT_SECONDS).toMillis()) {
50-
throw new PluginException(EXCEED_MAX_QUERY_TIMEOUT, "EXCEED_MAX_QUERY_TIMEOUT", MAX_QUERY_TIMEOUT_SECONDS);
48+
if (millis > Duration.ofSeconds(maxQueryTimeout).toMillis()) {
49+
throw new PluginException(EXCEED_MAX_QUERY_TIMEOUT, "EXCEED_MAX_QUERY_TIMEOUT", maxQueryTimeout);
5150
}
5251

5352
return millis;

server/api-service/lowcoder-plugins/restApiPlugin/src/main/java/org/lowcoder/plugin/restapi/RestApiExecutor.java

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,48 @@
1919

2020
package org.lowcoder.plugin.restapi;
2121

22-
import com.fasterxml.jackson.core.JsonProcessingException;
23-
import com.fasterxml.jackson.databind.JsonNode;
24-
import com.fasterxml.jackson.databind.node.ObjectNode;
25-
import com.google.common.collect.ImmutableMap;
26-
import lombok.Builder;
27-
import lombok.Getter;
22+
import static com.google.common.base.MoreObjects.firstNonNull;
23+
import static org.apache.commons.collections4.MapUtils.emptyIfNull;
24+
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
25+
import static org.lowcoder.plugin.restapi.RestApiError.REST_API_EXECUTION_ERROR;
26+
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isBinary;
27+
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isJson;
28+
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isJsonContentType;
29+
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isPicture;
30+
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isValidContentType;
31+
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.parseContentType;
32+
import static org.lowcoder.sdk.exception.PluginCommonError.JSON_PARSE_ERROR;
33+
import static org.lowcoder.sdk.exception.PluginCommonError.QUERY_ARGUMENT_ERROR;
34+
import static org.lowcoder.sdk.exception.PluginCommonError.QUERY_EXECUTION_ERROR;
35+
import static org.lowcoder.sdk.plugin.restapi.DataUtils.convertToMultiformFileValue;
36+
import static org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType.DIGEST_AUTH;
37+
import static org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType.OAUTH2_INHERIT_FROM_LOGIN;
38+
import static org.lowcoder.sdk.util.ExceptionUtils.propagateError;
39+
import static org.lowcoder.sdk.util.JsonUtils.readTree;
40+
import static org.lowcoder.sdk.util.JsonUtils.toJsonThrows;
41+
import static org.lowcoder.sdk.util.MustacheHelper.renderMustacheJson;
42+
import static org.lowcoder.sdk.util.MustacheHelper.renderMustacheString;
43+
import static org.lowcoder.sdk.util.StreamUtils.collectList;
44+
45+
import java.io.IOException;
46+
import java.net.URI;
47+
import java.net.URISyntaxException;
48+
import java.nio.charset.StandardCharsets;
49+
import java.text.ParseException;
50+
import java.time.Duration;
51+
import java.util.ArrayList;
52+
import java.util.Base64;
53+
import java.util.HashMap;
54+
import java.util.HashSet;
55+
import java.util.List;
56+
import java.util.Map;
57+
import java.util.Set;
58+
import java.util.function.Consumer;
59+
import java.util.stream.Collectors;
60+
import java.util.stream.Stream;
61+
62+
import javax.annotation.Nullable;
63+
2864
import org.apache.commons.collections4.CollectionUtils;
2965
import org.apache.commons.lang3.ObjectUtils;
3066
import org.apache.commons.lang3.StringUtils;
@@ -51,41 +87,29 @@
5187
import org.lowcoder.sdk.query.QueryVisitorContext;
5288
import org.lowcoder.sdk.webclient.WebClientBuildHelper;
5389
import org.pf4j.Extension;
54-
import org.springframework.http.*;
90+
import org.springframework.http.HttpCookie;
91+
import org.springframework.http.HttpHeaders;
92+
import org.springframework.http.HttpMethod;
93+
import org.springframework.http.HttpStatus;
94+
import org.springframework.http.MediaType;
95+
import org.springframework.http.ResponseEntity;
5596
import org.springframework.http.client.reactive.ClientHttpRequest;
97+
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
5698
import org.springframework.util.MultiValueMap;
5799
import org.springframework.web.reactive.function.BodyInserter;
58100
import org.springframework.web.reactive.function.BodyInserters;
59101
import org.springframework.web.reactive.function.client.ExchangeStrategies;
60102
import org.springframework.web.reactive.function.client.WebClient;
61-
import reactor.core.publisher.Mono;
62103

63-
import javax.annotation.Nullable;
64-
import java.io.IOException;
65-
import java.net.URI;
66-
import java.net.URISyntaxException;
67-
import java.nio.charset.StandardCharsets;
68-
import java.text.ParseException;
69-
import java.util.*;
70-
import java.util.function.Consumer;
71-
import java.util.stream.Collectors;
72-
import java.util.stream.Stream;
104+
import com.fasterxml.jackson.core.JsonProcessingException;
105+
import com.fasterxml.jackson.databind.JsonNode;
106+
import com.fasterxml.jackson.databind.node.ObjectNode;
107+
import com.google.common.collect.ImmutableMap;
73108

74-
import static com.google.common.base.MoreObjects.firstNonNull;
75-
import static org.apache.commons.collections4.MapUtils.emptyIfNull;
76-
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
77-
import static org.lowcoder.plugin.restapi.RestApiError.REST_API_EXECUTION_ERROR;
78-
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.*;
79-
import static org.lowcoder.sdk.exception.PluginCommonError.*;
80-
import static org.lowcoder.sdk.plugin.restapi.DataUtils.convertToMultiformFileValue;
81-
import static org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType.DIGEST_AUTH;
82-
import static org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType.OAUTH2_INHERIT_FROM_LOGIN;
83-
import static org.lowcoder.sdk.util.ExceptionUtils.propagateError;
84-
import static org.lowcoder.sdk.util.JsonUtils.readTree;
85-
import static org.lowcoder.sdk.util.JsonUtils.toJsonThrows;
86-
import static org.lowcoder.sdk.util.MustacheHelper.renderMustacheJson;
87-
import static org.lowcoder.sdk.util.MustacheHelper.renderMustacheString;
88-
import static org.lowcoder.sdk.util.StreamUtils.collectList;
109+
import lombok.Builder;
110+
import lombok.Getter;
111+
import reactor.core.publisher.Mono;
112+
import reactor.netty.http.client.HttpClient;
89113

90114
@Extension
91115
public class RestApiExecutor implements QueryExecutor<RestApiDatasourceConfig, Object, RestApiQueryExecutionContext> {
@@ -176,6 +200,7 @@ public RestApiQueryExecutionContext buildQueryExecutionContext(RestApiDatasource
176200
.authConfig(datasourceConfig.getAuthConfig())
177201
.sslConfig(datasourceConfig.getSslConfig())
178202
.authTokenMono(queryVisitorContext.getAuthTokenMono())
203+
.timeoutMs(queryConfig.getTimeoutMs())
179204
.build();
180205
}
181206

@@ -235,9 +260,13 @@ public Mono<QueryExecutionResult> executeQuery(Object webClientFilter, RestApiQu
235260
webClientBuilder.filter(new BufferingFilter());
236261
}
237262

263+
HttpClient httpClient = HttpClient.create()
264+
.responseTimeout(Duration.ofMillis(context.getTimeoutMs()));
265+
238266
webClientBuilder.defaultCookies(injectCookies(context));
239267
WebClient client = webClientBuilder
240268
.exchangeStrategies(exchangeStrategies)
269+
.clientConnector(new ReactorClientHttpConnector(httpClient))
241270
.build();
242271

243272
BodyInserter<?, ? super ClientHttpRequest> bodyInserter = buildBodyInserter(

server/api-service/lowcoder-plugins/restApiPlugin/src/main/java/org/lowcoder/plugin/restapi/model/RestApiQueryConfig.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,19 @@ public class RestApiQueryConfig {
3030
private final List<Property> params;
3131
private final List<Property> headers;
3232
private final List<Property> bodyFormData;
33+
private final long timeoutMs;
3334

3435
@JsonCreator
3536
private RestApiQueryConfig(HttpMethod httpMethod, boolean disableEncodingParams, String body, String path,
36-
List<Property> params, List<Property> headers, List<Property> bodyFormData) {
37+
List<Property> params, List<Property> headers, List<Property> bodyFormData, long timeoutMs) {
3738
this.httpMethod = httpMethod;
3839
this.disableEncodingParams = disableEncodingParams;
3940
this.body = body;
4041
this.path = path;
4142
this.params = params;
4243
this.headers = headers;
4344
this.bodyFormData = bodyFormData;
45+
this.timeoutMs = timeoutMs;
4446
}
4547

4648
public static RestApiQueryConfig from(Map<String, Object> queryConfigs) {

server/api-service/lowcoder-plugins/restApiPlugin/src/main/java/org/lowcoder/plugin/restapi/model/RestApiQueryExecutionContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public class RestApiQueryExecutionContext extends QueryExecutionContext {
4343
@Getter
4444
private Mono<List<Property>> authTokenMono;
4545
private SslConfig sslConfig;
46+
private long timeoutMs;
4647

4748
public URI getUri() {
4849
return uri;
@@ -96,4 +97,8 @@ public AuthConfig getAuthConfig() {
9697
public SslConfig getSslConfig() {
9798
return sslConfig;
9899
}
100+
101+
public long getTimeoutMs() {
102+
return timeoutMs;
103+
}
99104
}

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class CommonConfig {
3535
private String version;
3636
private boolean blockHoundEnable;
3737
private String cookieName;
38+
private int maxQueryTimeout = 300;
3839
private String maxUploadSize = "20MB";
3940
private String maxQueryRequestSize = "20MB";
4041
private String maxQueryResponseSize = "20MB";

server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ common:
4444
max-query-request-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
4545
max-query-response-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
4646
max-upload-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
47+
max-query-timeout: ${LOWCODER_MAX_QUERY_TIMEOUT:120}
4748

4849
material:
4950
mongodb-grid-fs:

0 commit comments

Comments
 (0)