$ curl "http://127.0.0.1:5555/d d" -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5555 (#0)
> GET /d d HTTP/1.1
> Host: 127.0.0.1:5555
> User-Agent: curl/7.54.0
> Accept: */*
>
* Empty reply from server
* Connection #0 to host 127.0.0.1 left intact
curl: (52) Empty reply from server
case s_req_server:
case s_req_server_with_at:
case s_req_path:
case s_req_query_string_start:
case s_req_query_string:
case s_req_fragment_start:
case s_req_fragment:
{
switch (ch) {
case " ":
UPDATE_STATE(s_req_http_start);
CALLBACK_DATA(url);
break;
case CR:
case LF:
parser->http_major = 0;
parser->http_minor = 9;
UPDATE_STATE((ch == CR) ?
s_req_line_almost_done :
s_header_field_start);
CALLBACK_DATA(url);
break;
default:
UPDATE_STATE(parse_url_char(CURRENT_STATE(), ch));
if (UNLIKELY(CURRENT_STATE() == s_dead)) {
SET_ERRNO(HPE_INVALID_URL);
goto error;
}
}
break;
}
在掃描的時候,如果當(dāng)前狀態(tài)是 URI 相關(guān)的(如 s_req_path、s_req_query_string 等),則執(zhí)行一個子 switch,里面的處理如下:
若當(dāng)前字符是空格,則將狀態(tài)改變?yōu)?s_req_http_start 并認(rèn)為 URI 已經(jīng)解析好了,通過宏 CALLBACK_DATA() 觸發(fā) URI 解析好的事件;
若當(dāng)前字符是換行符,則說明還在解析 URI 的時候就被換行了,后面就不可能跟著 HTTP 協(xié)議版本的申明了,所以設(shè)置默認(rèn)的 HTTP 版本為 0.9,并修改當(dāng)前狀態(tài),最后認(rèn)為 URI 已經(jīng)解析好了,通過宏 CALLBACK_DATA() 觸發(fā) URI 解析好的事件;
其余情況(所有其它字符)下,通過調(diào)用 parse_url_char() 函數(shù)來解析一些東西并更新當(dāng)前狀態(tài)。(因為哪怕是在解析 URI 狀態(tài)中,也還有各種不同的細(xì)分,如 s_req_path、s_req_query_string )
這里的重點還是當(dāng)狀態(tài)為解析 URI 的時候遇到了空格的處理,上面也解釋過了,一旦遇到這種情況,則會認(rèn)為 URI 已經(jīng)解析好了,并且將狀態(tài)修改為 s_req_http_start。也就是說,有“Bug”的那個數(shù)據(jù)包 GET /foo bar HTTP/1.1 在解析到 foo 后面的空格的時候它就將狀態(tài)改為 s_req_http_start 并且認(rèn)為 URI 已經(jīng)解析結(jié)束了。
好的,接下來我們看看 s_req_http_start 怎么處理:
case s_req_http_start:
switch (ch) {
case "H":
UPDATE_STATE(s_req_http_H);
break;
case " ":
break;
default:
SET_ERRNO(HPE_INVALID_CONSTANT);
goto error;
}
break;
case s_req_http_H:
STRICT_CHECK(ch != "T");
UPDATE_STATE(s_req_http_HT);
break;
case s_req_http_HT:
...
case s_req_http_HTT:
...
case s_req_http_HTTP:
...
case s_req_first_http_major:
...
長長的一個函數(shù)被我精簡成這么幾句話,重點很明顯。ret 就是從 socketOnData 傳進(jìn)來已解析的數(shù)據(jù)長度,但是在 C++ 代碼中我們也看到了它還有可能是一個錯誤對象。所以在這個函數(shù)中一開始就做了一個判斷,判斷解析的結(jié)果是不是一個錯誤對象,如果是錯誤對象則調(diào)用 socketOnError()。
function socketOnError(e) {
// Ignore further errors
this.removeListener("error", socketOnError);
this.on("error", () => {});
if (!this.server.emit("clientError", e, this))
this.destroy(e);
}
我們看到,如果真的不小心走到這一步的話,HTTP Server 對象會觸發(fā)一個 clientError 事件。
整個事情串聯(lián)起來了:
收到請求后會通過 http-parser 解析數(shù)據(jù)包;
GET /foo bar HTTP/1.1 會被解析出錯并返回一個錯誤對象;
錯誤對象會進(jìn)入 if (ret instanceof Error) 條件分支并調(diào)用 socketOnError() 函數(shù);
socketOnError() 函數(shù)中會對服務(wù)器觸發(fā)一個 clientError 事件;(this.server.emit("clientError", e, this))
至此,HTTP Server 并不會走到你的那個 function(req, resp) 中去,所以不會有任何的數(shù)據(jù)被返回就結(jié)束了,也就解答了一開始的問題——收不到任何數(shù)據(jù)就請求結(jié)束。
$ curl "http://127.0.0.1:5555/d d" -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5555 (#0)
> GET /d d HTTP/1.1
> Host: 127.0.0.1:5555
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
* no chunk, no close, no size. Assume close to signal end
<
* Closing connection 0
如愿以償?shù)剌敵隽?400 狀態(tài)碼。
引申
接下來我們要引申討論的一個點是,為什么這貨不是一個真正意義上的 Bug。
首先我們看看 Nginx 這么實現(xiàn)這個黑科技的吧。
Nginx 實現(xiàn)
打開 Nginx 源碼的相應(yīng)位置。
我們能看到它的狀態(tài)機(jī)對于 URI 和 HTTP 協(xié)議聲明中間多了一個中間狀態(tài),叫 sw_check_uri_http_09,專門處理 URI 后面的空格。
在各種 URI 解析狀態(tài)中,基本上都能找到這么一句話,表示若當(dāng)前狀態(tài)正則解析 URI 的各種狀態(tài)并且遇到空格的話,則將狀態(tài)改為 sw_check_uri_http_09。
case sw_check_uri:
switch (ch) {
case " ":
r->uri_end = p;
state = sw_check_uri_http_09;
break;
...
}
...
然后在 sw_check_uri_http_09 狀態(tài)時會做一些檢查:
case sw_check_uri_http_09:
switch (ch) {
case " ":
break;
case CR:
r->http_minor = 9;
state = sw_almost_done;
break;
case LF:
r->http_minor = 9;
goto done;
case "H":
r->http_protocol.data = p;
state = sw_http_H;
break;
default:
r->space_in_uri = 1;
state = sw_check_uri;
p--;
break;
}
break;
例如:
遇到空格則繼續(xù)保持當(dāng)前狀態(tài)開始掃描下一位;
如果是換行符則設(shè)置默認(rèn) HTTP 版本并繼續(xù)掃描;
如果遇到的是 H 才修改狀態(tài)為 sw_http_H 認(rèn)為接下去開始 HTTP 版本掃描;
如果是其它字符,則標(biāo)明一下 URI 中有空格,然后將狀態(tài)改回 sw_check_uri,然后倒退回一格以 sw_check_uri 繼續(xù)掃描當(dāng)前的空格。
URIs in HTTP can be represented in absolute form or relative to some known base URI, depending upon the context of their use. The two forms are differentiated by the fact that absolute URIs always begin with a scheme name followed by a colon. For definitive information on URL syntax and semantics, see "Uniform Resource Identifiers (URI): Generic Syntax and Semantics," RFC 2396 (which replaces RFCs 1738 and RFC 1808). This specification adopts the definitions of "URI-reference", "absoluteURI", "relativeURI", "port", "host","abs_path", "rel_path", and "authority" from that specification.
如果你有更多的想法,或者想了解螞蟻金服的 Node.js、前端以及設(shè)計小伙伴們的更多姿勢,可以報名首屆螞蟻體驗科技大會 SEE Conf,比如有死馬大大的《Developer Experience First —— Techless Web Application 的理念與實踐》,還有青梔大大的《螞蟻開發(fā)者工具,服務(wù)螞蟻生態(tài)的移動研發(fā) IDE》等等。