Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions nethttp-mysql/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM golang:1.22 AS build
WORKDIR /app
COPY go.mod go.sum* ./
RUN go mod download || true
COPY . .
RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o app .
Comment on lines +4 to +6
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go mod download || true will hide real module download failures and can make builds succeed with missing deps until a later step fails. Also, running go mod tidy during image build makes the build non-deterministic and can mutate the module graph at build time. Prefer failing fast on go mod download and running go build without go mod tidy here (keep tidy as a repo-side step).

Suggested change
RUN go mod download || true
COPY . .
RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o app .
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app .

Copilot uses AI. Check for mistakes.

FROM alpine:3.19
WORKDIR /app
RUN apk add --no-cache ca-certificates
COPY --from=build /app/app /app/app
EXPOSE 8080
ENTRYPOINT ["/app/app"]
56 changes: 56 additions & 0 deletions nethttp-mysql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# nethttp-mysql

Minimal Go sample that uses `net/http` + `database/sql` with the MySQL
driver. The app exposes three endpoints (`/health`, `/users`,
`/users/add`) and connects to a standard MySQL 8 instance.

This sample is wired into Keploy's end-to-end pipeline as a regression
guard for the MySQL replay command-phase loop: in replay mode the app
must successfully complete `db.Ping()` (which sends commands to the
mocked MySQL connection) and bind `:8080`. If the proxy ever re-
introduces a zero/overflow read deadline in the MySQL command phase the
Go driver blocks inside `db.Ping()`, the HTTP listener never starts,
and every replayed request fails with `connection reset by peer` —
making the regression loud.

## Run locally

```sh
# Terminal 1
docker network create keploy-network
docker run -d --name mysql --network keploy-network \
-e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=testdb mysql:8.0

# Terminal 2
docker build -t nethttp-mysql .
docker run --rm -p 8080:8080 --network keploy-network \
-e MYSQL_HOST=mysql -e MYSQL_USER=root -e MYSQL_PASSWORD=password \
-e MYSQL_DATABASE=testdb nethttp-mysql
```

Then hit:

```sh
curl http://localhost:8080/health
curl http://localhost:8080/users
curl "http://localhost:8080/users/add?name=charlie&email=charlie@test.com"
```

## Use with Keploy

```sh
keploy record -c "docker run -p 8080:8080 --name nethttp-mysql --network keploy-network \
-e MYSQL_HOST=mysql -e MYSQL_USER=root -e MYSQL_PASSWORD=password \
-e MYSQL_DATABASE=testdb nethttp-mysql" \
--container-name nethttp-mysql --network-name keploy-network --buildDelay 60

# ... exercise the endpoints ...

# Stop MySQL, then replay against mocks:
docker rm -f mysql
keploy test -c "docker run -p 8080:8080 --name nethttp-mysql --network keploy-network \
-e MYSQL_HOST=mysql -e MYSQL_USER=root -e MYSQL_PASSWORD=password \
-e MYSQL_DATABASE=testdb nethttp-mysql" \
--containerName nethttp-mysql --network-name keploy-network \
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Keploy CLI flag here uses --containerName, but other samples in this repo consistently use --container-name (e.g., http-postgres/README.md:58, fasthttp-postgres/README.md:25). If --containerName isn't a valid alias, the command will fail. Please align this to --container-name for consistency and correctness.

Suggested change
--containerName nethttp-mysql --network-name keploy-network \
--container-name nethttp-mysql --network-name keploy-network \

Copilot uses AI. Check for mistakes.
--apiTimeout 60 --delay 20
```
7 changes: 7 additions & 0 deletions nethttp-mysql/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/keploy/samples-go/nethttp-mysql

go 1.22

require github.com/go-sql-driver/mysql v1.8.1

require filippo.io/edwards25519 v1.1.0 // indirect
4 changes: 4 additions & 0 deletions nethttp-mysql/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
127 changes: 127 additions & 0 deletions nethttp-mysql/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Minimal net/http + database/sql + MySQL sample used by Keploy's
// end-to-end pipeline. Exercises the full MySQL connection-and-command
// phase so regressions in the replay proxy (e.g. a zero read deadline
// in the command-phase loop) surface as the Go MySQL driver blocking
// inside db.Ping(), which prevents the HTTP server from binding.
package main

import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"

_ "github.com/go-sql-driver/mysql"
)

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

var db *sql.DB

func envDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

func main() {
host := envDefault("MYSQL_HOST", "mysql")
port := envDefault("MYSQL_PORT", "3306")
user := envDefault("MYSQL_USER", "root")
pass := envDefault("MYSQL_PASSWORD", "password")
dbname := envDefault("MYSQL_DATABASE", "testdb")

dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", user, pass, host, port, dbname)
log.Printf("Connecting to MySQL at %s:%s/%s", host, port, dbname)

var err error
for attempt := 1; attempt <= 30; attempt++ {
db, err = sql.Open("mysql", dsn)
if err == nil {
err = db.Ping()
if err == nil {
break
}
}
log.Printf("waiting for mysql (attempt %d): %v", attempt, err)
time.Sleep(2 * time.Second)
}
if err != nil {
log.Fatalf("could not connect to mysql after retries: %v", err)
Comment on lines +46 to +58
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the retry loop, a new *sql.DB handle is created on every attempt, but failed attempts aren't closed. If Ping() fails, the previous handle can keep underlying connections/resources around. Consider calling Close() before reassigning, or Open() once and just retry Ping() on the same handle.

Suggested change
for attempt := 1; attempt <= 30; attempt++ {
db, err = sql.Open("mysql", dsn)
if err == nil {
err = db.Ping()
if err == nil {
break
}
}
log.Printf("waiting for mysql (attempt %d): %v", attempt, err)
time.Sleep(2 * time.Second)
}
if err != nil {
log.Fatalf("could not connect to mysql after retries: %v", err)
db, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("could not create mysql handle: %v; verify the DSN and MySQL driver configuration, then restart the service", err)
}
defer db.Close()
for attempt := 1; attempt <= 30; attempt++ {
err = db.Ping()
if err == nil {
break
}
log.Printf("waiting for mysql (attempt %d): %v; ensure the MySQL container is running and reachable at %s:%s, then retry", attempt, err, host, port)
time.Sleep(2 * time.Second)
}
if err != nil {
log.Fatalf("could not connect to mysql after retries: %v; confirm MySQL is healthy and the connection settings are correct, then restart the service", err)

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fatal error message doesn't give a clear next step for debugging (e.g., which env vars/host were used, or suggesting to check the MySQL container/network). Consider including the target host:port/db and a suggested action (verify MySQL is running/reachable and credentials are correct) to make CI/local failures faster to diagnose.

Suggested change
log.Fatalf("could not connect to mysql after retries: %v", err)
log.Fatalf("could not connect to MySQL after retries (target=%s:%s/%s): %v; verify MySQL is running and reachable at that address and that MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE are correct", host, port, dbname, err)

Copilot uses AI. Check for mistakes.
}

if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
)`); err != nil {
log.Fatalf("failed to create table: %v", err)
}

var count int
_ = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of QueryRow(...).Scan(&count) is intentionally ignored. If this query fails (e.g., missing permissions/table issues), count stays 0 and the app will try to seed, potentially masking the root cause. Please handle the Scan error and fail fast (or at least log it) so startup is deterministic.

Suggested change
_ = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
log.Fatalf("failed to count existing users; verify the users table exists and the configured MySQL user has SELECT access: %v", err)
}

Copilot uses AI. Check for mistakes.
if count == 0 {
if _, err := db.Exec("INSERT INTO users (name,email) VALUES ('alice','alice@example.com'), ('bob','bob@example.com')"); err != nil {
log.Fatalf("failed to seed: %v", err)
}
}

http.HandleFunc("/users", getUsers)
http.HandleFunc("/users/add", addUser)
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if err := db.Ping(); err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
fmt.Fprintln(w, "ok")
})

log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

func getUsers(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, name, email FROM users")
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /users query has no ORDER BY, so row order is not guaranteed and can change as the table grows/changes. For a sample used in record/replay regression, this can introduce flaky diffs. Consider adding an explicit ordering (e.g., by id) to keep responses deterministic.

Suggested change
rows, err := db.Query("SELECT id, name, email FROM users")
rows, err := db.Query("SELECT id, name, email FROM users ORDER BY id")

Copilot uses AI. Check for mistakes.
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()

users := make([]User, 0)
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
users = append(users, u)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(users)
}
Comment on lines +107 to +110
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After iterating rows.Next(), rows.Err() is not checked, and JSON encoding errors are ignored. This can cause partial/empty responses to be returned as 200 OK even when iteration/encoding fails. Please check rows.Err() after the loop and handle/return any Encode error.

Copilot uses AI. Check for mistakes.

func addUser(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
email := r.URL.Query().Get("email")
if name == "" || email == "" {
http.Error(w, "name and email required", http.StatusBadRequest)
return
}
res, err := db.Exec("INSERT INTO users (name,email) VALUES (?,?)", name, email)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id, _ := res.LastInsertId()
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

res.LastInsertId() error is ignored. If the driver doesn't support it or it fails, the handler will silently return id=0. Please handle the error and return a 500 (or similar) if the insert ID can't be retrieved.

Suggested change
id, _ := res.LastInsertId()
id, err := res.LastInsertId()
if err != nil {
http.Error(w, "failed to retrieve the inserted user ID; please retry or verify the database driver supports LastInsertId", http.StatusInternalServerError)
return
}

Copilot uses AI. Check for mistakes.
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(User{ID: int(id), Name: name, Email: email})
}
Loading