gofiber/fiber · error · ErrCursorEncode

%w: %w

Error message

%w: %w

What it means

SetNextCursor marshals the values map with json.Marshal (or a custom jsonMarshal) and wraps any failure with the ErrCursorEncode sentinel. This is the only failure mode for the encode step other than length (error 186). The map's values must all be JSON-encodable.

Source

Thrown at middleware/paginate/page_info.go:212

	if err := unmarshal(data, &values); err != nil {
		return nil
	}

	p.cursorData = values

	return values
}

// SetNextCursor encodes a key-value map into an opaque cursor token
// and sets both NextCursor and HasMore on the PageInfo.
func (p *PageInfo) SetNextCursor(values map[string]any) error {
	marshal := p.jsonMarshal
	if marshal == nil {
		marshal = json.Marshal
	}
	data, err := marshal(values)
	if err != nil {
		return fmt.Errorf("%w: %w", ErrCursorEncode, err)
	}

	encoded := base64.RawURLEncoding.EncodeToString(data)
	if len(encoded) > maxCursorLen {
		return fmt.Errorf("%w: cursor token exceeds maximum length (%d)", ErrCursorEncode, maxCursorLen)
	}

	p.NextCursor = encoded
	p.HasMore = true

	return nil
}

View on GitHub (pinned to a105acad6c)

Solutions

  1. Log the wrapped error (it names the offending type, e.g. json: unsupported type: chan struct{}).
  2. Coerce cursor values to JSON-safe primitives before calling SetNextCursor: string, int, float64, bool, time.Time (with format check), or a struct with exported fields only.
  3. Add a type-check helper that walks the map and rejects/converts unsupported types before encoding.
  4. If using a custom jsonMarshal (e.g. sonic), verify it accepts the same type set as encoding/json or pre-convert.
  5. Unit-test SetNextCursor with the exact map shape produced by your handler.

Example fix

// before: map carries an unsupported func
values := map[string]any{"id": id, "fn": someFunc}
pi.SetNextCursor(values) // error

// after: only JSON-safe scalars
values := map[string]any{"id": id}
pi.SetNextCursor(values)
Defensive patterns

Strategy: validation

Validate before calling

// Reject non-JSON-safe values before encoding.
func safeCursorValues(m map[string]any) (map[string]any, error) {
  for k, v := range m {
    switch v.(type) {
    case nil, bool, int, int64, float64, string, json.Marshaler:
    default:
      return nil, fmt.Errorf("cursor key %q has unsupported type %T", k, v)
    }
  }
  return m, nil
}

Type guard

func isJSONSafe(v any) bool {
  switch v.(type) {
  case nil, bool, int, int32, int64, uint, uint32, uint64, float32, float64, string:
    return true
  }
  if _, ok := v.(json.Marshaler); ok { return true }
  return false
}

Try / catch

if err := pi.SetNextCursor(values); err != nil {
  if errors.Is(err, paginate.ErrCursorEncode) { /* log and omit cursor */ }
}

Prevention

When it happens

Trigger: Passing a map containing a non-encodable Go type: chan, func, complex64/128, or a struct with an unexported field that json cannot reach. A cyclic structure via pointers/interfaces also triggers it. A custom jsonMarshal injected via the unexported field that itself errors.

Common situations: Cursor map built from request-derived data that includes a func or chan; storing a *big.Int or other type without a MarshalJSON method; building the map from interface{} values that occasionally hold an unsupported type at runtime.

Related errors


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