Skip to content

Commit 3c9c40f

Browse files
wenzhenghuyx-keith
andauthored
[fix](fe) Fix broken pipe risk on stream load redirect with unconsumed request body (#63332)
### What problem does this PR solve? Issue Number: close #63325 Problem Summary: **Problem** - Starting from Doris `3.1.3`, FE uses `Jetty 12`, and this introduced a compatibility change in the Stream Load redirect path. - When a Stream Load request is sent to FE, FE may return `307 Temporary Redirect` before the request body is fully consumed. Under `Jetty 12`, this behavior is more likely to cause early connection close or reset while the client is still writing the request body. - As a result, some `HTTP/1.1` streaming clients may observe errors such as `BrokenPipeError` or `ConnectionResetError` when sending Stream Load requests through FE. - The problem is more visible with chunked uploads, higher network latency, and clients that continue sending request body data before fully processing the redirect response. - In short, this is a compatibility regression introduced by the `Jetty 12` upgrade in Doris `3.1.3` and later. **Fix** - We keep the existing FE-to-BE redirect architecture unchanged, so FE still redirects Stream Load requests to BE instead of proxying the full request body. - We add a bounded request-body drain step on the FE Stream Load redirect path: - FE first writes the `307 Temporary Redirect` response. - FE then drains and discards only a bounded amount of the remaining request body. - This provides a small compatibility window for in-flight client writes and reduces the chance of early connection reset. - We also apply the same handling to token-authenticated Stream Load requests, so both password-authenticated and token-authenticated paths behave consistently. - In addition, we expose Jetty's unconsumed request content read setting through FE configuration and apply it to HTTP connectors, so operators can tune Jetty behavior for redirect scenarios where the request body is not fully consumed. - To make the compatibility path effective out of the box, this PR also enables the bounded drain path by default with a `1GB` drain limit and a `1000ms` idle wait window. **New Configurations** - `jetty_server_max_unconsumed_request_content_reads` - Controls how many extra reads Jetty performs for unconsumed request content. - `-1` means unlimited, `0` disables extra reads, and a positive value sets the maximum number of read attempts. - Default value in this PR: `-1`. - This helps tune Jetty behavior after the `Jetty 12` upgrade when FE returns a response before the request body is fully consumed. - `stream_load_redirect_bounded_drain_max_bytes` - Controls the maximum number of request body bytes FE drains after returning `307` for a Stream Load redirect. - `0` disables this compatibility logic. - A positive value enables bounded draining and limits how much data FE will discard. - Default value in this PR: `1GB`. - `stream_load_redirect_bounded_drain_max_idle_time_ms` - Controls how long FE waits for more readable request body data during the bounded drain process. - `0` disables the extra idle wait. - A positive value provides a small grace window for slow clients or delayed body chunks, helping absorb in-flight writes without keeping the connection open indefinitely. - Default value in this PR: `1000ms`. **Test Result / Validation** - Verified the behavior with the same Python `HTTP/1.1` chunked Stream Load reproduction used during issue analysis. - Reproduced requests were sent to FE with `Expect: 100-continue`, `Transfer-Encoding: chunked`, and paced body streaming to maximize the redirect race window. - Baseline validation on Doris `3.0` (`9030` / FE `8030`): - `payload_mb=1`, `chunk_kb=1`, `sleep_ms=0` - `payload_mb=8`, `chunk_kb=16`, `sleep_ms=10` - Both requests returned normal `307 Temporary Redirect`. - Validation on the fixed Doris `3.1.4` instance (`9034` / FE `8034`): - Before enabling the bounded drain config, the same reproduction still triggered `BrokenPipeError`. - After enabling the FE configs below: - `jetty_server_max_unconsumed_request_content_reads = -1` - `stream_load_redirect_bounded_drain_max_bytes = 16777216` - `stream_load_redirect_bounded_drain_max_idle_time_ms = 1000` - The same two reproduction requests both returned normal `307 Temporary Redirect`. - No `BrokenPipeError` or `ConnectionResetError` was observed after the config took effect. - The PR now further updates the default bounded drain byte limit from `16MB` to `1GB`, while keeping the default idle wait at `1000ms`, so the compatibility path is enabled by default with a more generous drain window. **Performance Validation** - I compared FE redirect response time between the Doris `3.0` baseline instance (`9030`) and the fixed Doris `3.1.4` instance (`9034`). - The goal was to check whether the additional bounded drain logic on FE introduces a noticeable regression compared with the original Jetty 9 behavior. **Test Setup** - Reproduction tool: `tools/stream_load_redirect_repro.py` - Target: FE endpoint on both instances - Client mode: `httpclient` - Common parameters: - `chunk_kb = 16` - `sleep_ms = 0` - Payload sizes: - `32MB` - `128MB` - `512MB` - Each case was executed `3` times on each instance, and the average `elapsed_seconds` was used for comparison. **Results** - `32MB` - `9030`: `9.515s / 12.032s / 9.727s` - Average: `10.425s` - `9034`: `10.695s / 10.847s / 8.763s` - Average: `10.102s` - Difference: `9034` was `0.323s` faster, about `3.1%` - `128MB` - `9030`: `37.174s / 34.111s / 37.090s` - Average: `36.125s` - `9034`: `38.910s / 36.423s / 38.337s` - Average: `37.890s` - Difference: `9034` was `1.765s` slower, about `4.9%` - `512MB` - `9030`: `157.181s / 161.148s / 174.421s` - Average: `164.250s` - `9034`: `172.310s / 176.692s / 160.068s` - Average: `169.690s` - Difference: `9034` was `5.440s` slower, about `3.3%` **Conclusion** - Across all tested payload sizes, the difference between `9030` and `9034` stayed within a small range, roughly `-3%` to `+5%`. - Based on these measurements, the FE bounded drain logic does not show a significant performance regression compared with the baseline FE redirect behavior. - In other words, the fix improves redirect compatibility while keeping FE redirect response time at a similar level in normal request sizes. --------- Co-authored-by: yaoxiao <yx136264032@163.com>
1 parent 477cb0c commit 3c9c40f

10 files changed

Lines changed: 1459 additions & 85 deletions

File tree

fe/fe-common/src/main/java/org/apache/doris/common/Config.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,13 @@ public class Config extends ConfigBase {
380380
@ConfField(description = {"The maximum HTTP POST size of Jetty, in bytes, the default value is 100MB."})
381381
public static int jetty_server_max_http_post_size = 100 * 1024 * 1024;
382382

383+
@ConfField(description = {
384+
"Jetty 在应用未消费完请求体时,额外尝试读取剩余内容的最大次数。"
385+
+ "-1 表示不限制,0 表示不额外读取,正数表示最大读取次数。",
386+
"The maximum number of extra reads Jetty performs for unconsumed request content. "
387+
+ "-1 means unlimited, 0 means disabled, and a positive value limits the read attempts."})
388+
public static int jetty_server_max_unconsumed_request_content_reads = -1;
389+
383390
@ConfField(description = {"The maximum HTTP header size of Jetty, in bytes, the default value is 1MB."})
384391
public static int jetty_server_max_http_header_size = 1048576;
385392

@@ -3328,6 +3335,23 @@ public static int metaServiceRpcRetryTimes() {
33283335
+ "public-private/public/private/direct/random-be and empty string."})
33293336
public static String streamload_redirect_policy = "";
33303337

3338+
@ConfField(mutable = true, description = {
3339+
"Stream Load redirect 场景下,FE 在返回 307 后额外丢弃请求体的最大字节数。"
3340+
+ "0 表示关闭该兼容逻辑,正数表示最大丢弃字节数。",
3341+
"The maximum number of request body bytes FE drains after returning 307 for Stream Load redirects. "
3342+
+ "0 disables the compatibility logic, and a positive value sets the byte limit."})
3343+
// Enable a generous bounded drain window by default to preserve FE redirect compatibility on Jetty 12.
3344+
public static long stream_load_redirect_bounded_drain_max_bytes = 1024L * 1024 * 1024;
3345+
3346+
@ConfField(mutable = true, description = {
3347+
"Stream Load redirect 场景下,FE 在检测到请求体暂时无可读数据后继续等待的最大空闲时长,单位毫秒。"
3348+
+ "0 表示不额外等待,用于给慢客户端或分段到达的数据保留一个有限的缓冲窗口。",
3349+
"The maximum idle wait time in milliseconds after FE detects no readable request body bytes "
3350+
+ "during Stream Load redirect drain. 0 disables the extra idle wait, while a positive value "
3351+
+ "keeps a bounded grace window for slow clients or delayed request body chunks."})
3352+
// Keep a small grace period for delayed body chunks after FE has already written the redirect.
3353+
public static int stream_load_redirect_bounded_drain_max_idle_time_ms = 1000;
3354+
33313355
@ConfField(mutable = true, description = {
33323356
"Whether to enable group commit streamload BE forward feature in cloud mode. "
33333357
+ "Solves the issue where LB random forwarding breaks group commit batching "

fe/fe-core/src/main/java/org/apache/doris/httpv2/config/WebServerFactoryCustomizerConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public void customize(ConfigurableJettyWebServerFactory factory) {
5050
if (httpFactory != null) {
5151
HttpConfiguration httpConfig = httpFactory.getHttpConfiguration();
5252
httpConfig.setRequestHeaderSize(Config.jetty_server_max_http_header_size);
53+
// Apply the unconsumed request content read limit to every HTTP connector.
54+
httpConfig.setMaxUnconsumedRequestContentReads(
55+
Config.jetty_server_max_unconsumed_request_content_reads);
5356
}
5457
}
5558
}

fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/LoadAction.java

Lines changed: 95 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.doris.httpv2.entity.ResponseEntityBuilder;
3333
import org.apache.doris.httpv2.entity.RestBaseResult;
3434
import org.apache.doris.httpv2.exception.UnauthorizedException;
35+
import org.apache.doris.httpv2.util.StreamLoadRedirectDrainUtil;
3536
import org.apache.doris.load.StreamLoadHandler;
3637
import org.apache.doris.mysql.privilege.PrivPredicate;
3738
import org.apache.doris.planner.GroupCommitPlanner;
@@ -59,8 +60,8 @@
5960
import org.springframework.web.bind.annotation.RestController;
6061
import org.springframework.web.servlet.view.RedirectView;
6162

63+
import java.io.IOException;
6264
import java.net.InetAddress;
63-
import java.net.URI;
6465
import java.util.Enumeration;
6566
import java.util.List;
6667
import java.util.Optional;
@@ -131,7 +132,7 @@ public Object streamLoad(HttpServletRequest request,
131132
if (!checkClusterToken(authToken)) {
132133
throw new UnauthorizedException("Invalid token: " + authToken);
133134
}
134-
return executeWithClusterToken(request, db, table, true);
135+
return executeWithClusterToken(request, response, db, table, true);
135136
} else {
136137
try {
137138
executeCheckPassword(request, response);
@@ -190,8 +191,7 @@ public Object streamLoadWithSql(HttpServletRequest request, HttpServletResponse
190191
LOG.info("redirect load action to destination={}, label: {}",
191192
redirectAddr.toString(), label);
192193

193-
RedirectView redirectView = redirectTo(request, redirectAddr);
194-
return redirectView;
194+
return createRedirectResponse(request, response, redirectAddr, true, null, null, label);
195195
} catch (Exception e) {
196196
return new RestBaseResult(e.getMessage());
197197
}
@@ -311,11 +311,17 @@ private Object executeWithoutPassword(HttpServletRequest request,
311311
redirectAddr.toString(), isStreamLoad, dbName, tableName, label);
312312
}
313313

314-
RedirectView redirectView = redirectTo(request, redirectAddr);
315-
return redirectView;
314+
return createRedirectResponse(request, response, redirectAddr, isStreamLoad, dbName, tableName, label);
316315
} catch (StreamLoadForwardException e) {
317-
// Special handling for stream load forwarding
318-
return e.getRedirectView();
316+
// Handle IOException from redirect response generation in the forwarding path.
317+
try {
318+
return createRedirectResponse(request, response, e.getRedirectView(),
319+
isStreamLoad, db, table, label);
320+
} catch (IOException ioException) {
321+
LOG.warn("stream load forward redirect failed, stream: {}, db: {}, tbl: {}, label: {}, err: {}",
322+
isStreamLoad, db, table, label, ioException.getMessage());
323+
return new RestBaseResult(ioException.getMessage());
324+
}
319325
} catch (Exception e) {
320326
LOG.warn("load failed, stream: {}, db: {}, tbl: {}, label: {}, err: {}",
321327
isStreamLoad, db, table, label, e.getMessage());
@@ -592,7 +598,7 @@ private Pair<String, Integer> splitHostAndPort(String hostPort) throws AnalysisE
592598
// AuditlogPlugin should be re-disigned carefully, and blow method focuses on
593599
// temporarily addressing the users' needs for audit logs.
594600
// So this function is not widely tested under general scenario
595-
private Object executeWithClusterToken(HttpServletRequest request, String db,
601+
private Object executeWithClusterToken(HttpServletRequest request, HttpServletResponse response, String db,
596602
String table, boolean isStreamLoad) {
597603
try {
598604
ConnectContext ctx = new ConnectContext();
@@ -635,29 +641,7 @@ private Object executeWithClusterToken(HttpServletRequest request, String db,
635641
+ "stream: {}, db: {}, tbl: {}, label: {}",
636642
redirectAddr.toString(), isStreamLoad, dbName, tableName, label);
637643

638-
URI urlObj = null;
639-
URI resultUriObj = null;
640-
String urlStr = request.getRequestURI();
641-
String userInfo = null;
642-
643-
try {
644-
urlObj = new URI(urlStr);
645-
resultUriObj = new URI("http", userInfo, redirectAddr.getHostname(),
646-
redirectAddr.getPort(), urlObj.getPath(), "", null);
647-
} catch (Exception e) {
648-
throw new RuntimeException(e);
649-
}
650-
String redirectUrl = resultUriObj.toASCIIString();
651-
if (!Strings.isNullOrEmpty(request.getQueryString())) {
652-
redirectUrl += request.getQueryString();
653-
}
654-
LOG.info("Redirect url: {}", "http://" + redirectAddr.getHostname() + ":"
655-
+ redirectAddr.getPort() + urlObj.getPath());
656-
RedirectView redirectView = new RedirectView(redirectUrl);
657-
redirectView.setContentType("text/html;charset=utf-8");
658-
redirectView.setStatusCode(org.springframework.http.HttpStatus.TEMPORARY_REDIRECT);
659-
660-
return redirectView;
644+
return createRedirectResponse(request, response, redirectAddr, isStreamLoad, dbName, tableName, label);
661645
} catch (Exception e) {
662646
LOG.warn("Failed to execute stream load with cluster token, {}", e.getMessage(), e);
663647
return new RestBaseResult(e.getMessage());
@@ -677,6 +661,80 @@ private String getAllHeaders(HttpServletRequest request) {
677661
return headers.toString();
678662
}
679663

664+
private Object createRedirectResponse(HttpServletRequest request, HttpServletResponse response,
665+
TNetworkAddress redirectAddr, boolean isStreamLoad, String dbName, String tableName, String label)
666+
throws IOException {
667+
String redirectUrl = buildRedirectUrl(request, redirectAddr);
668+
if (!shouldUseBoundedDrainForStreamLoad(isStreamLoad)) {
669+
return redirectTo(request, redirectAddr);
670+
}
671+
writeTemporaryRedirect(response, redirectUrl);
672+
DrainDecision drainDecision = decideDrainDecisionForStreamLoadRedirect(request);
673+
if (drainDecision != DrainDecision.DRAIN) {
674+
LOG.info("skip bounded drain after stream load redirect, target: {}, db: {}, tbl: {}, label: {},"
675+
+ " reason: {}",
676+
redirectAddr, dbName, tableName, label, drainDecision);
677+
return null;
678+
}
679+
drainStreamLoadRequestBodyAfterRedirect(request, redirectAddr.toString(), dbName, tableName, label);
680+
return null;
681+
}
682+
683+
private Object createRedirectResponse(HttpServletRequest request, HttpServletResponse response,
684+
RedirectView redirectView, boolean isStreamLoad, String dbName, String tableName, String label)
685+
throws IOException {
686+
if (!shouldUseBoundedDrainForStreamLoad(isStreamLoad)) {
687+
return redirectView;
688+
}
689+
writeTemporaryRedirect(response, redirectView.getUrl());
690+
DrainDecision drainDecision = decideDrainDecisionForStreamLoadRedirect(request);
691+
if (drainDecision != DrainDecision.DRAIN) {
692+
LOG.info("skip bounded drain after stream load redirect, target: {}, db: {}, tbl: {}, label: {},"
693+
+ " reason: {}",
694+
redirectView.getUrl(), dbName, tableName, label, drainDecision);
695+
return null;
696+
}
697+
drainStreamLoadRequestBodyAfterRedirect(request, redirectView.getUrl(), dbName, tableName, label);
698+
return null;
699+
}
700+
701+
private boolean shouldUseBoundedDrainForStreamLoad(boolean isStreamLoad) {
702+
return isStreamLoad && Config.stream_load_redirect_bounded_drain_max_bytes > 0;
703+
}
704+
705+
// Skip the bounded drain for header-only probes and oversized fixed-length bodies.
706+
private DrainDecision decideDrainDecisionForStreamLoadRedirect(HttpServletRequest request) {
707+
long contentLength = request.getContentLengthLong();
708+
String transferEncoding = request.getHeader(HttpHeaderNames.TRANSFER_ENCODING.toString());
709+
if (contentLength <= 0 && Strings.isNullOrEmpty(transferEncoding)) {
710+
return DrainDecision.SKIP_NO_REQUEST_BODY;
711+
}
712+
if (contentLength > Config.stream_load_redirect_bounded_drain_max_bytes) {
713+
return DrainDecision.SKIP_CONTENT_LENGTH_EXCEEDS_MAX_BYTES;
714+
}
715+
return DrainDecision.DRAIN;
716+
}
717+
718+
private void drainStreamLoadRequestBodyAfterRedirect(HttpServletRequest request, String redirectTarget,
719+
String dbName, String tableName, String label) {
720+
long drainLimit = Config.stream_load_redirect_bounded_drain_max_bytes;
721+
LOG.info("write stream load redirect and start bounded drain, target: {}, db: {}, tbl: {}, label: {},"
722+
+ " max_drain_bytes: {}",
723+
redirectTarget, dbName, tableName, label, drainLimit);
724+
StreamLoadRedirectDrainUtil.DrainResult drainResult =
725+
StreamLoadRedirectDrainUtil.drainRequestBodyAfterRedirect(request, drainLimit);
726+
LOG.info("finish bounded drain after stream load redirect, target: {}, db: {}, tbl: {}, label: {},"
727+
+ " drained_bytes: {}, elapsed_ms: {}, exit_reason: {}",
728+
redirectTarget, dbName, tableName, label, drainResult.getDrainedBytes(),
729+
drainResult.getElapsedMillis(), drainResult.getExitReason());
730+
}
731+
732+
private enum DrainDecision {
733+
SKIP_NO_REQUEST_BODY,
734+
SKIP_CONTENT_LENGTH_EXCEEDS_MAX_BYTES,
735+
DRAIN
736+
}
737+
680738
private boolean isSensitiveHeader(String headerName) {
681739
return "Authorization".equalsIgnoreCase(headerName)
682740
|| "Proxy-Authorization".equalsIgnoreCase(headerName)
@@ -738,34 +796,14 @@ private Backend selectBackendForGroupCommit(String clusterName, HttpServletReque
738796
*/
739797
private RedirectView redirectToStreamLoadForward(HttpServletRequest request, TNetworkAddress addr,
740798
String forwardTarget) {
741-
URI urlObj = null;
742-
URI resultUriObj = null;
743-
String urlStr = request.getRequestURI();
744-
String userInfo = null;
745-
String modifiedPath = null;
746-
747-
if (!Strings.isNullOrEmpty(request.getHeader("Authorization"))) {
748-
ActionAuthorizationInfo authInfo = getAuthorizationInfo(request);
749-
userInfo = authInfo.fullUserName + ":" + authInfo.password;
750-
}
751-
try {
752-
urlObj = new URI(urlStr);
753-
// Replace _stream_load with _stream_load_forward in the path
754-
modifiedPath = urlObj.getPath().replace("/_stream_load", "/_stream_load_forward");
755-
resultUriObj = new URI("http", userInfo, addr.getHostname(),
756-
addr.getPort(), modifiedPath, "", null);
757-
} catch (Exception e) {
758-
throw new RuntimeException(e);
759-
}
760-
String redirectUrl = resultUriObj.toASCIIString();
761-
762-
// Add forward_to parameter (note: toASCIIString() already includes '?' due to empty query)
799+
// Replace _stream_load with _stream_load_forward in the path.
800+
String modifiedPath = request.getRequestURI().replace("/_stream_load", "/_stream_load_forward");
763801
String queryString = request.getQueryString();
802+
String redirectQuery = "forward_to=" + forwardTarget;
764803
if (!Strings.isNullOrEmpty(queryString)) {
765-
redirectUrl += queryString + "&forward_to=" + forwardTarget;
766-
} else {
767-
redirectUrl += "forward_to=" + forwardTarget;
804+
redirectQuery = queryString + "&" + redirectQuery;
768805
}
806+
String redirectUrl = buildRedirectUrl(request, addr, modifiedPath, redirectQuery);
769807

770808
LOG.info("Redirect stream load forward url: {}, forward_to: {}",
771809
"http://" + addr.getHostname() + ":" + addr.getPort() + modifiedPath, forwardTarget);

fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/RestBaseController.java

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -88,36 +88,49 @@ public ActionAuthorizationInfo executeCheckPassword(HttpServletRequest request,
8888
return authInfo;
8989
}
9090

91-
public RedirectView redirectTo(HttpServletRequest request, TNetworkAddress addr) {
92-
RedirectView redirectView = new RedirectView(getRedirectUrL(request, addr));
93-
redirectView.setContentType("text/html;charset=utf-8");
94-
redirectView.setStatusCode(org.springframework.http.HttpStatus.TEMPORARY_REDIRECT);
95-
return redirectView;
91+
protected String buildRedirectUrl(HttpServletRequest request, TNetworkAddress addr) {
92+
return buildRedirectUrl(request, addr, request.getRequestURI(), request.getQueryString());
9693
}
9794

98-
public String getRedirectUrL(HttpServletRequest request, TNetworkAddress addr) {
99-
URI urlObj = null;
100-
URI resultUriObj = null;
101-
String urlStr = request.getRequestURI();
95+
protected String buildRedirectUrl(HttpServletRequest request, TNetworkAddress addr, String requestPath,
96+
String queryString) {
10297
String userInfo = null;
10398
if (!Strings.isNullOrEmpty(request.getHeader("Authorization"))) {
10499
ActionAuthorizationInfo authInfo = getAuthorizationInfo(request);
105100
userInfo = authInfo.fullUserName + ":" + authInfo.password;
106101
}
107102
try {
108-
urlObj = new URI(urlStr);
109-
resultUriObj = new URI(request.getScheme(), userInfo, addr.getHostname(),
110-
addr.getPort(), urlObj.getPath(), "", null);
103+
// Preserve the original request path to avoid re-encoding an already encoded URI path.
104+
URI authorityUri = new URI(request.getScheme(), userInfo, addr.getHostname(),
105+
addr.getPort(), null, null, null);
106+
String redirectUrl = authorityUri.toASCIIString() + requestPath;
107+
if (!Strings.isNullOrEmpty(queryString)) {
108+
redirectUrl += "?" + queryString;
109+
}
110+
LOG.info("Redirect url: {}", request.getScheme() + "://" + addr.getHostname() + ":"
111+
+ addr.getPort() + requestPath);
112+
return redirectUrl;
111113
} catch (Exception e) {
112114
throw new RuntimeException(e);
113115
}
114-
String redirectUrl = resultUriObj.toASCIIString();
115-
if (!Strings.isNullOrEmpty(request.getQueryString())) {
116-
redirectUrl += request.getQueryString();
117-
}
118-
LOG.info("Redirect url: {}", request.getScheme() + "://" + addr.getHostname() + ":"
119-
+ addr.getPort() + urlObj.getPath());
120-
return redirectUrl;
116+
}
117+
118+
protected void writeTemporaryRedirect(HttpServletResponse response, String redirectUrl) throws IOException {
119+
response.setContentType("text/html;charset=utf-8");
120+
response.setStatus(HttpStatus.TEMPORARY_REDIRECT.value());
121+
response.setHeader("Location", redirectUrl);
122+
response.flushBuffer();
123+
}
124+
125+
public RedirectView redirectTo(HttpServletRequest request, TNetworkAddress addr) {
126+
RedirectView redirectView = new RedirectView(buildRedirectUrl(request, addr));
127+
redirectView.setContentType("text/html;charset=utf-8");
128+
redirectView.setStatusCode(org.springframework.http.HttpStatus.TEMPORARY_REDIRECT);
129+
return redirectView;
130+
}
131+
132+
public String getRedirectUrL(HttpServletRequest request, TNetworkAddress addr) {
133+
return buildRedirectUrl(request, addr);
121134
}
122135

123136
public RedirectView redirectToObj(String sign) throws URISyntaxException {

0 commit comments

Comments
 (0)