From d5849bdf52c05b522729319091666a52dc26601c Mon Sep 17 00:00:00 2001 From: Abdellatif Anaflous <62770500+hktitof@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:09:53 +0100 Subject: [PATCH] fix(attachment): detect heic/heif mime type from filename extension (#6246) Signed-off-by: Abdel --- server/router/api/v1/attachment_mime_test.go | 54 ++++++++++++++++++++ server/router/api/v1/attachment_service.go | 34 ++++++++++-- 2 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 server/router/api/v1/attachment_mime_test.go diff --git a/server/router/api/v1/attachment_mime_test.go b/server/router/api/v1/attachment_mime_test.go new file mode 100644 index 00000000..58a5abfc --- /dev/null +++ b/server/router/api/v1/attachment_mime_test.go @@ -0,0 +1,54 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDetectAttachmentMimeType(t *testing.T) { + tests := []struct { + name string + filename string + content []byte + want string + }{ + { + name: "heic image via extension fallback", + filename: "photo.heic", + want: "image/heic", + }, + { + name: "heif image via extension fallback", + filename: "photo.heif", + want: "image/heif", + }, + { + name: "uppercase HEIC extension", + filename: "PHOTO.HEIC", + want: "image/heic", + }, + { + name: "regular image by builtin extension", + filename: "image.png", + want: "image/png", + }, + { + name: "unknown extension falls back to content sniffing", + filename: "photo.xyz", + content: []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}, + want: "image/png", + }, + { + name: "no extension and no sniffable content", + filename: "attachment", + want: "text/plain; charset=utf-8", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := detectAttachmentMimeType(tt.filename, tt.content) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/server/router/api/v1/attachment_service.go b/server/router/api/v1/attachment_service.go index 699ea912..07967b36 100644 --- a/server/router/api/v1/attachment_service.go +++ b/server/router/api/v1/attachment_service.go @@ -8,6 +8,7 @@ import ( "mime" "net/http" "path/filepath" + "strings" "time" "google.golang.org/grpc/codes" @@ -43,6 +44,33 @@ var exifCapableImageTypes = map[string]bool{ "image/heif": true, } +// extensionMimeTypeFallbacks maps image extensions that Go's builtin MIME +// table does not cover. HEIC/HEIF files are the common case: browsers report +// an empty MIME type for them, Go's builtin table omits the extension, and +// http.DetectContentType cannot sniff the ISO BMFF container, so without +// this fallback these uploads are stored as "application/octet-stream" on +// minimal runtimes that ship no system MIME database (e.g. the Alpine image). +var extensionMimeTypeFallbacks = map[string]string{ + ".heic": "image/heic", + ".heif": "image/heif", +} + +// detectAttachmentMimeType resolves the MIME type for an uploaded file that +// arrived without a client-supplied type. It prefers the filename extension +// (including the curated fallback above, which keeps the result identical on +// machines with and without a system MIME database), then sniffs the content +// as a last resort. +func detectAttachmentMimeType(filename string, content []byte) string { + ext := strings.ToLower(filepath.Ext(filename)) + if mimeType, ok := extensionMimeTypeFallbacks[ext]; ok { + return mimeType + } + if mimeType := mime.TypeByExtension(ext); mimeType != "" { + return mimeType + } + return http.DetectContentType(content) +} + func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.CreateAttachmentRequest) (*v1pb.Attachment, error) { user, err := s.fetchCurrentUser(ctx) if err != nil { @@ -64,11 +92,7 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat } normalizedMimeType := request.Attachment.Type if normalizedMimeType == "" { - ext := filepath.Ext(request.Attachment.Filename) - mimeType := mime.TypeByExtension(ext) - if mimeType == "" { - mimeType = http.DetectContentType(request.Attachment.Content) - } + mimeType := detectAttachmentMimeType(request.Attachment.Filename, request.Attachment.Content) if normalizedType, ok := normalizeMimeType(mimeType); ok { normalizedMimeType = normalizedType }