Release v1.60.0
🚀 Features
🔹 Opt-In Trie Router
GOFR_ROUTER=trie replaces mux's linear route scan with a radix trie, handing the final match decision back to Route.Match. mux stays the registry, so mux.Vars, route templates and registration are unchanged.
| routes | mux | trie | speedup |
|---|---|---|---|
| 10 | 506 ns | 461 ns | 1.1x |
| 50 | 1007 ns | 550 ns | 1.8x |
| 100 | 1642 ns | 548 ns | 3.0x |
| 200 | 2918 ns | 515 ns | 5.7x |
Gains start around 5-10 routes and cost 2 extra allocations per request. Routes the trie cannot safely index, and every non-match, fall back to mux, so an indexing mistake costs latency rather than 404ing a live route. Default behavior is unchanged.
🔹 Text Embeddings for LLMs
GoFr's ai package now supports text embeddings. Embed is a method on the LLM, alongside Chat and Stream, with the same tracing and token metrics. Vectors are placed by the provider's reported index rather than by response position, so a backend answering out of order cannot pair an input with the wrong vector.
// embeddings are usually a different model from your chat one
app.AddLLM(&llm.Client{Provider: llm.OpenAI, Model: "text-embedding-3-small"}, gofr.WithName("embed"))
// in a handler
resp, err := ctx.LLM("embed").Embed(ctx, []string{"the quick brown fox"})
// resp.Embeddings[i] holds the vector for input[i]A chat-only model reports ai.ErrEmbedNotSupported and an unregistered one ai.ErrLLMNotConfigured, so a misconfiguration surfaces as an ordinary error rather than a panic.
🔹 LLM Concurrency Limiting
llm.Client.MaxConcurrentRequests caps in-flight provider requests, for local models or tight rate-limit tiers where concurrent handler calls queue up and inflate tail latency. Excess Chat, Embed and Stream calls block until a slot frees, honoring their context deadline. Health probes are now single-flight. The default of 0 is unlimited.
🔧 Enhancements
🔹 Reduced Per-Request Allocations
The request hot path now uses 8 fewer allocations and 225 fewer bytes per request (49 → 41 allocs, 3915 → 3690 B), measured through a real GoFr handler. Four changes, none of which alters output:
- Lazy Context Logger: the 32-character trace ID was formatted, a one-entry marker map allocated and the wrapper heap-allocated at construction — before knowing whether the handler would log at all. The trace ID is now formatted at log time, the marker is a typed string rather than a map, and the wrapper is returned by value. A request that logs nothing costs 0 B and 0 allocs, down from 432 B and 5.
- Gated Request Logs:
handleRequestLogbuilt the entry, formatted a timestamp and resolved the client IP before the logger decided whether to emit it. An optionalLogEnabled()lets the middleware ask first, saving 193 B and 4 allocs per request atLOG_LEVEL=NOTICEand above. The defaultINFOis unaffected, since the request log is written at INFO. - Co-Allocated Request Objects:
Context,RequestandResponderare created together and discarded together, so they now live in one struct instead of three separate heap objects. - Trace ID off the Hot Path:
handler.ServeHTTPresolved a 32-character trace ID on every request for a consumer that only runs when a handler returns an error. It is built on the error path now.
Alongside those, remoteLogger.LogEnabled no longer takes a per-request RWMutex.RLock on a field it never read, and the log level moved to an atomic.Int64 — clearing 3 data races in pkg/gofr/logging/remotelogger under -race.
The first change moves work from per-request to per-log-call, so it breaks even at 3 discarded log lines per request. Output is byte-identical to v1.59.0: 348 request cases and every log line at all 7 levels were differenced with no changes.
🛠️ Fixes
-
CORS Origin Bypass — A differently-cased
access-control-allow-originkey slipped past both the header guard andparseOrigins, so a configured allow-list could answer an untrusted origin with*. Header names are now canonicalized at construction. -
CORS Data Race —
append(routes, "OPTIONS")wrote into the backing array of the router'sRegisteredRoutes. Reported 3 data races under-race, now clean. -
Content-TypeMatching — Matching was case-sensitive and untrimmed, soApplication/JSONandapplication/json ; charset=utf-8bound nothing. Now parsed per RFC 9110, withapplication/octet-streamaccepted alongsidebinary/octet-stream. -
BindSilently Discarded Bodies —Bindinto a non-pointer, andapplication/octet-streaminto a target that is not[]byte, unmarshalled into a throwaway and returnednil, so the handler saw success and no data. Both are reported now, which means a handler that returns the error responds500where it previously returned201with an empty struct. GoFr's ownbinary/octet-streamspelling already errored this way; the RFC spelling and the case variants now match their siblings. -
BindNil Body Panic — A request built by hand, as in a handler unit test, panicked inio.ReadAll. An absent body is now treated as empty. -
Dropped Response Headers —
Headersset on a*response.Responsenever reached the wire. -
gRPC Shutdown Race —
grpcServer.serverwas written and read across goroutines unsynchronized, and aShutdownreading a stalenilreported success without stopping anything, letting the process exit with the server still serving. -
Cron Data Loss on Shutdown —
Stop()returned as soon as the ticker halted without joining running jobs, so a job mid-work carried on against datasourcesApp.Shutdownwas already closing and failed withsql: database is closed, losing the rest of a half-written batch.Stop()now joins in-flight jobs, so it blocks for as long as the job runs;App.Shutdowncaps that at its own context deadline. A container with no logger also segfaulted injob.run. -
Flaky Example Tests — All 31
time.Sleepcalls replaced with real readiness signals, andusing-migrationsnow owns its own MySQL and Redis databases.
📦 CI & Dependencies
-
Dependency Updates — 26 Dependabot PRs consolidated: OpenTelemetry 1.45, prometheus exporter 0.67,
go-redis9.22.0 (withMockRedisregenerated), plusaws-sdk-go-v2,clickhouse-go,cloudsqlconn,x/cryptoandsqlite. Note that the prometheus exporter now formats label values viaattribute.Value.String(), so aStringSlicerenders as[a,b]rather than["a","b"]. -
CI Hardening — Concurrency groups and job timeouts, actions pinned by commit SHA, service health checks in
Example-Unit-Testing, and Node 24 in the website workflows. Dependabot now coverspkg/gofr/metrics/exporters/gcpand all 11 Dockerfiles, one of which had been left onalpine:3.14.
Full changelog: v1.59.0...v1.60.0