1. /json client IP (codex #1, Medium): HandleJson now validates
c.ClientIP() and returns 400 on a malformed value instead of letting
Query fail into a 500. HandleIpInfo gained a selfLookup flag so a valid
but DB-absent self-IP preserves the 9d07639 graceful 200 {"ip": ip}
default (private/loopback addresses are never in a public GeoIP DB),
while the explicit /:ip route still returns 404 on not-found.
2. updated_at=0 on initial load (codex #2, Low): stampDbUpdatedAt() now
records the canonical mmdb's mtime both on initial load (existing
files) and after each successful update, so /metrics never reports 0
while serving real data.
3. Merge corruption -> permanent outage: mmdbmeld.WriteMMDB truncates the
canonical mmdb before writing; a failed merge previously left it empty
and the saved etag made the next daily run 304-skip the retry, so a
restart failed to open the file and the service went 503 permanently.
fetchDbs now backs up the canonical file before merge, restores it on
merge/reload failure, and drops the etags so the next run retries.
4. Temp-file leaks: Reload removes the temp clone and restores d.dbFile
when reader.Open fails (it previously assigned d.dbFile before Open);
cloneDBFile removes the temp on a failed copy.
Verified: go build, go vet, gofmt -l, go test -race ./... all pass.
Co-Authored-By: Claude <noreply@anthropic.com>
Concurrency & correctness:
- #10: guard IpDb Query/IsLoaded with RLock so Reload() (which reassigns d.r
and closes the old reader) cannot race with concurrent lookups. Confirmed
via -race regression test.
- #12: replace DbUpdatedAt time.Time with atomic.Int64 (cron writes,
/metrics reads) to fix the read/write data race.
- #11: graceful HTTP shutdown (srv.Shutdown with 10s timeout) instead of
srv.Close() aborting in-flight requests.
- #14: stop swallowing mergeMmdb() errors in fetchDbs() — keep the previous
DB when a merge fails instead of reloading a possibly-empty output.
- #13: add resty timeout (2m) + retry (x2) to download() so a hung CDN can't
stall the daily cron forever.
- #15: correct HTTP status codes (503 db loading, 404 not found via new
ErrNotFound sentinel, 500 otherwise) instead of 200 on query error.
Robustness:
- #16: surface osx.Copy dstFile.Close() errors (flush may fail) via named
return + defer.
Dependency migration:
- #17: migrate maxminddb-golang v1 -> v2. v2 is a breaking API
(LookupNetwork -> Lookup returning Result, netip.Addr), so Query was
rewritten; v1 dropped from go.mod.
Tests:
- #21: add internal/data unit tests (valid/invalid/not-found lookup) plus a
concurrent Query/Reload race regression test. chdir to repo root in TestMain
because data helpers use relative paths.
Cleanup:
- #18: README Go version 1.22 -> 1.25.
- #19: replace stray fmt.Printf with zerolog in ipdb.go.
- #20: .env.example API_PORT 28080 -> 8080 (container port, matches
docker-compose 28080:8080 mapping) with an explanatory comment.
Closes#10, #11, #12, #13, #14, #15, #16, #17, #18, #19, #20, #21
Co-Authored-By: Claude <noreply@anthropic.com>