mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-29 00:10:42 +01:00
s3api: always write XML timestamps with three fractional digits (#11520)
CopyObject responses carried LastModified values such as
"2026-09-29T20:30:04.56Z": trailing zeros of the fractional seconds were
trimmed, and a whole-second value had no fraction at all. AWS S3 always
writes exactly three digits ("...04.560Z"), and clients that parse with a
fixed-width pattern reject anything else. minio-java 8.6.0
(yyyy-MM-dd'T'HH:mm:ss.SSS'Z') throws DateTimeParseException, so roughly
one CopyObject in ten fails on the client even though the copy succeeded.
Two causes:
- xsdDateTime marshalled with "2006-01-02T15:04:05.999999999", which
drops trailing zeros. It now writes UTC with ".000Z".
- CopyObjectResult.MarshalXML had a pointer receiver, but the handlers
pass the result by value, so encoding/xml never called it and fell back
to time.Time's RFC 3339 encoding. It now has a value receiver.
CopyPartResult had no custom marshaller at all; it now uses xsdDateTime.
Follow-up to #8394 / #8398, which truncated these timestamps to
milliseconds but kept the trimmed format.
This commit is contained in:
1 parent
9a454bbe2b
commit
38ce95d960
4 files changed
+84
-4
No files matched your search
@@ -0,0 +1,56 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// S3 clients such as minio-java parse LastModified with a fixed-width
|
||||
// "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" pattern, so trailing zeros in the
|
||||
// fractional seconds must not be trimmed.
|
||||
func TestXMLTimestampsHaveFixedMilliseconds(t *testing.T) {
|
||||
cases := []struct {
|
||||
in time.Time
|
||||
want string
|
||||
}{
|
||||
{time.Date(2026, 9, 29, 20, 30, 4, 560_000_000, time.UTC), "2026-09-29T20:30:04.560Z"},
|
||||
{time.Date(2026, 9, 29, 20, 30, 4, 500_000_000, time.UTC), "2026-09-29T20:30:04.500Z"},
|
||||
{time.Date(2026, 9, 29, 20, 30, 4, 0, time.UTC), "2026-09-29T20:30:04.000Z"},
|
||||
{time.Date(2026, 9, 29, 20, 30, 4, 123_456_789, time.UTC), "2026-09-29T20:30:04.123Z"},
|
||||
{time.Date(2026, 9, 29, 14, 30, 4, 560_000_000, time.FixedZone("MDT", -6*3600)), "2026-09-29T20:30:04.560Z"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
for name, v := range map[string]any{
|
||||
// by value, the way the handlers pass them to writeSuccessResponseXML
|
||||
"CopyObjectResult": CopyObjectResult{ETag: "e", LastModified: c.in},
|
||||
"CopyPartResult": CopyPartResult{ETag: "e", LastModified: c.in},
|
||||
"CopyObjectResult (ptr)": &CopyObjectResult{ETag: "e", LastModified: c.in},
|
||||
"CopyPartResult (ptr)": &CopyPartResult{ETag: "e", LastModified: c.in},
|
||||
} {
|
||||
out, err := xml.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
if !strings.Contains(string(out), "<LastModified>"+c.want+"</LastModified>") {
|
||||
t.Errorf("%s(%v): got %s, want LastModified %s", name, c.in, out, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMLTimestampRoundTrip(t *testing.T) {
|
||||
in := CopyObjectResult{ETag: "e", LastModified: time.Date(2026, 9, 29, 20, 30, 4, 560_000_000, time.UTC)}
|
||||
out, err := xml.Marshal(&in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var back CopyObjectResult
|
||||
if err := xml.Unmarshal(out, &back); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !back.LastModified.Equal(in.LastModified) {
|
||||
t.Fatalf("round trip: got %v, want %v", back.LastModified, in.LastModified)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user