{"id":"1ce68bbd8ba6b10c","repo":"gofiber/fiber","slug":"failed-to-read-response-w-1ce68b","errorCode":null,"errorMessage":"failed to read response: %w","messagePattern":"failed to read response: %w","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"middleware/idempotency/idempotency.go","lineNumber":72,"sourceCode":"\t\t}\n\t\treturn key\n\t}\n\n\t// Snapshot the configured names so later mutation of the caller's slice\n\t// cannot change an already-constructed handler. Matching uses\n\t// utils.EqualFold, so no lowercased copies are needed and comparing\n\t// against the canonical-case names fasthttp reports stays allocation-free.\n\tkeepResponseHeaders := slices.Clone(cfg.KeepResponseHeaders)\n\n\tshouldKeepHeader := func(header string) bool {\n\t\treturn slices.ContainsFunc(keepResponseHeaders, func(keep string) bool {\n\t\t\treturn utils.EqualFold(header, keep)\n\t\t})\n\t}\n\n\tmaybeWriteCachedResponse := func(c fiber.Ctx, key string) (bool, error) {\n\t\tif val, err := cfg.Storage.GetWithContext(c, key); err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to read response: %w\", err)\n\t\t} else if val != nil {\n\t\t\tvar res response\n\t\t\tif _, err := res.UnmarshalMsg(val); err != nil {\n\t\t\t\treturn false, fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t\t\t}\n\n\t\t\t_ = c.Status(res.StatusCode)\n\n\t\t\tfor header, vals := range res.Headers {\n\t\t\t\tfor _, val := range vals {\n\t\t\t\t\tc.RequestCtx().Response.Header.Add(header, val)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(res.Body) != 0 {\n\t\t\t\tif err := c.Send(res.Body); err != nil {\n\t\t\t\t\treturn true, err\n\t\t\t\t}","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/gofiber/fiber/blob/9a4c7e57fe0b080a04235d28a4b0d2b4b353d58c/middleware/idempotency/idempotency.go#L54-L90","documentation":"Returned by maybeWriteCachedResponse (idempotency.go:71-72) when cfg.Storage.GetWithContext fails while trying to load a previously cached response for the idempotency key. This is a backend/storage failure, not a cache miss (a miss returns nil,nil).","triggerScenarios":"Configured Storage (Redis, MySQL, MongoDB, etc.) is unreachable, returns a network error, or the request context is cancelled/timed out before GetWithContext returns. Fires on the fast path before locking (line 119) and again after locking (line 135 — wrapped as [171]).","commonSituations":"Redis connection dropped; storage query exceeded the request deadline; misconfigured storage DSN; storage pod restarted mid-request; TLS handshake to storage failing intermittently.","solutions":["Verify Storage connectivity and credentials at startup (ping/healthcheck).","Increase request timeout or storage operation timeout so GetWithContext doesn't race the context.","Use a more resilient Storage (cluster mode, retries inside the driver).","Consider a local in-memory Storage fallback so a remote outage degrades rather than fails."],"exampleFix":"// before\napp.Use(idempotency.New()) // in-memory only, fine, but...\n\n// after — robust shared storage with healthcheck at boot\nstore := redis.New()\nif err := store.Ping(); err != nil { log.Fatal(err) }\napp.Use(idempotency.New(idempotency.Config{Storage: store}))","handlingStrategy":"retry","validationCode":"// startup connectivity check\nstore := redis.New()\nctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\ndefer cancel()\nif _, err := store.GetWithContext(ctx, \"__hc__\"); err != nil {\n    log.Fatalf(\"storage unreachable: %v\", err)\n}","typeGuard":null,"tryCatchPattern":"// fail open on transient read failures (idempotency is best-effort)\n// wrap your handler so storage errors do not surface as 500","preventionTips":["Healthcheck Storage at startup and periodically.","Set request timeouts that exceed worst-case storage latency.","Use a Storage driver with built-in retries for transient errors."],"tags":["idempotency","storage","network","redis"],"analyzedSha":"9a4c7e57fe0b080a04235d28a4b0d2b4b353d58c","analyzedAt":"2026-08-04T21:44:03.395Z","schemaVersion":2}