$ 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》等等。