Skip to content

Commit 01becde

Browse files
committed
perf(http_proxy): stream the response to avoid memory bloat
1 parent eb339c6 commit 01becde

File tree

6 files changed

+245
-45
lines changed

6 files changed

+245
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1010
### Fixed
1111
- Correct FAPI header to `x-fapi-interaction-id` [PR #1557](https://github.com/3scale/APIcast/pull/1557) [THREESCALE-11957](https://issues.redhat.com/browse/THREESCALE-11957)
1212
- Only validate oidc setting if authentication method is set to oidc [PR #1568](https://github.com/3scale/APIcast/pull/1568) [THREESCALE-11441](https://issues.redhat.com/browse/THREESCALE-11441)
13+
- Reduce memory consumption when returning large response that has been routed through a proxy server. [PR #1572](https://github.com/3scale/APIcast/pull/1572) [THREESCALE-12258](https://issues.redhat.com/browse/THREESCALE-12258)
1314

1415
### Added
1516
- Update APIcast schema manifest [PR #1550](https://github.com/3scale/APIcast/pull/1550)

gateway/src/apicast/http_proxy.lua

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ local file_reader = require("resty.file").file_reader
1313
local file_size = require("resty.file").file_size
1414
local client_body_reader = require("resty.http.request_reader").get_client_body_reader
1515
local send_response = require("resty.http.response_writer").send_response
16-
local concat = table.concat
16+
local proxy_response = require("resty.http.response_writer").proxy_response
1717

1818
local _M = { }
1919

@@ -161,13 +161,19 @@ local function forward_https_request(proxy_uri, uri, proxy_opts)
161161

162162
if res then
163163
if opts.request_unbuffered and raw then
164-
local bytes, err = send_response(sock, res, DEFAULT_CHUNKSIZE)
165-
if not bytes then
164+
err = send_response(sock, res, DEFAULT_CHUNKSIZE)
165+
if err then
166166
ngx.log(ngx.ERR, "failed to send response: ", err)
167-
return sock:send("HTTP/1.1 502 Bad Gateway")
167+
sock:close()
168+
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
168169
end
169170
else
170-
httpc:proxy_response(res)
171+
err = proxy_response(res, DEFAULT_CHUNKSIZE)
172+
if err then
173+
ngx.log(ngx.ERR, 'failed to proxy request to: ', proxy_uri, ' err : ', err)
174+
httpc:close()
175+
return
176+
end
171177
httpc:set_keepalive()
172178
end
173179
else

gateway/src/resty/http/response_writer.lua

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ local str_lower = string.lower
33
local insert = table.insert
44
local concat = table.concat
55

6+
local ngx = ngx
7+
68
local _M = {
79
}
810

@@ -31,6 +33,45 @@ local function send(socket, data)
3133
return socket:send(data)
3234
end
3335

36+
local function send_chunk(chunk)
37+
if not chunk then
38+
return nil
39+
end
40+
41+
local ok, err = ngx.print(chunk)
42+
if not ok then
43+
return "output response failed: " .. (err or "")
44+
end
45+
46+
return nil
47+
end
48+
49+
-- forward_body reads chunks from a body_reader and passes them to the callback
50+
-- function cb.
51+
-- cb(chunk) should return a true on success, or nil/false, err on failure.
52+
local function forward_body(reader, cb, chunksize)
53+
if not reader then
54+
return "no body reader"
55+
end
56+
57+
local buffer_size = chunksize or 65536
58+
59+
repeat
60+
local buffer, read_err, send_err
61+
buffer, read_err = reader(buffer_size)
62+
if read_err then
63+
return "failed to read response body: " .. read_err
64+
end
65+
66+
if buffer then
67+
send_err = cb(buffer)
68+
if send_err then
69+
return "failed to send response body: " .. (send_err or "unknown")
70+
end
71+
end
72+
until not buffer
73+
end
74+
3475
-- write_response writes response body reader to sock in the HTTP/1.x server response format,
3576
-- The connection is closed if send() fails or when returning a non-zero
3677
function _M.send_response(sock, response, chunksize)
@@ -42,7 +83,7 @@ function _M.send_response(sock, response, chunksize)
4283
end
4384

4485
if not sock then
45-
return nil, "socket not initialized yet"
86+
return "socket not initialized yet"
4687
end
4788

4889
-- Build status line + headers into a single buffer to minimize send() calls
@@ -62,32 +103,31 @@ function _M.send_response(sock, response, chunksize)
62103

63104
local bytes, err = sock:send(concat(buf))
64105
if not bytes then
65-
return nil, "failed to send headers, err: " .. (err or "unknown")
66-
end
67-
68-
-- Write body
69-
local reader = response.body_reader
70-
if not reader then
71-
return nil, "no body reader"
106+
return "failed to send headers, err: " .. (err or "unknown")
72107
end
73108

74-
repeat
75-
local chunk, read_err
76-
77-
chunk, read_err = reader(chunksize)
78-
if read_err then
79-
return nil, "failed to read response body, err: " .. (err or "unknown")
109+
return forward_body(response.body_reader, function(chunk)
110+
bytes, err = send(sock, chunk)
111+
if not bytes then
112+
return "failed to send response body, err: " .. (err or "unknown")
80113
end
114+
end, chunksize)
115+
end
81116

82-
if chunk then
83-
bytes, err = send(sock, chunk)
84-
if not bytes then
85-
return nil, "failed to send response body, err: " .. (err or "unknown")
86-
end
87-
end
88-
until not chunk
117+
function _M.proxy_response(res, chunksize)
118+
if not res then
119+
ngx.log(ngx.ERR, "no response provided")
120+
return
121+
end
122+
123+
ngx.status = res.status
124+
for k, v in pairs(res.headers) do
125+
if not HOP_BY_HOP_HEADERS[str_lower(k)] then
126+
ngx.header[k] = v
127+
end
128+
end
89129

90-
return true, nil
130+
return forward_body(res.body_reader, send_chunk, chunksize)
91131
end
92132

93133
return _M

spec/http_proxy_spec.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ describe('http_proxy', function()
2020

2121
local resty_http_proxy = require 'resty.http.proxy'
2222
stub(resty_http_proxy, 'new', function() return httpc end)
23+
local http_writer = require 'resty.http.response_writer'
24+
stub(http_writer, 'proxy_response')
2325
end
2426

2527
before_each(function()

0 commit comments

Comments
 (0)