frieren

~ / frieren

a self-hosted git server in one binary — everyone reads, only the owner writes

fix: serve real content types for media on the raw endpoint

5a6be8ea963f9cf191e51dc163dd91f742637e55

justin06lee · Aug 18, 2026 16:24 (4h ago)

 e2e_test.go | 11 +++++++++++
 web.go      | 24 +++++++++++++++++++++---
 2 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/e2e_test.go b/e2e_test.go
index ab841c4..fd9b89d 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -149,6 +149,7 @@ func TestWebPages(t *testing.T) {
 	os.MkdirAll(filepath.Join(dir, "docs"), 0o755)
 	os.WriteFile(filepath.Join(dir, "README.md"), []byte("# site\nreadme body here\n"), 0o644)
 	os.WriteFile(filepath.Join(dir, "docs", "guide.txt"), []byte("guide line one\n"), 0o644)
+	os.WriteFile(filepath.Join(dir, "logo.svg"), []byte("<svg xmlns=\"http://www.w3.org/2000/svg\"/>\n"), 0o644)
 	mustGit(t, dir, "add", ".")
 	mustGit(t, dir, "commit", "-m", "add docs")
 	mustGit(t, dir, "push", withToken(t, ts.URL+"/site.git"), "master")
@@ -182,6 +183,16 @@ func TestWebPages(t *testing.T) {
 		t.Errorf("commit page: code %d, diff shown: %v", code, strings.Contains(body, "guide line one"))
 	}
 
+	// Media files get their real content type so README embeds render.
+	resp, err := http.Get(ts.URL + "/site/raw/master/logo.svg")
+	if err != nil {
+		t.Fatal(err)
+	}
+	resp.Body.Close()
+	if ct := resp.Header.Get("Content-Type"); ct != "image/svg+xml" {
+		t.Errorf("svg raw content-type = %q", ct)
+	}
+
 	// Traversal and junk stay 404.
 	for _, path := range []string{"/nope", "/site/blob/master/../../etc/passwd", "/site/raw/master/%2e%2e/x"} {
 		if code, _ := get(t, ts.URL+path); code != http.StatusNotFound {
diff --git a/web.go b/web.go
index 0e22055..c2a72d2 100644
--- a/web.go
+++ b/web.go
@@ -244,6 +244,20 @@ func (srv *Server) blobPage(w http.ResponseWriter, r *http.Request) {
 	srv.render(w, "blob", data)
 }
 
+var rawTypes = map[string]string{
+	".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg",
+	".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
+	".ico": "image/x-icon", ".avif": "image/avif", ".pdf": "application/pdf",
+	".mp4": "video/mp4", ".webm": "video/webm", ".mp3": "audio/mpeg",
+}
+
+func extOf(path string) string {
+	if i := strings.LastIndexByte(path, '.'); i >= 0 && !strings.ContainsRune(path[i:], '/') {
+		return strings.ToLower(path[i:])
+	}
+	return ""
+}
+
 func (srv *Server) rawFile(w http.ResponseWriter, r *http.Request) {
 	repo := srv.openRepo(w, r)
 	if repo == nil {
@@ -259,11 +273,15 @@ func (srv *Server) rawFile(w http.ResponseWriter, r *http.Request) {
 		http.NotFound(w, r)
 		return
 	}
-	// Never let the browser interpret repository content as HTML.
+	// Never let the browser interpret repository content as HTML. Known
+	// media types get their real content type so <img>/<video> embeds work.
 	w.Header().Set("X-Content-Type-Options", "nosniff")
-	if isBinary(blob) {
+	switch {
+	case rawTypes[extOf(path)] != "":
+		w.Header().Set("Content-Type", rawTypes[extOf(path)])
+	case isBinary(blob):
 		w.Header().Set("Content-Type", "application/octet-stream")
-	} else {
+	default:
 		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 	}
 	w.Write(blob)