diff --git a/api/handler/delete_test.go b/api/handler/delete_test.go index 68fdbb52..157f0001 100644 --- a/api/handler/delete_test.go +++ b/api/handler/delete_test.go @@ -578,6 +578,18 @@ func checkFound(t *testing.T, hc *handlerContext, bktName, objName, version stri assertStatus(t, w, http.StatusOK) } +func headObjectWithHeaders(hc *handlerContext, bktName, objName, version string, headers map[string]string) *httptest.ResponseRecorder { + query := make(url.Values) + query.Add(api.QueryVersionID, version) + + w, r := prepareTestFullRequest(hc, bktName, objName, query, nil) + for k, v := range headers { + r.Header.Set(k, v) + } + hc.Handler().HeadObjectHandler(w, r) + return w +} + func headObjectBase(hc *handlerContext, bktName, objName, version string) *httptest.ResponseRecorder { query := make(url.Values) query.Add(api.QueryVersionID, version) diff --git a/api/handler/encryption_test.go b/api/handler/encryption_test.go index 3561c658..8efdc451 100644 --- a/api/handler/encryption_test.go +++ b/api/handler/encryption_test.go @@ -399,6 +399,15 @@ func getObject(hc *handlerContext, bktName, objName string) ([]byte, http.Header return getObjectBase(hc, w, r) } +func getObjectWithHeaders(hc *handlerContext, bktName, objName string, headers map[string]string) *httptest.ResponseRecorder { + w, r := prepareTestRequest(hc, bktName, objName, nil) + for k, v := range headers { + r.Header.Set(k, v) + } + hc.Handler().GetObjectHandler(w, r) + return w +} + func getObjectBase(hc *handlerContext, w *httptest.ResponseRecorder, r *http.Request) ([]byte, http.Header) { hc.Handler().GetObjectHandler(w, r) assertStatus(hc.t, w, http.StatusOK) diff --git a/api/handler/get.go b/api/handler/get.go index 8140f50e..c524dfc1 100644 --- a/api/handler/get.go +++ b/api/handler/get.go @@ -78,6 +78,27 @@ func addSSECHeaders(responseHeader http.Header, requestHeader http.Header) { responseHeader.Set(api.AmzServerSideEncryptionCustomerKeyMD5, requestHeader.Get(api.AmzServerSideEncryptionCustomerKeyMD5)) } +func writeNotModifiedHeaders(h http.Header, extendedInfo *data.ExtendedObjectInfo, tagSetLength int, isBucketUnversioned, md5Enabled bool) { + h.Set(api.ETag, data.Quote(extendedInfo.ObjectInfo.ETag(md5Enabled))) + h.Set(api.LastModified, extendedInfo.ObjectInfo.Created.UTC().Format(http.TimeFormat)) + h.Set(api.AmzTaggingCount, strconv.Itoa(tagSetLength)) + + if !isBucketUnversioned { + h.Set(api.AmzVersionID, extendedInfo.Version()) + } + + if cacheControl := extendedInfo.ObjectInfo.Headers[api.CacheControl]; cacheControl != "" { + h.Set(api.CacheControl, cacheControl) + } + + for key, val := range extendedInfo.ObjectInfo.Headers { + if layer.IsSystemHeader(key) { + continue + } + h[api.MetadataPrefix+key] = []string{val} + } +} + func writeHeaders(h http.Header, requestHeader http.Header, extendedInfo *data.ExtendedObjectInfo, tagSetLength int, isBucketUnversioned, md5Enabled bool) { info := extendedInfo.ObjectInfo @@ -158,7 +179,28 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { } info := extendedInfo.ObjectInfo + bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo) + if err != nil { + h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err) + return + } + + t := &data.ObjectVersion{ + BktInfo: bktInfo, + ObjectName: info.Name, + VersionID: info.VersionID(), + } + + tagSet, lockInfo, err := h.obj.GetObjectTaggingAndLock(ctx, t, extendedInfo.NodeVersion) + if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchKey) { + h.logAndSendError(ctx, w, "could not get object meta data", reqInfo, err) + return + } + if err = checkPreconditions(info, conditional, h.cfg.MD5Enabled()); err != nil { + if errors.IsS3Error(err, errors.ErrNotModified) { + writeNotModifiedHeaders(w.Header(), extendedInfo, len(tagSet), bktSettings.Unversioned(), h.cfg.MD5Enabled()) + } h.logAndSendError(ctx, w, "precondition failed", reqInfo, err) return } @@ -185,18 +227,6 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { return } - t := &data.ObjectVersion{ - BktInfo: bktInfo, - ObjectName: info.Name, - VersionID: info.VersionID(), - } - - tagSet, lockInfo, err := h.obj.GetObjectTaggingAndLock(ctx, t, extendedInfo.NodeVersion) - if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchKey) { - h.logAndSendError(ctx, w, "could not get object meta data", reqInfo, err) - return - } - if layer.IsAuthenticatedRequest(ctx) { overrideResponseHeaders(w.Header(), reqInfo.URL.Query()) } @@ -206,12 +236,6 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { return } - bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo) - if err != nil { - h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err) - return - } - getPayloadParams := &layer.GetObjectParams{ ObjectInfo: info, Versioned: p.Versioned(), diff --git a/api/handler/get_test.go b/api/handler/get_test.go index 30304e1f..f55ab5ab 100644 --- a/api/handler/get_test.go +++ b/api/handler/get_test.go @@ -210,6 +210,27 @@ func TestGetObjectEnabledMD5(t *testing.T) { require.Equal(t, data.Quote(objInfo.MD5Sum), headers.Get(api.ETag)) } +func TestGetObjectNotModifiedHeaders(t *testing.T) { + hc := prepareHandlerContextWithMinCache(t) + bktName, objName, metadataHeader := "bucket", "obj", api.MetadataPrefix+"header" + createVersionedBucket(hc, bktName) + header := putObjectWithHeaders(hc, bktName, objName, map[string]string{api.CacheControl: "value", metadataHeader: "value"}) + etag, versionID := header.Get(api.ETag), header.Get(api.AmzVersionID) + require.NotEmpty(t, etag) + require.NotEmpty(t, versionID) + + putObjectTagging(t, hc, bktName, objName, map[string]string{"key": "value"}) + + w := getObjectWithHeaders(hc, bktName, objName, map[string]string{api.IfNoneMatch: etag}) + require.Equal(t, http.StatusNotModified, w.Code) + require.Equal(t, "1", w.Header().Get(api.AmzTaggingCount)) + require.Equal(t, etag, w.Header().Get(api.ETag)) + require.NotEmpty(t, w.Header().Get(api.LastModified)) + require.Equal(t, versionID, w.Header().Get(api.AmzVersionID)) + require.Equal(t, "value", w.Header().Get(api.CacheControl)) + require.Equal(t, []string{"value"}, w.Header()[metadataHeader]) +} + func putObjectContent(hc *handlerContext, bktName, objName, content string) http.Header { body := bytes.NewReader([]byte(content)) w, r := prepareTestPayloadRequest(hc, bktName, objName, body) diff --git a/api/handler/head.go b/api/handler/head.go index 87fb3621..ba183d36 100644 --- a/api/handler/head.go +++ b/api/handler/head.go @@ -66,8 +66,9 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if err = checkPreconditions(info, conditional, h.cfg.MD5Enabled()); err != nil { - h.logAndSendError(ctx, w, "precondition failed", reqInfo, err) + bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo) + if err != nil { + h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err) return } @@ -83,6 +84,14 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { return } + if err = checkPreconditions(info, conditional, h.cfg.MD5Enabled()); err != nil { + if errors.IsS3Error(err, errors.ErrNotModified) { + writeNotModifiedHeaders(w.Header(), extendedInfo, len(tagSet), bktSettings.Unversioned(), h.cfg.MD5Enabled()) + } + h.logAndSendError(ctx, w, "precondition failed", reqInfo, err) + return + } + if len(info.ContentType) == 0 { if info.ContentType = layer.MimeByFilePath(info.Name); len(info.ContentType) == 0 { getParams := &layer.GetObjectParams{ @@ -113,12 +122,6 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { return } - bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo) - if err != nil { - h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err) - return - } - writeHeaders(w.Header(), r.Header, extendedInfo, len(tagSet), bktSettings.Unversioned(), h.cfg.MD5Enabled()) w.WriteHeader(http.StatusOK) } diff --git a/api/handler/head_test.go b/api/handler/head_test.go index 47d7417e..4eebc8c8 100644 --- a/api/handler/head_test.go +++ b/api/handler/head_test.go @@ -99,6 +99,27 @@ func TestHeadObject(t *testing.T) { headObjectAssertS3Error(hc, bktName, objName, emptyVersion, apierr.ErrNoSuchKey) } +func TestHeadObjectNotModifiedHeaders(t *testing.T) { + hc := prepareHandlerContextWithMinCache(t) + bktName, objName, metadataHeader := "bucket", "obj", api.MetadataPrefix+"header" + createVersionedBucket(hc, bktName) + header := putObjectWithHeaders(hc, bktName, objName, map[string]string{api.CacheControl: "value", metadataHeader: "value"}) + etag, versionID := header.Get(api.ETag), header.Get(api.AmzVersionID) + require.NotEmpty(t, etag) + require.NotEmpty(t, versionID) + + putObjectTagging(t, hc, bktName, objName, map[string]string{"key": "value"}) + + w := headObjectWithHeaders(hc, bktName, objName, emptyVersion, map[string]string{api.IfNoneMatch: etag}) + require.Equal(t, http.StatusNotModified, w.Code) + require.Equal(t, "1", w.Header().Get(api.AmzTaggingCount)) + require.Equal(t, etag, w.Header().Get(api.ETag)) + require.NotEmpty(t, w.Header().Get(api.LastModified)) + require.Equal(t, versionID, w.Header().Get(api.AmzVersionID)) + require.Equal(t, "value", w.Header().Get(api.CacheControl)) + require.Equal(t, []string{"value"}, w.Header()[metadataHeader]) +} + func TestIsAvailableToResolve(t *testing.T) { list := []string{"container", "s3"}