gohugoio/hugo · error

failed to decode gif

Error message

failed to decode gif: %w

What it means

Returned by Codec.DecodeFormat in the GIF branch when gif.DecodeAll fails on the reader. It wraps the standard library's decode error with %w. GIFs are decoded via gif.DecodeAll (not image.Decode) to support animated multi-frame GIFs, so any malformation the GIF decoder rejects surfaces here.

Solutions

  1. Validate the file is a real GIF (check magic bytes 'GIF87a'/'GIF89a') before processing.
  2. Re-download or replace the corrupted GIF asset.
  3. Inspect the wrapped error from image/gif for specifics (e.g. 'gif: invalid header').

Example fix

// before: assets/banner.gif is actually a renamed png
{{ $img := resources.Get "images/banner.gif" }}
{{ $resized := $img.Resize "300x" }}  // fails: gif decode

// after: fix the asset extension to match content
{{ $img := resources.Get "images/banner.png" }}
{{ $resized := $img.Resize "300x" }}
Defensive patterns

Strategy: validation

Validate before calling

// Sniff GIF magic bytes before processing.
func isProbablyGIF(b []byte) bool {
    return len(b) >= 6 && (string(b[:6]) == "GIF87a" || string(b[:6]) == "GIF89a")
}

Type guard

func looksLikeGIF(header []byte) bool {
    return len(header) >= 6 && (string(header[:6]) == "GIF87a" || string(header[:6]) == "GIF89a")
}

Try / catch

if _, err := codec.DecodeFormat(images.GIF, r); err != nil {
    if strings.Contains(err.Error(), "failed to decode gif") { /* replace corrupt asset */ }
}

Prevention

When it happens

Trigger: Processing a .gif resource whose bytes are truncated, corrupted, or not actually a valid GIF (e.g. a file renamed to .gif). gif.DecodeAll returns an error and this wraps it.

Common situations: A .gif file that is actually a JPEG/PNG renamed; a truncated download; a GIF with an invalid header or block; an empty file served as a GIF.

Understand the failure class

Related errors


AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09). Data as JSON: /api/errors/eaa62c8f82bb68e4. Report an issue: GitHub.

Appendix: source

Thrown at resources/images/codec.go:189

		logg.StringFunc(
			func() string {
				return fmt.Sprintf("Decoding image from format %s", f)
			},
		),
	)
	switch f {
	case JPEG, PNG:
		// We reworked this decode/encode setup to get full WebP support in v0.153.0.
		// In the first take of that we used f to decide whether to call png.Decode or jpeg.Decode here,
		// but testing it on some sites, it seems that it's not uncommon to store JPEGs with PNG extensions and vice versa.
		// So, to reduce some noise in that release, we fallback to the standard library here,
		// which will read the magic bytes and decode accordingly.
		img, _, err := image.Decode(r)
		return img, err
	case GIF:
		g, err := gif.DecodeAll(r)
		if err != nil {
			return nil, fmt.Errorf("failed to decode gif: %w", err)
		}
		if len(g.Delay) > 1 {
			return &giphy{gif: g, Image: g.Image[0]}, nil
		}
		return g.Image[0], nil
	case TIFF:
		return tiff.Decode(r)
	case BMP:
		return bmp.Decode(r)
	case AVIF:
		return d.avif.Decode(r)
	case WEBP:
		img, err := d.webp.Decode(r)
		if err == nil {
			return img, nil
		}
		if rs, ok := r.(io.ReadSeeker); ok {
			// See issue 14288. Turns out it's not uncommon to e.g. name their PNG files with a WEBP extension.

View on GitHub (pinned to 52c9bd7908)