Repository: testcontainers/testcontainers-go (Go)

Issue:
[Enhancement]: wait.ForHTTP should default to examining mapped ports instead of port 80
### Proposal

Currently, `wait.ForHTTP` defaults to port 80

https://github.com/testcontainers/testcontainers-go/blob/main/wait/http.go#L44

This causes a lot of confusion if leaving out `WithPort` to specify a correct port, with a "can't find port" error that isn't very explanatory, and I think happens in many cases since a port of 80 is often not exposed in normal containers.

It would be great if it instead inspected the mapped ports to do the check when an explicit one is not satisfied. I believe this is how Java behaves.

Alternatively, I would have `ForHttp` accept two parameters, the path and port, making port a required parameter rather than hoping port 80 may work, I think it's quite rare.


Discussion: Hi @anuraaga thanks for opening this report. Indeed, having that default could be misleading to consumers just using the defaults of the wait strategy, for that reason the API provides a way to set the port to whatever the consumer needs, as you already pointed out.

> I think it's quite rare.

On the other hand, defaulting to port 80 could be something kind-of expected for a HTTP strategy, being port 80 the default port for HTTP traffic 🤷  Please also consider that line has been there for 4 years, and sometimes the maintenance work could not detect that. That's why I'm glad you found room for improvement in that sense.

> It would be great if it instead inspected the mapped ports to do the check when an explicit one is not satisfied. I believe this is how Java behaves.

At the same time, I'd like to ask if you would like to participate in the potential enhancement: we do have a `Ports` method at the DockerContainer struct, which retrieves the exposed ports from the `inspect.NetworkSettings.Ports` Docker type. I think it would be easy to loop through that struct and get what you proposed. Please let me know whether to reserve bandwidth or not to it.
Thanks @mdelapenya - if the change to inspect ports makes sense I can send a PR.
> Thanks @mdelapenya - if the change to inspect ports makes sense I can send a PR.

Yes please! 🙏 Also add a link to the Java implementation in the PR, for reference



Fix (patch):
diff --git a/docs/features/wait/http.md b/docs/features/wait/http.md
index 9d91d37701..81a42df292 100644
--- a/docs/features/wait/http.md
+++ b/docs/features/wait/http.md
@@ -2,7 +2,7 @@
 
 The HTTP wait strategy will check the result of an HTTP(S) request against the container and allows to set the following conditions:
 
-- the port to be used.
+- the port to be used. If no port is passed, it will use the first exposed port in the image.
 - the path to be used.
 - the HTTP method to be used.
 - the HTTP request body to be sent.
@@ -18,7 +18,13 @@ Variations on the HTTP wait strategy are supported, including:
 ## Match an HTTP method
 
 <!--codeinclude-->
-[Waiting for an HTTP endpoint](../../../wait/http_test.go) inside_block:waitForHTTP
+[Waiting for an HTTP endpoint using image's default port](../../../wait/http_test.go) inside_block:waitForHTTPWithDefaultPort
+<!--/codeinclude-->
+
+## Match an HTTP method with Port
+
+<!--codeinclude-->
+[Waiting for an HTTP endpoint including port](../../../wait/http_test.go) inside_block:waitForHTTPWithPort
 <!--/codeinclude-->
 
 ## Match an HTTP method with Basic Auth
diff --git a/wait/http.go b/wait/http.go
index 83de9f28d3..3ebd4469b0 100644
--- a/wait/http.go
+++ b/wait/http.go
@@ -41,7 +41,7 @@ type HTTPStrategy struct {
 // NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200
 func NewHTTPStrategy(path string) *HTTPStrategy {
 	return &HTTPStrategy{
-		Port:              "80/tcp",
+		Port:              "",
 		Path:              path,
 		StatusCodeMatcher: defaultStatusCodeMatcher,
 		ResponseMatcher:   func(body io.Reader) bool { return true },
@@ -142,24 +142,52 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
 		return
 	}
 
-	var port nat.Port
-	port, err = target.MappedPort(ctx, ws.Port)
+	var mappedPort nat.Port
+	if ws.Port == "" {
+		ports, err := target.Ports(ctx)
+		for err != nil {
+			select {
+			case <-ctx.Done():
+				return fmt.Errorf("%s:%w", ctx.Err(), err)
+			case <-time.After(ws.PollInterval):
+				if err := checkTarget(ctx, target); err != nil {
+					return err
+				}
+
+				ports, err = target.Ports(ctx)
+			}
+		}
 
-	for port == "" {
-		select {
-		case <-ctx.Done():
-			return fmt.Errorf("%s:%w", ctx.Err(), err)
-		case <-time.After(ws.PollInterval):
-			if err := checkTarget(ctx, target); err != nil {
-				return err
+		for k, bindings := range ports {
+			if len(bindings) == 0 || k.Proto() != "tcp" {
+				continue
 			}
+			mappedPort, _ = nat.NewPort(k.Proto(), bindings[0].HostPort)
+			break
+		}
 
-			port, err = target.MappedPort(ctx, ws.Port)
+		if mappedPort == "" {
+			return errors.New("No exposed tcp ports or mapped ports - cannot wait for status")
+		}
+	} else {
+		mappedPort, err = target.MappedPort(ctx, ws.Port)
+
+		for mappedPort == "" {
+			select {
+			case <-ctx.Done():
+				return fmt.Errorf("%s:%w", ctx.Err(), err)
+			case <-time.After(ws.PollInterval):
+				if err := checkTarget(ctx, target); err != nil {
+					return err
+				}
+
+				mappedPort, err = target.MappedPort(ctx, ws.Port)
+			}
 		}
-	}
 
-	if port.Proto() != "tcp" {
-		return errors.New("Cannot use HTTP client on non-TCP ports")
+		if mappedPort.Proto() != "tcp" {
+			return errors.New("Cannot use HTTP client on non-TCP ports")
+		}
 	}
 
 	switch ws.Method {
@@ -203,7 +231,7 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
 	}
 
 	client := http.Client{Transport: tripper, Timeout: time.Second}
-	address := net.JoinHostPort(ipAddress, strconv.Itoa(port.Int()))
+	address := net.JoinHostPort(ipAddress, strconv.Itoa(mappedPort.Int()))
 
 	endpoint := url.URL{
 		Scheme: proto,