diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 6dba5ee80dc18b6e416081d1b5ebe3288af10b00..c9255e40f7d14d9dd997fc4b2caaf6e2573b549a 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -390,3 +390,38 @@ func (h *HTTPBin) DigestAuth(w http.ResponseWriter, r *http.Request) { } http.Error(w, "Not Implemented", http.StatusNotImplemented) } + +// Stream responds with max(n, 100) lines of JSON-encoded request data. +func (h *HTTPBin) Stream(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + if len(parts) != 3 { + http.Error(w, "Not found", http.StatusNotFound) + return + } + n, err := strconv.Atoi(parts[2]) + if err != nil { + http.Error(w, "Invalid integer", http.StatusBadRequest) + } + + if n > 100 { + n = 100 + } else if n < 1 { + n = 1 + } + + resp := &streamResponse{ + Args: r.URL.Query(), + Headers: r.Header, + Origin: getOrigin(r), + URL: getURL(r).String(), + } + + f := w.(http.Flusher) + for i := 0; i < n; i++ { + resp.ID = i + line, _ := json.Marshal(resp) + w.Write(line) + w.Write([]byte("\n")) + f.Flush() + } +} diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index 8744a0a47dd95b8ada8befbd491bddd968acba33..a9e7aea0153ca209356fb85c7fd336c685239a41 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -1,6 +1,7 @@ package httpbin import ( + "bufio" "bytes" "compress/flate" "compress/gzip" @@ -1163,3 +1164,64 @@ func TestDeflate(t *testing.T) { t.Fatalf("expected compressed body") } } + +func TestStream(t *testing.T) { + var okTests = []struct { + url string + expectedLines int + }{ + {"/stream/20", 20}, + {"/stream/100", 100}, + {"/stream/1000", 100}, + {"/stream/0", 1}, + {"/stream/-100", 1}, + } + for _, test := range okTests { + t.Run("ok"+test.url, func(t *testing.T) { + r, _ := http.NewRequest("GET", test.url, nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + // The stdlib seems to automagically unchunk these responses and + // I'm not quite sure how to test this + // assertHeader(t, w, "Transfer-Encoding", "chunked") + + var resp *streamResponse + var err error + + i := 0 + scanner := bufio.NewScanner(w.Body) + for scanner.Scan() { + err = json.Unmarshal(scanner.Bytes(), &resp) + if err != nil { + t.Fatalf("error unmarshalling response: %s", err) + } + if resp.ID != i { + t.Fatalf("bad id: %v != %v", resp.ID, i) + } + i++ + } + if err := scanner.Err(); err != nil { + t.Fatalf("error scanning streaming response: %s", err) + } + }) + } + + var badTests = []struct { + url string + code int + }{ + {"/stream", http.StatusNotFound}, + {"/stream/foo", http.StatusBadRequest}, + {"/stream/3.1415", http.StatusBadRequest}, + } + + for _, test := range badTests { + t.Run("bad"+test.url, func(t *testing.T) { + r, _ := http.NewRequest("GET", test.url, nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + assertStatusCode(t, w, test.code) + }) + } +} diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index ebd84dd92cb2ae3afc722cdee19102cddc7a1058..5de82089129633e933cb0bcb8d5b9d61934ddc8c 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -59,6 +59,16 @@ type deflateResponse struct { Deflated bool `json:"deflated"` } +// An actual stream response body will be made up of one or more of these +// structs, encoded as JSON and separated by newlines +type streamResponse struct { + ID int `json:"id"` + Args url.Values `json:"args"` + Headers http.Header `json:"headers"` + Origin string `json:"origin"` + URL string `json:"url"` +} + // Options are used to configure HTTPBin type Options struct { MaxMemory int64 @@ -104,6 +114,8 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/deflate", h.Deflate) mux.HandleFunc("/gzip", h.Gzip) + mux.HandleFunc("/stream/", h.Stream) + // Make sure our ServeMux doesn't "helpfully" redirect these invalid // endpoints by adding a trailing slash. See the ServeMux docs for more // info: https://golang.org/pkg/net/http/#ServeMux @@ -114,6 +126,7 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/redirect", http.NotFound) mux.HandleFunc("/relative-redirect", http.NotFound) mux.HandleFunc("/status", http.NotFound) + mux.HandleFunc("/stream", http.NotFound) return logger(cors(mux)) }