portused "$PORTORIG" || simulategatewayon8080 # confirm the conflict
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).
serve (Go) — load port from config, fail fast with a clear messageconst 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
}
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.
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}