# 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