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