Skip to content

Commit 7037b7b

Browse files
authored
Merge pull request #1572 from tkan145/THREESCALE-12258
THREESCALE-12258 Stream response back when using proxy
2 parents e37125e + 90aa6cf commit 7037b7b

6 files changed

Lines changed: 495 additions & 45 deletions

File tree

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: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
local format = string.format
22
local tostring = tostring
3+
local ngx = ngx
34
local ngx_get_method = ngx.req.get_method
45
local ngx_http_version = ngx.req.http_version
6+
local ngx_req_get_headers = ngx.req.get_headers
57

68
local resty_url = require "resty.url"
79
local url_helper = require('resty.url_helper')
@@ -11,7 +13,7 @@ local file_reader = require("resty.file").file_reader
1113
local file_size = require("resty.file").file_size
1214
local client_body_reader = require("resty.http.request_reader").get_client_body_reader
1315
local send_response = require("resty.http.response_writer").send_response
14-
local concat = table.concat
16+
local proxy_response = require("resty.http.response_writer").proxy_response
1517

1618
local _M = { }
1719

@@ -49,9 +51,9 @@ local function forward_https_request(proxy_uri, uri, proxy_opts)
4951
local sock
5052
local opts = proxy_opts or {}
5153
local req_method = ngx_get_method()
52-
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
54+
local encoding = ngx_req_get_headers()["Transfer-Encoding"]
5355
local is_chunked = encoding and encoding:lower() == "chunked"
54-
local content_type = ngx.req.get_headers()["Content-Type"]
56+
local content_type = ngx_req_get_headers()["Content-Type"]
5557
local content_type_is_urlencoded = content_type and content_type:lower() == "application/x-www-form-urlencoded"
5658
local raw = false
5759

@@ -138,9 +140,9 @@ local function forward_https_request(proxy_uri, uri, proxy_opts)
138140

139141
local request = {
140142
uri = uri,
141-
method = ngx.req.get_method(),
142-
headers = ngx.req.get_headers(0, true),
143-
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
143+
method = req_method,
144+
headers = ngx_req_get_headers(0, true),
145+
path = (ngx.var.uri or '') .. (ngx.var.is_args or '') .. (ngx.var.query_string or ''),
144146
body = body,
145147
proxy_uri = proxy_uri,
146148
proxy_options = opts
@@ -159,13 +161,19 @@ local function forward_https_request(proxy_uri, uri, proxy_opts)
159161

160162
if res then
161163
if opts.request_unbuffered and raw then
162-
local bytes, err = send_response(sock, res, DEFAULT_CHUNKSIZE)
163-
if not bytes then
164+
err = send_response(sock, res, DEFAULT_CHUNKSIZE)
165+
if err then
164166
ngx.log(ngx.ERR, "failed to send response: ", err)
165-
return sock:send("HTTP/1.1 502 Bad Gateway")
167+
sock:close()
168+
return ngx.exit(ngx.HTTP_BAD_GATEWAY)
166169
end
167170
else
168-
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
169177
httpc:set_keepalive()
170178
end
171179
else
@@ -194,7 +202,7 @@ function _M.request(upstream, proxy_uri)
194202
local proxy_auth
195203

196204
if proxy_uri.user or proxy_uri.password then
197-
proxy_auth = "Basic " .. ngx.encode_base64(concat({ proxy_uri.user or '', proxy_uri.password or '' }, ':'))
205+
proxy_auth = "Basic " .. ngx.encode_base64((proxy_uri.user or '') .. ":" .. (proxy_uri.password or ''))
198206
end
199207

200208
if uri.scheme == 'http' then -- rewrite the request to use http_proxy
Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
local fmt = string.format
22
local str_lower = string.lower
3+
local insert = table.insert
4+
local concat = table.concat
5+
6+
local ngx = ngx
37

48
local _M = {
59
}
@@ -29,10 +33,48 @@ local function send(socket, data)
2933
return socket:send(data)
3034
end
3135

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+
3275
-- write_response writes response body reader to sock in the HTTP/1.x server response format,
3376
-- The connection is closed if send() fails or when returning a non-zero
3477
function _M.send_response(sock, response, chunksize)
35-
local bytes, err
3678
chunksize = chunksize or 65536
3779

3880
if not response then
@@ -41,53 +83,51 @@ function _M.send_response(sock, response, chunksize)
4183
end
4284

4385
if not sock then
44-
return nil, "socket not initialized yet"
86+
return "socket not initialized yet"
4587
end
4688

47-
-- Status line
48-
-- TODO: get HTTP version from request
49-
local status = fmt("HTTP/%d.%d %03d %s\r\n", 1, 1, response.status, response.reason)
50-
bytes, err = send(sock, status)
51-
if not bytes then
52-
return nil, "failed to send status line, err: " .. (err or "unknown")
53-
end
89+
-- Build status line + headers into a single buffer to minimize send() calls
90+
local buf = {
91+
fmt("HTTP/1.1 %03d %s\r\n", response.status, response.reason)
92+
}
5493

5594
-- Filter out hop-by-hop headeres
5695
for k, v in pairs(response.headers) do
5796
if not HOP_BY_HOP_HEADERS[str_lower(k)] then
58-
local header = fmt("%s: %s\r\n", k, v)
59-
bytes, err = sock:send(header)
60-
if not bytes then
61-
return nil, "failed to send status line, err: " .. (err or "unknown")
62-
end
97+
insert(buf, k .. ": " .. v .. cr_lf)
6398
end
6499
end
65100

66101
-- End-of-header
67-
bytes, err = send(sock, cr_lf)
102+
insert(buf, cr_lf)
103+
104+
local bytes, err = sock:send(concat(buf))
68105
if not bytes then
69-
return nil, "failed to send status line, err: " .. (err or "unknown")
106+
return "failed to send headers, err: " .. (err or "unknown")
70107
end
71108

72-
-- Write body
73-
local reader = response.body_reader
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)