gofiber/fiber · error

failed to unmarshal response

Error message

failed to unmarshal response: %w

What it means

Returned by idempotency.maybeWriteCachedResponse when a stored response blob exists but fails to msgpack-unmarshal into the response struct. The wrapped %w is a msgpack decode error. It indicates the cached bytes are corrupted or were written by an incompatible schema/encoding version.

Solutions

  1. Flush the idempotency storage after middleware upgrades that change the response schema.
  2. Namespace keys by version (KeyHeader or a key prefix) so schemas do not collide.
  3. Inspect a sample blob to confirm it is msgpack-encoded and matches the expected fields.
  4. If using Redis, check for key collisions or migration tooling that rewrote values.

Example fix

// before
Storage: redis.New(redis.Config{Key: "idem:"})
// after — version-prefixed keys on upgrade
KeyHeader: "X-Idempotency-Key"
// and bump a key prefix in storage to invalidate old blobs
Storage: redis.New(redis.Config{Key: "idem-v2:"})
Defensive patterns

Strategy: fallback

Prevention

When it happens

Trigger: Storage returns a non-nil blob for the idempotency key, but res.UnmarshalMsg(blob) fails — e.g. the blob was written by a different (older/newer) version of the response struct, was hand-edited, or the storage backend returned garbage due to corruption.

Common situations: Deploying a new version of the middleware whose msgpack-generated response schema differs, mixing data from a different serializer in the same storage key namespace, storage byte-level corruption, or a TTL collision that returns bytes from another key (rare).

Related errors


AI-assisted analysis of gofiber/fiber@a105acad6c (2026-08-11). Data as JSON: /api/errors/3010c8ec51713671. Report an issue: GitHub.

Appendix: source

Thrown at middleware/idempotency/idempotency.go:76

	// Snapshot the configured names so later mutation of the caller's slice
	// cannot change an already-constructed handler. Matching uses
	// utils.EqualFold, so no lowercased copies are needed and comparing
	// against the canonical-case names fasthttp reports stays allocation-free.
	keepResponseHeaders := slices.Clone(cfg.KeepResponseHeaders)

	shouldKeepHeader := func(header string) bool {
		return slices.ContainsFunc(keepResponseHeaders, func(keep string) bool {
			return utils.EqualFold(header, keep)
		})
	}

	maybeWriteCachedResponse := func(c fiber.Ctx, key string) (bool, error) {
		if val, err := cfg.Storage.GetWithContext(c, key); err != nil {
			return false, fmt.Errorf("failed to read response: %w", err)
		} else if val != nil {
			var res response
			if _, err := res.UnmarshalMsg(val); err != nil {
				return false, fmt.Errorf("failed to unmarshal response: %w", err)
			}

			_ = c.Status(res.StatusCode)

			for header, vals := range res.Headers {
				for _, val := range vals {
					c.RequestCtx().Response.Header.Add(header, val)
				}
			}

			if len(res.Body) != 0 {
				if err := c.Send(res.Body); err != nil {
					return true, err
				}
			}

			_ = c.Locals(localsKeyIsFromCache, true)

View on GitHub (pinned to a105acad6c)