Skip to content

Commit

Permalink
refactor: minor tweaks to /drip implementation (#185)
Browse files Browse the repository at this point in the history
A handful of drive-by tweaks to the `/drip` implementation:
- Update docs to clarify intended uses (a helpful reminder for myself,
  if nothing else)
- Ensure all bad requests use standard JSON error responses
- Switch from binary to text content type

This also includes a small fix to the internal testing library, dropping
the `assert.RoughDuration()` helper in favor of an updated generic
`assert.RoughlyEqual()` implementation that works with the
`time.Duration` type.
  • Loading branch information
mccutchen authored Sep 17, 2024
1 parent 24529f4 commit dc8fb20
Show file tree
Hide file tree
Showing 5 changed files with 28 additions and 24 deletions.
23 changes: 15 additions & 8 deletions httpbin/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,8 +610,15 @@ func (h *HTTPBin) Delay(w http.ResponseWriter, r *http.Request) {
h.RequestWithBody(w, r)
}

// Drip returns data over a duration after an optional initial delay, then
// (optionally) returns with the given status code.
// Drip simulates a slow HTTP server by writing data over a given duration
// after an optional initial delay.
//
// Because this endpoint is intended to simulate a slow HTTP connection, it
// intentionally does NOT use chunked transfer encoding even though its
// implementation writes the response incrementally.
//
// See Stream (/stream) or StreamBytes (/stream-bytes) for endpoints that
// respond using chunked transfer encoding.
func (h *HTTPBin) Drip(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()

Expand Down Expand Up @@ -660,7 +667,7 @@ func (h *HTTPBin) Drip(w http.ResponseWriter, r *http.Request) {
}

if duration+delay > h.MaxDuration {
http.Error(w, "Too much time", http.StatusBadRequest)
writeError(w, http.StatusBadRequest, fmt.Errorf("too much time: %v+%v > %v", duration, delay, h.MaxDuration))
return
}

Expand All @@ -682,23 +689,23 @@ func (h *HTTPBin) Drip(w http.ResponseWriter, r *http.Request) {
}
}

w.Header().Set("Content-Type", binaryContentType)
w.Header().Set("Content-Type", textContentType)
w.Header().Set("Content-Length", fmt.Sprintf("%d", numBytes))
w.WriteHeader(code)

// what we write with each increment of the ticker
b := []byte{'*'}

// special case when we do not need to pause between each write
if pause == 0 {
for i := int64(0); i < numBytes; i++ {
w.Write([]byte{'*'})
}
w.Write(bytes.Repeat(b, int(numBytes)))
return
}

// otherwise, write response body byte-by-byte
ticker := time.NewTicker(pause)
defer ticker.Stop()

b := []byte{'*'}
flusher := w.(http.Flusher)
for i := int64(0); i < numBytes; i++ {
w.Write(b)
Expand Down
8 changes: 4 additions & 4 deletions httpbin/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ func testRequestWithBodyExpect100Continue(t *testing.T, verb, path string) {
})

t.Run("zero content-length ignored", func(t *testing.T) {
// The Go stdlib's Expect:100-continue handling requires either a a)
// The Go stdlib's Expect:100-continue handling requires either a)
// non-zero Content-Length header or b) Transfer-Encoding:chunked
// header to be present. Otherwise, the Expect header is ignored and
// the request is processed normally.
Expand Down Expand Up @@ -2037,7 +2037,7 @@ func TestDrip(t *testing.T) {
elapsed := time.Since(start)

assert.StatusCode(t, resp, test.code)
assert.ContentType(t, resp, binaryContentType)
assert.ContentType(t, resp, textContentType)
assert.Header(t, resp, "Content-Length", strconv.Itoa(test.numbytes))
if elapsed < test.duration {
t.Fatalf("expected minimum duration of %s, request took %s", test.duration, elapsed)
Expand Down Expand Up @@ -2125,7 +2125,7 @@ func TestDrip(t *testing.T) {
// (allowing for minor mismatch in local timers and server timers)
// after the first byte.
if i > 0 {
assert.RoughDuration(t, gotPause, wantPauseBetweenWrites, 3*time.Millisecond)
assert.RoughlyEqual(t, gotPause, wantPauseBetweenWrites, 3*time.Millisecond)
}
}

Expand Down Expand Up @@ -3240,7 +3240,7 @@ func TestSSE(t *testing.T) {
// (allowing for minor mismatch in local timers and server timers)
// after the first byte.
if i > 0 {
assert.RoughDuration(t, gotPause, wantPauseBetweenWrites, 3*time.Millisecond)
assert.RoughlyEqual(t, gotPause, wantPauseBetweenWrites, 3*time.Millisecond)
}

eventCount++
Expand Down
2 changes: 1 addition & 1 deletion httpbin/static/index.html.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
<li><a href="{{.Prefix}}/deny"><code>{{.Prefix}}/deny</code></a> Denied by robots.txt file.</li>
<li><a href="{{.Prefix}}/digest-auth/auth/user/password"><code>{{.Prefix}}/digest-auth/:qop/:user/:password</code></a> Challenges HTTP Digest Auth using default MD5 algorithm</li>
<li><a href="{{.Prefix}}/digest-auth/auth/user/password/SHA-256"><code>{{.Prefix}}/digest-auth/:qop/:user/:password/:algorithm</code></a> Challenges HTTP Digest Auth using specified algorithm (MD5 or SHA-256)</li>
<li><a href="{{.Prefix}}/drip?code=200&amp;numbytes=5&amp;duration=5"><code>{{.Prefix}}/drip?numbytes=n&amp;duration=s&amp;delay=s&amp;code=code</code></a> Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code.</li>
<li><a href="{{.Prefix}}/drip?code=200&amp;numbytes=5&amp;duration=5"><code>{{.Prefix}}/drip?numbytes=n&amp;duration=s&amp;delay=s&amp;code=code</code></a> Drips data over the given duration after an optional initial delay, simulating a slow HTTP server.</li>
<li><a href="{{.Prefix}}/dump/request"><code>{{.Prefix}}/dump/request</code></a> Returns the given request in its HTTP/1.x wire approximate representation.</li>
<li><a href="{{.Prefix}}/encoding/utf8"><code>{{.Prefix}}/encoding/utf8</code></a> Returns page containing UTF-8 data.</li>
<li><a href="{{.Prefix}}/etag/etag"><code>{{.Prefix}}/etag/:etag</code></a> Assumes the resource has the given etag and responds to If-None-Match header with a 200 or 304 and If-Match with a 200 or 412 as appropriate.</li>
Expand Down
6 changes: 3 additions & 3 deletions httpbin/websocket/websocket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ func TestConnectionLimits(t *testing.T) {
elapsed := time.Since(start)

assert.Error(t, err, io.EOF)
assert.RoughDuration(t, elapsed, maxDuration, 25*time.Millisecond)
assert.RoughlyEqual(t, elapsed, maxDuration, 25*time.Millisecond)
}
})

Expand Down Expand Up @@ -363,11 +363,11 @@ func TestConnectionLimits(t *testing.T) {
conn.Close()

assert.Equal(t, os.IsTimeout(err), true, "expected timeout error")
assert.RoughDuration(t, elapsedClientTime, clientTimeout, 10*time.Millisecond)
assert.RoughlyEqual(t, elapsedClientTime, clientTimeout, 10*time.Millisecond)

// wait for the server to finish
wg.Wait()
assert.RoughDuration(t, elapsedServerTime, clientTimeout, 10*time.Millisecond)
assert.RoughlyEqual(t, elapsedServerTime, clientTimeout, 10*time.Millisecond)
}
})
}
Expand Down
13 changes: 5 additions & 8 deletions internal/testing/assert/assert.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,14 @@ func DurationRange(t *testing.T, got, min, max time.Duration) {
}
}

// RoughDuration asserts that a duration is within a certain tolerance of a
// given value.
func RoughDuration(t *testing.T, got, want time.Duration, tolerance time.Duration) {
t.Helper()
DurationRange(t, got, want-tolerance, want+tolerance)
type Number interface {
~int64 | ~float64
}

// RoughlyEqual asserts that a float64 is within a certain tolerance.
func RoughlyEqual(t *testing.T, got, want float64, epsilon float64) {
// RoughlyEqual asserts that a numeric value is within a certain tolerance.
func RoughlyEqual[T Number](t *testing.T, got, want T, epsilon T) {
t.Helper()
if got < want-epsilon || got > want+epsilon {
t.Fatalf("expected value between %f and %f, got %f", want-epsilon, want+epsilon, got)
t.Fatalf("expected value between %v and %v, got %v", want-epsilon, want+epsilon, got)
}
}

0 comments on commit dc8fb20

Please sign in to comment.