Skip to content

Commit 8401944

Browse files
committed
Fix 'Transfer-Encoding: chunked' issue when sending request via proxy
When a request with the HTTP "Transfer-Encoding: chunked" header is sent, APIcast buffers the entire request because by default it does not support sending chunked requests. However, when sending via proxy, APIcast does not remove the header sent in the initial request, which tells the server that the client is sending a chunk request. This then causes an Bad Request error because the upstream will not be able to determine the end of the chunk from the request. This commit removes the "Transfer-Encoding: chunked" header from the request when sending through a proxy.
1 parent 526da16 commit 8401944

File tree

5 files changed

+984
-23
lines changed

5 files changed

+984
-23
lines changed

gateway/src/apicast/http_proxy.lua

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
local format = string.format
2+
local tostring = tostring
23

34
local resty_url = require "resty.url"
45
local resty_resolver = require 'resty.resolver'
56
local round_robin = require 'resty.balancer.round_robin'
67
local http_proxy = require 'resty.http.proxy'
78
local file_reader = require("resty.file").file_reader
9+
local file_size = require("resty.file").file_size
810
local concat = table.concat
911

1012
local _M = { }
@@ -86,40 +88,60 @@ local function forward_https_request(proxy_uri, proxy_auth, uri, skip_https_conn
8688
-- This is needed to call ngx.req.get_body_data() below.
8789
ngx.req.read_body()
8890

89-
local request = {
90-
uri = uri,
91-
method = ngx.req.get_method(),
92-
headers = ngx.req.get_headers(0, true),
93-
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
94-
95-
-- We cannot use resty.http's .get_client_body_reader().
96-
-- In POST requests with HTTPS, the result of that call is nil, and it
97-
-- results in a time-out.
98-
--
99-
--
100-
-- If ngx.req.get_body_data is nil, can be that the body is too big to
101-
-- read and need to be cached in a local file. This request will return
102-
-- nil, so after this we need to read the temp file.
103-
-- https://github.com/openresty/lua-nginx-module#ngxreqget_body_data
104-
body = ngx.req.get_body_data(),
105-
proxy_uri = proxy_uri,
106-
proxy_auth = proxy_auth
107-
}
108-
109-
if not request.body then
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 encoding = ngx.req.get_headers()["Transfer-Encoding"]
102+
103+
if not body then
110104
local temp_file_path = ngx.req.get_body_file()
111105
ngx.log(ngx.INFO, "HTTPS Proxy: Request body is bigger than client_body_buffer_size, read the content from path='", temp_file_path, "'")
112106

113107
if temp_file_path then
114-
local body, err = file_reader(temp_file_path)
108+
body, err = file_reader(temp_file_path)
115109
if err then
116110
ngx.log(ngx.ERR, "HTTPS proxy: Failed to read temp body file, err: ", err)
117111
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
118112
end
119-
request.body = body
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)
122+
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
123+
end
124+
125+
ngx.req.set_header("Content-Length", tostring(contentLength))
126+
end
120127
end
121128
end
122129

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)
133+
end
134+
135+
local request = {
136+
uri = uri,
137+
method = ngx.req.get_method(),
138+
headers = ngx.req.get_headers(0, true),
139+
path = format('%s%s%s', ngx.var.uri, ngx.var.is_args, ngx.var.query_string or ''),
140+
body = body,
141+
proxy_uri = proxy_uri,
142+
proxy_auth = proxy_auth
143+
}
144+
123145
local httpc, err = http_proxy.new(request, skip_https_connect)
124146

125147
if not httpc then

gateway/src/resty/file.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,20 @@ function _M.file_reader(filename)
2828
end)
2929
end
3030

31+
function _M.file_size(filename)
32+
local handle, err = open(filename)
33+
34+
if err then
35+
return nil, err
36+
end
37+
38+
local current = handle:seek()
39+
local size = handle:seek("end")
40+
41+
handle:seek("set", current)
42+
handle:close()
43+
44+
return size
45+
end
46+
3147
return _M

t/apicast-policy-camel.t

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,3 +506,239 @@ using proxy: http://foo:bar\@127.0.0.1:$Test::Nginx::Util::PROXY_SSL_PORT,
506506
EOF
507507
--- no_error_log eval
508508
[qr/\[error\]/, qr/\got header line: Proxy-Authorization: Basic Zm9vOmJhcg==/]
509+
510+
511+
512+
=== TEST 8: API backend connection uses http proxy with chunked request
513+
--- configuration
514+
{
515+
"services": [
516+
{
517+
"id": 42,
518+
"backend_version": 1,
519+
"backend_authentication_type": "service_token",
520+
"backend_authentication_value": "token-value",
521+
"proxy": {
522+
"api_backend": "http://test-upstream.lvh.me:$TEST_NGINX_SERVER_PORT/",
523+
"proxy_rules": [
524+
{ "pattern": "/", "http_method": "POST", "metric_system_name": "hits", "delta": 2 }
525+
],
526+
"policy_chain": [
527+
{
528+
"name": "apicast.policy.apicast"
529+
},
530+
{
531+
"name": "apicast.policy.camel",
532+
"configuration": {
533+
"http_proxy": "$TEST_NGINX_HTTP_PROXY"
534+
}
535+
}
536+
]
537+
}
538+
}
539+
]
540+
}
541+
--- backend
542+
location /transactions/authrep.xml {
543+
content_by_lua_block {
544+
ngx.exit(ngx.OK)
545+
}
546+
}
547+
--- upstream
548+
server_name test-upstream.lvh.me;
549+
location / {
550+
access_by_lua_block {
551+
assert = require('luassert')
552+
local content_length = ngx.req.get_headers()["Content-Length"]
553+
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
554+
assert.equal('12', content_length)
555+
assert.falsy(encoding)
556+
}
557+
echo_read_request_body;
558+
echo $request_body;
559+
}
560+
--- more_headers
561+
Transfer-Encoding: chunked
562+
--- request eval
563+
"POST /?user_key=value
564+
7\r
565+
hello, \r
566+
5\r
567+
world\r
568+
0\r
569+
\r
570+
"
571+
--- response_body
572+
hello, world
573+
--- error_code: 200
574+
--- error_log env
575+
using proxy: $TEST_NGINX_HTTP_PROXY
576+
--- no_error_log
577+
[error]
578+
579+
580+
581+
=== TEST 9: API backend using all_proxy with chunked request
582+
--- configuration
583+
{
584+
"services": [
585+
{
586+
"id": 42,
587+
"backend_version": 1,
588+
"backend_authentication_type": "service_token",
589+
"backend_authentication_value": "token-value",
590+
"proxy": {
591+
"api_backend": "http://test-upstream.lvh.me:$TEST_NGINX_SERVER_PORT/",
592+
"proxy_rules": [
593+
{ "pattern": "/", "http_method": "POST", "metric_system_name": "hits", "delta": 2 }
594+
],
595+
"policy_chain": [
596+
{
597+
"name": "apicast.policy.apicast"
598+
},
599+
{
600+
"name": "apicast.policy.http_proxy",
601+
"configuration": {
602+
"all_proxy": "$TEST_NGINX_HTTP_PROXY"
603+
}
604+
}
605+
]
606+
}
607+
}
608+
]
609+
}
610+
--- backend
611+
location /transactions/authrep.xml {
612+
content_by_lua_block {
613+
local expected = "service_token=token-value&service_id=42&usage%5Bhits%5D=2&user_key=value"
614+
require('luassert').same(ngx.decode_args(expected), ngx.req.get_uri_args(0))
615+
}
616+
}
617+
--- upstream
618+
server_name test-upstream.lvh.me;
619+
location / {
620+
access_by_lua_block {
621+
assert = require('luassert')
622+
local content_length = ngx.req.get_headers()["Content-Length"]
623+
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
624+
assert.equal('12', content_length)
625+
assert.falsy(encoding)
626+
}
627+
echo_read_request_body;
628+
echo $request_body;
629+
}
630+
--- more_headers
631+
Transfer-Encoding: chunked
632+
--- request eval
633+
"POST /?user_key=value
634+
7\r
635+
hello, \r
636+
5\r
637+
world\r
638+
0\r
639+
\r
640+
"
641+
--- response_body
642+
hello, world
643+
--- error_code: 200
644+
--- error_log env
645+
using proxy: $TEST_NGINX_HTTP_PROXY
646+
--- no_error_log
647+
[error]
648+
649+
650+
651+
=== TEST 10: using HTTPS proxy for backend with chunked request
652+
--- init eval
653+
$Test::Nginx::Util::PROXY_SSL_PORT = Test::APIcast::get_random_port();
654+
$Test::Nginx::Util::ENDPOINT_SSL_PORT = Test::APIcast::get_random_port();
655+
--- configuration random_port env eval
656+
<<EOF
657+
{
658+
"services": [
659+
{
660+
"backend_version": 1,
661+
"proxy": {
662+
"api_backend": "https://test-upstream.lvh.me:$Test::Nginx::Util::ENDPOINT_SSL_PORT",
663+
"proxy_rules": [
664+
{ "pattern": "/", "http_method": "POST", "metric_system_name": "hits", "delta": 2 }
665+
],
666+
"policy_chain": [
667+
{
668+
"name": "apicast.policy.apicast"
669+
},
670+
{
671+
"name": "apicast.policy.camel",
672+
"configuration": {
673+
"https_proxy": "http://127.0.0.1:$Test::Nginx::Util::PROXY_SSL_PORT"
674+
}
675+
}
676+
]
677+
}
678+
}
679+
]
680+
}
681+
EOF
682+
--- backend
683+
location /transactions/authrep.xml {
684+
content_by_lua_block {
685+
ngx.exit(ngx.OK)
686+
}
687+
}
688+
--- upstream eval
689+
<<EOF
690+
# Endpoint config
691+
server_name test-upstream.lvh.me;
692+
693+
listen $Test::Nginx::Util::ENDPOINT_SSL_PORT ssl;
694+
ssl_certificate $Test::Nginx::Util::ServRoot/html/server.crt;
695+
ssl_certificate_key $Test::Nginx::Util::ServRoot/html/server.key;
696+
697+
location / {
698+
access_by_lua_block {
699+
assert = require('luassert')
700+
local content_length = ngx.req.get_headers()["Content-Length"]
701+
local encoding = ngx.req.get_headers()["Transfer-Encoding"]
702+
assert.equal('12', content_length)
703+
assert.falsy(encoding)
704+
}
705+
echo_read_request_body;
706+
echo_request_body;
707+
}
708+
}
709+
server {
710+
# Proxy config
711+
listen $Test::Nginx::Util::PROXY_SSL_PORT ssl;
712+
713+
ssl_certificate $Test::Nginx::Util::ServRoot/html/server.crt;
714+
ssl_certificate_key $Test::Nginx::Util::ServRoot/html/server.key;
715+
716+
717+
server_name _ default_server;
718+
719+
location ~ /.* {
720+
proxy_http_version 1.1;
721+
proxy_pass https://\$http_host;
722+
}
723+
EOF
724+
--- more_headers
725+
Transfer-Encoding: chunked
726+
--- request eval
727+
"POST /?user_key=value
728+
7\r
729+
hello, \r
730+
5\r
731+
world\r
732+
0\r
733+
\r
734+
"
735+
--- response_body chomp
736+
hello, world
737+
--- error_code: 200
738+
--- error_log eval
739+
<<EOF
740+
using proxy: http://127.0.0.1:$Test::Nginx::Util::PROXY_SSL_PORT,
741+
EOF
742+
--- no_error_log
743+
[error]
744+
--- user_files fixture=tls.pl eval

0 commit comments

Comments
 (0)