From 7378cbbafe6179fcdbe7f175e3c08dcc5649d328 Mon Sep 17 00:00:00 2001 From: k1low Date: Thu, 4 Jan 2024 11:28:43 +0900 Subject: [PATCH] Use the Date header field value first. A recipient with a clock that receives a response with an invalid Date header field value MAY replace that value with the time that response was received. https://httpwg.org/specs/rfc9110.html#rfc.section.6.6.1 --- rfc9111/shared.go | 70 ++++++++++++++++++++------------------------ testdata/go_test.mod | 2 +- testdata/go_test.sum | 4 +-- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/rfc9111/shared.go b/rfc9111/shared.go index 8b50b68..abe7906 100644 --- a/rfc9111/shared.go +++ b/rfc9111/shared.go @@ -323,17 +323,8 @@ func (s *Shared) storableWithExtendedRules(req *http.Request, res *http.Response ok, age := rule.Cacheable(req, res) if ok { // Add Expires header field - var expires time.Time - if res.Header.Get("Date") != "" { - date, err := http.ParseTime(res.Header.Get("Date")) - if err == nil { - expires = date.Add(age) - } else { - expires = now.Add(age) - } - } else { - expires = now.Add(age) - } + od := originDate(res.Header, now) + expires := od.Add(age) res.Header.Set("Expires", expires.UTC().Format(http.TimeFormat)) return true, expires } @@ -341,46 +332,35 @@ func (s *Shared) storableWithExtendedRules(req *http.Request, res *http.Response return false, time.Time{} } -func CalclateExpires(d *ResponseDirectives, header http.Header, heuristicExpirationRatio float64, now time.Time) time.Time { +func CalclateExpires(d *ResponseDirectives, resHeader http.Header, heuristicExpirationRatio float64, now time.Time) time.Time { // 4.2.1. Calculating Freshness Lifetime // A cache can calculate the freshness lifetime (denoted as freshness_lifetime) of a response by evaluating the following rules and using the first match: - // - If the cache is shared and the s-maxage response directive (https://httpwg.org/specs/rfc9111.html#rfc.section.5.2.2.10) is present, use its value, or + // - If the cache is shared and the s-maxage response directive (https://httpwg.org/specs/rfc9111.html#rfc.section.5.2.2.10) is present, use its value if d.SMaxAge != nil { - return now.Add(time.Duration(*d.SMaxAge) * time.Second) + od := originDate(resHeader, now) + return od.Add(time.Duration(*d.SMaxAge) * time.Second) } - // - If the max-age response directive (https://httpwg.org/specs/rfc9111.html#rfc.section.5.2.2.1) is present, use its value, or + // - If the max-age response directive (https://httpwg.org/specs/rfc9111.html#rfc.section.5.2.2.1) is present, use its value if d.MaxAge != nil { - return now.Add(time.Duration(*d.MaxAge) * time.Second) + od := originDate(resHeader, now) + return od.Add(time.Duration(*d.MaxAge) * time.Second) } - if header.Get("Expires") != "" { - // - If the Expires response header field (https://httpwg.org/specs/rfc9111.html#rfc.section.5.3) is present, use its value minus the value of the Date response header field - et, err := http.ParseTime(header.Get("Expires")) + if resHeader.Get("Expires") != "" { + // - If the Expires response header field (https://httpwg.org/specs/rfc9111.html#rfc.section.5.3) is present, use its value minus the value of the Date response header field (using the time the message was received if it is not present, as per Section 6.6.1 of [HTTP]) + et, err := http.ParseTime(resHeader.Get("Expires")) if err == nil { - if header.Get("Date") != "" { - dt, err := http.ParseTime(header.Get("Date")) - if err == nil { - return now.Add(et.Sub(dt)) - } - } else { - // (using the time the message was received if it is not present, as per https://httpwg.org/specs/rfc9110.html#rfc.section.6.6.1 of [HTTP]) - return et // == return now.Add(et.Sub(now)) - } + od := originDate(resHeader, now) + return now.Add(et.Sub(od)) } } // Otherwise, no explicit expiration time is present in the response. A heuristic freshness lifetime might be applicable; see https://httpwg.org/specs/rfc9111.html#rfc.section.4.2.2. - if header.Get("Last-Modified") != "" { - lt, err := http.ParseTime(header.Get("Last-Modified")) + if resHeader.Get("Last-Modified") != "" { + lt, err := http.ParseTime(resHeader.Get("Last-Modified")) if err == nil { // If the response has a Last-Modified header field (https://httpwg.org/specs/rfc9110.html#rfc.section.8.8.2 of [HTTP]), caches are encouraged to use a heuristic expiration value that is no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%. - if header.Get("Date") != "" { - dt, err := http.ParseTime(header.Get("Date")) - if err == nil { - return dt.Add(time.Duration(float64(dt.Sub(lt)) * heuristicExpirationRatio)) - } - } else { - return now.Add(time.Duration(float64(now.Sub(lt)) * heuristicExpirationRatio)) - } + od := originDate(resHeader, now) + return od.Add(time.Duration(float64(od.Sub(lt)) * heuristicExpirationRatio)) } } @@ -388,6 +368,20 @@ func CalclateExpires(d *ResponseDirectives, header http.Header, heuristicExpirat return time.Time{} } +func originDate(resHeader http.Header, now time.Time) time.Time { + if resHeader.Get("Date") != "" { + t, err := http.ParseTime(resHeader.Get("Date")) + if err == nil { + return t + } + } + // A recipient with a clock that receives a response with an invalid Date header field value MAY replace that value with the time that response was received. (https://httpwg.org/specs/rfc9110.html#rfc.section.6.6.1 of [HTTP]) + // + // ...using the time the message was received if it is not present, as per https://httpwg.org/specs/rfc9110.html#rfc.section.6.6.1 of [HTTP] + // (https://httpwg.org/specs/rfc9111.html#rfc.section.4.2.1) + return now +} + func contains[T comparable](v T, vv []T) bool { for _, vvv := range vv { if vvv == v { diff --git a/testdata/go_test.mod b/testdata/go_test.mod index 5eecb02..f6ae98c 100644 --- a/testdata/go_test.mod +++ b/testdata/go_test.mod @@ -4,7 +4,7 @@ go 1.21.5 require ( github.com/google/go-cmp v0.6.0 - github.com/k1LoW/httpstub v0.11.1 + github.com/k1LoW/httpstub v0.12.0 ) require ( diff --git a/testdata/go_test.sum b/testdata/go_test.sum index 72fdf2a..1fdcf71 100644 --- a/testdata/go_test.sum +++ b/testdata/go_test.sum @@ -19,8 +19,8 @@ github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/k1LoW/httpstub v0.11.1 h1:amfxTttNX82F12kmUr9vrptpLl+nCUzHrEpvUqoCWQ0= -github.com/k1LoW/httpstub v0.11.1/go.mod h1:PYUmCF/2A7+TPhRPVJ4xSynM0O1x2mIjh+Tzd/DmiG8= +github.com/k1LoW/httpstub v0.12.0 h1:BDiTmO8G8FIdvDavHaYgl7+kVlQQACLESpC+02/DzQ0= +github.com/k1LoW/httpstub v0.12.0/go.mod h1:PYUmCF/2A7+TPhRPVJ4xSynM0O1x2mIjh+Tzd/DmiG8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=