github gofr-dev/gofr v1.60.0

3 hours ago

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: handleRequestLog built the entry, formatted a timestamp and resolved the client IP before the logger decided whether to emit it. An optional LogEnabled() lets the middleware ask first, saving 193 B and 4 allocs per request at LOG_LEVEL=NOTICE and above. The default INFO is unaffected, since the request log is written at INFO.
  • Co-Allocated Request Objects: Context, Request and Responder are 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.ServeHTTP resolved 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-origin key slipped past both the header guard and parseOrigins, so a configured allow-list could answer an untrusted origin with *. Header names are now canonicalized at construction.

  • CORS Data Raceappend(routes, "OPTIONS") wrote into the backing array of the router's RegisteredRoutes. Reported 3 data races under -race, now clean.

  • Content-Type Matching — Matching was case-sensitive and untrimmed, so Application/JSON and application/json ; charset=utf-8 bound nothing. Now parsed per RFC 9110, with application/octet-stream accepted alongside binary/octet-stream.

  • Bind Silently Discarded BodiesBind into a non-pointer, and application/octet-stream into a target that is not []byte, unmarshalled into a throwaway and returned nil, so the handler saw success and no data. Both are reported now, which means a handler that returns the error responds 500 where it previously returned 201 with an empty struct. GoFr's own binary/octet-stream spelling already errored this way; the RFC spelling and the case variants now match their siblings.

  • Bind Nil Body Panic — A request built by hand, as in a handler unit test, panicked in io.ReadAll. An absent body is now treated as empty.

  • Dropped Response HeadersHeaders set on a *response.Response never reached the wire.

  • gRPC Shutdown RacegrpcServer.server was written and read across goroutines unsynchronized, and a Shutdown reading a stale nil reported success without stopping anything, letting the process exit with the server still serving.

  • Cron Data Loss on ShutdownStop() returned as soon as the ticker halted without joining running jobs, so a job mid-work carried on against datasources App.Shutdown was already closing and failed with sql: 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.Shutdown caps that at its own context deadline. A container with no logger also segfaulted in job.run.

  • Flaky Example Tests — All 31 time.Sleep calls replaced with real readiness signals, and using-migrations now owns its own MySQL and Redis databases.

📦 CI & Dependencies

  • Dependency Updates — 26 Dependabot PRs consolidated: OpenTelemetry 1.45, prometheus exporter 0.67, go-redis 9.22.0 (with MockRedis regenerated), plus aws-sdk-go-v2, clickhouse-go, cloudsqlconn, x/crypto and sqlite. Note that the prometheus exporter now formats label values via attribute.Value.String(), so a StringSlice renders 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 covers pkg/gofr/metrics/exporters/gcp and all 11 Dockerfiles, one of which had been left on alpine:3.14.


Full changelog: v1.59.0...v1.60.0

Don't miss a new gofr release

NewReleases is sending notifications on new releases.