Skip to content

Commit 4cbf7b6

Browse files
pintsizedTieske
andauthored
Ensure request body Content-Length is set correctly (#230)
* Avoid sending non-string body values Fixes #217 * Add newline at EOF * Document support for table of strings in request body * Revert change preventing non string request bodies * Add tests for different request body types * Ensure request body Content-Length is set correctly * Don't blindly set request content length * Add more explicit support for chunked request body iterators * Use new transfer encoding utility * Add tests for client body reader Fails on chunked input until supported by ngx_lua * Improve transfer encoding check Co-authored-by: Thijs Schreijer <thijs@thijsschreijer.nl> Co-authored-by: Thijs Schreijer <thijs@thijsschreijer.nl>
1 parent fa054c3 commit 4cbf7b6

File tree

5 files changed

+288
-25
lines changed

5 files changed

+288
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ The `params` table expects the following fields:
239239
* `path`: The path string. Defaults to `/`.
240240
* `query`: The query string, presented as either a literal string or Lua table..
241241
* `headers`: A table of request headers.
242-
* `body`: The request body as a string, or an iterator function (see [get\_client\_body\_reader](#get_client_body_reader)).
242+
* `body`: The request body as a string, a table of strings, or an iterator function yielding strings until nil when exhausted. Note that you must specify a `Content-Length` for the request body, or specify `Transfer-Encoding: chunked` and have your function implement the encoding. See also: [get\_client\_body\_reader](#get_client_body_reader)).
243243

244244
When the request is successful, `res` will contain the following fields:
245245

lib/resty/http.lua

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,23 @@ local function _receive_headers(sock)
398398
end
399399

400400

401+
local function transfer_encoding_is_chunked(headers)
402+
local te = headers["Transfer-Encoding"]
403+
if not te then
404+
return false
405+
end
406+
407+
-- Handle duplicate headers
408+
-- This shouldn't happen but can in the real world
409+
if type(te) ~= "string" then
410+
te = tbl_concat(te, ",")
411+
end
412+
413+
return str_find(str_lower(te), "chunked", 1, true) ~= nil
414+
end
415+
_M.transfer_encoding_is_chunked = transfer_encoding_is_chunked
416+
417+
401418
local function _chunked_body_reader(sock, default_chunk_size)
402419
return co_wrap(function(max_chunk_size)
403420
local remaining = 0
@@ -575,7 +592,7 @@ end
575592

576593

577594
local function _send_body(sock, body)
578-
if type(body) == 'function' then
595+
if type(body) == "function" then
579596
repeat
580597
local chunk, err, partial = body()
581598

@@ -627,12 +644,13 @@ function _M.send_request(self, params)
627644
local body = params.body
628645
local headers = http_headers.new()
629646

630-
local params_headers = params.headers or {}
631-
-- We assign one by one so that the metatable can handle case insensitivity
647+
-- We assign one-by-one so that the metatable can handle case insensitivity
632648
-- for us. You can blame the spec for this inefficiency.
649+
local params_headers = params.headers or {}
633650
for k, v in pairs(params_headers) do
634651
headers[k] = v
635652
end
653+
636654
if not headers["Proxy-Authorization"] then
637655
-- TODO: next major, change this to always override the provided
638656
-- header. Can't do that yet because it would be breaking.
@@ -644,12 +662,28 @@ function _M.send_request(self, params)
644662
-- Ensure minimal headers are set
645663

646664
if not headers["Content-Length"] then
647-
if type(body) == 'string' then
648-
headers["Content-Length"] = #body
665+
local body_type = type(body)
666+
667+
if body_type == "function" then
668+
if not transfer_encoding_is_chunked(headers) then
669+
return nil, "Request body is a function but a length or chunked encoding is not specified"
670+
end
671+
672+
elseif body_type == "table" then
673+
local length = 0
674+
for _, v in ipairs(body) do
675+
length = length + #tostring(v)
676+
end
677+
headers["Content-Length"] = length
678+
649679
elseif body == nil and EXPECTING_BODY[str_upper(params.method)] then
650680
headers["Content-Length"] = 0
681+
682+
elseif body ~= nil then
683+
headers["Content-Length"] = #tostring(body)
651684
end
652685
end
686+
653687
if not headers["Host"] then
654688
if (str_sub(self.host, 1, 5) == "unix:") then
655689
return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one."
@@ -751,22 +785,8 @@ function _M.read_response(self, params)
751785
if _should_receive_body(params.method, status) then
752786
has_body = true
753787

754-
local te = res_headers["Transfer-Encoding"]
755-
756-
-- Handle duplicate headers
757-
-- This shouldn't happen but can in the real world
758-
if type(te) == "table" then
759-
te = tbl_concat(te, "")
760-
end
761-
762-
local ok, encoding = pcall(str_lower, te)
763-
if not ok then
764-
encoding = ""
765-
end
766-
767-
if version == 1.1 and str_find(encoding, "chunked", 1, true) ~= nil then
788+
if version == 1.1 and transfer_encoding_is_chunked(res_headers) then
768789
body_reader, err = _chunked_body_reader(sock)
769-
770790
else
771791
local ok, length = pcall(tonumber, res_headers["Content-Length"])
772792
if not ok then
@@ -775,9 +795,7 @@ function _M.read_response(self, params)
775795
end
776796

777797
body_reader, err = _body_reader(sock, length)
778-
779798
end
780-
781799
end
782800

783801
if res_headers["Trailer"] then
@@ -941,10 +959,9 @@ function _M.get_client_body_reader(_, chunksize, sock)
941959

942960
local headers = ngx_req_get_headers()
943961
local length = headers.content_length
944-
local encoding = headers.transfer_encoding
945962
if length then
946963
return _body_reader(sock, tonumber(length), chunksize)
947-
elseif encoding and str_lower(encoding) == 'chunked' then
964+
elseif transfer_encoding_is_chunked(headers) then
948965
-- Not yet supported by ngx_lua but should just work...
949966
return _chunked_body_reader(sock, chunksize)
950967
else

t/02-chunked.t

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,35 @@ table
238238
--- no_error_log
239239
[error]
240240
[warn]
241+
242+
243+
=== TEST 5: transfer_encoding_is_chunked utility.
244+
--- http_config eval: $::HttpConfig
245+
--- config
246+
location = /a {
247+
content_by_lua_block {
248+
local http_headers = require("resty.http_headers")
249+
local http = require("resty.http")
250+
251+
local headers = http_headers:new()
252+
assert(http.transfer_encoding_is_chunked(headers) == false,
253+
"empty headers should return false")
254+
255+
headers["Transfer-Encoding"] = "chunked"
256+
assert(http.transfer_encoding_is_chunked(headers) == true,
257+
"te set to `chunked` should return true`")
258+
259+
headers["Transfer-Encoding"] = " ChuNkEd "
260+
assert(http.transfer_encoding_is_chunked(headers) == true,
261+
"te set to ` ChuNkEd ` should return true`")
262+
263+
headers["Transfer-Encoding"] = { "chunked", " ChuNkEd " }
264+
assert(http.transfer_encoding_is_chunked(headers) == true,
265+
"te set to table values containing `chunked` should return true`")
266+
}
267+
}
268+
--- request
269+
GET /a
270+
--- no_error_log
271+
[error]
272+
[warn]

t/03-requestbody.t

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,161 @@ Expectation Failed
203203
--- no_error_log
204204
[error]
205205
[warn]
206+
207+
208+
=== TEST 5: Non string request bodies are converted with correct length
209+
--- http_config eval: $::HttpConfig
210+
--- config
211+
location = /a {
212+
content_by_lua '
213+
local httpc = require("resty.http").new()
214+
215+
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/b"
216+
217+
for _, body in ipairs({ 12345,
218+
true,
219+
"string",
220+
{ "tab", "le" },
221+
{ "mix", 123, "ed", "tab", "le" } }) do
222+
223+
local res, err = assert(httpc:request_uri(uri, {
224+
body = body,
225+
}))
226+
227+
ngx.say(res.body)
228+
end
229+
';
230+
}
231+
location = /b {
232+
content_by_lua '
233+
ngx.req.read_body()
234+
ngx.print(ngx.req.get_body_data())
235+
';
236+
}
237+
--- request
238+
GET /a
239+
--- response_body
240+
12345
241+
true
242+
string
243+
table
244+
mix123edtable
245+
--- no_error_log
246+
[error]
247+
[warn]
248+
249+
250+
=== TEST 6: Request body as iterator
251+
--- http_config eval: $::HttpConfig
252+
--- config
253+
location = /a {
254+
content_by_lua '
255+
local httpc = require("resty.http").new()
256+
257+
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/b"
258+
259+
local res, err = assert(httpc:request_uri(uri, {
260+
body = coroutine.wrap(function()
261+
coroutine.yield("foo")
262+
coroutine.yield("bar")
263+
end),
264+
headers = {
265+
["Content-Length"] = 6
266+
}
267+
}))
268+
269+
ngx.say(res.body)
270+
';
271+
}
272+
location = /b {
273+
content_by_lua '
274+
ngx.req.read_body()
275+
ngx.print(ngx.req.get_body_data())
276+
';
277+
}
278+
--- request
279+
GET /a
280+
--- response_body
281+
foobar
282+
--- no_error_log
283+
[error]
284+
[warn]
285+
286+
287+
=== TEST 7: Request body as iterator, errors with missing length
288+
--- http_config eval: $::HttpConfig
289+
--- config
290+
location = /a {
291+
content_by_lua '
292+
local httpc = require("resty.http").new()
293+
294+
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/b"
295+
296+
local res, err = httpc:request_uri(uri, {
297+
body = coroutine.wrap(function()
298+
coroutine.yield("foo")
299+
coroutine.yield("bar")
300+
end),
301+
})
302+
303+
assert(not res)
304+
ngx.say(err)
305+
';
306+
}
307+
location = /b {
308+
content_by_lua '
309+
ngx.req.read_body()
310+
ngx.print(ngx.req.get_body_data())
311+
';
312+
}
313+
--- request
314+
GET /a
315+
--- response_body
316+
Request body is a function but a length or chunked encoding is not specified
317+
--- no_error_log
318+
[error]
319+
[warn]
320+
321+
322+
=== TEST 8: Request body as iterator with chunked encoding
323+
--- http_config eval: $::HttpConfig
324+
--- config
325+
location = /a {
326+
content_by_lua_block {
327+
local httpc = require("resty.http").new()
328+
local yield = coroutine.yield
329+
330+
local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/b"
331+
332+
local res, err = assert(httpc:request_uri(uri, {
333+
body = coroutine.wrap(function()
334+
yield("3\r\n")
335+
yield("foo\r\n")
336+
337+
yield("3\r\n")
338+
yield("bar\r\n")
339+
340+
yield("0\r\n")
341+
yield("\r\n")
342+
end),
343+
headers = {
344+
["Transfer-Encoding"] = "chunked"
345+
}
346+
}))
347+
348+
ngx.say(res.body)
349+
}
350+
}
351+
location = /b {
352+
content_by_lua '
353+
ngx.req.read_body()
354+
ngx.print(ngx.req.get_body_data())
355+
';
356+
}
357+
--- request
358+
GET /a
359+
--- response_body
360+
foobar
361+
--- no_error_log
362+
[error]
363+
[warn]

t/10-clientbodyreader.t

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,59 @@ OK
6464
[error]
6565
[warn]
6666
67+
68+
=== TEST 2: Read request body
69+
--- http_config eval: $::HttpConfig
70+
--- config
71+
location = /a {
72+
content_by_lua_block {
73+
local httpc = require("resty.http").new()
74+
75+
local reader, err = assert(httpc:get_client_body_reader())
76+
77+
repeat
78+
local buffer, err = reader()
79+
if err then
80+
ngx.log(ngx.ERR, err)
81+
end
82+
83+
if buffer then
84+
ngx.print(buffer)
85+
end
86+
until not buffer
87+
}
88+
}
89+
--- request
90+
POST /a
91+
foobar
92+
--- response_body: foobar
93+
--- no_error_log
94+
[error]
95+
[warn]
96+
97+
98+
=== TEST 2: Read chunked request body, errors as not yet supported
99+
--- http_config eval: $::HttpConfig
100+
--- config
101+
location = /a {
102+
content_by_lua_block {
103+
local httpc = require("resty.http").new()
104+
local _, err = httpc:get_client_body_reader()
105+
ngx.log(ngx.ERR, err)
106+
}
107+
}
108+
--- more_headers
109+
Transfer-Encoding: chunked
110+
--- request eval
111+
"POST /a
112+
3\r
113+
foo\r
114+
3\r
115+
bar\r
116+
0\r
117+
\r
118+
"
119+
--- error_log
120+
chunked request bodies not supported yet
121+
--- no_error_log
122+
[warn]

0 commit comments

Comments
 (0)