Skip to content

Commit

Permalink
http: Support caching POST requests. (#769)
Browse files Browse the repository at this point in the history
This commit supports caching POST requests, though only if the user
explicitly sets the TTL.
  • Loading branch information
betterengineering authored May 16, 2023
1 parent 08616f2 commit 37d4267
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 18 deletions.
31 changes: 14 additions & 17 deletions runtime/httpcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ var cacheableStatusCodes = map[int]bool{
405: true,
410: true,
414: true,
429: true, // Not technically cachable, but it is in our system.
501: true,
}

Expand Down Expand Up @@ -76,7 +75,7 @@ func (c *cacheClient) RoundTrip(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("failed to generate cache key: %w", err)
}

if req.Method == "GET" {
if req.Method == "GET" || req.Method == "HEAD" || req.Method == "POST" {
b, exists, err := c.cache.Get(nil, key)
if exists && err == nil {
if res, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(b)), req); err == nil {
Expand All @@ -91,7 +90,7 @@ func (c *cacheClient) RoundTrip(req *http.Request) (*http.Response, error) {
resp.Body = http.MaxBytesReader(nil, resp.Body, MaxResponseBytes)
}

if err == nil && req.Method == "GET" {
if err == nil && (req.Method == "GET" || req.Method == "HEAD" || req.Method == "POST") {
ser, err := httputil.DumpResponse(resp, true)
if err != nil {
// if httputil.DumpResponse fails, it leaves the response body in an
Expand Down Expand Up @@ -145,11 +144,6 @@ func DetermineTTL(req *http.Request, resp *http.Response) time.Duration {
}

func determineTTL(req *http.Request, resp *http.Response) time.Duration {
// Check if the request is cachable.
if !isCachable(req, resp) {
return MinRequestTTL
}

// If the response is a 429, we want to cache the response for the duration
// the remote server told us to wait before retrying.
if resp.StatusCode == 429 {
Expand All @@ -168,9 +162,21 @@ func determineTTL(req *http.Request, resp *http.Response) time.Duration {
return retry
}

// Check the status code to determine if the response is cacheable.
_, ok := cacheableStatusCodes[resp.StatusCode]
if !ok {
return MinRequestTTL
}

// Determine the TTL based on the developer's configuration.
ttl := determineDeveloperTTL(req)

// We don't want to cache POST requests unless the developer explicitly
// requests it.
if ttl == 0 && !(req.Method == "GET" || req.Method == "HEAD") {
return MinRequestTTL
}

// If the developer didn't configure a TTL, determine the TTL based on the
// response.
if ttl == 0 {
Expand Down Expand Up @@ -224,15 +230,6 @@ func determineDeveloperTTL(req *http.Request) time.Duration {
return 0
}

func isCachable(req *http.Request, resp *http.Response) bool {
if !(req.Method == "GET" || req.Method == "HEAD") {
return false
}

_, ok := cacheableStatusCodes[resp.StatusCode]
return ok
}

func parseCacheControl(header string) map[string]interface{} {
directives := make(map[string]interface{})

Expand Down
16 changes: 15 additions & 1 deletion runtime/httpcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,26 @@ func TestDetermineTTL(t *testing.T) {
expectedTTL: 5 * time.Second,
},
"test DELETE request": {
ttl: 30,
ttl: 0,
resHeader: "",
statusCode: 200,
method: "DELETE",
expectedTTL: 5 * time.Second,
},
"test POST request configured with TTL": {
ttl: 30,
resHeader: "",
statusCode: 200,
method: "POST",
expectedTTL: 30 * time.Second,
},
"test POST request without configured TTL": {
ttl: 0,
resHeader: "",
statusCode: 200,
method: "POST",
expectedTTL: 5 * time.Second,
},
"test 429 request": {
ttl: 30,
retryAfter: 60,
Expand Down
18 changes: 18 additions & 0 deletions runtime/testdata/httpcache.star
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ def main(config):
)
assert.eq(resp.headers.get("Tidbyt-Cache-Status"), "HIT")

resp = http.post(
url = "https://example.com",
ttl_seconds = 0,
)
assert.eq(resp.headers.get("Tidbyt-Cache-Status"), "MISS")

resp = http.post(
url = "https://example.com",
ttl_seconds = 60,
)
assert.eq(resp.headers.get("Tidbyt-Cache-Status"), "MISS")

resp = http.post(
url = "https://example.com",
ttl_seconds = 60,
)
assert.eq(resp.headers.get("Tidbyt-Cache-Status"), "HIT")

return render.Root(
child = render.Box(
width = 64,
Expand Down

0 comments on commit 37d4267

Please sign in to comment.