Skip to content

Commit daeab61

Browse files
topdown: http.send to cache responses based on status code
Currently http.send caches all responses. Now if the response had a status code of `500`(Internal Server Error ) for example, it's possible OPA will return a reponse from the cache for the next query. This can have unintened consequences as OPA will keep serving the cached response till it's fresh while at the same time it's possible the server has a proper response available. To avoid such as scenario this change updates the caching behavior to take into account the status code of the HTTP response before inserting a value into the cache. The list of status codes that can be cached is per https://www.rfc-editor.org/rfc/rfc7231#section-6.1. Fixes: #5617 Signed-off-by: Ashutosh Narkar <[email protected]>
1 parent 44f9c3a commit daeab61

File tree

3 files changed

+136
-3
lines changed

3 files changed

+136
-3
lines changed

docs/content/policy-reference.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,8 @@ instead of halting evaluation, if `http.send` encounters an error, it can return
902902
set to `0` and `error` describing the actual error. This can be activated by setting the `raise_error` field
903903
in the `request` object to `false`.
904904

905-
If the `cache` field in the `request` object is `true`, `http.send` will return a cached response after it checks its freshness and validity.
905+
If the `cache` field in the `request` object is `true`, `http.send` will return a cached response after it checks its
906+
freshness and validity.
906907

907908
`http.send` uses the `Cache-Control` and `Expires` response headers to check the freshness of the cached response.
908909
Specifically if the [max-age](https://tools.ietf.org/html/rfc7234#section-5.2.2.8) `Cache-Control` directive is set, `http.send`
@@ -919,6 +920,10 @@ conjunction with the `force_cache_duration_seconds` field. If `force_cache` is `
919920

920921
Also, if `force_cache` is `true`, it overrides the `cache` field.
921922

923+
`http.send` only caches responses with the following HTTP status codes: `200`, `203`, `204`, `206`, `300`, `301`,
924+
`404`, `405`, `410`, `414`, and `501`. This is behavior is as per https://www.rfc-editor.org/rfc/rfc7231#section-6.1 and
925+
is enforced when caching responses within a single query or across queries via the `cache` and `force_cache` request fields.
926+
922927
{{< info >}}
923928
`http.send` uses the `Date` response header to calculate the current age of the response by comparing it with the current time.
924929
This value is used to determine the freshness of the cached response. As per https://tools.ietf.org/html/rfc7231#section-7.1.1.2,

topdown/http.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,24 @@ var allowedKeyNames = [...]string{
6969
"caching_mode",
7070
}
7171

72+
// ref: https://www.rfc-editor.org/rfc/rfc7231#section-6.1
73+
var cacheableHTTPStatusCodes = [...]int{
74+
http.StatusOK,
75+
http.StatusNonAuthoritativeInfo,
76+
http.StatusNoContent,
77+
http.StatusPartialContent,
78+
http.StatusMultipleChoices,
79+
http.StatusMovedPermanently,
80+
http.StatusNotFound,
81+
http.StatusMethodNotAllowed,
82+
http.StatusGone,
83+
http.StatusRequestURITooLong,
84+
http.StatusNotImplemented,
85+
}
86+
7287
var (
7388
allowedKeys = ast.NewSet()
89+
cacheableCodes = ast.NewSet()
7490
requiredKeys = ast.NewSet(ast.StringTerm("method"), ast.StringTerm("url"))
7591
httpSendLatencyMetricKey = "rego_builtin_" + strings.ReplaceAll(ast.HTTPSend.Name, ".", "_")
7692
httpSendInterQueryCacheHits = httpSendLatencyMetricKey + "_interquery_cache_hits"
@@ -162,6 +178,7 @@ func getHTTPResponse(bctx BuiltinContext, req ast.Object) (*ast.Term, error) {
162178

163179
func init() {
164180
createAllowedKeys()
181+
createCacheableHTTPStatusCodes()
165182
initDefaults()
166183
RegisterBuiltinFunc(ast.HTTPSend.Name, builtinHTTPSend)
167184
}
@@ -779,7 +796,7 @@ func (c *interQueryCache) checkHTTPSendInterQueryCache() (ast.Value, error) {
779796

780797
// insertIntoHTTPSendInterQueryCache inserts given key and value in the inter-query cache
781798
func insertIntoHTTPSendInterQueryCache(bctx BuiltinContext, key ast.Value, resp *http.Response, respBody []byte, cacheParams *forceCacheParams) error {
782-
if resp == nil || (!forceCaching(cacheParams) && !canStore(resp.Header)) {
799+
if resp == nil || (!forceCaching(cacheParams) && !canStore(resp.Header)) || !cacheableCodes.Contains(ast.IntNumberTerm(resp.StatusCode)) {
783800
return nil
784801
}
785802

@@ -817,6 +834,12 @@ func createAllowedKeys() {
817834
}
818835
}
819836

837+
func createCacheableHTTPStatusCodes() {
838+
for _, element := range cacheableHTTPStatusCodes {
839+
cacheableCodes.Add(ast.IntNumberTerm(element))
840+
}
841+
}
842+
820843
func parseTimeout(timeoutVal ast.Value) (time.Duration, error) {
821844
var timeout time.Duration
822845
switch t := timeoutVal.(type) {
@@ -1321,7 +1344,10 @@ func (c *intraQueryCache) InsertIntoCache(value *http.Response) (ast.Value, erro
13211344
return nil, handleHTTPSendErr(c.bctx, err)
13221345
}
13231346

1324-
insertIntoHTTPSendCache(c.bctx, c.key, result)
1347+
if cacheableCodes.Contains(ast.IntNumberTerm(value.StatusCode)) {
1348+
insertIntoHTTPSendCache(c.bctx, c.key, result)
1349+
}
1350+
13251351
return result, nil
13261352
}
13271353

topdown/http_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2626,6 +2626,108 @@ func TestCertSelectionLogic(t *testing.T) {
26262626
}
26272627
}
26282628

2629+
func TestHTTPSendCacheDefaultStatusCodesIntraQueryCache(t *testing.T) {
2630+
2631+
// run test server
2632+
var requests []*http.Request
2633+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2634+
requests = append(requests, r)
2635+
if len(requests)%2 == 0 {
2636+
headers := w.Header()
2637+
headers["Cache-Control"] = []string{"max-age=290304000, public"}
2638+
w.WriteHeader(http.StatusOK)
2639+
} else {
2640+
w.WriteHeader(http.StatusInternalServerError)
2641+
}
2642+
}))
2643+
2644+
defer ts.Close()
2645+
2646+
t.Run("non-cacheable status code: intra-query cache", func(t *testing.T) {
2647+
base := fmt.Sprintf(`http.send({"method": "get", "url": %q, "cache": true})`, ts.URL)
2648+
query := fmt.Sprintf("%v;%v;%v", base, base, base)
2649+
2650+
q := NewQuery(ast.MustParseBody(query))
2651+
2652+
// Execute three http.send calls within a query.
2653+
// Since the server returns a http.StatusInternalServerError on the first request, this should NOT be cached as
2654+
// http.StatusInternalServerError is not a cacheable status code. The second request should result in OPA reaching
2655+
// out to the server again and getting a http.StatusOK response status code.
2656+
// The third request should now be served from the cache.
2657+
2658+
_, err := q.Run(context.Background())
2659+
if err != nil {
2660+
t.Fatal(err)
2661+
}
2662+
2663+
expectedReqCount := 2
2664+
if len(requests) != expectedReqCount {
2665+
t.Fatalf("Expected to get %d requests, got %d", expectedReqCount, len(requests))
2666+
}
2667+
})
2668+
}
2669+
2670+
func TestHTTPSendCacheDefaultStatusCodesInterQueryCache(t *testing.T) {
2671+
2672+
// run test server
2673+
var requests []*http.Request
2674+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2675+
requests = append(requests, r)
2676+
if len(requests)%2 == 0 {
2677+
headers := w.Header()
2678+
headers["Cache-Control"] = []string{"max-age=290304000, public"}
2679+
w.WriteHeader(http.StatusOK)
2680+
} else {
2681+
w.WriteHeader(http.StatusInternalServerError)
2682+
}
2683+
}))
2684+
2685+
defer ts.Close()
2686+
2687+
t.Run("non-cacheable status code: inter-query cache", func(t *testing.T) {
2688+
2689+
// add an inter-query cache
2690+
config, _ := iCache.ParseCachingConfig(nil)
2691+
interQueryCache := iCache.NewInterQueryCache(config)
2692+
2693+
m := metrics.New()
2694+
2695+
q := NewQuery(ast.MustParseBody(fmt.Sprintf(`http.send({"method": "get", "url": %q, "cache": true})`, ts.URL))).
2696+
WithMetrics(m).WithInterQueryBuiltinCache(interQueryCache)
2697+
2698+
// Execute three queries.
2699+
// Since the server returns a http.StatusInternalServerError on the first request, this should NOT be cached as
2700+
// http.StatusInternalServerError is not a cacheable status code. The second request should result in OPA reaching
2701+
// out to the server again and getting a http.StatusOK response status code.
2702+
// The third request should now be served from the cache.
2703+
2704+
_, err := q.Run(context.Background())
2705+
if err != nil {
2706+
t.Fatal(err)
2707+
}
2708+
2709+
_, err = q.Run(context.Background())
2710+
if err != nil {
2711+
t.Fatal(err)
2712+
}
2713+
2714+
_, err = q.Run(context.Background())
2715+
if err != nil {
2716+
t.Fatal(err)
2717+
}
2718+
2719+
expectedReqCount := 2
2720+
if len(requests) != expectedReqCount {
2721+
t.Fatalf("Expected to get %d requests, got %d", expectedReqCount, len(requests))
2722+
}
2723+
2724+
// verify http.send inter-query cache hit metric is incremented due to the third request.
2725+
if exp, act := uint64(1), m.Counter(httpSendInterQueryCacheHits).Value(); exp != act {
2726+
t.Fatalf("expected %d cache hits, got %d", exp, act)
2727+
}
2728+
})
2729+
}
2730+
26292731
func TestHTTPSendMetrics(t *testing.T) {
26302732

26312733
// run test server

0 commit comments

Comments
 (0)