diff --git a/httpbin/handlers.go b/httpbin/handlers.go index d4d42c5da3aef79c252c8acfa91dae53ba6c82c6..0feec5c819d3c74d0f17354aaa177f2efdc60bd3 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -583,6 +583,7 @@ func (h *HTTPBin) Cache(w http.ResponseWriter, r *http.Request) { seconds, err := strconv.ParseInt(parts[2], 10, 64) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) + return } w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds)) } @@ -604,8 +605,36 @@ func (h *HTTPBin) CacheControl(w http.ResponseWriter, r *http.Request) { seconds, err := strconv.ParseInt(parts[2], 10, 64) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) + return } w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds)) h.Get(w, r) } + +// ETag assumes the resource has the given etag and response to If-None-Match +// and If-Match headers appropriately. +func (h *HTTPBin) ETag(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + if len(parts) != 3 { + http.Error(w, "Not found", http.StatusNotFound) + return + } + + etag := parts[2] + w.Header().Set("ETag", fmt.Sprintf(`"%s"`, etag)) + + // TODO: This mostly duplicates the work of Get() above, should this be + // pulled into a little helper? + resp := &getResponse{ + Args: r.URL.Query(), + Headers: r.Header, + Origin: getOrigin(r), + URL: getURL(r).String(), + } + body, _ := json.Marshal(resp) + + // Let http.ServeContent deal with If-None-Match and If-Match headers: + // https://golang.org/pkg/net/http/#ServeContent + http.ServeContent(w, r, "response.json", time.Now(), bytes.NewReader(body)) +} diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index d73234da9a910f6288a4fd535ca04182bd7fd97d..0580736d37d07fe599284647458537f25e452918 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -1589,8 +1589,6 @@ func TestCache(t *testing.T) { } }) - // Note: httpbin rejects these requests with invalid range headers, but the - // go stdlib just ignores them. var tests = []struct { headerKey string headerVal string @@ -1621,8 +1619,6 @@ func TestCacheControl(t *testing.T) { assertHeader(t, w, "Cache-Control", "public, max-age=60") }) - // Note: httpbin rejects these requests with invalid range headers, but the - // go stdlib just ignores them. var badTests = []struct { url string expectedStatus int @@ -1640,3 +1636,58 @@ func TestCacheControl(t *testing.T) { }) } } + +func TestETag(t *testing.T) { + t.Run("ok_no_headers", func(t *testing.T) { + url := "/etag/abc" + r, _ := http.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + assertStatusCode(t, w, http.StatusOK) + assertHeader(t, w, "ETag", `"abc"`) + }) + + var tests = []struct { + name string + etag string + headerKey string + headerVal string + expectedStatus int + }{ + {"if_none_match_matches", "abc", "If-None-Match", `"abc"`, http.StatusNotModified}, + {"if_none_match_matches_list", "abc", "If-None-Match", `"123", "abc"`, http.StatusNotModified}, + {"if_none_match_matches_star", "abc", "If-None-Match", "*", http.StatusNotModified}, + {"if_none_match_matches_w_prefix", "c3piozzzz", "If-None-Match", `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"`, http.StatusNotModified}, + {"if_none_match_has_no_match", "abc", "If-None-Match", `"123"`, http.StatusOK}, + + {"if_match_matches", "abc", "If-Match", `"abc"`, http.StatusOK}, + {"if_match_matches_list", "abc", "If-Match", `"123", "abc"`, http.StatusOK}, + {"if_match_matches_star", "abc", "If-Match", "*", http.StatusOK}, + {"if_match_has_no_match", "abc", "If-Match", `"xxxxxx"`, http.StatusPreconditionFailed}, + } + for _, test := range tests { + t.Run("ok_"+test.name, func(t *testing.T) { + url := "/etag/" + test.etag + r, _ := http.NewRequest("GET", url, nil) + r.Header.Add(test.headerKey, test.headerVal) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + assertStatusCode(t, w, test.expectedStatus) + }) + } + + var badTests = []struct { + url string + expectedStatus int + }{ + {"/etag/foo/bar", http.StatusNotFound}, + } + for _, test := range badTests { + t.Run(fmt.Sprintf("bad/%s", test.url), func(t *testing.T) { + r, _ := http.NewRequest("GET", test.url, nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + assertStatusCode(t, w, test.expectedStatus) + }) + } +} diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index 679c4710276d005c086c3893dfaf185ff57a8e74..70da32ee1d1a7fc099d35e675c469f55d8eb96bd 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -128,6 +128,7 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/cache", h.Cache) mux.HandleFunc("/cache/", h.CacheControl) + mux.HandleFunc("/etag/", h.ETag) // Make sure our ServeMux doesn't "helpfully" redirect these invalid // endpoints by adding a trailing slash. See the ServeMux docs for more