$ 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;
}
在掃描的時候,如果當前狀態是 URI 相關的(如 s_req_path、s_req_query_string 等),則執行一個子 switch,里面的處理如下:
若當前字符是空格,則將狀態改變為 s_req_http_start 并認為 URI 已經解析好了,通過宏 CALLBACK_DATA() 觸發 URI 解析好的事件;
若當前字符是換行符,則說明還在解析 URI 的時候就被換行了,后面就不可能跟著 HTTP 協議版本的申明了,所以設置默認的 HTTP 版本為 0.9,并修改當前狀態,最后認為 URI 已經解析好了,通過宏 CALLBACK_DATA() 觸發 URI 解析好的事件;
其余情況(所有其它字符)下,通過調用 parse_url_char() 函數來解析一些東西并更新當前狀態。(因為哪怕是在解析 URI 狀態中,也還有各種不同的細分,如 s_req_path、s_req_query_string )
這里的重點還是當狀態為解析 URI 的時候遇到了空格的處理,上面也解釋過了,一旦遇到這種情況,則會認為 URI 已經解析好了,并且將狀態修改為 s_req_http_start。也就是說,有“Bug”的那個數據包 GET /foo bar HTTP/1.1 在解析到 foo 后面的空格的時候它就將狀態改為 s_req_http_start 并且認為 URI 已經解析結束了。
好的,接下來我們看看 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:
...
長長的一個函數被我精簡成這么幾句話,重點很明顯。ret 就是從 socketOnData 傳進來已解析的數據長度,但是在 C++ 代碼中我們也看到了它還有可能是一個錯誤對象。所以在這個函數中一開始就做了一個判斷,判斷解析的結果是不是一個錯誤對象,如果是錯誤對象則調用 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 對象會觸發一個 clientError 事件。
整個事情串聯起來了:
收到請求后會通過 http-parser 解析數據包;
GET /foo bar HTTP/1.1 會被解析出錯并返回一個錯誤對象;
錯誤對象會進入 if (ret instanceof Error) 條件分支并調用 socketOnError() 函數;
socketOnError() 函數中會對服務器觸發一個 clientError 事件;(this.server.emit("clientError", e, this))
至此,HTTP Server 并不會走到你的那個 function(req, resp) 中去,所以不會有任何的數據被返回就結束了,也就解答了一開始的問題——收不到任何數據就請求結束。
$ 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
如愿以償地輸出了 400 狀態碼。
引申
接下來我們要引申討論的一個點是,為什么這貨不是一個真正意義上的 Bug。
首先我們看看 Nginx 這么實現這個黑科技的吧。
Nginx 實現
打開 Nginx 源碼的相應位置。
我們能看到它的狀態機對于 URI 和 HTTP 協議聲明中間多了一個中間狀態,叫 sw_check_uri_http_09,專門處理 URI 后面的空格。
在各種 URI 解析狀態中,基本上都能找到這么一句話,表示若當前狀態正則解析 URI 的各種狀態并且遇到空格的話,則將狀態改為 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 狀態時會做一些檢查:
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;
例如:
遇到空格則繼續保持當前狀態開始掃描下一位;
如果是換行符則設置默認 HTTP 版本并繼續掃描;
如果遇到的是 H 才修改狀態為 sw_http_H 認為接下去開始 HTTP 版本掃描;
如果是其它字符,則標明一下 URI 中有空格,然后將狀態改回 sw_check_uri,然后倒退回一格以 sw_check_uri 繼續掃描當前的空格。
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、前端以及設計小伙伴們的更多姿勢,可以報名首屆螞蟻體驗科技大會 SEE Conf,比如有死馬大大的《Developer Experience First —— Techless Web Application 的理念與實踐》,還有青梔大大的《螞蟻開發者工具,服務螞蟻生態的移動研發 IDE》等等。