Repository: google/certificate-transparency-go
Pull request #635 (2020-01-06 11:07:58)

Add Cache-Control header to some CTFE responses
Responses containing entries or proofs can be cached, because they should never change.

<!---
Describe your changes in detail here.
If this fixes an issue, please write "Fixes #123", substituting the issue number.
-->

### Checklist

<!---
Go over all the following points, and put an `x` in all the boxes that apply.
Feel free to not tick any boxes that don't apply to this PR (e.g. refactoring may not need a CHANGELOG update).
If you're unsure about any of these, don't hesitate to ask. We're here to help!
-->

- [ ] I have updated the [CHANGELOG](CHANGELOG.md).
  - Adjust the draft version number according to [semantic versioning](https://semver.org/) rules.
- [ ] I have updated [documentation](docs/) accordingly.


codecov[bot]: # [Codecov](https://codecov.io/gh/google/certificate-transparency-go/pull/635?src=pr&el=h1) Report
> Merging [#635](https://codecov.io/gh/google/certificate-transparency-go/pull/635?src=pr&el=desc) into [master](https://codecov.io/gh/google/certificate-transparency-go/commit/a5a726ea40bd6d1a74635726eb7ee1b32a54e0b7?src=pr&el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/google/certificate-transparency-go/pull/635/graphs/tree.svg?width=650&token=kYPnjCLppA&height=150&src=pr)](https://codecov.io/gh/google/certificate-transparency-go/pull/635?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #635      +/-   ##
==========================================
+ Coverage   71.74%   71.75%   +0.01%     
==========================================
  Files          90       90              
  Lines        9850     9854       +4     
==========================================
+ Hits         7067     7071       +4     
  Misses       2286     2286              
  Partials      497      497
```


| [Impacted Files](https://codecov.io/gh/google/certificate-transparency-go/pull/635?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [trillian/ctfe/handlers.go](https://codecov.io/gh/google/certificate-transparency-go/pull/635/diff?src=pr&el=tree#diff-dHJpbGxpYW4vY3RmZS9oYW5kbGVycy5nbw==) | `80.95% <100%> (+0.13%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/google/certificate-transparency-go/pull/635?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/certificate-transparency-go/pull/635?src=pr&el=footer). Last update [a5a726e...da20a9b](https://codecov.io/gh/google/certificate-transparency-go/pull/635?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/trillian/ctfe/handlers.go b/trillian/ctfe/handlers.go
index dc7f6081..6889ee9c 100644
--- a/trillian/ctfe/handlers.go
+++ b/trillian/ctfe/handlers.go
@@ -49,6 +49,10 @@ import (
 var getByRange = flag.Bool("by_range", true, "Use trillian.GetEntriesByRange for get-entries processing")
 
 const (
+	// HTTP Cache-Control header
+	cacheControlHeader = "Cache-Control"
+	// Value for Cache-Control header when response contains immutable data, i.e. entries or proofs. Allows the response to be cached for 1 day.
+	cacheControlImmutable = "public, max-age=86400"
 	// HTTP content type header
 	contentTypeHeader string = "Content-Type"
 	// MIME content type for JSON
@@ -612,6 +616,7 @@ func getSTHConsistency(ctx context.Context, li *logInfo, w http.ResponseWriter,
 		jsonRsp.Consistency = emptyProof
 	}
 
+	w.Header().Set(cacheControlHeader, cacheControlImmutable)
 	w.Header().Set(contentTypeHeader, contentTypeJSON)
 	jsonData, err := json.Marshal(&jsonRsp)
 	if err != nil {
@@ -690,6 +695,7 @@ func getProofByHash(ctx context.Context, li *logInfo, w http.ResponseWriter, r *
 		proofRsp.AuditPath = emptyProof
 	}
 
+	w.Header().Set(cacheControlHeader, cacheControlImmutable)
 	w.Header().Set(contentTypeHeader, contentTypeJSON)
 	jsonData, err := json.Marshal(&proofRsp)
 	if err != nil {
@@ -791,6 +797,7 @@ func getEntries(ctx context.Context, li *logInfo, w http.ResponseWriter, r *http
 		return http.StatusInternalServerError, fmt.Errorf("failed to process leaves returned from backend: %s", err)
 	}
 
+	w.Header().Set(cacheControlHeader, cacheControlImmutable)
 	w.Header().Set(contentTypeHeader, contentTypeJSON)
 	jsonData, err := json.Marshal(&jsonRsp)
 	if err != nil {
@@ -872,6 +879,7 @@ func getEntryAndProof(ctx context.Context, li *logInfo, w http.ResponseWriter, r
 		AuditPath: rsp.Proof.Hashes,
 	}
 
+	w.Header().Set(cacheControlHeader, cacheControlImmutable)
 	w.Header().Set(contentTypeHeader, contentTypeJSON)
 	jsonData, err := json.Marshal(&jsonRsp)
 	if err != nil {


Test patch:
diff --git a/trillian/ctfe/handlers_test.go b/trillian/ctfe/handlers_test.go
index 2df4c446..62ab4b42 100644
--- a/trillian/ctfe/handlers_test.go
+++ b/trillian/ctfe/handlers_test.go
@@ -1071,6 +1071,9 @@ func runTestGetEntries(t *testing.T) {
 		if test.want != http.StatusOK {
 			continue
 		}
+		if got, want := w.Header().Get("Cache-Control"), "public"; !strings.Contains(got, want) {
+			t.Errorf("GetEntries(%q): Cache-Control response header = %q, want %q", test.req, got, want)
+		}
 		// Leaf data should be passed through as-is even if invalid.
 		var jsonMap map[string][]ct.LeafEntry
 		if err := json.Unmarshal(w.Body.Bytes(), &jsonMap); err != nil {
@@ -1536,6 +1539,9 @@ func TestGetProofByHash(t *testing.T) {
 		if test.want != http.StatusOK {
 			continue
 		}
+		if got, want := w.Header().Get("Cache-Control"), "public"; !strings.Contains(got, want) {
+			t.Errorf("proofByHash(%q): Cache-Control response header = %q, want %q", test.req, got, want)
+		}
 		jsonData, err := ioutil.ReadAll(w.Body)
 		if err != nil {
 			t.Errorf("failed to read response body: %v", err)
@@ -1889,6 +1895,9 @@ func TestGetSTHConsistency(t *testing.T) {
 		if test.want != http.StatusOK {
 			continue
 		}
+		if got, want := w.Header().Get("Cache-Control"), "public"; !strings.Contains(got, want) {
+			t.Errorf("getSTHConsistency(%q): Cache-Control response header = %q, want %q", test.req, got, want)
+		}
 		jsonData, err := ioutil.ReadAll(w.Body)
 		if err != nil {
 			t.Errorf("failed to read response body: %v", err)
@@ -2219,6 +2228,10 @@ func TestGetEntryAndProof(t *testing.T) {
 			continue
 		}
 
+		if got, want := w.Header().Get("Cache-Control"), "public"; !strings.Contains(got, want) {
+			t.Errorf("getEntryAndProof(%q): Cache-Control response header = %q, want %q", test.req, got, want)
+		}
+
 		var resp ct.GetEntryAndProofResponse
 		if err = json.NewDecoder(w.Body).Decode(&resp); err != nil {
 			t.Errorf("Failed to unmarshal json response %s: %v", w.Body.Bytes(), err)