knadh/listmonk · error

failed to marshal Altcha challenge: %w

Error message

failed to marshal Altcha challenge: %w

What it means

GenerateChallenge wraps an error from json.Marshal when serializing the freshly created altcha.Challenge struct to JSON. This is nearly impossible with a well-formed challenge struct and indicates a structural problem — typically an unsupported field type (e.g. channel, func, or cyclic data) in the challenge object or an out-of-memory condition.

Source

Thrown at internal/captcha/captcha.go:134

// For Altcha, this returns a JSON challenge.
func (c *Captcha) GenerateChallenge() (string, error) {
	switch c.provider {
	case ProviderAltcha:
		exp := time.Now().Add(5 * time.Minute)
		challenge, err := altcha.CreateChallenge(altcha.ChallengeOptions{
			Algorithm:  altcha.SHA256,
			MaxNumber:  int64(c.altcha.Complexity),
			SaltLength: 12,
			HMACKey:    c.altcha.HMACKey,
			Expires:    &exp,
		})
		if err != nil {
			return "", fmt.Errorf("failed to create Altcha challenge: %w", err)
		}

		challengeJSON, err := json.Marshal(challenge)
		if err != nil {
			return "", fmt.Errorf("failed to marshal Altcha challenge: %w", err)
		}

		return string(challengeJSON), nil
	case ProviderHCaptcha:
		// hCaptcha generates challenges client-side.
		return "", nil
	default:
		return "", fmt.Errorf("no captcha provider enabled")
	}
}

// Verify verifies a CAPTCHA response.
func (c *Captcha) Verify(token string) (error, bool) {
	switch c.provider {
	case ProviderAltcha:
		return c.verifyAltcha(token)
	case ProviderHCaptcha:
		return c.verifyHCaptcha(token)

View on GitHub (pinned to 670c01717d)

Solutions

  1. Check the version of the altcha library and whether its Challenge type changed recently (go.mod diff).
  2. Marshal the challenge manually into a plain struct of JSON-safe fields if customization added non-serializable members.
  3. Unwrap with errors.As to confirm it is a *json.UnsupportedTypeError and log the offending field type.

Example fix

// before: marshal library struct directly
challengeJSON, err := json.Marshal(challenge)
// after: map to explicit JSON-safe struct
out := struct {
  Algorithm string `json:"algorithm"`
  Challenge string `json:"challenge"`
  Salt      string `json:"salt"`
  Signature string `json:"signature"`
}{challenge.Algorithm, challenge.Challenge, challenge.Salt, challenge.Signature}
challengeJSON, err := json.Marshal(out)
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: ensure the challenge marshals in a test
var _ = func() error {
  ch, err := altcha.Challenge(altcha.ChallengeOptions{MaxNumber: 1000, SaltLength: 8, HMACKey: "test"})
  if err != nil {
    return err
  }
  _, err = json.Marshal(ch)
  return err
}

Try / catch

challenge, err := captcha.GenerateChallenge(ctx)
if err != nil {
  if strings.Contains(err.Error(), "failed to marshal Altcha challenge") {
    log.Error("challenge not serializable — check altcha library version", "err", err)
    http.Error(w, "captcha unavailable", http.StatusInternalServerError)
    return
  }
}

Prevention

When it happens

Trigger: Calling GenerateChallenge with ProviderAltcha after altcha.Challenge succeeds but json.Marshal(challenge) fails, e.g. after a library upgrade changes the challenge struct to contain a non-serializable field.

Common situations: Upgrading the altcha dependency introduces a non-marshalable field; custom Challenge wrappers add fields like func or chan; extreme memory pressure.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/47784a4a3fb90ac1. Report an issue: GitHub.