Node.js 中遇到含空格 URL 的神奇「Bug」——小範圍深入 HTTP 協議
首先聲明,我在「Bug」字眼上加了引號,自然是為了說明它並非一個真 Bug。
1. 問題拋出
昨天有個童鞋在看後台監控的時候,突然發現了一個錯誤:
[error] 000001#0: ... upstream prematurely closed connection while reading response header from upstream. client: 10.10.10.10 server: foo.com request: "GET /foo/bar?rmicmd,begin run clean docker images job HTTP/1.1" upstream: "http://..."
大概意思就是說:一台伺服器通過 HTTP 協議去請求另一台伺服器的時候,單方面被對方伺服器斷開了連接——並且並沒有任何返回。
2. 開始重現
2.1. 客戶端 CURL 指令
其實這次請求的一些貓膩很容易就能發現——在 URL 中有空格。所以我們能簡化出一條最簡單的 CURL 指令:
$ curl "http://foo/bar baz" -v
注意:不帶任何轉義。
2.2. 最小 Node.js 源碼
好的,那麼接下去開始寫相應的最簡單的 Node.js HTTP 服務端源碼。
"use strict";const http = require("http");const server = http.createServer(function(req, resp) { console.log("??"); resp.end("hello world");});server.listen(5555);
大功告成,啟動這段 Node.js 代碼,開始試試看上面的指令吧。
如果你也正在跟著嘗試這件事情的話,你就會發現 Node.js 的命令行沒有輸出任何信息,尤其是嘲諷的 "?
?",而在 CURL 的結果中,你將會看見:
$ 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 intactcurl: (52) Empty reply from server
瞧,Empty reply from server。
2.3. Nginx
發現了問題之後,就有另一個問題值得思考了:就 Node.js 會出現這種情況呢,還是其它一些 HTTP 伺服器也會有這種情況呢。
於是拿小白鼠 Nginx 做了個實驗。我寫了這麼一個配置:
server { listen 5555; location / { return 200 $uri; }}
接著也執行一遍 CURL,得到了如下的結果:
$ 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 200 OK< Server: openresty/1.11.2.1< Date: Tue, 12 Dec 2017 09:07:56 GMT< Content-Type: application/octet-stream< Content-Length: 4< Connection: keep-alive<* Connection #0 to host 127.0.0.1 left intact/d d
於是乎,理所當然,我暫時將這個事件定性為 Node.js 的一個 Bug。
3. Node.js 源碼排查
認定了它是個 Bug 之後,我就開始了一貫的看源碼環節——由於這個 Bug 的復現條件比較明顯,我暫時將其定性為「Node.js HTTP 服務端模塊在接到請求後解析 HTTP 數據包的時候解析 URI 時出了問題」。
3.1. http.js -> _http_server.js -> _http_common.js
源碼以 Node.js 8.9.2 為準。
這裡先預留一下我們能馬上想到的 http://node_http_parser.cc,而先講這幾個文件,是有原因的——這涉及到最後的一個應對方式。
首先看看 lib/http.js 的相應源碼:
...const server = require("_http_server");const { Server } = server;function createServer(requestListener) { return new Server(requestListener);}
那麼,馬上進入 lib/_http_server.js 看吧。
首先是創建一個 HttpParser 並綁上監聽獲取到 HTTP 數據包後解析結果的回調函數的代碼:
const { parsers, ...} = require("_http_common");function connectionListener(socket) { ... var parser = parsers.alloc(); parser.reinitialize(HTTPParser.REQUEST); parser.socket = socket; socket.parser = parser; parser.incoming = null; ... state.onData = socketOnData.bind(undefined, this, socket, parser, state); ... socket.on("data", state.onData); ...}function socketOnData(server, socket, parser, state, d) { assert(!socket._paused); debug("SERVER socketOnData %d", d.length); var ret = parser.execute(d); onParserExecuteCommon(server, socket, parser, state, ret, d);}
從源碼中文我們能看到,當一個 HTTP 請求過來的時候,監聽函數 connectionListener()
會拿著 Socket 對象加上一個 data
事件監聽——一旦有請求連接過來,就去執行 socketOnData()
函數。
而在 socketOnData()
函數中,做的主要事情就是 parser.execute(d)
來解析 HTTP 數據包,在解析完成後執行一下回調函數 onParserExecuteCommon()
。
至於這個 parser
,我們能看到它是從 lib/_http_common.js 中來的。
var parsers = new FreeList("parsers", 1000, function() { var parser = new HTTPParser(HTTPParser.REQUEST); ... parser[kOnHeaders] = parserOnHeaders; parser[kOnHeadersComplete] = parserOnHeadersComplete; parser[kOnBody] = parserOnBody; parser[kOnMessageComplete] = parserOnMessageComplete; parser[kOnExecute] = null; return parser;});
能看出來 parsers
是 HTTPParser
的一條 Free List(效果類似於最簡易的動態內存池),每個 Parser 在初始化的時候綁定上了各種回調函數。具體的一些回調函數就不細講了,有興趣的童鞋可自行翻閱。
這麼一來,鏈路就比較明晰了:
請求進來的時候,Server 對象會為該次請求的 Socket 分配一個 HttpParser
對象,並調用其 execute()
函數進行解析,在解析完成後調用 onParserExecuteCommon()
函數。
3.2. http://node_http_parser.cc
我們在 lib/_http_common.js 中能發現,HTTPParser
的實現存在於 src/http://node_http_parser.cc 中:
const binding = process.binding("http_parser");const { methods, HTTPParser } = binding;
至於為什麼
const binding = process.binding("http_parser")
就是對應到 src/http://node_http_parser.cc 文件,以及這一小節中下面的一些 C++ 源碼相關分析,不明白且有興趣的童鞋可自行去閱讀更深一層的源碼,或者網上搜索答案,或者我提前無恥硬廣一下我快要上市的書《Node.js:來一打 C++ 擴展》——裡面也有說明,以及我的有一場知乎 Live《深入理解 Node.js 包與模塊機制》。
總而言之,我們接下去要看的就是 src/node_http_parser.cc 了。
env->SetProtoMethod(t, "close", Parser::Close);env->SetProtoMethod(t, "execute", Parser::Execute);env->SetProtoMethod(t, "finish", Parser::Finish);env->SetProtoMethod(t, "reinitialize", Parser::Reinitialize);env->SetProtoMethod(t, "pause", Parser::Pause<true>);env->SetProtoMethod(t, "resume", Parser::Pause<false>);env->SetProtoMethod(t, "consume", Parser::Consume);env->SetProtoMethod(t, "unconsume", Parser::Unconsume);env->SetProtoMethod(t, "getCurrentBuffer", Parser::GetCurrentBuffer);
如代碼片段所示,前文中 parser.execute()
所對應的函數就是 Parser::Execute()
了。
class Parser : public AsyncWrap { ... static void Execute(const FunctionCallbackInfo<Value>& args) { Parser* parser; ... Local<Object> buffer_obj = args[0].As<Object>(); char* buffer_data = Buffer::Data(buffer_obj); size_t buffer_len = Buffer::Length(buffer_obj); ... Local<Value> ret = parser->Execute(buffer_data, buffer_len); if (!ret.IsEmpty()) args.GetReturnValue().Set(ret); } Local<Value> Execute(char* data, size_t len) { EscapableHandleScope scope(env()->isolate()); current_buffer_len_ = len; current_buffer_data_ = data; got_exception_ = false; size_t nparsed = http_parser_execute(&parser_, &settings, data, len); Save(); // Unassign the "buffer_" variable current_buffer_.Clear(); current_buffer_len_ = 0; current_buffer_data_ = nullptr; // If there was an exception in one of the callbacks if (got_exception_) return scope.Escape(Local<Value>()); Local<Integer> nparsed_obj = Integer::New(env()->isolate(), nparsed); // If there was a parse error in one of the callbacks // TODO(bnoordhuis) What if there is an error on EOF? if (!parser_.upgrade && nparsed != len) { enum http_errno err = HTTP_PARSER_ERRNO(&parser_); Local<Value> e = Exception::Error(env()->parse_error_string()); Local<Object> obj = e->ToObject(env()->isolate()); obj->Set(env()->bytes_parsed_string(), nparsed_obj); obj->Set(env()->code_string(), OneByteString(env()->isolate(), http_errno_name(err))); return scope.Escape(e); } return scope.Escape(nparsed_obj); }}
首先進入 Parser
的靜態 Execute()
函數,我們看到它把傳進來的 Buffer
轉化為 C++ 下的 char*
指針,並記錄其數據長度,同時去執行當前調用的 parser
對象所對應的 Execute()
函數。
在這個 Execute()
函數中,有個最重要的代碼,就是:
size_t nparsed = http_parser_execute(&parser_, &settings, data, len);
這段代碼是調用真正解析 HTTP 數據包的函數,它是 Node.js 這個項目的一個自研依賴,叫 http-parser。它獨立的項目地址在 https://github.com/nodejs/http-parser,我們本文中用的是 Node.js v8.9.2 中所依賴的源碼,應該會有偏差。
3.3. http-parser
3.3.1. HTTP Request 數據包體
如果你已經對 HTTP 包體了解了,可以略過這一節。
HTTP 的 Request 數據包其實是文本格式的,在 Raw 的狀態下,大概是以這樣的形式存在:
方法 URI HTTP/版本頭1: 我是頭1頭2: 我是頭2
簡單起見,這裡就寫出最基礎的一些內容,至於 Body 什麼的大家自己找資料看吧。
上面的是什麼意思呢?我們看看 CURL 的結果就知道了,實際上對應 curl ... -v
的中間輸出:
GET /test HTTP/1.1Host: 127.0.0.1:5555User-Agent: curl/7.54.0Accept: */*
所以實際上大家平時在文章中、瀏覽器調試工具中看到的什麼請求頭啊什麼的,都是以文本形式存在的,以換行符分割。
而——重點來了,導致我們本文所述「Bug」出現的請求,它的請求包如下:
GET /foo bar HTTP/1.1Host: 127.0.0.1:5555User-Agent: curl/7.54.0Accept: */*
重點在第一行:
GET /foo bar HTTP/1.1
3.3.2. 源碼解析
話不多少,我們之間前往 http-parser 的 http_parser.c 看 http_parser_execute ()
函數中的狀態機變化。
從源碼中文我們能看到,http-parser 的流程是從頭到尾以 O(n) 的時間複雜度對字元串逐字掃描,並且不後退也不往前跳。
那麼掃描到每個字元的時候,都有屬於當前的一個狀態,如「正在掃描處理 uri」、「正在掃描處理 HTTP 協議並且處理到了 H」、「正在掃描處理 HTTP 協議並且處理到了 HT」、「正在掃描處理 HTTP 協議並且處理到了 HTT」、「正在掃描處理 HTTP 協議並且處理到了 HTTP」、……
憋笑,這是真的,我們看看代碼就知道了:
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: ...
如代碼所見,若當前狀態為 s_req_http_start
,則先判斷當前字元是不是合標。因為就 HTTP 請求包體的格式來看,如果 URI 解析結束的話,理應出現類似 HTTP/1.1
的這麼一個版本申明。所以這個時候 http-parser 會直接判斷當前字元是否為 H
。
- 若是
H
,則將狀態改為s_req_http_H
並繼續掃描循環的下一位,同理在s_req_http_H
下若合法狀態就會變成s_req_http_HT
,以此類推; +若是空格,則認為是多餘的空格,那麼當前狀態不做任何改變,並繼續下一個掃描; - 但如果當前字元既不是空格也不是
H
,那麼好了,http-parser 直接認為你的請求包不合法,將你本次的解析設置錯誤HPE_INVALID_CONSTANT
並goto
到error
代碼塊。
至此,我們基本上已經明白了原因了:
http-parser 認為在 HTTP 請求包體中,第一行的 URI 解析階段一旦出現了空格,就會認為 URI 解析完成,繼而解析 HTTP 協議版本。但若此時緊跟著的不是 HTTP 協議版本的標準格式,http-parser 就會認為你這是一個 HPE_INVALID_CONSTANT
的數據包。
不過,我們還是繼續看看它的 error
代碼塊吧:
error: if (HTTP_PARSER_ERRNO(parser) == HPE_OK) { SET_ERRNO(HPE_UNKNOWN); } RETURN(p - data);
這段代碼中首先判斷一下當跳到這段代碼的時候有沒有設置錯誤,若沒有設置錯誤則將錯誤設置為未知錯誤(HPE_UNKNOWN
),然後返回已解析的數據包長度。
p
是當前解析字元指針,data
是這個數據包的起始指針,所以p - data
就是已解析的數據長度。如果成功解析完,這個數據包理論上是等於這個數據包的完整長度,若不等則理論上說明肯定是中途出錯提前返回。
3.4. 回到 http://node_http_parser.cc
看完了 http-parser 的原理後,很多地方茅塞頓開。現在我們回到它的調用地 node_http_parser.cc 繼續閱讀吧。
Local<Value> Execute(char* data, size_t len) { ... size_t nparsed = http_parser_execute(&parser_, &settings, data, len); Local<Integer> nparsed_obj = Integer::New(env()->isolate(), nparsed); if (!parser_.upgrade && nparsed != len) { enum http_errno err = HTTP_PARSER_ERRNO(&parser_); Local<Value> e = Exception::Error(env()->parse_error_string()); Local<Object> obj = e->ToObject(env()->isolate()); obj->Set(env()->bytes_parsed_string(), nparsed_obj); obj->Set(env()->code_string(), OneByteString(env()->isolate(), http_errno_name(err))); return scope.Escape(e); } return scope.Escape(nparsed_obj);}
從調用處我們能看見,在執行完 http_parser_execute()
後有一個判斷,若當前請求不是 upgrade
請求(即請求頭中有說明 Upgrade
,通常用於 WebSocket),並且解析長度不等於原數據包長度(前文說了這種情況屬於出錯了)的話,那麼進入中間的錯誤代碼塊。
在錯誤代碼塊中,先 HTTP_PARSER_ERRNO(&parser_)
拿到錯誤碼,然後通過 Exception::Error()
生成錯誤對象,將錯誤信息塞進錯誤對象中,最後返回錯誤對象。
如果沒錯,則返回解析長度(nparsed_obj
即 nparsed
)。
在這個文件中,眼尖的童鞋可能發現了,執行
Execute()
有好多處,這是因為實際上一個 HTTP 請求可能是流式的,所以有時候可能會只拿到部分數據包。所以最後有一個結束符需要被確認。這也是為什麼 http-parser 在解析的時候只能逐字解析而不能跳躍或者後退了。
3.5. 回到 _http_server.js
我們把 Parser::Execute()
也就是 JavaScript 代碼中的 parser.execute()
給搞清楚後,我們就能回到 _http_server.js 看代碼了。
前文說了,socketOnData
在解析完數據包後會執行 onParserExecuteCommon
函數,現在就來看看這個 onParserExecuteCommon()
函數。
function onParserExecuteCommon(server, socket, parser, state, ret, d) { resetSocketTimeout(server, socket, state); if (ret instanceof Error) { debug("parse error", ret); socketOnError.call(socket, ret); } else if (parser.incoming && parser.incoming.upgrade) { ... }}
長長的一個函數被我精簡成這麼幾句話,重點很明顯。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)
中去,所以不會有任何的數據被返回就結束了,也就解答了一開始的問題——收不到任何數據就請求結束。
這就是我要逐級進來看代碼,而不是直達 http-parser 的原因了——clientError
是一個關鍵。
4. 處理辦法
要解決這個「Bug」其實不難,直接監聽 clientError 事件並做一些處理即可。
"use strict";const http = require("http");const server = http.createServer(function(req, resp) { console.log("??"); resp.end("hello world");}).on("clientError", function(err, sock) { console.log("??"); sock.end("HTTP/1.1 400 Bad Request
");});server.listen(5555);
注意:由於運行到
clientError
事件時,並沒有任何 Request 和 Response 的封裝,你能拿到的是一個 Node.js 中原始的 Socket 對象,所以當你要返回數據的時候需要自己按照 HTTP 返回數據包的格式來輸出。
這個時候再揮起你的小手試一下 CURL 吧:
$ 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 狀態碼。
5. 引申
接下來我們要引申討論的一個點是,為什麼這貨不是一個真正意義上的 Bug。
首先我們看看 Nginx 這麼實現這個黑科技的吧。
5.1. 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
繼續掃描當前的空格。
在理解了這個「黑科技」後,我們很快能找到一個很好玩的點,開啟你的 Nginx 並用 CURL 請求以下面的例子一下它看看吧:
$ curl "http://xcoder.in:5555/d H" -v* Trying 103.238.225.181...* TCP_NODELAY set* Connected to xcoder.in (103.238.225.181) port 5555 (#0)> GET /d H HTTP/1.1> Host: xcoder.in:5555> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 400 Bad Request< Server: openresty/1.11.2.1< Date: Tue, 12 Dec 2017 11:18:13 GMT< Content-Type: text/html< Content-Length: 179< Connection: close<<html><head><title>400 Bad Request</title></head><body bgcolor="white"><center><h1>400 Bad Request</h1></center><hr><center>openresty/1.11.2.1</center></body></html>* Closing connection 0
怎麼樣?是不是發現結果跟之前的不一樣了——它居然也返回了 400 Bad Request。
原因為何就交給童鞋們自己考慮吧。
5.2. RFC 2616 與 RFC 2396
那麼,為什麼即使在 Nginx 支持空格 URI 的情況下,我還說 Node.js 這個不算 Bug,並且指明 Nginx 是「黑科技」呢?
後來我去看了 HTTP 協議 RFC。
原因在於 Network Working Group 的 RFC 2616,關於 HTTP 協議的規範。
在 RFC 2616 的 3.2.1 節中做了一些說明,它說了在 HTTP 協議中關於 URI 的文法和語義參照了 RFC 2396。
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.
而在 RFC 2396 中,我們同樣找到了它的 2.4.3 節。裡面對於 Disallow 的 US-ASCII 字元做了解釋,其中有:
- 控制符,指 ASCII 碼在 0x00-0x1F 範圍內以及 0x7F;
控制符通常不可見;
- 空格,指 0x20;
空格不可控,如經由一些排版軟體轉錄後可能會有變化,而到了 HTTP 協議這層時,反正空格不推薦使用了,所以就索性用空格作為首行分隔符了;
- 分隔符,
"<"
、">"
、"#"
、"%"
、"""
。
如 #
將用於瀏覽器地址欄的 Hash;而 %
則會與 URI 轉義一同使用,所以不應單獨出現在 URI 中。
於是乎,HTTP 請求中,包體的 URI 似乎本就不應該出現空格,而 Nginx 是一個黑魔法的姿勢。
6. 小結
嚯,寫得累死了。本次的一個探索基於了一個有空格非正常的 URI 通過 CURL 或者其它一些客戶端請求時,Node.js 出現的 Bug 狀態。
實際上發現這個 Bug 的時候,客戶端請求似乎是因為那邊的開發者手抖,不小心將不應該拼接進來的內容給拼接到了 URL 中,類似於
$ rm -rf /
。
一開始我以為這是 Node.js 的 Bug,在探尋之後發現是因為我們自己沒用 Node.js HTTP Server 提供的 clientError
事件做正確的處理。而 Nginx 的正常請求則是它的黑科技。這些答案都能從 RFC 中尋找——再次體現了遇到問題看源碼看規範的重要性。
另,我本打算給 http-parser 也加上黑魔法,後來我快寫好的時候發現它是流式的,很多狀態沒法在現有的體系中保留下來,最後放棄了,反正這也不算 Bug。不過在以後有時間的時候,感覺還是可以好好整理一下代碼,好好修改一下給提個 PR 上去,以此自勉。
最後,求 fafa。
7. 交流
如果你有更多的想法,或者想了解螞蟻金服的 Node.js、前端以及設計小夥伴們的更多姿勢,可以報名首屆螞蟻體驗科技大會 SEE Conf,比如有死馬大大的《Developer Experience First —— Techless Web Application 的理念與實踐》,還有青梔大大的《螞蟻開發者工具,服務螞蟻生態的移動研發 IDE》等等。
報名官網:SEE Conf · 螞蟻體驗科技大會
期待您的光臨。
推薦閱讀:
※如何 windows下nginx+django+flup python3?
※把nginx改為一個普通的tcp伺服器,應用層協議自己定義,有可行性嗎?
※準確地配置 NginX (1)
※如何配置nginx+uwsgi+django?
※使用uwsgi和nginx做伺服器和django框架,為什麼每次修改代碼都需要重啟uwsgi呢?