From ad5dc45b036ea43b9eca8947a4bc42b527984972 Mon Sep 17 00:00:00 2001
From: Will McCutchen <will@mccutch.org>
Date: Sun, 18 Dec 2016 18:57:41 -0800
Subject: [PATCH] Add /stream/:n

---
 httpbin/handlers.go      | 35 +++++++++++++++++++++++
 httpbin/handlers_test.go | 62 ++++++++++++++++++++++++++++++++++++++++
 httpbin/httpbin.go       | 13 +++++++++
 3 files changed, 110 insertions(+)

diff --git a/httpbin/handlers.go b/httpbin/handlers.go
index 6dba5ee..c9255e4 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 8744a0a..a9e7aea 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 ebd84dd..5de8208 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))
 }
-- 
GitLab