Skip to content

Commit 831b440

Browse files
committed
fix: handle empty proxy environment variables
When proxy environment variables (http_proxy, https_proxy, etc.) are set to empty strings or whitespace-only values, hackney would crash with a pattern match error in hackney_url:parse_netloc/2. This fix: - Checks for empty strings in do_get_proxy_env/1 - Strips whitespace from proxy URLs and treats whitespace-only as empty - Adds comprehensive unit tests to prevent regression - Exports proxy functions for testing when compiled with -DTEST Fixes the issue where export http_proxy="" would cause hackney requests to fail.
1 parent 8c00789 commit 831b440

File tree

2 files changed

+150
-1
lines changed

2 files changed

+150
-1
lines changed

src/hackney.erl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
pause_stream/1,
3434
resume_stream/1]).
3535

36+
-ifdef(TEST).
37+
-export([get_proxy_env/1, do_get_proxy_env/1]).
38+
-endif.
39+
3640
-define(METHOD_TPL(Method),
3741
-export([Method/1, Method/2, Method/3, Method/4])).
3842
-include("hackney_methods.hrl").
@@ -830,7 +834,14 @@ get_proxy_env(S) when S =:= http; S =:= http_unix ->
830834
do_get_proxy_env([Var | Rest]) ->
831835
case os:getenv(Var) of
832836
false -> do_get_proxy_env(Rest);
833-
Url -> {ok, Url}
837+
"" -> do_get_proxy_env(Rest);
838+
Url ->
839+
%% Trim all whitespace (spaces, tabs, newlines, etc.)
840+
TrimmedUrl = re:replace(Url, "^\\s+|\\s+$", "", [global, {return, list}]),
841+
case TrimmedUrl of
842+
"" -> do_get_proxy_env(Rest);
843+
_ -> {ok, TrimmedUrl}
844+
end
834845
end;
835846
do_get_proxy_env([]) ->
836847
false.

test/hackney_proxy_tests.erl

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
%%% -*- erlang -*-
2+
%%%
3+
%%% This file is part of hackney released under the Apache 2 license.
4+
%%% See the NOTICE for more information.
5+
%%%
6+
%%% Copyright (c) 2025 Benoît Chesneau <[email protected]>
7+
%%%
8+
9+
-module(hackney_proxy_tests).
10+
-include_lib("eunit/include/eunit.hrl").
11+
12+
%% Test empty proxy environment variables handling
13+
empty_proxy_env_test_() ->
14+
{setup,
15+
fun setup/0,
16+
fun teardown/1,
17+
fun(State) ->
18+
[
19+
{"Empty http_proxy", fun() -> test_empty_proxy_env("http_proxy", State) end},
20+
{"Empty HTTP_PROXY", fun() -> test_empty_proxy_env("HTTP_PROXY", State) end},
21+
{"Empty https_proxy", fun() -> test_empty_proxy_env("https_proxy", State) end},
22+
{"Empty HTTPS_PROXY", fun() -> test_empty_proxy_env("HTTPS_PROXY", State) end},
23+
{"Whitespace http_proxy", fun() -> test_whitespace_proxy_env("http_proxy", State) end},
24+
{"Whitespace HTTPS_PROXY", fun() -> test_whitespace_proxy_env("HTTPS_PROXY", State) end},
25+
{"Multiple spaces proxy", fun() -> test_multiple_spaces_proxy_env("http_proxy", State) end},
26+
{"Valid proxy URL", fun() -> test_valid_proxy_env("http_proxy", State) end},
27+
{"Valid proxy with spaces", fun() -> test_valid_proxy_with_spaces("HTTP_PROXY", State) end}
28+
]
29+
end}.
30+
31+
setup() ->
32+
%% Save current environment variables
33+
SavedEnv = [{Var, os:getenv(Var)} || Var <- ["http_proxy", "HTTP_PROXY",
34+
"https_proxy", "HTTPS_PROXY",
35+
"all_proxy", "ALL_PROXY"]],
36+
%% Clear all proxy environment variables
37+
[os:unsetenv(Var) || {Var, _} <- SavedEnv],
38+
%% Clear hackney's cached proxy settings
39+
application:unset_env(hackney, http_proxy),
40+
application:unset_env(hackney, https_proxy),
41+
SavedEnv.
42+
43+
teardown(SavedEnv) ->
44+
%% Restore original environment variables
45+
lists:foreach(fun
46+
({Var, false}) -> os:unsetenv(Var);
47+
({Var, Value}) -> os:putenv(Var, Value)
48+
end, SavedEnv),
49+
%% Clear hackney's cached proxy settings
50+
application:unset_env(hackney, http_proxy),
51+
application:unset_env(hackney, https_proxy).
52+
53+
test_empty_proxy_env(Var, _State) ->
54+
os:putenv(Var, ""),
55+
%% Clear cached values first
56+
application:unset_env(hackney, http_proxy),
57+
application:unset_env(hackney, https_proxy),
58+
%% This should not crash and should return false (no proxy)
59+
Result = hackney:get_proxy_env(http),
60+
?assertEqual(false, Result),
61+
os:unsetenv(Var).
62+
63+
test_whitespace_proxy_env(Var, _State) ->
64+
os:putenv(Var, " "),
65+
%% Clear cached values first
66+
application:unset_env(hackney, http_proxy),
67+
application:unset_env(hackney, https_proxy),
68+
%% This should not crash and should return false (no proxy)
69+
Result = hackney:get_proxy_env(http),
70+
?assertEqual(false, Result),
71+
os:unsetenv(Var).
72+
73+
test_multiple_spaces_proxy_env(Var, _State) ->
74+
os:putenv(Var, " \t \n "),
75+
%% Clear cached values first
76+
application:unset_env(hackney, http_proxy),
77+
application:unset_env(hackney, https_proxy),
78+
%% This should not crash and should return false (no proxy)
79+
Result = hackney:get_proxy_env(http),
80+
?assertEqual(false, Result),
81+
os:unsetenv(Var).
82+
83+
test_valid_proxy_env(Var, _State) ->
84+
ProxyUrl = "http://proxy.example.com:8080",
85+
os:putenv(Var, ProxyUrl),
86+
%% Clear cached values first
87+
application:unset_env(hackney, http_proxy),
88+
application:unset_env(hackney, https_proxy),
89+
%% This should return the proxy URL
90+
Result = hackney:get_proxy_env(http),
91+
?assertEqual({ok, ProxyUrl}, Result),
92+
os:unsetenv(Var).
93+
94+
test_valid_proxy_with_spaces(Var, _State) ->
95+
ProxyUrl = " http://proxy.example.com:8080 ",
96+
ExpectedUrl = "http://proxy.example.com:8080",
97+
os:putenv(Var, ProxyUrl),
98+
%% Clear cached values first
99+
application:unset_env(hackney, http_proxy),
100+
application:unset_env(hackney, https_proxy),
101+
%% This should return the proxy URL with stripped spaces
102+
Result = hackney:get_proxy_env(http),
103+
?assertEqual({ok, ExpectedUrl}, Result),
104+
os:unsetenv(Var).
105+
106+
%% Test that empty proxy doesn't cause crash in actual request
107+
integration_empty_proxy_test_() ->
108+
{setup,
109+
fun() ->
110+
setup(),
111+
{ok, _} = application:ensure_all_started(hackney)
112+
end,
113+
fun(State) ->
114+
teardown(State),
115+
application:stop(hackney)
116+
end,
117+
fun(_) ->
118+
[
119+
{"Request with empty http_proxy", fun test_request_with_empty_proxy/0},
120+
{"Request with whitespace https_proxy", fun test_request_with_whitespace_proxy/0}
121+
]
122+
end}.
123+
124+
test_request_with_empty_proxy() ->
125+
os:putenv("http_proxy", ""),
126+
%% This request should not crash
127+
{ok, StatusCode, _Headers, Ref} = hackney:request(get, "http://httpbin.org/status/200"),
128+
?assertEqual(200, StatusCode),
129+
hackney:close(Ref),
130+
os:unsetenv("http_proxy").
131+
132+
test_request_with_whitespace_proxy() ->
133+
os:putenv("https_proxy", " "),
134+
%% This request should not crash
135+
{ok, StatusCode, _Headers, Ref} = hackney:request(get, "https://httpbin.org/status/200"),
136+
?assertEqual(200, StatusCode),
137+
hackney:close(Ref),
138+
os:unsetenv("https_proxy").

0 commit comments

Comments
 (0)