From 6121d52778339edd498bba00a487aaaf0f146b6d Mon Sep 17 00:00:00 2001 From: mu-adnan Date: Sat, 29 Aug 2026 00:38:34 +0500 Subject: [PATCH 1/2] 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) From 072081508d0cb4129479ed74020dd9f33ae1b2f6 Mon Sep 17 00:00:00 2001 From: mu-adnan Date: Sat, 29 Aug 2026 12:06:50 +0500 Subject: [PATCH 2/2] fix: add integeration tests --- tests/integration/PostObject.go | 58 ++++++++++++++++++++++++++++++++ tests/integration/group-tests.go | 2 ++ 2 files changed, 60 insertions(+) diff --git a/tests/integration/PostObject.go b/tests/integration/PostObject.go index fcef5c0b..722aa85e 100644 --- a/tests/integration/PostObject.go +++ b/tests/integration/PostObject.go @@ -976,6 +976,64 @@ func PostObject_success_with_meta_properties(s *S3Conf) error { }) } +func PostObject_default_content_type(s *S3Conf) error { + testName := "PostObject_default_content_type" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + for _, tt := range []struct { + key string + policyConditions []any + extraFields map[string]string + }{ + { + key: "omitted-content-type", + }, + { + key: "empty-content-type", + policyConditions: []any{ + []any{"starts-with", "$Content-Type", ""}, + }, + extraFields: map[string]string{ + "Content-Type": "", + }, + }, + } { + resp, err := sendPostObject(PostRequestConfig{ + bucket: bucket, + key: tt.key, + s3Conf: s, + fileContent: []byte("dummy data"), + policyConditions: tt.policyConditions, + extraFields: tt.extraFields, + }) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("%s: expected status code to be 204, instead got %d", + tt.key, resp.StatusCode) + } + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + out, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucket, + Key: &tt.key, + }) + cancel() + if err != nil { + return err + } + + if getString(out.ContentType) != defaultContentType { + return fmt.Errorf("%s: expected default %s Content-Type, instead got %s", + tt.key, defaultContentType, getString(out.ContentType)) + } + } + + return nil + }) +} + func PostObject_invalid_website_redirect_location(s *S3Conf) error { testName := "PostObject_invalid_website_redirect_location" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 66b88ba9..40258da9 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -2033,6 +2033,7 @@ func TestPostObject(ts *TestState) { ts.Run(PostObject_success_status_201) ts.Run(PostObject_should_ignore_anything_after_file) ts.Run(PostObject_success_with_meta_properties) + ts.Run(PostObject_default_content_type) ts.Run(PostObject_invalid_website_redirect_location) ts.Run(PostObject_invalid_tagging) ts.Run(PostObject_success_with_tagging) @@ -3494,6 +3495,7 @@ func GetIntTests() IntTests { "PostObject_success_status_201": PostObject_success_status_201, "PostObject_should_ignore_anything_after_file": PostObject_should_ignore_anything_after_file, "PostObject_success_with_meta_properties": PostObject_success_with_meta_properties, + "PostObject_default_content_type": PostObject_default_content_type, "PostObject_invalid_website_redirect_location": PostObject_invalid_website_redirect_location, "PostObject_invalid_tagging": PostObject_invalid_tagging, "PostObject_success_with_tagging": PostObject_success_with_tagging,