mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-25 06:20:42 +01:00
* filer: match If-Match and If-None-Match against "*" and ETag lists checkPreconditions compared the whole header as one ETag, so "*", a list of ETags and a weak W/ tag in If-None-Match never matched (RFC 9110 13.1.1, 13.1.2). * volume: check If-None-Match before If-Modified-Since and match ETag lists A volume GET answered 304 from If-Modified-Since even when If-None-Match named a different ETag, and compared If-None-Match as one exact string, so "*", lists and weak tags never matched (RFC 9110 13.1.2, 13.2.2). * filer: split etag lists only outside quoted tags A comma inside a quoted opaque tag is a legal etagc, not a list separator; strings.Split cut such tags into fragments that CanonicalizeETag could turn into a false match. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: match repeated If-Match and If-None-Match header lines Header.Get reads only the first field line, so a match on a later repeated header was missed; join all field values like any other comma-separated list. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: match repeated If-None-Match header lines Header.Get reads only the first field line; join all field values so a match on a later repeated header is seen. Also covers a comma inside a quoted opaque tag. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: send the etag on a 304 like the filer does RFC 9110 15.4.5 lists ETag among the fields a 304 must carry when a 200 would send it; SetEtag now runs before the notModified check. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: mirror the conditional-GET fixes in the rust server not_modified_response now checks If-None-Match before If-Modified-Since, matches "*", weak tags, quoted commas and repeated field lines through the same list rules, and sends the etag on a 304. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: treat an empty If-None-Match like the Go server does * filer: ignore empty elements in etag precondition lists * volume: ignore empty elements in etag lists like the filer * filer: let a present-but-empty precondition line suppress the date check * volume: gate If-Modified-Since on a present If-None-Match line * volume: gate If-Modified-Since on header presence like the Go server --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: chrislusf <chris.lu@gmail.com>
99 lines
4.0 KiB
Go
99 lines
4.0 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestInternalErrorWritesTheReadError(t *testing.T) {
|
|
readErr := errors.New("invalid CRC for needle 2a (got 0a1b2c3d, want 4e5f6071), data on disk corrupted: needle data corrupted")
|
|
w := httptest.NewRecorder()
|
|
InternalError(w, httptest.NewRequest(http.MethodGet, "/1,abc", nil), readErr)
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status %d, want 500", w.Code)
|
|
}
|
|
if body := strings.TrimSpace(w.Body.String()); body != `{"error":"`+readErr.Error()+`"}` {
|
|
t.Fatalf("body %q", body)
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
InternalError(w, httptest.NewRequest(http.MethodGet, "/1,abc", nil), nil)
|
|
if w.Code != http.StatusInternalServerError || w.Body.Len() != 0 {
|
|
t.Fatalf("without an error: status %d body %q, want a bare 500", w.Code, w.Body.String())
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
InternalError(w, httptest.NewRequest(http.MethodHead, "/1,abc", nil), readErr)
|
|
if w.Code != http.StatusInternalServerError || w.Body.Len() != 0 {
|
|
t.Fatalf("on a HEAD request: status %d body %q, want a bare 500", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// A HEAD only attempts a metadata-only read, like a GET: a needle up to
|
|
// PagedReadLimit is read whole. Demanding metadata only would turn that one
|
|
// read into a header read followed by a trailer read.
|
|
func TestShouldAttemptStreamWrite(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
method string
|
|
target string
|
|
ext string
|
|
hasVolume bool
|
|
wantAttempt bool
|
|
wantMustMeta bool
|
|
}{
|
|
{"HEAD", http.MethodHead, "/1,abc.jpg", ".jpg", true, true, false},
|
|
{"GET", http.MethodGet, "/1,abc.jpg", ".jpg", true, true, false},
|
|
{"GET of a resized image", http.MethodGet, "/1,abc.jpg?width=100", ".jpg", true, false, false},
|
|
{"HEAD of a resized image", http.MethodHead, "/1,abc.jpg?width=100", ".jpg", true, false, false},
|
|
{"HEAD of a cropped image", http.MethodHead, "/1,abc.jpg?crop_x1=0&crop_y1=0&crop_x2=100&crop_y2=100", ".jpg", true, false, false},
|
|
{"HEAD without a local volume", http.MethodHead, "/1,abc.jpg", ".jpg", false, false, false},
|
|
} {
|
|
r := httptest.NewRequest(tc.method, tc.target, nil)
|
|
attempt, mustMeta := shouldAttemptStreamWrite(tc.hasVolume, tc.ext, r)
|
|
if attempt != tc.wantAttempt || mustMeta != tc.wantMustMeta {
|
|
t.Errorf("%s: attempt %v, must be metadata only %v; want %v, %v", tc.name, attempt, mustMeta, tc.wantAttempt, tc.wantMustMeta)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNotModifiedChecksIfNoneMatchFirst(t *testing.T) {
|
|
const etag = "353dd8be"
|
|
const lastModified = 1700000000
|
|
before := time.Unix(lastModified-60, 0).UTC().Format(http.TimeFormat)
|
|
after := time.Unix(lastModified+60, 0).UTC().Format(http.TimeFormat)
|
|
|
|
tests := []struct {
|
|
name string
|
|
headers [][2]string
|
|
want bool
|
|
}{
|
|
{"matching etag", [][2]string{{"If-None-Match", `"353dd8be"`}}, true},
|
|
{"weak etag", [][2]string{{"If-None-Match", `W/"353dd8be"`}}, true},
|
|
{"etag in a list", [][2]string{{"If-None-Match", `"x", "353dd8be"`}}, true},
|
|
{"star", [][2]string{{"If-None-Match", `*`}}, true},
|
|
{"other etag", [][2]string{{"If-None-Match", `"x"`}}, false},
|
|
{"etag across repeated lines", [][2]string{{"If-None-Match", `"x"`}, {"If-None-Match", `"353dd8be"`}}, true},
|
|
{"comma inside quotes", [][2]string{{"If-None-Match", `"353dd8be,old"`}}, false},
|
|
{"other etag ignores If-Modified-Since", [][2]string{{"If-None-Match", `"x"`}, {"If-Modified-Since", after}}, false},
|
|
{"empty If-None-Match ignores If-Modified-Since", [][2]string{{"If-None-Match", ``}, {"If-Modified-Since", after}}, false},
|
|
{"not modified since", [][2]string{{"If-Modified-Since", after}}, true},
|
|
{"modified since", [][2]string{{"If-Modified-Since", before}}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, "/3,01637037d6", nil)
|
|
for _, kv := range tt.headers {
|
|
r.Header.Add(kv[0], kv[1])
|
|
}
|
|
if got := notModified(r, etag, lastModified); got != tt.want {
|
|
t.Errorf("notModified = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|