{"record":{"id":"b327d51d916e6f6a","repo":"TryGhost/Ghost","slug":"invalid-archive","errorCode":"invalid_archive","errorMessage":"Failed to open the theme archive. Download the theme again and retry.","messagePattern":"Failed to open the theme archive\\. Download the theme again and retry\\.","errorType":"validation","errorClass":"ThemeArchiveExtractionError","httpStatus":null,"severity":"error","filePath":"apps/admin/src/settings/app/components/settings/site/theme/theme-editor-utils.ts","lineNumber":200,"sourceCode":"    });\n\n    return entries;\n};\n\nconst assertThemeArchiveLimits = (entries: Array<[string, JSZip.JSZipObject]>) => {\n    if (entries.length > THEME_EDITOR_ARCHIVE_LIMITS.maxFiles) {\n        throw new ThemeArchiveExtractionError(\n            'too_many_files',\n            `This theme archive contains too many files for the browser editor (${entries.length}/${THEME_EDITOR_ARCHIVE_LIMITS.maxFiles}).`\n        );\n    }\n};\n\nconst loadThemeArchive = async (arrayBuffer: ArrayBuffer) => {\n    try {\n        return await JSZip.loadAsync(arrayBuffer);\n    } catch {\n        throw new ThemeArchiveExtractionError('invalid_archive', invalidArchiveMessage);\n    }\n};\n\nconst readThemeBinaryFile = async (entry: JSZip.JSZipObject) => {\n    try {\n        return await entry.async('uint8array');\n    } catch {\n        throw new ThemeArchiveExtractionError('invalid_archive', invalidArchiveMessage);\n    }\n};\n\nconst readThemeTextFile = async (entry: JSZip.JSZipObject) => {\n    try {\n        return await entry.async('string');\n    } catch {\n        throw new ThemeArchiveExtractionError('invalid_archive', invalidArchiveMessage);\n    }\n};","sourceCodeStart":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/admin/src/settings/app/components/settings/site/theme/theme-editor-utils.ts#L182-L218","documentation":"Thrown by loadThemeArchive when JSZip.loadAsync rejects (the ArrayBuffer is not a valid zip), and by readThemeBinaryFile/readThemeTextFile/getNormalizedArchivePath when an individual entry can't be read or its path is malformed (contains ./.. or doesn't survive normalisation). It signals the downloaded/uploaded file is corrupt or not a zip archive at all. Thrown as ThemeArchiveExtractionError with reason 'invalid_archive'.","triggerScenarios":"JSZip.loadAsync(arrayBuffer) rejects — truncated download, a file that isn't a zip (e.g. an HTML error page saved as .zip), an unsupported compression method, or CRC errors. Also thrown when a zip entry path contains '.' or '..' segments or doesn't equal its normalised form, or when entry.async('string'|'uint8array') throws for a specific corrupted entry.","commonSituations":"Theme download was interrupted/truncated; server returned an HTML 404/500 page that got saved with a .zip extension; zip created with a tool/encryption JSZip can't read (e.g. AES-encrypted, unsupported codec); a path inside the archive uses backslashes or relative segments that fail normalisation.","solutions":["Re-download the theme from its source and verify the file size/extension before uploading.","Confirm the file is actually a zip: unzip -t theme.zip locally, or check the first bytes are the PK zip magic (50 4B).","If the archive was created with encryption or a non-DEFLATE/store method, re-export it without encryption using a standard zip tool.","Catch ThemeArchiveExtractionError and show the bundled 'Download the theme again and retry' message to the user."],"exampleFix":"// before — uncaught rejection surfaces as a generic error\nconst snapshot = await extractThemeArchive(arrayBuffer);\n\n// after — branch on the typed reason for invalid archives\nimport {ThemeArchiveExtractionError} from './theme-editor-utils';\ntry {\n    const snapshot = await extractThemeArchive(arrayBuffer);\n} catch (e) {\n    if (e instanceof ThemeArchiveExtractionError && e.reason === 'invalid_archive') {\n        setUploadError('Failed to open the theme archive. Download the theme again and retry.');\n        return;\n    }\n    throw e;\n}","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"import {ThemeArchiveExtractionError} from './theme-editor-utils';\nfunction isInvalidArchive(e: unknown): e is ThemeArchiveExtractionError {\n    return e instanceof ThemeArchiveExtractionError && e.reason === 'invalid_archive';\n}","tryCatchPattern":"import {extractThemeArchive, ThemeArchiveExtractionError} from './theme-editor-utils';\ntry {\n    const snapshot = await extractThemeArchive(arrayBuffer);\n} catch (e) {\n    if (e instanceof ThemeArchiveExtractionError && e.reason === 'invalid_archive') {\n        setUploadError('Failed to open the theme archive. Download the theme again and retry.');\n        return;\n    }\n    throw e;\n}","preventionTips":["Verify the file is a real zip before upload (check the PK magic bytes / unzip -t).","Re-download the theme if the download may have been truncated.","Avoid encrypted or non-DEFLATE/store zip methods — JSZip can't read them.","Branch on ThemeArchiveExtractionError.reason for specific messaging."],"tags":["theme","archive","zip","validation","browser"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}