From e1fa8d6d2fed00c4c7e44d75a12446d4bd61bb35 Mon Sep 17 00:00:00 2001 From: nakatani-yo Date: Thu, 9 Apr 2026 15:12:47 +0900 Subject: [PATCH 1/4] fix(server): fall back to text/plain when MIME type is unknown in artifact server Signed-off-by: nakatani-yo --- server/artifacts/artifact_server.go | 9 ++++++++- server/artifacts/artifact_server_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/server/artifacts/artifact_server.go b/server/artifacts/artifact_server.go index 188176d2f216..b79fa4241830 100644 --- a/server/artifacts/artifact_server.go +++ b/server/artifacts/artifact_server.go @@ -544,7 +544,14 @@ func (a *ArtifactServer) returnArtifact(ctx context.Context, w http.ResponseWrit key, _ := art.GetKey() w.Header().Add("Content-Disposition", fmt.Sprintf(`filename="%s"`, path.Base(key))) - w.Header().Add("Content-Type", mime.TypeByExtension(path.Ext(key))) + // mime.TypeByExtension relies on the system MIME database (/etc/mime.types), which may not + // exist in minimal container images such as distroless. Fall back to "text/plain" so that + // unrecognized extensions (e.g. ".log") are still served with a valid Content-Type. + contentType := mime.TypeByExtension(path.Ext(key)) + if contentType == "" { + contentType = "text/plain; charset=utf-8" + } + w.Header().Add("Content-Type", contentType) a.setSecurityHeaders(w) _, err = io.Copy(w, stream) diff --git a/server/artifacts/artifact_server_test.go b/server/artifacts/artifact_server_test.go index cb36ff906d52..3a8b18c60b2d 100644 --- a/server/artifacts/artifact_server_test.go +++ b/server/artifacts/artifact_server_test.go @@ -65,6 +65,7 @@ var bucketsOfKeys = map[string][]string{ "my-wf/my-node-1/my-gcs-artifact.tgz", "my-wf/my-node-1/my-oss-artifact.zip", "my-wf/my-node-1/my-s3-artifact.tgz", + "my-wf/my-node-1/main.log", "my-wf/my-node-inline/main.log", }, "my-bucket-2": { @@ -264,6 +265,18 @@ func newServer(t *testing.T) *ArtifactServer { }, }, }, + { + // Log artifact created when archiveLogs is enabled + // (see workflow/executor/executor.go:saveContainerLogs). + // The key ends with ".log", which is not in the MIME database + // on minimal container images (e.g. distroless). + Name: "main-logs", + ArtifactLocation: wfv1.ArtifactLocation{ + S3: &wfv1.S3Artifact{ + Key: "my-wf/my-node-1/main.log", + }, + }, + }, }, }, }, @@ -391,6 +404,7 @@ func TestArtifactServer_GetArtifactFile(t *testing.T) { // success bool isDirectory bool directoryFiles []string // verify these files are in there, if this is a directory + contentType string // expected Content-Type for non-directory artifacts (if set, assert exact match) }{ { path: "/artifact-files/my-ns/workflows/my-wf/my-node-1/outputs/my-s3-artifact-directory", @@ -474,6 +488,12 @@ func TestArtifactServer_GetArtifactFile(t *testing.T) { statusCode: 200, isDirectory: false, }, + { + path: "/artifact-files/my-ns/workflows/my-wf/my-node-1/outputs/main-logs", + statusCode: 200, + isDirectory: false, + contentType: "text/plain; charset=utf-8", // .log is not in the MIME db on all platforms → fallback + }, } for _, tt := range tests { @@ -502,6 +522,10 @@ func TestArtifactServer_GetArtifactFile(t *testing.T) { } } else { assert.Equal(t, "my-data", string(all)) + assert.NotEmpty(t, recorder.Header().Get("Content-Type"), "Content-Type header must not be empty") + if tt.contentType != "" { + assert.Equal(t, tt.contentType, recorder.Header().Get("Content-Type")) + } } } }) From 4c4a0dc027018a8cec0c68eb498c18a12d0351f8 Mon Sep 17 00:00:00 2001 From: nakatani-yo Date: Thu, 9 Apr 2026 17:32:44 +0900 Subject: [PATCH 2/4] fix(server): remove platform-dependent MIME type assertion for .log artifacts Signed-off-by: nakatani-yo --- server/artifacts/artifact_server_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/artifacts/artifact_server_test.go b/server/artifacts/artifact_server_test.go index 3a8b18c60b2d..ed323fa1441c 100644 --- a/server/artifacts/artifact_server_test.go +++ b/server/artifacts/artifact_server_test.go @@ -492,7 +492,9 @@ func TestArtifactServer_GetArtifactFile(t *testing.T) { path: "/artifact-files/my-ns/workflows/my-wf/my-node-1/outputs/main-logs", statusCode: 200, isDirectory: false, - contentType: "text/plain; charset=utf-8", // .log is not in the MIME db on all platforms → fallback + // .log may resolve to "text/x-log" where the system MIME db has it, + // or fall back to "text/plain" on minimal images. Either is fine; + // the non-empty assertion below covers both cases. }, } From 69c88b6678b9fe4191405fc2c0f3b7b3e3d47839 Mon Sep 17 00:00:00 2001 From: nakatani-yo Date: Tue, 14 Apr 2026 17:36:49 +0900 Subject: [PATCH 3/4] fix comment Signed-off-by: nakatani-yo --- server/artifacts/artifact_server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/artifacts/artifact_server.go b/server/artifacts/artifact_server.go index b79fa4241830..b7861195bbbe 100644 --- a/server/artifacts/artifact_server.go +++ b/server/artifacts/artifact_server.go @@ -544,8 +544,8 @@ func (a *ArtifactServer) returnArtifact(ctx context.Context, w http.ResponseWrit key, _ := art.GetKey() w.Header().Add("Content-Disposition", fmt.Sprintf(`filename="%s"`, path.Base(key))) - // mime.TypeByExtension relies on the system MIME database (/etc/mime.types), which may not - // exist in minimal container images such as distroless. Fall back to "text/plain" so that + // mime.TypeByExtension relies on the system MIME database, which may not exist in + // minimal container images such as distroless. Fall back to "text/plain; charset=utf-8" so that // unrecognized extensions (e.g. ".log") are still served with a valid Content-Type. contentType := mime.TypeByExtension(path.Ext(key)) if contentType == "" { From 3a7d6410a02e228604613dc0f0e24b6a2e53c550 Mon Sep 17 00:00:00 2001 From: nakatani-yo Date: Tue, 14 Apr 2026 17:55:17 +0900 Subject: [PATCH 4/4] fix test Signed-off-by: nakatani-yo --- server/artifacts/artifact_server_test.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/server/artifacts/artifact_server_test.go b/server/artifacts/artifact_server_test.go index ed323fa1441c..5b757d8673b7 100644 --- a/server/artifacts/artifact_server_test.go +++ b/server/artifacts/artifact_server_test.go @@ -404,7 +404,6 @@ func TestArtifactServer_GetArtifactFile(t *testing.T) { // success bool isDirectory bool directoryFiles []string // verify these files are in there, if this is a directory - contentType string // expected Content-Type for non-directory artifacts (if set, assert exact match) }{ { path: "/artifact-files/my-ns/workflows/my-wf/my-node-1/outputs/my-s3-artifact-directory", @@ -492,9 +491,11 @@ func TestArtifactServer_GetArtifactFile(t *testing.T) { path: "/artifact-files/my-ns/workflows/my-wf/my-node-1/outputs/main-logs", statusCode: 200, isDirectory: false, - // .log may resolve to "text/x-log" where the system MIME db has it, - // or fall back to "text/plain" on minimal images. Either is fine; - // the non-empty assertion below covers both cases. + // Verify that .log artifacts are served with a non-empty Content-Type. + // The ".log" extension may not be in the system MIME database on minimal + // container images (e.g. distroless), causing mime.TypeByExtension to + // return "". The fallback in returnArtifact ensures "text/plain; charset=utf-8" + // is used instead. }, } @@ -525,9 +526,6 @@ func TestArtifactServer_GetArtifactFile(t *testing.T) { } else { assert.Equal(t, "my-data", string(all)) assert.NotEmpty(t, recorder.Header().Get("Content-Type"), "Content-Type header must not be empty") - if tt.contentType != "" { - assert.Equal(t, tt.contentType, recorder.Header().Get("Content-Type")) - } } } })