Skip to content

Commit 0bfc146

Browse files
committed
Return a tunnel timeout error when the CONNECT deadline elapses
receive_response/3 recomputes the remaining timeout from the deadline on every loop iteration and feeds it straight into the receive's after clause. A proxy that keeps dribbling bytes of the CONNECT response until the deadline passes mid-loop produces a negative timeout, which raises ErlangError :timeout_value in the calling process instead of returning {:error, %Mint.HTTPError{reason: {:proxy, :tunnel_timeout}}}. Clamp the timeout to 0 so an elapsed deadline yields the documented error. Also document the :tunnel_timeout option in the Proxying section; it was previously only mentioned in the Mint.HTTPError docs.
1 parent 16d6b56 commit 0bfc146

3 files changed

Lines changed: 47 additions & 1 deletion

File tree

lib/mint/http.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,15 @@ defmodule Mint.HTTP do
287287
uses TLS and the TLS session to the host is nested inside it. *HTTPS proxies
288288
for HTTPS connections are available since v1.10.0*.
289289
290+
The `opts` in the `:proxy` tuple are the options of the connection to the proxy
291+
itself and support the same options as `connect/4`. Tunnel proxies additionally
292+
support:
293+
294+
* `:tunnel_timeout` - the maximum time (in milliseconds) to wait for the proxy
295+
to reply to the `CONNECT` request. If the tunnel is not established within
296+
this time, `connect/4` returns a `Mint.HTTPError` with reason
297+
`{:proxy, :tunnel_timeout}`. Defaults to `30_000` (30 seconds).
298+
290299
## Transport options
291300
292301
The options specified in `:transport_opts` are passed to the module that

lib/mint/tunnel_proxy.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ defmodule Mint.TunnelProxy do
5656
end
5757

5858
defp receive_response(conn, ref, timeout_deadline) do
59-
timeout = timeout_deadline - System.monotonic_time(:millisecond)
59+
timeout = max(timeout_deadline - System.monotonic_time(:millisecond), 0)
6060
socket = HTTP1.get_socket(conn)
6161

6262
receive do

test/mint/tunnel_proxy_connect_test.exs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ defmodule Mint.TunnelProxyConnectTest do
3737
assert merge_body(rest, request) == "hello"
3838
end
3939

40+
test "a proxy that stalls the CONNECT response yields a tunnel timeout error" do
41+
proxy_port = start_silent_proxy()
42+
43+
assert {:error, %Mint.HTTPError{reason: {:proxy, :tunnel_timeout}}} =
44+
HTTP.connect(:https, "localhost", 443,
45+
proxy: {:http, "localhost", proxy_port, [tunnel_timeout: 50]}
46+
)
47+
end
48+
49+
test "an already-elapsed tunnel deadline yields a tunnel timeout error instead of crashing" do
50+
proxy_port = start_silent_proxy()
51+
52+
assert {:error, %Mint.HTTPError{reason: {:proxy, :tunnel_timeout}}} =
53+
HTTP.connect(:https, "localhost", 443,
54+
proxy: {:http, "localhost", proxy_port, [tunnel_timeout: -1]}
55+
)
56+
end
57+
4058
test "IPv6 address targets produce a bracketed CONNECT authority" do
4159
{proxy_port, proxy_ref} = start_capturing_proxy()
4260

@@ -82,6 +100,25 @@ defmodule Mint.TunnelProxyConnectTest do
82100
{port, ref}
83101
end
84102

103+
# Starts a one-shot proxy that accepts a single connection and never
104+
# responds.
105+
defp start_silent_proxy do
106+
{:ok, listen_socket} =
107+
:gen_tcp.listen(0, mode: :binary, packet: :raw, active: false, reuseaddr: true)
108+
109+
{:ok, port} = :inet.port(listen_socket)
110+
111+
spawn_link(fn ->
112+
{:ok, socket} = :gen_tcp.accept(listen_socket)
113+
114+
receive do
115+
:stop -> :gen_tcp.close(socket)
116+
end
117+
end)
118+
119+
port
120+
end
121+
85122
# Starts a one-shot server that accepts a single connection, reports the raw
86123
# request head to the test process, and closes the connection.
87124
defp start_capturing_proxy do

0 commit comments

Comments
 (0)