Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Age header #28

Merged
merged 2 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions rc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,30 @@ func TestRC(t *testing.T) {
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/2")},
}, []*http.Response{
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
}, 2},
{"all cache 2", testutil.NewAllCache(t), []*http.Request{
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/2")},
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
}, []*http.Response{
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":1}`)},
}, 1},
{"get only", testutil.NewGetOnlyCache(t), []*http.Request{
{Method: http.MethodPost, URL: testutil.MustParseURL("http://example.com/1")},
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
{Method: http.MethodDelete, URL: testutil.MustParseURL("http://example.com/1")},
{Method: http.MethodGet, URL: testutil.MustParseURL("http://example.com/1")},
}, []*http.Response{
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":3}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":2}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":1}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":2}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}}, Body: testutil.NewBody(`{"count":3}`)},
{StatusCode: http.StatusOK, Header: http.Header{"Age": []string{"0"}, "Cache-Control": []string{"max-age=60"}, "Content-Type": []string{"application/json"}, "X-Cache": []string{"HIT"}}, Body: testutil.NewBody(`{"count":2}`)},
}, 1},
}
for _, tt := range tests {
Expand Down
54 changes: 54 additions & 0 deletions rfc9111/age.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package rfc9111

import (
"net/http"
"strconv"
"time"
)

func setAgeHeader(useCached bool, resHeader http.Header, now time.Time) {
// 4.2.3. Calculating Age
if !useCached {
if resHeader.Get("Age") == "" {
resHeader.Set("Age", "0")
}
return
}
// The following is straight code with the expectation that it will be optimized by the compiler
var (
// age_value
ageValue int
// date_value
dateValue time.Time
// now
// request_time
requestTime time.Time
// response_time
responseTime time.Time
err error
)
ageValue, err = strconv.Atoi(resHeader.Get("Age"))
if err != nil {
ageValue = 0
}

dateValue, err = http.ParseTime(resHeader.Get("Date"))
if err != nil {
return
}
requestTime = dateValue // Approximate value.
responseTime = now // Approximate value.
// apparent_age = max(0, response_time - date_value);
apparentAge := max(0, int(responseTime.Sub(dateValue)/time.Second))
// response_delay = response_time - request_time
responseDelay := int(responseTime.Sub(requestTime) / time.Second)
// corrected_age_value = age_value + response_delay
correctedAgeValue := ageValue + responseDelay
// corrected_initial_age = max(apparent_age, corrected_age_value)
correctedInitialAge := max(apparentAge, correctedAgeValue)
// resident_time = now - response_time;
residentTime := int(now.Sub(responseTime) / time.Second)
// current_age = corrected_initial_age + resident_time;
currentAge := correctedInitialAge + residentTime
resHeader.Set("Age", strconv.Itoa(currentAge))
}
116 changes: 116 additions & 0 deletions rfc9111/age_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package rfc9111

import (
"net/http"
"testing"
"time"
)

func TestSetAgeHeader(t *testing.T) {
now := time.Now().UTC()

tests := []struct {
name string
useCached bool
resHeader http.Header
now time.Time
wantAge string
wantHeader http.Header
}{
{
name: "No cache and no Age header",
useCached: false,
resHeader: http.Header{},
now: now,
wantAge: "0",
wantHeader: http.Header{
"Age": []string{"0"},
},
},
{
name: "No cache and Age header +5sec",
useCached: false,
resHeader: http.Header{
"Age": []string{"5"},
},
now: now,
wantAge: "5",
wantHeader: http.Header{
"Age": []string{"5"},
},
},
{
name: "Cached +10sec",
useCached: true,
resHeader: http.Header{
"Date": []string{now.Add(-10 * time.Second).Format(http.TimeFormat)},
},
now: now,
wantAge: "10",
wantHeader: http.Header{
"Age": []string{"10"},
"Date": []string{now.Add(-10 * time.Second).Format(http.TimeFormat)},
},
},
{
name: "Cached +10sec with Age header +5sec",
useCached: true,
resHeader: http.Header{
"Age": []string{"5"},
"Date": []string{now.Add(-10 * time.Second).Format(http.TimeFormat)},
},
now: now,
wantAge: "15",
wantHeader: http.Header{
"Age": []string{"15"},
"Date": []string{now.Add(-10 * time.Second).Format(http.TimeFormat)},
},
},
{
name: "invalid Age header",
useCached: true,
resHeader: http.Header{
"Age": []string{"invalid"},
"Date": []string{now.Add(-10 * time.Second).Format(http.TimeFormat)},
},
now: now,
wantAge: "10",
wantHeader: http.Header{
"Age": []string{"10"},
"Date": []string{now.Add(-10 * time.Second).Format(http.TimeFormat)},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setAgeHeader(tt.useCached, tt.resHeader, tt.now)
gotAge := tt.resHeader.Get("Age")
if gotAge != tt.wantAge {
t.Errorf("Age header got = %v, want %v", gotAge, tt.wantAge)
}
if !headersEqual(tt.resHeader, tt.wantHeader) {
t.Errorf("Headers got = %v, want %v", tt.resHeader, tt.wantHeader)
}
})
}
}

// headersEqual compares two http.Header objects for equality.
func headersEqual(a, b http.Header) bool {
if len(a) != len(b) {
return false
}
for key, av := range a {
bv, ok := b[key]
if !ok {
return false
}
for i, v := range av {
if v != bv[i] {
return false
}
}
}
return true
}
7 changes: 6 additions & 1 deletion rfc9111/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ func (s *Shared) Storable(req *http.Request, res *http.Response, now time.Time)
return false, time.Time{}
}

func (s *Shared) Handle(req *http.Request, cachedReq *http.Request, cachedRes *http.Response, do func(*http.Request) (*http.Response, error), now time.Time) (bool, *http.Response, error) {
func (s *Shared) Handle(req *http.Request, cachedReq *http.Request, cachedRes *http.Response, do func(*http.Request) (*http.Response, error), now time.Time) (useCached bool, r *http.Response, _ error) {
defer func() {
// 5.1 Age (https://httpwg.org/specs/rfc9111.html#rfc.section.5.1)
setAgeHeader(useCached, r.Header, now)
}()

if cachedReq == nil || cachedRes == nil {
res, err := do(req)
return false, res, err
Expand Down
Loading
Loading