{"record":{"id":"eaa62c8f82bb68e4","repo":"gohugoio/hugo","slug":"failed-to-decode-gif-w","errorCode":null,"errorMessage":"failed to decode gif: %w","messagePattern":"failed to decode gif: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"resources/images/codec.go","lineNumber":189,"sourceCode":"\t\tlogg.StringFunc(\n\t\t\tfunc() string {\n\t\t\t\treturn fmt.Sprintf(\"Decoding image from format %s\", f)\n\t\t\t},\n\t\t),\n\t)\n\tswitch f {\n\tcase JPEG, PNG:\n\t\t// We reworked this decode/encode setup to get full WebP support in v0.153.0.\n\t\t// In the first take of that we used f to decide whether to call png.Decode or jpeg.Decode here,\n\t\t// but testing it on some sites, it seems that it's not uncommon to store JPEGs with PNG extensions and vice versa.\n\t\t// So, to reduce some noise in that release, we fallback to the standard library here,\n\t\t// which will read the magic bytes and decode accordingly.\n\t\timg, _, err := image.Decode(r)\n\t\treturn img, err\n\tcase GIF:\n\t\tg, err := gif.DecodeAll(r)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to decode gif: %w\", err)\n\t\t}\n\t\tif len(g.Delay) > 1 {\n\t\t\treturn &giphy{gif: g, Image: g.Image[0]}, nil\n\t\t}\n\t\treturn g.Image[0], nil\n\tcase TIFF:\n\t\treturn tiff.Decode(r)\n\tcase BMP:\n\t\treturn bmp.Decode(r)\n\tcase AVIF:\n\t\treturn d.avif.Decode(r)\n\tcase WEBP:\n\t\timg, err := d.webp.Decode(r)\n\t\tif err == nil {\n\t\t\treturn img, nil\n\t\t}\n\t\tif rs, ok := r.(io.ReadSeeker); ok {\n\t\t\t// See issue 14288. Turns out it's not uncommon to e.g. name their PNG files with a WEBP extension.","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/gohugoio/hugo/blob/52c9bd7908b4d02d4d0ff8f82a888834d6ee10d2/resources/images/codec.go#L171-L207","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Validate the file is a real GIF (check magic bytes 'GIF87a'/'GIF89a') before processing.","Re-download or replace the corrupted GIF asset.","Inspect the wrapped error from image/gif for specifics (e.g. 'gif: invalid header')."],"exampleFix":"// before: assets/banner.gif is actually a renamed png\n{{ $img := resources.Get \"images/banner.gif\" }}\n{{ $resized := $img.Resize \"300x\" }}  // fails: gif decode\n\n// after: fix the asset extension to match content\n{{ $img := resources.Get \"images/banner.png\" }}\n{{ $resized := $img.Resize \"300x\" }}","handlingStrategy":"validation","validationCode":"// Sniff GIF magic bytes before processing.\nfunc isProbablyGIF(b []byte) bool {\n    return len(b) >= 6 && (string(b[:6]) == \"GIF87a\" || string(b[:6]) == \"GIF89a\")\n}","typeGuard":"func looksLikeGIF(header []byte) bool {\n    return len(header) >= 6 && (string(header[:6]) == \"GIF87a\" || string(header[:6]) == \"GIF89a\")\n}","tryCatchPattern":"if _, err := codec.DecodeFormat(images.GIF, r); err != nil {\n    if strings.Contains(err.Error(), \"failed to decode gif\") { /* replace corrupt asset */ }\n}","preventionTips":["Ensure .gif files are genuinely GIF-encoded, not renamed.","Validate downloads completed (not truncated) before processing.","Keep a CI check that decodes all committed GIFs."],"tags":["image-processing","decode","gif","corrupt-file"],"backgroundTag":null,"analyzedSha":"52c9bd7908b4d02d4d0ff8f82a888834d6ee10d2","analyzedAt":"2026-08-09T21:49:36.660Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}