From 6121d52778339edd498bba00a487aaaf0f146b6d Mon Sep 17 00:00:00 2001 From: mu-adnan Date: Sat, 29 Aug 2026 00:38:34 +0500 Subject: [PATCH] fix: apply default Content-Type in POSTObject Browser-based POST uploads (POST /{bucket}) read Content-Type straight out of the form fields, so a form without a content-type field stored the object with an empty Content-Type. On read, fasthttp substitutes its own default, so the object came back as "text/plain; charset=utf-8" rather than just missing a type. Fall back to defaultContentType ("binary/octet-stream") when the form field is absent or empty, matching PutObject, CopyObject and CreateMultipartUpload, as well as AWS S3 and Ceph RGW. --- s3api/controllers/bucket-post.go | 3 ++ s3api/controllers/bucket-post_test.go | 45 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/s3api/controllers/bucket-post.go b/s3api/controllers/bucket-post.go index 564fbf81..4ddea6a5 100644 --- a/s3api/controllers/bucket-post.go +++ b/s3api/controllers/bucket-post.go @@ -118,6 +118,9 @@ func (c S3ApiController) POSTObject(ctx fiber.Ctx) (*Response, error) { parsed := utils.ContextKeyObjectPostResult.Get(ctx).(middlewares.PostObjectResult) bucket := ctx.Params("bucket") contentType := parsed.Fields["content-type"] + if contentType == "" { + contentType = defaultContentType + } contentEncoding := parsed.Fields["content-encoding"] contentDisposition := parsed.Fields["content-disposition"] contentLanguage := parsed.Fields["content-language"] diff --git a/s3api/controllers/bucket-post_test.go b/s3api/controllers/bucket-post_test.go index 20a50dad..ef1d64d1 100644 --- a/s3api/controllers/bucket-post_test.go +++ b/s3api/controllers/bucket-post_test.go @@ -620,6 +620,47 @@ func TestS3ApiController_POSTObject(t *testing.T) { }, }, }, + { + name: "default content type when field is omitted", + input: testInput{ + beRes: s3response.PutObjectOutput{ + ETag: "etag-123", + }, + locals: postObjectLocalsForTest(middlewares.PostObjectResult{ + Fields: baseFields, + FileRdr: newMockFileReader("payload"), + ContentLength: int64(len("payload")), + }), + }, + output: testOutput{ + response: &Response{ + Headers: map[string]*string{ + "Etag": utils.GetStringPtr("etag-123"), + "Location": &location, + "x-amz-checksum-crc32": nil, + "x-amz-checksum-crc32c": nil, + "x-amz-checksum-crc64nvme": nil, + "x-amz-checksum-sha1": nil, + "x-amz-checksum-sha256": nil, + "x-amz-checksum-sha512": nil, + "x-amz-checksum-md5": nil, + "x-amz-checksum-xxhash64": nil, + "x-amz-checksum-xxhash3": nil, + "x-amz-checksum-xxhash128": nil, + "x-amz-checksum-type": nil, + "x-amz-version-id": nil, + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + ContentLength: int64(len("payload")), + ObjectETag: utils.GetStringPtr("etag-123"), + ObjectSize: int64(len("payload")), + EventName: s3event.EventObjectCreatedPost, + Status: http.StatusNoContent, + }, + }, + }, + }, { name: "anonymous upload with policy is evaluated", input: testInput{ @@ -665,6 +706,10 @@ func TestS3ApiController_POSTObject(t *testing.T) { assert.Equal(t, "anon-payload", string(body)) } + if tt.name == "default content type when field is omitted" { + assert.Equal(t, defaultContentType, *putObjectInput.ContentType) + } + if tt.name == "successful created response" { assert.Equal(t, "bucket", *putObjectInput.Bucket) assert.Equal(t, "uploads/photo.jpg", *putObjectInput.Key)