Treat a clean EOF with unknown total as the whole asset
This commit is contained in:
@@ -62,7 +62,7 @@ func AssetStatusError(url string, resp *http.Response) error {
|
|||||||
|
|
||||||
// ReadPrefix reads up to n bytes of a 200 or 206 asset response. full reports
|
// ReadPrefix reads up to n bytes of a 200 or 206 asset response. full reports
|
||||||
// that the body is the whole object (per Content-Length or the Content-Range
|
// that the body is the whole object (per Content-Length or the Content-Range
|
||||||
// total); a body ending before n bytes that is not the whole object is a
|
// total, or a clean EOF before n when the total is unknown); a body ending before n bytes that is not the whole object is a
|
||||||
// transient short read.
|
// transient short read.
|
||||||
func ReadPrefix(resp *http.Response, n int64) (body []byte, full bool, err error) {
|
func ReadPrefix(resp *http.Response, n int64) (body []byte, full bool, err error) {
|
||||||
body, err = io.ReadAll(io.LimitReader(resp.Body, n))
|
body, err = io.ReadAll(io.LimitReader(resp.Body, n))
|
||||||
@@ -70,15 +70,18 @@ func ReadPrefix(resp *http.Response, n int64) (body []byte, full bool, err error
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
total := resp.ContentLength
|
total := resp.ContentLength
|
||||||
|
unknown := resp.StatusCode == http.StatusOK && total == -1
|
||||||
if resp.StatusCode == http.StatusPartialContent {
|
if resp.StatusCode == http.StatusPartialContent {
|
||||||
total = -1
|
total = -1
|
||||||
if _, t, ok := strings.Cut(resp.Header.Get("Content-Range"), "/"); ok {
|
if _, t, ok := strings.Cut(resp.Header.Get("Content-Range"), "/"); ok {
|
||||||
if v, perr := strconv.ParseInt(t, 10, 64); perr == nil {
|
if v, perr := strconv.ParseInt(t, 10, 64); perr == nil {
|
||||||
total = v
|
total = v
|
||||||
}
|
}
|
||||||
|
unknown = t == "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
full = total == int64(len(body))
|
// net/http errors on a premature close, so a clean EOF before n with an unknown total is the whole object.
|
||||||
|
full = total == int64(len(body)) || (unknown && int64(len(body)) < n)
|
||||||
if !full && int64(len(body)) < n {
|
if !full && int64(len(body)) < n {
|
||||||
return nil, false, fmt.Errorf("short read: got %d of %d bytes", len(body), n)
|
return nil, false, fmt.Errorf("short read: got %d of %d bytes", len(body), n)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -88,18 +88,37 @@ func TestReadPrefix(t *testing.T) {
|
|||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{name: "200 whole object", status: 200, cl: 5, body: "hello", n: 32, wantFull: true},
|
{name: "200 whole object", status: 200, cl: 5, body: "hello", n: 32, wantFull: true},
|
||||||
{name: "200 larger than n", status: 200, cl: 100, body: "hello", n: 4},
|
{name: "200 larger than n", status: 200, cl: 5, body: "hello", n: 4},
|
||||||
{name: "200 unknown length short", status: 200, cl: -1, body: "hello", n: 32, wantErr: true},
|
{name: "200 chunked shorter than n", status: 200, cl: -1, body: "hello", n: 32, wantFull: true},
|
||||||
|
{name: "200 chunked larger than n", status: 200, cl: -1, body: "hello", n: 4},
|
||||||
{name: "206 whole object", status: 206, crange: "bytes 0-4/5", body: "hello", n: 32, wantFull: true},
|
{name: "206 whole object", status: 206, crange: "bytes 0-4/5", body: "hello", n: 32, wantFull: true},
|
||||||
{name: "206 prefix", status: 206, crange: "bytes 0-3/100", body: "hell", n: 4},
|
{name: "206 prefix", status: 206, crange: "bytes 0-3/100", body: "hell", n: 4},
|
||||||
{name: "206 short read", status: 206, crange: "bytes 0-31/100", body: "hello", n: 32, wantErr: true},
|
{name: "206 short read", status: 206, crange: "bytes 0-31/100", body: "hello", n: 32, wantErr: true},
|
||||||
{name: "206 no content-range short", status: 206, body: "hello", n: 32, wantErr: true},
|
{name: "206 no content-range short", status: 206, body: "hello", n: 32, wantErr: true},
|
||||||
|
{name: "206 unknown total shorter than n", status: 206, cl: -1, crange: "bytes 0-4/*", body: "hello", n: 32, wantFull: true},
|
||||||
|
{name: "206 unknown total prefix", status: 206, cl: -1, crange: "bytes 0-3/*", body: "hell", n: 4},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
resp := &http.Response{StatusCode: tc.status, ContentLength: tc.cl, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(tc.body))}
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
if tc.crange != "" {
|
if tc.cl > 0 {
|
||||||
resp.Header.Set("Content-Range", tc.crange)
|
w.Header().Set("Content-Length", strconv.FormatInt(tc.cl, 10))
|
||||||
|
}
|
||||||
|
if tc.crange != "" {
|
||||||
|
w.Header().Set("Content-Range", tc.crange)
|
||||||
|
}
|
||||||
|
w.WriteHeader(tc.status)
|
||||||
|
_, _ = io.WriteString(w, tc.body)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
resp, err := http.Get(srv.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if tc.cl == -1 && resp.ContentLength != -1 {
|
||||||
|
t.Fatalf("response not chunked: ContentLength=%d", resp.ContentLength)
|
||||||
}
|
}
|
||||||
body, full, err := ReadPrefix(resp, tc.n)
|
body, full, err := ReadPrefix(resp, tc.n)
|
||||||
if (err != nil) != tc.wantErr {
|
if (err != nil) != tc.wantErr {
|
||||||
|
|||||||
Reference in New Issue
Block a user