为什么没有收到预期的413状态码

HTTP状态码413的含义是请求实体过长,RFC7231对413状态码的定义如下:

6.5.11. 413 Payload Too Large
The 413 (Payload Too Large) status code indicates that the server is refusing to process a request because the request payload is larger than the server is willing or able to process. The server MAY close the connection to prevent the client from continuing the request.

从上面的描述我们可以看出,当http请求的实体过长时,server端可以返回一个413的状态码,而当client端收到413的状态码时,则意味着client端知道消息过长,本次请求失败。这是一个看似很常规的request/response逻辑。但似乎又没我们想的那么简单

问题描述

遇到的问题来自本次618活动我们实际发生的线上问题。我们的消息中间件PubSub系统对外提供HTTP RESTful的API,用户可以通过HTTP POST请求执行消息Pub动作,通过HTTP GET请求执行消息Sub动作。当用户执行HTTP POST时,收到状态码201,则表示消息Pub成功,而收到其他状态码均意味着消息Pub失败。对于消息的大小,PubSub系统会有一定的控制,根据实际情况,线上系统设置的MAX_MSG_SIZE是512K,当用户提交的数据超过512K时,后端系统将返回如上提到的413(Payload Too Large)状态码,告知用户消息体过大,Pub失败。发送端通过在HTTP请求上设置Content-Length字段来明确消息的实际大小。

上述交互设计看似没有问题,当遗憾的是,在618活动当天,在拒绝过大消息体的Pub请求时,客户端未能获取到413的错误码,这是为什么?

原因分析

为了将问题说明清楚,我将客户端代码和服务端代码做了简化抽离,以便直观的重现问题。

客户端代码如下(Java实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public static HttpConfig buildHttpSettings() {
HttpConfig httpConfig = new HttpConfig();
httpConfig.setMaxTotalConnections(5);
return httpConfig;
}
public static HttpClient createHttpClient(HttpConfig config) {
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, config.getConnectTimeOut());
HttpConnectionParams.setSoTimeout(params, config.getSocketTimeOut());
HttpConnectionParams.setSoKeepalive(params, true);
HttpConnectionParams.setStaleCheckingEnabled(params, true);
HttpConnectionParams.setTcpNoDelay(params, true);
PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager();
connectionManager.closeIdleConnections(config.getIdleBeforeColse(), TimeUnit.MILLISECONDS);
connectionManager.setMaxTotal(config.getMaxTotalConnections());
connectionManager.setDefaultMaxPerRoute(config.getMaxTotalConnections());
DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, params);
httpClient.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
return httpClient;
}
private static void postMsg(byte[] msg) {
HttpConfig httpConf = buildHttpSettings();
HttpClient httpClient = createHttpClient(httpConf);
String targetUrl = "http://192.168.149.150:8080/big";
HttpPost httpPost = new HttpPost(targetUrl);
httpPost.setHeader("User-Agent", Constants.USER_AGENT);
int msgLen = msg.length;
logger.debug("msgLen:" + msgLen);
try {
httpPost.setEntity(new ByteArrayEntity(msg));
HttpResponse response = httpClient.execute(httpPost);
int statusCode = 0;
if (response.getStatusLine() != null) {
statusCode = response.getStatusLine().getStatusCode();
} else {
logger.debug("StatusLine is null ");
return;
}
String result = null;
HttpEntity entity = response.getEntity();
if (statusCode != 201) {
// get errmsg
if (entity != null) {
result = EntityUtils.toString(entity, "UTF-8");
JSONObject jsonObject = JSONObject.parseObject(result);
logger.debug("errmsg:" + jsonObject.getString("errmsg"));
logger.debug("statusCode:" + statusCode);
httpPost.abort();
}
} else {
// close entity
if (entity != null) {
result = EntityUtils.toString(entity, "UTF-8");
logger.debug(result);
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
logger.error("UnsupportedEncodingException");
return;
} catch (ClientProtocolException e) {
e.printStackTrace();
logger.error("ClientProtocolException");
return;
} catch (IOException e) {
e.printStackTrace();
logger.error("IOException");
return;
}
return;
}

服务端代码如下(Go实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var (
ResponseOk = []byte(`{"ok":1}`)
)
func bigPostHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
contentLen := r.ContentLength
MaxMsgSize := int64(512 * 1024)
log4go.Debug("contentLen:%d", contentLen)
// check content-length
if contentLen > MaxMsgSize {
log4go.Error("too big content, contentLen:%d, max:%d", contentLen, MaxMsgSize)
// reject big contentLen
errStr := "too big message"
errCode := http.StatusRequestEntityTooLarge
w.Header().Set("Connection", "close")
writeError(w, errStr, errCode)
return
}
// read all data and return
msgContent := make([]byte, contentLen+1, contentLen+1)
lbr := io.LimitReader(r.Body, MaxMsgSize+1)
if x, err := io.ReadAtLeast(lbr, msgContent, int(contentLen)); err != nil {
log4go.Error("contentLen: %d, x:%d, ioReader error, %+v", contentLen, x, err)
// read error
errStr := "read body error"
errCode := http.StatusInternalServerError
w.Header().Set("Connection", "close")
writeError(w, errStr, errCode)
return
}
log4go.Info("len: %d, content:[%s], header:%+v", contentLen, msgContent, w.Header())
// return 201 resp
w.WriteHeader(http.StatusCreated)
if _, err := w.Write(ResponseOk); err != nil {
log4go.Error("resp: %v", err)
return
}
}
func writeError(resp http.ResponseWriter, errMsg string, errCode int) {
var out = map[string]string{
"errmsg": errMsg,
}
b, _ := json.Marshal(out)
http.Error(resp, string(b), errCode)
}

从上面的示例代码我们可以看到,客户端是使用java实现的简单的http客户端,他依赖apache的httpclient包,向服务端发送post请求,并在完成post请求发送后,获取HttpResponse信息,从中获取返回码,以及响应信息等内容。而服务端则是由go实现的,服务端在处理post请求时,会从请求头中获取对应的ContentLength信息(r.ContentLength),将该长度信息与最大长度MaxMsgSize进行比较,当消息过长时,则设置状态码为http.StatusRequestEntityTooLarge(413)并返回;当消息长度没有超出最大长度限制时,则将消息体读出来进行处理,并在处理完成之后,返回http.StatusCreated(201)。在实际运行过程中,当消息体未超过512k时,客户端在发送消息之后都能正确的接收到201状态码的响应。然而,当消息超出512k时,却不能。

为探寻具体的原因,我们对代码进行跟踪调试,并在调试的过程中通过wireshark进行了抓包分析。在调试时,我们在客户端发现HttpResponse response = httpClient.execute(httpPost);此段代码抛出了异常:

从上图我们可以看到明显的异常java.net.SocketException: Connection reset by peer: socket write error

再通过wireshark抓包,我们来继续看看究竟网络交互上发生了什么:

上图显示,客户端主动向服务器端发起了post请求,并且,请求中的Content-Length字段被设置成了800610,该值已超过最大限度512k

而从上图我们可以看到,在收到客户端的post请求后,服务端的确是发回了413状态码的响应,进一步观察我们可以看到,在发回响应之后,服务端便关闭了连接(Fin数据包)。从消息流来看,客户端并没有关注服务端的413响应,而是继续在向服务端推送剩余的Post请求体数据,由于该连接已经被服务端关闭,在网络上,服务端会向客户端发送重置的数据包(RST),反映在客户端,则是Connection reset by peer

从抓包的网络数据交互过程我们可以看出,实际上,Server端在接收到Content-Lenght: 800610的请求后,就已经回复了413(“too big message”)的响应,但是,此时,由于httpclient还未完成Post请求的发送,他并没有去接收响应,而是继续发送post请求体,但服务端在发送完413的响应之后,则对链接进行了关闭,在一个已关闭的连接上写入数据,client端最终收到了Connection reset by peer的exception,在收到exception之后报错退出。

解决方案

通过上面的抓包和调试,我们已经明确了:

  1. 413的状态码已经发回来
  2. 服务端关闭了连接
  3. 客户端没有去获取响应数据,而是继续在关闭的连接上发送数据,导致exception

方案一:异步处理

之所以客户端没有获取响应数据,而是继续发送数据,是因为我们目前使用的是同步的httpclient客户端。使用同步客户端,就意味着必须是请求发送完毕,再获取响应数据。显然在上述场景下,同步模式的客户端无法正常获取到响应数据。既然同步模式下无法在请求发送完成前获取响应数据,那我们可以尝试使用异步模式,在发送请求的同时,及时的监听响应的回传,并在收到响应后及时进行处理。实际上,java的org.apache.httpcomponents包中不但提供了同步的httpclient,还提供了异步的httpasyncclient。而通过异步模式,我们就可以在发送数据的同时,监听响应回传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private static void asyncPostMsg(byte[] msg) {
CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.createDefault();
httpAsyncClient.start();
String targetUrl = "http://192.168.149.150:8080/big";
HttpPost httpPost = new HttpPost(targetUrl);
httpPost.setHeader("User-Agent", Constants.USER_AGENT);
int msgLen = msg.length;
logger.debug("msgLen:" + msgLen);
try {
httpPost.setEntity(new ByteArrayEntity(msg));
Future<HttpResponse> future = httpAsyncClient.execute(httpPost,null);
HttpResponse response = future.get();
int statusCode = 0;
if (response.getStatusLine() != null) {
statusCode = response.getStatusLine().getStatusCode();
} else {
logger.debug("StatusLine is null ");
return;
}
String result = null;
HttpEntity entity = response.getEntity();
if (statusCode != 201) {
// get errmsg
if (entity != null) {
result = EntityUtils.toString(entity, "UTF-8");
JSONObject jsonObject = JSONObject.parseObject(result);
logger.debug("errmsg:" + jsonObject.getString("errmsg"));
logger.debug("statusCode:" + statusCode);
httpPost.abort();
}
} else {
// close entity
if (entity != null) {
result = EntityUtils.toString(entity, "UTF-8");
logger.debug(result);
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
logger.error("UnsupportedEncodingException");
return;
} catch (ClientProtocolException e) {
e.printStackTrace();
logger.error("ClientProtocolException");
return;
} catch (IOException e) {
e.printStackTrace();
logger.error("IOException");
return;
} catch (InterruptedException e) {
e.printStackTrace();
logger.error("InterruptedException");
return;
} catch (ExecutionException e) {
e.printStackTrace();
logger.error("ExecutionException");
return;
}
}

从上面的代码逻辑我们可以看到,原来使用的同步httpclient,被我们替换成了异步的httpasyncclient:CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.createDefault();,基于异步的client,执行post请求,并通过future对象,异步的获取响应结果:ttpResponse response = future.get();

从运行结果看,客户端正常获取到了413的响应信息。同时,通过wireshark抓包,我们也可以看到,链接通道被双向正常关闭:

方案二:遵循RFC标准的同步交互流程

客户端的异步处理模式能够异步的接收到响应数据,解决上述请求未发送完成,响应已返回需处理的问题。但直观的感觉,一来一回的request-response同步模式,似乎才是主流HTTP交互模式,在同步模式下难道没有标准的解决流程吗?应该能找到对应的方案。

基于RFC 7231中对Expect: 100-continue → 100 continue的交互流程实现数据提交前数据长度确认。(https://tools.ietf.org/html/rfc7231#section-5.1.1)

简单说,HTTP 1.1允许客户端在发送实际消息体之前,通过Expect: 100-continue请求告知server端,后续即将发送多大的消息,server端可以基于该请求,响应是否接受该长度的消息(回复100 continue的响应)。client端将在发送Expect之后等待100 continue响应,如果timeout之后没有任何响应,client也将启动实际数据的发送。

a client MAY proceed to send the message body even if it has not yet received a response. Furthermore, since 100 (Continue) responses cannot be sent through an HTTP/1.0 intermediary, such a client SHOULD NOT wait for an indefinite period before sending the message body.

具体方案

  1. java sdk端引入Expect: 100-continue支持

    params.setBooleanParameter("http.protocol.expect-continue",true);

  2. nginx根据Options.MinPubSize设置对应的client_max_body_size 512k;

    在有nginx做反向代理时,面对带有Expect: 100-continue的请求,nginx会根据自身client_max_body_size设置,完成100 continue或者413的回应。
    nginx不会直接转发带有Expect: 100-continue的请求。在nginx-1.12.0以及tengine-2.2.0上均抓包验证发现,nginx会自动对Expect进行回应。通过启动nginx的unbuffered upload模式proxy_request_buffering off;或者是关闭body size控制client_max_body_size 0,也没有效果,nginx始终自动处理Expect,主动返回100 continue或者413。从6年前nginx作者Igor Sysoev的答复来看,nginx在设计上就是要这么做,而且从nginx的trac来看,3年前有人提出类似的问题,该问题被标记为wontfix,意味着还不打算变

  3. server在处理pub请求时,判断是否有Expect标志,如果有,则判断Content-Length信息,过大,则413; 符合要求,则回复100 continue,并在完成pub之后,返回最终结果

    根据RFC 7231的说明,server方在处理Expect时,返回100 continue之后,还需返回最终处理结果:

    A server that sends a 100 (Continue) response MUST ultimately send a final status code, once the message body is received and processed, unless the connection is closed prematurely.

    之所以在nginx控制后,还需要server进行控制,是确保在无nginx前置的环境下,还能高效、正常工作(server不返回100 continue,也是可以的,只是这种情况下,发送端会有一个默认等待的timeout时间,在此之后客户端才会发送具体msg body,kateway返回100 continue,就是为了避免这个timeout时间)

    从上面支持Expect: 100-continue的交互流程我们可以看出,在允许body发送时,server端需要首先向客户端返回HTTP/1.1 100 continue,接着在完成消息处理后需最终返回HTTP/1.1 201 Created的状态码。这也就要求在同一条HTTP请求上两次返回状态码。对于这种交互流程,golang的net/http库标库是不支持的,golang标准库中ResponseWriter的status code一旦写入,就没法改。为了支持Expect: 100-continue的两段式交互,我们需要对net/http请求进行hijack处理(类似的,在支持websocket时,也需要做hijack处理)

客户端实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public static HttpClient createHttpClient(HttpConfig config) {
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, config.getConnectTimeOut());
HttpConnectionParams.setSoTimeout(params, config.getSocketTimeOut());
HttpConnectionParams.setSoKeepalive(params, true);
HttpConnectionParams.setStaleCheckingEnabled(params, true);
HttpConnectionParams.setTcpNoDelay(params, true);
// open expect-continue flag
params.setBooleanParameter("http.protocol.expect-continue",true);
PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager();
connectionManager.closeIdleConnections(config.getIdleBeforeColse(), TimeUnit.MILLISECONDS);
connectionManager.setMaxTotal(config.getMaxTotalConnections());
connectionManager.setDefaultMaxPerRoute(config.getMaxTotalConnections());
DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, params);
httpClient.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
return httpClient;
}
private static void postMsg(byte[] msg) {
HttpConfig httpConf = buildHttpSettings();
HttpClient httpClient = createHttpClient(httpConf, supportContinue);
//bigSupportContinue is the backend which support 100-continue
targetUrl = "http://192.168.149.150:8080/bigSupportContinue";
HttpPost httpPost = new HttpPost(targetUrl);
httpPost.setHeader("User-Agent", Constants.USER_AGENT);
int msgLen = msg.length;
logger.debug("msgLen:" + msgLen);
try {
httpPost.setEntity(new ByteArrayEntity(msg));
HttpResponse response = httpClient.execute(httpPost);
int statusCode = 0;
if (response.getStatusLine() != null) {
statusCode = response.getStatusLine().getStatusCode();
} else {
logger.debug("StatusLine is null ");
return;
}
String result = null;
HttpEntity entity = response.getEntity();
if (statusCode != 201) {
// get errmsg
if (entity != null) {
result = EntityUtils.toString(entity, "UTF-8");
JSONObject jsonObject = JSONObject.parseObject(result);
logger.debug("errmsg:" + jsonObject.getString("errmsg"));
logger.debug("statusCode:" + statusCode);
httpPost.abort();
}
} else {
// close entity
if (entity != null) {
result = EntityUtils.toString(entity, "UTF-8");
logger.debug(result);
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
logger.error("UnsupportedEncodingException");
return;
} catch (ClientProtocolException e) {
e.printStackTrace();
logger.error("ClientProtocolException");
return;
} catch (IOException e) {
e.printStackTrace();
logger.error("IOException");
return;
}
return;
}

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
func bigContinuePostHandle(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
contentLen := r.ContentLength
MaxMsgSize := int64(512 * 1024)
log4go.Debug("bigSupportContinue")
log4go.Debug("contentLen:%d", contentLen)
// check content-length
if contentLen > MaxMsgSize {
log4go.Error("too big content, contentLen:%d, max:%d", contentLen, MaxMsgSize)
// reject big contentLen
errStr := "too big message"
errCode := http.StatusRequestEntityTooLarge
w.Header().Set("Connection", "close")
writeError(w, errStr, errCode)
return
} else {
// check Expect: 100-continue
if r.Header.Get("Expect") == "100-continue" {
log4go.Info("expect, len: %d, header:%+v", contentLen, w.Header())
conn, _, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
constinueMsg := "HTTP/1.1 100 Continue\r\n\r\n"
conn.Write([]byte(constinueMsg))
// read all data and return
msgContent := make([]byte, contentLen+1, contentLen+1)
lbr := io.LimitReader(conn, MaxMsgSize+1)
if x, err := io.ReadAtLeast(lbr, msgContent, int(contentLen)); err != nil {
log4go.Error("contentLen: %d, x:%d, ioReader error, %+v", contentLen, x, err)
errMsg := "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nDate: Sat, 08 Jul 2017 06:06:20 GMT\r\nContent-Length: 28\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{\"errmsg\":\"read body error\"}"
conn.Write([]byte(errMsg))
conn.Close()
return
}
log4go.Debug("msgContent:%s", msgContent)
// return ok response
okMsg := "HTTP/1.1 201 Created\r\nDate: Sat, 08 Jul 2017 06:06:20 GMT\r\nContent-Length: 8\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{\"ok\":1}"
conn.Write([]byte(okMsg))
conn.Close()
return
//conn.
} else {
// read all data and return
msgContent := make([]byte, contentLen+1, contentLen+1)
lbr := io.LimitReader(r.Body, MaxMsgSize+1)
if x, err := io.ReadAtLeast(lbr, msgContent, int(contentLen)); err != nil {
log4go.Error("contentLen: %d, x:%d, ioReader error, %+v", contentLen, x, err)
// read error
errStr := "read body error"
errCode := http.StatusInternalServerError
w.Header().Set("Connection", "close")
writeError(w, errStr, errCode)
return
}
log4go.Info("len: %d, content:[%s], header:%+v", contentLen, msgContent, w.Header())
// return 201 resp
w.WriteHeader(http.StatusCreated)
log4go.Info("final code:%+v", w.Header())
if _, err := w.Write(ResponseOk); err != nil {
log4go.Error("resp: %v", err)
return
}
}
}
}

在有nginx前置反向代理的环境,对nginx进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
http {
...
server {
listen 80;
server_name localhost;
client_max_body_size 512k;
...
location /bigForward {
proxy_pass http://192.168.149.150:8080/bigSupportContinue;
}
...
}
...
}

从上面的代码我们可以看到,client端打开了expect的配置:params.setBooleanParameter("http.protocol.expect-continue",true);。同时,server端为了支持100 continue的两段式交互,在遇到r.Header.Get("Expect") == "100-continue"时,将对http连接进行hijack(conn, _, err := w.(http.Hijacker).Hijack()),基于hijack的连接,发送两段响应:

  1. 100 continue

    1
    2
    constinueMsg := "HTTP/1.1 100 Continue\r\n\r\n"
    conn.Write([]byte(constinueMsg))
  2. 最终处理ok

    1
    2
    3
    okMsg := "HTTP/1.1 201 Created\r\nDate: Sat, 08 Jul 2017 06:06:20 GMT\r\nContent-Length: 8\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{\"ok\":1}"
    conn.Write([]byte(okMsg))
    conn.Close()

通过wireshark抓包,我们可以进一步观察支持expect: 100-continue的整个逻辑

  1. 对于未超出长度大小的请求,将通过两段式交互完成消息处理

  2. 对于超出长度大小的请求,server端会直接413,client端不会发送数据

方案三:客户端sdk控制

回到最初的问题,我们遇到的问题,实际上是因为客户端提交的消息体过大,如果采用最简单粗暴的做法,我们实际上可以直接通过在客户端sdk上判断带提交的消息体大小,当消息体大小过大时,直接拒绝提交。当然这种方法的缺点也比较明显,不够灵活,必须得写死(或者是配死),因为客户端sdk是部署在客户端侧,相对于服务端而言,我们对客户端sdk的控制力是比较弱的,如果遇到消息体最大大小的调整,这类写死在客户端sdk中的方法就相对笨拙了。

结论

通过分析本次618遇到的问题,我们分析了为什么无法如预期般收到413状态码的问题。进一步的我们尝试通过三种方案来解决该问题。目前线上的这个问题,我们并没有去解决,原因是,虽然没能收到413的状态码,但实际上客户端只要没有收到2xx的状态码,就已经认为处理失败了,没能收到413,影响到的点是有限的(确切的说只是不知道是什么原因导致了失败),基于上述理由,我们并没有立刻尝试解决该问题。

对比三个方案,我个人更倾向于选择方案三:客户端sdk控制。

方案一:异步处理

  1. 改动点可控;从示例代码可以看出来,调整为异步模式,sdk的修改点还是较少的;
  2. 没有写死,对于后续控制调整,更为灵活。

方案二:遵循RFC标准的同步交互流程

  1. 需要前后端调整,涉及到的改动点较多
  2. 如果前端不使用sdk,不遵循expect: 100-continue的模式,实际上也还是无法达到效果
  3. 多了异步交互流程,引入了更多的处理延时

方案三:客户端SDK控制

  1. 够简单,其实也够用(毕竟MaxMsgSize的调整需求不会很多,且即使引起了Exception,也不会影响处理逻辑,是指错误原因丢失)
  2. 写死在客户端,不是那么灵活

虽然,我觉得选择方案三更为合理,但对问题的原因进行深入分析,并针对性的给出不同方案的尝试,我觉得还是挺有意义的事情。

文章目录
  1. 1. 问题描述
  2. 2. 原因分析
  3. 3. 解决方案
    1. 3.1. 方案一:异步处理
    2. 3.2. 方案二:遵循RFC标准的同步交互流程
      1. 3.2.1. 具体方案
    3. 3.3. 方案三:客户端sdk控制
    4. 3.4. 结论
|