Release v1.62.0
๐ Features
๐น Google Pub/Sub Flow Control
Three new configs control how many messages a Google Pub/Sub subscriber pulls at once. They are applied to the subscription before Receive:
| config | description | default |
|---|---|---|
GOOGLE_MAX_OUTSTANDING_MESSAGES
| Most unacknowledged messages held at once. A negative value means no limit. | 1000
|
GOOGLE_MAX_OUTSTANDING_BYTES
| Most unacknowledged bytes held at once. A negative value means no limit. | 1000000000 (1 GB)
|
GOOGLE_NUM_GOROUTINES
| Number of StreamingPull streams. This is not handler concurrency: GoFr delivers one message at a time per topic, so raising it does not raise throughput. | 10
|
The defaults match the SDK's own, so leaving these configs unset keeps the current behavior. An unset, zero or non-integer value uses the default, and a non-integer value is also logged as an error. The defaults are exported as google.DefaultMaxOutstandingMessages, DefaultMaxOutstandingBytes and DefaultNumGoroutines, and the same fields are on google.Config. Query is a one-shot read and does not use these configs.
๐น Keyless Trace Export to Google Cloud
TRACE_EXPORTER=gcp sends spans directly to Google Cloud's Telemetry (OTLP) API using Application Default Credentials. On Cloud Run, traces reach Cloud Trace with an IAM role alone: no key file and no Collector sidecar. Tokens refresh on their own, which a static TRACER_HEADERS value can't do.
import _ "gofr.dev/pkg/gofr/traces/exporters/gcp"TRACE_EXPORTER=gcp
Grant the service account roles/telemetry.tracesWriter. It is the least-privilege role that carries telemetry.traces.write, the permission this endpoint checks. roles/telemetry.writer and roles/cloudtrace.agent also carry it, but they grant more besides. With user credentials instead of a service account, also set GOOGLE_CLOUD_QUOTA_PROJECT. The exporter is its own module, so gofr.dev gains no Google dependencies. The module also sets the gcp.project_id resource attribute from the ambient credentials, falling back to GOOGLE_CLOUD_PROJECT.
๐ง Enhancements
๐น Trace Exporter Registry
Trace exporters now register themselves, the same way metrics exporters already do. A vendor exporter can live in its own module instead of pulling its SDK into gofr.dev. Also in this change:
- The trace resource reads
OTEL_RESOURCE_ATTRIBUTESand recordsframework_version.service.namestill comes fromAPP_NAME. - Spans are flushed when the app shuts down.
- If an exporter can't be built, the app starts with a
NeverSampleprovider instead of crashing, soX-Correlation-IDstill carries a real trace ID.
๐น Build Tags to Leave Out Unused Dependencies
Six opt-in build tags drop code a service doesn't use. A build that sets none of them is unchanged.
| tag | leaves out |
|---|---|
gofr_nosqldrivers
| the blank-imported SQL drivers |
gofr_nopubsub
| the Kafka, Google and MQTT clients |
gofr_nographql
| graphql-go and gqlparser |
gofr_nogrpc
| the gRPC server, reflection, health and recovery middleware |
gofr_nodgraph
| the Dgraph migrator |
gofr_nootlp
| the OTLP trace and metric transports |
go build -tags gofr_nosqldrivers,gofr_nopubsub,gofr_nographql ./...A minimal service built with those three tags is 41.26 MB instead of 57.10 MB (โ27.7%) and compiles 554 packages instead of 831.
โก Performance
| path | before | after |
|---|---|---|
| trie router, static route (100 routes) | 14 allocs, 1072 B, ~570 ns | 8 allocs, 536 B, ~264 ns |
| request log line | 10 allocs | 8 allocs |
- Router: the middleware chain for each route is built once and reused, instead of being rebuilt on every request.
- Logging: the request-log path no longer wraps each entry in a one-element slice. The output bytes are identical.
๐ ๏ธ Fixes
-
Auth Could Start Disabled โ If the RBAC config couldn't be loaded, the app still started and served every route with no role checks. The only sign was one log line.
EnableBasicAuth(no credentials, or an odd number of arguments) andEnableOAuth(a bad JWKS URL) failed the same way. All three now return an error, so the app can refuse to start:if err := app.EnableRBAC(path); err != nil { app.Logger().Fatalf("%v", err) }. -
Retries After the Caller Gave Up โ
RetryConfigkept retrying after the caller's context was done. Each of those attempts failed immediately, yet each was logged and recorded as a503. In one case a single closed health page produced 28 of them in 0 ms. Retrying now stops once the context is done. -
Circuit Breaker Opened by Cancellations โ When a caller cancelled its own request, the breaker counted it as an upstream failure, so callers going away could open the breaker for everyone. Those cancellations are no longer counted, and they no longer reset the failure count either.
-
New Connection for Every Health Check โ The health-check response body was closed without being read, so net/http never reused the connection. Every check opened a new TCP connection, plus a new TLS handshake over TLS. The body is now drained.
-
ClickHouse Durations Were ~0ยตs โ
Exec,SelectandAsyncInserttook their start time after the call had already returned. Theapp_clickhouse_statshistogram, the logdurationand the span attribute now measure the real call. -
Cassandra and ScyllaDB Swallowed Query Errors โ
QueryWithCtxnever closed the gocql iterator, and closing it is the only place gocql reports a failed query. So a failed query looked like an empty result. The error is now returned. -
Dgraph
MutateDidn't Persist โ The transaction was committed only if the caller had setCommitNow. Otherwise nothing was written, the error wasnil, and the transaction stayed open on the server.Mutatenow commits. -
GCP Metrics Exporter Stopped Exporting โ Google rejects any request with more than 200 points. A long-running process eventually grew past that, and from then on every collection was lost. Exports are now split into requests of at most 200 points.
-
Kafka
QueryHeld a Timer for 30s โ AQuerywhose caller set no deadline got a 30s read timeout whose cancel func was thrown away. So every such query left a timer and a child context alive for the full 30s after it returned. The timeout is now cancelled as soon as the read returns. -
Event Hub Health Probe Leak โ Against a broker that accepts a connection but never answers, every health poll left another goroutine stuck. At most one probe runs at a time now.
-
GCS, FTP and Azure File Stores Raced on Reconnect โ When the first connect failed, the background retry wrote the connection fields while requests read them, with no lock. GCS
Stat,Open,ReadDir,RemoveandRename(and AzureStat/Removeon a directory) also panicked if called before the store connected. Connection state is now guarded, a store that isn't connected yet returns its "not initialized" error, and a redundant connection from a concurrentConnectis closed instead of leaked. -
WebSocket Stack Overflow โ The handler's context was set as its own parent, so
.Done(), or a.Value()lookup for any other key, overflowed the stack. A data race onc.Contextbetween the request goroutine andApp.WebSocketis fixed too. -
Typed-Nil Pub/Sub Client โ When
kafka.Neworgoogle.Newrejected a config, the typed nil they returned passed!= nilchecks, and a health check then called a method on it. It is now filtered out, including inGetPublisher()andGetSubscriber(), andisNilno longer panics. -
EnableMCPExited the Process โ A busy MCP port triggeredFatalfduring setup, which skipped every shutdown hook. The port is now claimed withnet.ListenwhenRunstarts, before any server starts. If it is busy, orMCP_PORTis outside 1-65535, startup stops cleanly: the error is logged, open datasources are released, and nothing callsos.Exit. SetMCP_PORT=0to run without the MCP transport. -
Static Files With a Trailing Slash โ
AddStaticFiles("static/", dir)logged success but served 404.static,/static,static/and/static/now all map to/static. -
Silent Remote Log-Level Changes โ 6 of the 30 possible level changes took effect without being logged: every change out of
FATAL, plusERROR โ FATAL. All of them are logged now. -
GraphQL Error Response โ A failure to encode the error response is now logged, as it already was for successful responses.
Full Changelog: v1.61.0...v1.62.0