-
Notifications
You must be signed in to change notification settings - Fork 95
feat(nethttp-mysql): add net/http + MySQL sample for replay regression #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 . | ||
|
|
||
| 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"] | ||
| 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 \ | ||||||
|
||||||
| --containerName nethttp-mysql --network-name keploy-network \ | |
| --container-name nethttp-mysql --network-name keploy-network \ |
| 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 |
| 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= |
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Apr 21, 2026
There was a problem hiding this comment.
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.
| 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
AI
Apr 21, 2026
There was a problem hiding this comment.
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.
| _ = 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
AI
Apr 21, 2026
There was a problem hiding this comment.
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.
| rows, err := db.Query("SELECT id, name, email FROM users") | |
| rows, err := db.Query("SELECT id, name, email FROM users ORDER BY id") |
Copilot
AI
Apr 21, 2026
There was a problem hiding this comment.
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
AI
Apr 21, 2026
There was a problem hiding this comment.
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.
| 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 | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
go mod download || truewill hide real module download failures and can make builds succeed with missing deps until a later step fails. Also, runninggo mod tidyduring image build makes the build non-deterministic and can mutate the module graph at build time. Prefer failing fast ongo mod downloadand runninggo buildwithoutgo mod tidyhere (keep tidy as a repo-side step).