# SPDX-License-Identifier: MIT
defmodule Testcontainers.WaitStrategy.HttpWaitStrategy do
@moduledoc """
Awaits a successful HTTP response from the container.
"""
@retry_delay 200
defstruct [:ip, :port, :path, :status_code, :timeout, retry_delay: @retry_delay]
@doc """
Creates a new HttpWaitStrategy to wait until a successful HTTP response is received from the container.
"""
def new(
ip,
port,
path \\ "/",
status_code \\ 200,
timeout \\ 5000,
retry_delay \\ @retry_delay
),
do: %__MODULE__{
ip: ip,
port: port,
path: path,
status_code: status_code,
timeout: timeout,
retry_delay: retry_delay
}
end
defimpl Testcontainers.WaitStrategy, for: Testcontainers.WaitStrategy.HttpWaitStrategy do
alias Testcontainers.Container
alias Testcontainers.Docker
require Logger
def wait_until_container_is_ready(wait_strategy, container_id) do
with {:ok, %Container{} = container} <- Docker.Api.get_container(container_id) do
host_port = Container.mapped_port(container, wait_strategy.port)
case wait_for_http(
container_id,
wait_strategy,
host_port,
current_time_millis()
) do
{:ok, :http_is_ready} ->
:ok
{:error, reason} ->
{:error, reason, wait_strategy}
end
end
end
defp wait_for_http(container_id, wait_strategy, host_port, start_time)
when is_integer(host_port) and is_integer(start_time) do
if wait_strategy.timeout + start_time < current_time_millis() do
{:error, strategy_timed_out(wait_strategy.timeout, start_time)}
else
case http_request(
wait_strategy.ip,
host_port,
wait_strategy.path,
wait_strategy.status_code
) do
{:ok, _response} ->
{:ok, :http_is_ready}
{:error, _reason} ->
delay = max(0, wait_strategy.retry_delay)
Logger.debug(
"Http endpoint #{"http://#{wait_strategy.ip}:#{host_port}#{wait_strategy.path}"} in container #{container_id} didnt respond with #{wait_strategy.status_code}, retrying in #{delay}ms."
)
:timer.sleep(delay)
wait_for_http(container_id, wait_strategy, host_port, start_time)
end
end
end
defp http_request(ip, port, path, expected_status_code) when is_integer(expected_status_code) do
url = "http://" <> ip <> ":" <> Integer.to_string(port) <> path
case :httpc.request(:get, {to_charlist(url), []}, [], []) do
{:ok, {{~c"HTTP/1.1", ^expected_status_code, _reason_phrase}, _headers, _body}} ->
{:ok, :http_ok}
{:ok, {{~c"HTTP/1.1", status_code, _reason_phrase}, _headers, _body}}
when status_code != expected_status_code ->
{:error, {:unexpected_status_code, status_code}}
{:error, reason} ->
{:error, reason}
end
end
defp current_time_millis, do: System.monotonic_time(:millisecond)
defp strategy_timed_out(timeout, started_at) when is_number(timeout) and is_number(started_at),
do: {:http_wait_strategy, :timeout, timeout, elapsed_time: current_time_millis() - started_at}
end