github gofr-dev/gofr v1.61.0

4 hours ago

Release v1.61.0

🚀 Features

🔹 HTTP QUERY Method (RFC 10008)

QUERY is a safe, idempotent method that puts the query in the request body. It avoids GET's URL-length limit and query strings leaking into logs, without losing safety the way POST does. GoFr now supports it for incoming routes and on the service client:

// inbound
app.QUERY("/search", func(ctx *gofr.Context) (any, error) {
    var q SearchRequest
    if err := ctx.Bind(&q); err != nil {
        return nil, err
    }
    return search(ctx, q)
})

// outbound
resp, err := ctx.GetHTTPService("catalog").Query(ctx, "search", nil, body)

As RFC 10008 requires, a QUERY request with no Content-Type gets 400, and one GoFr cannot decode gets 415, before your handler runs. The 415 is the new typed ErrorUnsupportedMediaType. Unknown paths still return 404. On the client, QUERY gets circuit-breaker and retry handling like GET. The circuit breaker now returns an error for an unrecognised method; before, it quietly returned a nil response.

🔹 Readiness Checks for /.well-known/health

/.well-known/health always answered 200, so a Kubernetes readiness probe on it sent traffic to a pod whose dependencies weren't reachable yet. AddReadinessCheck makes the endpoint answer 503 until your check passes:

// GoFr's own datasource checks must pass, and so must this one
app.AddReadinessCheck(func(ctx *gofr.Context) error {
    return licenseDB.PingContext(ctx)
})

// your check decides alone; GoFr's checks are not run
app.AddReadinessCheck(func(ctx *gofr.Context) error {
    if redisUp(ctx) || sqlUp(ctx) {
        return nil
    }
    return errNoBackingStore
}, gofr.ReplaceFrameworkChecks())

You can register several checks. They run in the order you added them, and the first failure decides. FrameworkReadiness(ctx) returns GoFr's own result, so a replace-mode check can still use it. The error is logged, never written to the response, whose body is just DOWN. With no check registered, the endpoint behaves exactly as before.

🔹 Configurable Metric Cardinality Limit

METRICS_CARDINALITY_LIMIT sets how many attribute sets each OpenTelemetry instrument keeps. Once an instrument passes the limit, extra series are merged into one otel.metric.overflow series, so a label explosion can't overwhelm your backend.

value behavior
unset SDK default (2000, or OTEL_GO_X_CARDINALITY_LIMIT) — unchanged
> 0 that limit; takes precedence over OTEL_GO_X_CARDINALITY_LIMIT
0 or negative unlimited

🔹 Circuit Breaker Open Counter

app_circuit_open_count{service} goes up once each time a circuit actually goes from closed to open. It does not count every failing request that sees the trip, so a burst of concurrent failures still adds exactly one.

🔹 Azure Event Hub Health Check

Health() for Event Hub was a stub that returned an empty status. Container.Health didn't count an empty status as DOWN, so an app that couldn't reach its Event Hub reported UP. It now makes a real connectivity check, limited to 2s, and reports UP/DOWN with backend, eventHub and partitionCount. The 2s limit is enforced by GoFr itself, because azeventhubs ignores the caller's deadline on this call.

🔧 Enhancements

🔹 Concurrent Health Checks

Container.Health used to check each dependency one after another, so the endpoint took as long as all the checks added together. The checks now run in parallel, and callers that arrive at the same time share one set of results:

before after
one /health probe 1605 ms 401 ms
20 concurrent probes 1623 ms, 80 backend calls 402 ms, 4 backend calls

(MySQL, Redis and four HTTP dependencies, each taking 400 ms to answer.)

A panicking check reports its dependency as DOWN instead of crashing the process. Two new settings are both off by default:

config description
HEALTH_CACHE_TTL Reuse a result for this long (5s, 1m) before checking again.
HEALTH_CHECK_TIMEOUT Limit a round of checks (2s). Dependencies that haven't answered are left out, the status is DEGRADED, and the partial result is never cached.

🔹 MQTT Tracing Semantics

MQTT publish and subscribe spans now set SpanKind and the messaging.* attributes, as Kafka, NATS, SQS and Google Pub/Sub already do. The consume span used to start on context.Background() and ended up in a separate trace. It now sits inside the trace of the request that consumed the message. MQTT 3.1.1 has nowhere to carry traceparent, so there is still no link across the broker. With tracing off, publish allocates exactly what it did before.

To match the other providers, MQTT's app_pubsub_subscribe_total_count now counts Subscribe calls, so it lines up with the success counter. It used to count messages arriving.

⚡ Performance

Fewer allocations on every request. Output is byte-identical in each case.

path before after
CORS headers (wildcard config) 5 allocs, 160 B 0 allocs, 0 B
CORS headers (named origins) 6 allocs, 176 B 2 allocs, 32 B
HTTP metrics histogram observation 4 allocs, 424 B 0 allocs, 0 B
JSON response 9 allocs, 288 B 7 allocs, 224 B
RBAC rule resolution (51 rules) 9,684 allocs, 373,669 ns 6 allocs, 1,717 ns
  • CORS: the fixed header values are built once, on the first request, instead of on every response.
  • Metrics: the measurement option is built once for each (route, method, status) and reused. The cache is keyed on the route template and only admits templated routes, so unique unmatched paths can't fill it up.
  • Responder: the response envelope is pooled with its encoder and cleared before it goes back to the pool. The canonical Content-Type value is built once.
  • RBAC: endpoint patterns are compiled once when the config loads. The old code added a route to a shared router on every request, which caused a data race and unbounded growth (0 → 114 routes after 100 requests). RBAC cost no longer grows with the number of rules.

🛠️ Fixes

  • RBAC Wildcard Rules Ignored — Rules with "methods": ["*"], or with no methods, never matched, so the routes they protected let every request through. They are now enforced.

  • Auth Exemption Matched Too Broadly — Paths that merely started with /.well-known, such as /.well-knownprivate, skipped auth and rate limiting. The exemption now requires /.well-known/.

  • Tracer Credentials in Logs — Credentials in TRACER_URL were printed in startup logs. They are now logged as REDACTED.

  • TRACER_URL With a Scheme Exported Nothinghttp:// and https:// endpoints were not valid gRPC targets. They now work, and TRACER_INSECURE controls host:port endpoints.

  • Scanner Traffic Hid Real Routes in Metrics — Random unmatched URLs filled the metrics series limit. They now collapse into __unmatched__, so real routes keep their series.

  • SIGTERM During Startup Was Lost — A signal that arrived before the server started left it running until SIGKILL. The server now stops.

  • WebSocket Route Panic — A plain HTTP request to a WebSocket route panicked. It now returns an error.

  • Auto-CRUD Panics and Mangled Namesuint, bool and time.Time fields panicked, and digits were corrupted in table and column names. Both are fixed.

  • SQL Log Duration Unit — SQL query logs now report microseconds, like every other datasource.

  • SFTP/S3 Observability — Failed SFTP opens were logged as SUCCESS, and some operations used the wrong names. Status and labels are now correct.

  • Kafka and Google Pub/Sub Panics — Closing a Kafka client, or subscribing to several Google Pub/Sub topics, could crash with concurrent map access. Both are now synchronized.

  • Dgraph Migrations — Migrations now run inside a transaction.

Full Changelog: v1.60.1...v1.61.0

Don't miss a new gofr release

NewReleases is sending notifications on new releases.