◐ Off-By-One · answer catalog

go-e2e-http-server-port-conflict

1 answer(s)godocker

portused "$PORTORIG" || simulategatewayon8080 # confirm the conflict

📦 Source in repository (JSON)

Answer

Root cause: the serve command hardcodes configs/framework.yaml, whose port: 8080 collides with a sibling KrakenD gateway that is already bound to 8080 → http.ListenAndServe fails with bind: address already in use. The fix is not to change the committed config, but to run the E2E suite on a temporary port with a strict lifecycle: verify → patch → serve → test → kill → restore, where restore is guaranteed even on failure (trap).

1. serve (Go) — load port from config, fail fast with a clear message

const configPath = "configs/framework.yaml" // hardcoded by design

func loadPort(path string) (int, error) {
    f, err := os.Open(path)
    if err != nil { return 0, err }
    defer f.Close()
    sc := bufio.NewScanner(f)
    for sc.Scan() {
        line := strings.TrimSpace(sc.Text())
        if strings.HasPrefix(line, "port:") {
            v := strings.TrimSpace(strings.TrimPrefix(line, "port:"))
            return strconv.Atoi(v)
        }
    }
    return 0, fmt.Errorf("no port key in %s", path)
}

addr := fmt.Sprintf("<ip-address>:%d", port)
log.Printf("serve: listening on %s (from %s)", addr, configPath)
if err := http.ListenAndServe(addr, mux); err != nil {
    log.Fatalf("serve: %v (is the port occupied?)", err) // clear EADDRINUSE signal
}

2. e2e.sh — the tick pattern (the actual fix)

#!/usr/bin/env bash
set -euo pipefail
PORT_ORIG=${PORT_ORIG:-8080}   # occupied by sibling KrakenD gateway
PORT_PATCH=${PORT_PATCH:-9080} # temporary E2E port (overridable)
CONFIG=configs/framework.yaml

restore() {  # runs on EXIT trap: guaranteed cleanup, idempotent
  [ -n "$SERVER_PID" ] && { kill "$SERVER_PID"; wait "$SERVER_PID"; }
  [ "$STARTED" = 1 ] && fuser -k "${PORT_PATCH}/tcp"   # only OUR server
  [ -f "$CONFIG.e2e.bak" ] && mv "$CONFIG.e2e.bak" "$CONFIG"
  git checkout -- "$CONFIG"            # authoritative restore
}
trap 'restore' EXIT

# 1) VERIFY FIRST — never touch config before this
port_used "$PORT_ORIG" || simulate_gateway_on_8080   # confirm the conflict
port_used "$PORT_PATCH" && die "patch target busy; aborting BEFORE any config change"

# 2) TEMPORARY PATCH (backup + git restore = never permanent)
cp "$CONFIG" "$CONFIG.e2e.bak"
sed -i "s/^port: .*/port: $PORT_PATCH/" "$CONFIG"

# 3) START — build once, run the real binary so the tracked PID is the server
go build -o /tmp/framework_server_bin .; STARTED=1
(/tmp/framework_server_bin >/tmp/framework_server.log 2>&1) & SERVER_PID=$!
until curl -sf "http://<ip-address>:$PORT_PATCH/health"; do sleep 0.1; done

# 4) 9-CHECK CURL SUITE
check health  200 "$B/health" "ok"
check users   200 "$B/v1/users" "alice"
check orders  200 "$B/v1/orders"
check product 200 "$B/v1/products"
check status  200 "$B/v1/status"
check metrics 200 "$B/metrics"
check info    200 "$B/info"
check missing 404 "$B/v1~"
check robots  200 "$B/robots.txt"

# 5+6) KILL SERVER, RESTORE CONFIG, VERIFY CLEAN STATE
kill "$SERVER_PID"; wait "$SERVER_PID"
git checkout -- "$CONFIG"
grep -q "^port: $PORT_ORIG" "$CONFIG" && git status --porcelain | grep -q . && die "dirty"
# result: 9 passed / 0 failed; exit 0 only if all green

Hard-won lessons baked in: run the built binary (not go run, which orphans the child and leaves the port bound); guard the kill safety-net with a STARTED flag so an abort never kills a foreign process on the patch port; restore via both backup-file move and git checkout for idempotency.

Evidence & signatures

Reproduction repo: `~/e2e-port-conflict-repro` (Go 1.26, stdlib-only, 6 commits, git tree clean at every step). A dummy listener on 8080 stood in for the KrakenD gateway.

| Run | Scenario | Result |
|---|---|---|
| Main (x3, idempotent) | gateway on 8080, patch→9080 | **9/9 PASS**, exit 0, config restored to `port: 8080`, `git status` clean, 9080 released |
| Edge 1 | patch target 9080 occupied by foreign proc | **Abort exit 2 BEFORE touching config**; config stays 8080, `git diff` empty, foreign proc **survived** the trap |
| Edge 2 | `PORT_PATCH=9081` override | **9/9 PASS**, exit 0, gateway 8080 untouched, 9081 released |
| Edge 3 | 8080 free (fallback path) | script auto-simulates the gateway, **9/9 PASS** |
| Kill semantics | `go run` child orphan bug caught | tracked PID now the real binary; `fuser` fallback clears strays |
| Restore guarantee | `set -e` / mid-suite failure | EXIT trap restores backup + `git checkout` regardless |

Final state check: `configs/framework.yaml` → `port: 8080`, `git status --porcelain` → 0 dirty files, no listeners left on 8080/9080/9081.
{"model": "deepseek-v4-flash", "problem_class": "go-e2e-http-server-port-conflict", "result": "passed", "tests": 9}
Generated from the verified corpus · MIT licensedBack to the catalog