Skip to content
Snippets Groups Projects
Commit 05a30fa7 authored by Will McCutchen's avatar Will McCutchen
Browse files

Add /drip

parent 355c0a67
No related branches found
No related tags found
No related merge requests found
...@@ -448,3 +448,71 @@ func (h *HTTPBin) Delay(w http.ResponseWriter, r *http.Request) { ...@@ -448,3 +448,71 @@ func (h *HTTPBin) Delay(w http.ResponseWriter, r *http.Request) {
<-time.After(delay) <-time.After(delay)
h.RequestWithBody(w, r) h.RequestWithBody(w, r)
} }
// Drip returns data over a duration after an optional initial delay, then
// (optionally) returns with the given status code.
func (h *HTTPBin) Drip(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
duration := time.Duration(0)
delay := time.Duration(0)
numbytes := int64(10)
code := http.StatusOK
var err error
userDuration := q.Get("duration")
if userDuration != "" {
duration, err = parseBoundedDuration(userDuration, 0, h.options.MaxResponseTime)
if err != nil {
http.Error(w, "Invalid duration", http.StatusBadRequest)
return
}
}
userDelay := q.Get("delay")
if userDelay != "" {
delay, err = parseBoundedDuration(userDelay, 0, h.options.MaxResponseTime)
if err != nil {
http.Error(w, "Invalid delay", http.StatusBadRequest)
return
}
}
userNumBytes := q.Get("numbytes")
if userNumBytes != "" {
numbytes, err = strconv.ParseInt(userNumBytes, 10, 64)
if err != nil || numbytes <= 0 || numbytes > h.options.MaxResponseSize {
http.Error(w, "Invalid numbytes", http.StatusBadRequest)
return
}
}
userCode := q.Get("code")
if userCode != "" {
code, err = strconv.Atoi(userCode)
if err != nil || code < 100 || code >= 600 {
http.Error(w, "Invalid code", http.StatusBadRequest)
return
}
}
if duration+delay > h.options.MaxResponseTime {
http.Error(w, "Too much time", http.StatusBadRequest)
return
}
pause := duration / time.Duration(numbytes)
<-time.After(delay)
w.WriteHeader(code)
w.Header().Set("Content-Type", "application/octet-stream")
f := w.(http.Flusher)
for i := int64(0); i < numbytes; i++ {
w.Write([]byte("*"))
f.Flush()
<-time.After(pause)
}
}
...@@ -20,10 +20,12 @@ import ( ...@@ -20,10 +20,12 @@ import (
) )
const maxMemory int64 = 1024 * 1024 const maxMemory int64 = 1024 * 1024
const maxResponseSize = 1024
const maxResponseTime time.Duration = 1 * time.Second const maxResponseTime time.Duration = 1 * time.Second
var app = NewHTTPBin(&Options{ var app = NewHTTPBin(&Options{
MaxMemory: maxMemory, MaxMemory: maxMemory,
MaxResponseSize: maxResponseSize,
MaxResponseTime: maxResponseTime, MaxResponseTime: maxResponseTime,
}) })
...@@ -1291,3 +1293,104 @@ func TestDelay(t *testing.T) { ...@@ -1291,3 +1293,104 @@ func TestDelay(t *testing.T) {
}) })
} }
} }
func TestDrip(t *testing.T) {
var okTests = []struct {
params *url.Values
duration time.Duration
numbytes int
code int
}{
// there are useful defaults for all values
{&url.Values{}, 0, 10, http.StatusOK},
// go-style durations are accepted
{&url.Values{"duration": {"5ms"}}, 5 * time.Millisecond, 10, http.StatusOK},
{&url.Values{"duration": {"0h"}}, 0, 10, http.StatusOK},
{&url.Values{"delay": {"5ms"}}, 5 * time.Millisecond, 10, http.StatusOK},
{&url.Values{"delay": {"0h"}}, 0, 10, http.StatusOK},
// or floating point seconds
{&url.Values{"duration": {"0.25"}}, 250 * time.Millisecond, 10, http.StatusOK},
{&url.Values{"duration": {"0"}}, 0, 10, http.StatusOK},
{&url.Values{"duration": {"1"}}, 1 * time.Second, 10, http.StatusOK},
{&url.Values{"delay": {"0.25"}}, 250 * time.Millisecond, 10, http.StatusOK},
{&url.Values{"delay": {"0"}}, 0, 10, http.StatusOK},
{&url.Values{"numbytes": {"1"}}, 0, 1, http.StatusOK},
{&url.Values{"numbytes": {"101"}}, 0, 101, http.StatusOK},
{&url.Values{"numbytes": {fmt.Sprintf("%d", maxResponseSize)}}, 0, maxResponseSize, http.StatusOK},
{&url.Values{"code": {"100"}}, 0, 10, 100},
{&url.Values{"code": {"404"}}, 0, 10, 404},
{&url.Values{"code": {"599"}}, 0, 10, 599},
{&url.Values{"code": {"567"}}, 0, 10, 567},
{&url.Values{"duration": {"750ms"}, "delay": {"250ms"}}, 1 * time.Second, 10, http.StatusOK},
{&url.Values{"duration": {"250ms"}, "delay": {"0.25s"}}, 500 * time.Millisecond, 10, http.StatusOK},
}
for _, test := range okTests {
t.Run(fmt.Sprintf("ok/%s", test.params.Encode()), func(t *testing.T) {
url := "/drip?" + test.params.Encode()
start := time.Now()
r, _ := http.NewRequest("GET", url, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
elapsed := time.Now().Sub(start)
assertHeader(t, w, "Content-Type", "application/octet-stream")
assertStatusCode(t, w, test.code)
if len(w.Body.Bytes()) != test.numbytes {
t.Fatalf("expected %d bytes, got %d", test.numbytes, len(w.Body.Bytes()))
}
if elapsed < test.duration {
t.Fatalf("expected minimum duration of %s, request took %s", test.duration, elapsed)
}
})
}
var badTests = []struct {
params *url.Values
code int
}{
{&url.Values{"duration": {"1m"}}, http.StatusBadRequest},
{&url.Values{"duration": {"-1ms"}}, http.StatusBadRequest},
{&url.Values{"duration": {"1001"}}, http.StatusBadRequest},
{&url.Values{"duration": {"-1"}}, http.StatusBadRequest},
{&url.Values{"duration": {"foo"}}, http.StatusBadRequest},
{&url.Values{"delay": {"1m"}}, http.StatusBadRequest},
{&url.Values{"delay": {"-1ms"}}, http.StatusBadRequest},
{&url.Values{"delay": {"1001"}}, http.StatusBadRequest},
{&url.Values{"delay": {"-1"}}, http.StatusBadRequest},
{&url.Values{"delay": {"foo"}}, http.StatusBadRequest},
{&url.Values{"numbytes": {"foo"}}, http.StatusBadRequest},
{&url.Values{"numbytes": {"0"}}, http.StatusBadRequest},
{&url.Values{"numbytes": {"-1"}}, http.StatusBadRequest},
{&url.Values{"numbytes": {"0xff"}}, http.StatusBadRequest},
{&url.Values{"numbytes": {fmt.Sprintf("%d", maxResponseSize+1)}}, http.StatusBadRequest},
{&url.Values{"code": {"foo"}}, http.StatusBadRequest},
{&url.Values{"code": {"-1"}}, http.StatusBadRequest},
{&url.Values{"code": {"25"}}, http.StatusBadRequest},
{&url.Values{"code": {"600"}}, http.StatusBadRequest},
// request would take too long
{&url.Values{"duration": {"750ms"}, "delay": {"500ms"}}, http.StatusBadRequest},
}
for _, test := range badTests {
t.Run(fmt.Sprintf("bad/%s", test.params.Encode()), func(t *testing.T) {
url := "/drip?" + test.params.Encode()
r, _ := http.NewRequest("GET", url, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
assertStatusCode(t, w, test.code)
})
}
}
...@@ -106,7 +106,8 @@ func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse, maxMe ...@@ -106,7 +106,8 @@ func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse, maxMe
} }
// parseDuration takes a user's input as a string and attempts to convert it // parseDuration takes a user's input as a string and attempts to convert it
// into a time.Duration // into a time.Duration. If not given as a go-style duration string, the input
// is assumed to be seconds as a float.
func parseDuration(input string) (time.Duration, error) { func parseDuration(input string) (time.Duration, error) {
d, err := time.ParseDuration(input) d, err := time.ParseDuration(input)
if err != nil { if err != nil {
......
...@@ -73,6 +73,7 @@ type streamResponse struct { ...@@ -73,6 +73,7 @@ type streamResponse struct {
// Options are used to configure HTTPBin // Options are used to configure HTTPBin
type Options struct { type Options struct {
MaxMemory int64 MaxMemory int64
MaxResponseSize int64
MaxResponseTime time.Duration MaxResponseTime time.Duration
} }
...@@ -118,6 +119,7 @@ func (h *HTTPBin) Handler() http.Handler { ...@@ -118,6 +119,7 @@ func (h *HTTPBin) Handler() http.Handler {
mux.HandleFunc("/stream/", h.Stream) mux.HandleFunc("/stream/", h.Stream)
mux.HandleFunc("/delay/", h.Delay) mux.HandleFunc("/delay/", h.Delay)
mux.HandleFunc("/drip", h.Drip)
// Make sure our ServeMux doesn't "helpfully" redirect these invalid // Make sure our ServeMux doesn't "helpfully" redirect these invalid
// endpoints by adding a trailing slash. See the ServeMux docs for more // endpoints by adding a trailing slash. See the ServeMux docs for more
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
func main() { func main() {
h := httpbin.NewHTTPBin(&httpbin.Options{ h := httpbin.NewHTTPBin(&httpbin.Options{
MaxMemory: 1024 * 1024 * 5, MaxMemory: 1024 * 1024 * 5,
MaxResponseSize: 1024 * 1024,
MaxResponseTime: 10 * time.Second, MaxResponseTime: 10 * time.Second,
}) })
log.Printf("listening on 9999") log.Printf("listening on 9999")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment