gotify/server · error

file must be an image

Error message

file must be an image

What it means

After reading the first 261 bytes of the uploaded file, the handler runs filetype.IsImage(head) to sniff the magic bytes. If the content does not match a known image format, it rejects the upload with this 400 error — content sniffing takes precedence over the client-declared filename or MIME type.

Source

Thrown at api/application.go:427

	withID(ctx, "id", func(id uint) {
		app, err := a.DB.GetApplicationByID(id)
		if success := successOrAbort(ctx, 500, err); !success {
			return
		}
		if app != nil && app.UserID == auth.GetUserID(ctx) {
			file, err := ctx.FormFile("file")
			if err == http.ErrMissingFile {
				ctx.AbortWithError(400, errors.New("file with key 'file' must be present"))
				return
			} else if err != nil {
				ctx.AbortWithError(500, err)
				return
			}
			head := make([]byte, 261)
			open, _ := file.Open()
			open.Read(head)
			if !filetype.IsImage(head) {
				ctx.AbortWithError(400, errors.New("file must be an image"))
				return
			}

			ext := filepath.Ext(file.Filename)
			if !ValidApplicationImageExt(ext) {
				ctx.AbortWithError(400, errors.New("invalid file extension"))
				return
			}

			name := generateNonExistingImageName(a.ImageDir, func() string {
				return generateImageName() + ext
			})

			err = ctx.SaveUploadedFile(file, a.ImageDir+name)
			if err != nil {
				ctx.AbortWithError(500, err)
				return
			}

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Upload an actual image file in a supported format (png, jpg, gif, webp, etc.)
  2. Do not rely on renaming the extension — the content must genuinely be image bytes
  3. Re-export/convert the file to PNG or JPEG if using an exotic format the sniffer rejects
  4. If it is a valid image that fails, check the file is not truncated/empty and that the client is not sending only a partial body

Example fix

// before
fd.append('file', new Blob(['not an image'], {type:'image/png'}));
// after
fd.append('file', file, 'photo.png'); // real image bytes from input[type=file]
Defensive patterns

Strategy: validation

Validate before calling

const isImage = (f) => f.type.startsWith('image/') && f.size > 0 && !f.name.endsWith('.svg');
if (!isImage(file)) throw new Error('upload must be a real image file');

Type guard

function isUploadableImage(f) {
  return f instanceof File && f.size > 0 && ['image/png','image/jpeg','image/gif','image/webp'].includes(f.type);
}

Prevention

When it happens

Trigger: Uploading a file whose actual bytes are not an image (PDF, text, zip, executable) to the image-upload endpoint; also occurs when the file is smaller than the sniffed header or corrupted so magic bytes are unreadable.

Common situations: Renaming evil.txt to evil.png and uploading; uploading SVG/HEIC/other formats not recognized by the filetype library; empty or truncated files; build artifacts accidentally attached.

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/5d49575346d07ca1. Report an issue: GitHub.