diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go
index 42b28da99..e8658b3db 100644
--- a/internal/base/middleware/avatar.go
+++ b/internal/base/middleware/avatar.go
@@ -21,6 +21,7 @@ package middleware
import (
"fmt"
+ "mime"
"net/http"
"net/url"
"os"
@@ -73,7 +74,7 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
ctx.Abort()
return
}
- ctx.Header("content-type", fmt.Sprintf("image/%s", strings.TrimLeft(path.Ext(filePath), ".")))
+ ctx.Header("Content-Type", contentTypeByExt(path.Ext(filePath)))
_, err = ctx.Writer.Write(avatarFile)
if err != nil {
log.Error(err)
@@ -86,9 +87,23 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
ctx.Next()
return
}
- ext := strings.TrimPrefix(filepath.Ext(urlInfo.Path), ".")
- ctx.Header("content-type", fmt.Sprintf("image/%s", ext))
+ ctx.Header("Content-Type", contentTypeByExt(filepath.Ext(urlInfo.Path)))
}
ctx.Next()
}
}
+
+// contentTypeByExt returns the MIME type for a file extension (with leading dot).
+// It prefers the registered MIME table (e.g. ".svg" -> "image/svg+xml", which browsers
+// require to render SVG in
) and falls back to the legacy "image/" guess.
+// Paths without an extension get application/octet-stream rather than an invalid "image/".
+func contentTypeByExt(ext string) string {
+ ext = strings.ToLower(ext)
+ if ct := mime.TypeByExtension(ext); ct != "" {
+ return ct
+ }
+ if name := strings.TrimPrefix(ext, "."); name != "" {
+ return fmt.Sprintf("image/%s", name)
+ }
+ return "application/octet-stream"
+}
diff --git a/internal/base/middleware/avatar_test.go b/internal/base/middleware/avatar_test.go
new file mode 100644
index 000000000..12b32dafe
--- /dev/null
+++ b/internal/base/middleware/avatar_test.go
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package middleware
+
+import (
+ "mime"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestContentTypeByExt(t *testing.T) {
+ // compare the media type only: mime.TypeByExtension may add parameters (e.g. charset)
+ // depending on the platform's MIME database
+ assert.Equal(t, "image/svg+xml", mediaType(t, contentTypeByExt(".svg")))
+ assert.Equal(t, "image/png", mediaType(t, contentTypeByExt(".png")))
+ assert.Equal(t, "image/jpeg", mediaType(t, contentTypeByExt(".JPG")))
+ assert.Equal(t, "image/webp", mediaType(t, contentTypeByExt(".webp")))
+ assert.Equal(t, "application/octet-stream", contentTypeByExt(""))
+ assert.Equal(t, "application/octet-stream", contentTypeByExt("."))
+ // unknown extension keeps the legacy behaviour
+ assert.Equal(t, "image/unknownext", contentTypeByExt(".unknownext"))
+}
+
+func TestAvatarThumbSetsContentTypeForUploads(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+ r := gin.New()
+ r.Use((&AvatarMiddleware{}).AvatarThumb())
+ r.GET("/uploads/branding/:file", func(ctx *gin.Context) { ctx.Status(http.StatusOK) })
+
+ for path, want := range map[string]string{
+ "/uploads/branding/logo.svg": "image/svg+xml",
+ "/uploads/branding/logo.png": "image/png",
+ } {
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodGet, path, nil)
+ req.RequestURI = path // set by the real server; the middleware reads it
+ r.ServeHTTP(w, req)
+ assert.Equal(t, want, mediaType(t, w.Header().Get("Content-Type")), path)
+ }
+}
+
+// mediaType strips any parameters from a Content-Type value
+func mediaType(t *testing.T, contentType string) string {
+ t.Helper()
+ mt, _, err := mime.ParseMediaType(contentType)
+ if err != nil {
+ t.Fatalf("invalid content type %q: %v", contentType, err)
+ }
+ return mt
+}