Skip to content

Commit 94a825f

Browse files
committed
Support chunked requests when talking to proxy servers with request buffering disabled
1 parent 8401944 commit 94a825f

File tree

7 files changed

+1439
-45
lines changed

7 files changed

+1439
-45
lines changed

gateway/src/apicast/http_proxy.lua

Lines changed: 126 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
local format = string.format
22
local tostring = tostring
3+
local ngx_flush = ngx.flush
4+
local ngx_get_method = ngx.req.get_method
5+
local ngx_http_version = ngx.req.http_version
6+
local ngx_send_headers = ngx.send_headers
37

48
local resty_url = require "resty.url"
59
local resty_resolver = require 'resty.resolver'
610
local round_robin = require 'resty.balancer.round_robin'
711
local http_proxy = require 'resty.http.proxy'
812
local file_reader = require("resty.file").file_reader
913
local file_size = require("resty.file").file_size
14+
local client_body_reader = require("resty.http.request_reader").get_client_body_reader
15+
local send_response = require("resty.http.response_writer").send_response
1016
local concat = table.concat
1117

1218
local _M = { }
1319

20+
local http_methods_with_body = {
21+
POST = true,
22+
PUT = true,
23+
PATCH = true
24+
}
25+
26+
local DEFAULT_CHUNKSIZE = 32 * 1024
27+
1428
function _M.reset()
1529
_M.balancer = round_robin.new()
1630
_M.resolver = resty_resolver
@@ -84,52 +98,105 @@ local function absolute_url(uri)
8498
)
8599
end
86100

87-
local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_connect)
88-
-- This is needed to call ngx.req.get_body_data() below.
89-
ngx.req.read_body()
90-
91-
-- We cannot use resty.http's .get_client_body_reader().
92-
-- In POST requests with HTTPS, the result of that call is nil, and it
93-
-- results in a time-out.
94-
--
95-
--
96-
-- If ngx.req.get_body_data is nil, can be that the body is too big to
97-
-- read and need to be cached in a local file. This request will return
98-
-- nil, so after this we need to read the temp file.
99-
-- https://github.com/openresty/lua-nginx-module#ngxreqget_body_data
100-
local body = ngx.req.get_body_data()
101+
local function handle_expect()
102+
local expect = ngx.req.get_headers()["Expect"]
103+
if type(expect) == "table" then
104+
expect = expect[1]
105+
end
106+
107+
if expect and expect:lower() == "100-continue" then
108+
ngx.status = 100
109+
local ok, err = ngx_send_headers()
110+
111+
if not ok then
112+
return nil, "failed to send response header: " .. (err or "unknown")
113+
end
114+
115+
ok, err = ngx_flush(true)
116+
if not ok then
117+
return nil, "failed to flush response header: " .. (err or "unknown")
118+
end
119+
end
120+
end
121+
122+
local function forward_https_request(proxy_uri, uri, proxy_opts)
123+
local body, err
124+
local sock
125+
local opts = proxy_opts or {}
126+
local req_method = ngx_get_method()
101127
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
128+
local is_chunked = encoding and encoding:lower() == "chunked"
129+
130+
if http_methods_with_body[req_method] then
131+
if opts.request_unbuffered and ngx_http_version() == 1.1 then
132+
local _, err = handle_expect()
133+
if err then
134+
ngx.log(ngx.ERR, "failed to handle expect header, err: ", err)
135+
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
136+
end
102137

103-
if not body then
104-
local temp_file_path = ngx.req.get_body_file()
105-
ngx.log(ngx.INFO, "HTTPS Proxy: Request body is bigger than client_body_buffer_size, read the content from path='", temp_file_path, "'")
106-
107-
if temp_file_path then
108-
body, err = file_reader(temp_file_path)
109-
if err then
110-
ngx.log(ngx.ERR, "HTTPS proxy: Failed to read temp body file, err: ", err)
111-
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
112-
end
113-
114-
if encoding == "chunked" then
115-
-- If the body is smaller than "client_boby_buffer_size" the Content-Length header is
116-
-- set based on the size of the buffer. However, when the body is rendered to a file,
117-
-- we will need to calculate and manually set the Content-Length header based on the
118-
-- file size
119-
local contentLength, err = file_size(temp_file_path)
120-
if err then
121-
ngx.log(ngx.ERR, "HTTPS proxy: Failed to set content length, err: ", err)
138+
if is_chunked then
139+
-- The default ngx reader does not support chunked request
140+
-- so we will need to get the raw request socket and manually
141+
-- decode the chunked request
142+
sock, err = ngx.req.socket(true)
143+
else
144+
sock, err = ngx.req.socket()
145+
end
146+
147+
if not sock then
148+
ngx.log(ngx.ERR, "unable to obtain request socket: ", err)
149+
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
150+
end
151+
152+
body = client_body_reader(sock, DEFAULT_CHUNKSIZE, is_chunked)
153+
else
154+
-- This is needed to call ngx.req.get_body_data() below.
155+
ngx.req.read_body()
156+
157+
-- We cannot use resty.http's .get_client_body_reader().
158+
-- In POST requests with HTTPS, the result of that call is nil, and it
159+
-- results in a time-out.
160+
--
161+
--
162+
-- If ngx.req.get_body_data is nil, can be that the body is too big to
163+
-- read and need to be cached in a local file. This request will return
164+
-- nil, so after this we need to read the temp file.
165+
-- https://github.com/openresty/lua-nginx-module#ngxreqget_body_data
166+
body = ngx.req.get_body_data()
167+
168+
if not body then
169+
local temp_file_path = ngx.req.get_body_file()
170+
ngx.log(ngx.INFO, "HTTPS Proxy: Request body is bigger than client_body_buffer_size, read the content from path='", temp_file_path, "'")
171+
172+
if temp_file_path then
173+
body, err = file_reader(temp_file_path)
174+
if err then
175+
ngx.log(ngx.ERR, "HTTPS proxy: Failed to read temp body file, err: ", err)
122176
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
177+
end
178+
179+
if is_chunked then
180+
-- If the body is smaller than "client_boby_buffer_size" the Content-Length header is
181+
-- set based on the size of the buffer. However, when the body is rendered to a file,
182+
-- we will need to calculate and manually set the Content-Length header based on the
183+
-- file size
184+
local contentLength, err = file_size(temp_file_path)
185+
if err then
186+
ngx.log(ngx.ERR, "HTTPS proxy: Failed to set content length, err: ", err)
187+
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
188+
end
189+
190+
ngx.req.set_header("Content-Length", tostring(contentLength))
191+
end
123192
end
124-
125-
ngx.req.set_header("Content-Length", tostring(contentLength))
126-
end
127193
end
128-
end
129194

130-
-- The whole request is buffered in the memory so remove the Transfer-Encoding: chunked
131-
if ngx.var.http_transfer_encoding == "chunked" then
132-
ngx.req.set_header("Transfer-Encoding", nil)
195+
-- The whole request is buffered in the memory so remove the Transfer-Encoding: chunked
196+
if is_chunked then
197+
ngx.req.set_header("Transfer-Encoding", nil)
198+
end
199+
end
133200
end
134201

135202
local request = {
@@ -139,10 +206,10 @@ local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_conn
139206
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
140207
body = body,
141208
proxy_uri = proxy_uri,
142-
proxy_auth = proxy_auth
209+
proxy_auth = opts.proxy_auth
143210
}
144211

145-
local httpc, err = http_proxy.new(request, skip_https_connect)
212+
local httpc, err = http_proxy.new(request, opts.skip_https_connect)
146213

147214
if not httpc then
148215
ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', err)
@@ -154,8 +221,16 @@ local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_conn
154221
res, err = httpc:request(request)
155222

156223
if res then
157-
httpc:proxy_response(res)
158-
httpc:set_keepalive()
224+
if opts.request_unbuffered and is_chunked then
225+
local bytes, err = send_response(sock, res, DEFAULT_CHUNKSIZE)
226+
if not bytes then
227+
ngx.log(ngx.ERR, "failed to send response: ", err)
228+
return sock:send("HTTP/1.1 502 Bad Gateway")
229+
end
230+
else
231+
httpc:proxy_response(res)
232+
httpc:set_keepalive()
233+
end
159234
else
160235
ngx.log(ngx.ERR, 'failed to proxy request to: ', proxy_uri, ' err : ', err)
161236
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
@@ -208,7 +283,13 @@ function _M.request(upstream, proxy_uri)
208283
return
209284
elseif uri.scheme == 'https' then
210285
upstream:rewrite_request()
211-
forward_https_request(proxy_uri, proxy_auth, uri, upstream.skip_https_connect)
286+
local proxy_opts = {
287+
proxy_auth = proxy_auth,
288+
skip_https_connect = upstream.skip_https_connect,
289+
request_unbuffered = upstream.request_unbuffered
290+
}
291+
292+
forward_https_request(proxy_uri, uri, proxy_opts)
212293
return ngx.exit(ngx.OK) -- terminate phase
213294
else
214295
ngx.log(ngx.ERR, 'could not connect to proxy: ', proxy_uri, ' err: ', 'invalid request scheme')

gateway/src/apicast/upstream.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ function _M:call(context)
241241
self:set_skip_https_connect_on_proxy();
242242
end
243243

244+
self.request_unbuffered = context.request_unbuffered
244245
http_proxy.request(self, proxy_uri)
245246
else
246247
local err = self:rewrite_request()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
local httpc = require "resty.resolver.http"
2+
3+
local _M = {
4+
}
5+
6+
local cr_lf = "\r\n"
7+
8+
-- chunked_reader return a body reader that translates the data read from
9+
-- lua-resty-http client_body_reader to HTTP "chunked" format before returning it
10+
--
11+
-- The chunked reader return nil when the final 0-length chunk is read
12+
local function chunked_reader(sock, chunksize)
13+
chunksize = chunksize or 65536
14+
local eof = false
15+
local reader = httpc:get_client_body_reader(chunksize, sock)
16+
if not reader then
17+
return nil
18+
end
19+
20+
return function()
21+
if eof then
22+
return nil
23+
end
24+
25+
local buffer, err = reader()
26+
if err then
27+
return nil, err
28+
end
29+
if buffer then
30+
local chunk = string.format("%x\r\n", #buffer) .. buffer .. cr_lf
31+
return chunk
32+
else
33+
eof = true
34+
return "0\r\n\r\n"
35+
end
36+
end
37+
end
38+
39+
function _M.get_client_body_reader(sock, chunksize, is_chunked)
40+
if is_chunked then
41+
return chunked_reader(sock, chunksize)
42+
else
43+
return httpc:get_client_body_reader(chunksize, sock)
44+
end
45+
end
46+
47+
return _M
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
local _M = {
2+
}
3+
4+
local cr_lf = "\r\n"
5+
6+
local function send(socket, data)
7+
if not data or data == '' then
8+
ngx.log(ngx.DEBUG, 'skipping sending nil')
9+
return
10+
end
11+
12+
return socket:send(data)
13+
end
14+
15+
-- write_response writes response body reader to sock in the HTTP/1.x server response format,
16+
-- The connection is closed if send() fails or when returning a non-zero
17+
function _M.send_response(sock, response, chunksize)
18+
local bytes, err
19+
chunksize = chunksize or 65536
20+
21+
if not response then
22+
ngx.log(ngx.ERR, "no response provided")
23+
return
24+
end
25+
26+
if not sock then
27+
return nil, "socket not initialized yet"
28+
end
29+
30+
-- Status line
31+
local status = "HTTP/1.1 " .. response.status .. " " .. response.reason .. cr_lf
32+
bytes, err = send(sock, status)
33+
if not bytes then
34+
return nil, "failed to send status line, err: " .. (err or "unknown")
35+
end
36+
37+
-- Write body
38+
local reader = response.body_reader
39+
repeat
40+
local chunk, read_err
41+
42+
chunk, read_err = reader(chunksize)
43+
if read_err then
44+
return nil, "failed to read response body, err: " .. (err or "unknown")
45+
end
46+
47+
if chunk then
48+
bytes, err = send(sock, chunk)
49+
if not bytes then
50+
return nil, "failed to send response body, err: " .. (err or "unknown")
51+
end
52+
end
53+
until not chunk
54+
55+
return true, nil
56+
end
57+
58+
return _M

0 commit comments

Comments
 (0)