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

Add /range

parent 7d42369a
No related branches found
No related tags found
No related merge requests found
...@@ -516,3 +516,36 @@ func (h *HTTPBin) Drip(w http.ResponseWriter, r *http.Request) { ...@@ -516,3 +516,36 @@ func (h *HTTPBin) Drip(w http.ResponseWriter, r *http.Request) {
<-time.After(pause) <-time.After(pause)
} }
} }
// Range returns up to N bytes, with support for HTTP Range requests.
//
// This departs from httpbin by not supporting the chunk_size or duration
// parameters.
func (h *HTTPBin) Range(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")
if len(parts) != 3 {
http.Error(w, "Not found", http.StatusNotFound)
return
}
numBytes, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Add("ETag", fmt.Sprintf("range%d", numBytes))
w.Header().Add("Accept-Ranges", "bytes")
if numBytes <= 0 || numBytes > h.options.MaxMemory {
http.Error(w, "Invalid number of bytes", http.StatusBadRequest)
return
}
content := &syntheticReadSeeker{
numBytes: numBytes,
byteFactory: func(offset int64) byte { return byte(97 + (offset % 26)) },
}
var modtime time.Time
http.ServeContent(w, r, "", modtime, content)
}
...@@ -53,6 +53,13 @@ func assertBodyContains(t *testing.T, w *httptest.ResponseRecorder, needle strin ...@@ -53,6 +53,13 @@ func assertBodyContains(t *testing.T, w *httptest.ResponseRecorder, needle strin
} }
} }
func assertBodyEquals(t *testing.T, w *httptest.ResponseRecorder, want string) {
have := w.Body.String()
if want != have {
t.Fatalf("expected body = %v, got %v", want, have)
}
}
func TestNewHTTPBin__NilOptions(t *testing.T) { func TestNewHTTPBin__NilOptions(t *testing.T) {
h := NewHTTPBin(nil) h := NewHTTPBin(nil)
if h.options.MaxMemory != 0 { if h.options.MaxMemory != 0 {
...@@ -1394,3 +1401,136 @@ func TestDrip(t *testing.T) { ...@@ -1394,3 +1401,136 @@ func TestDrip(t *testing.T) {
}) })
} }
} }
func TestRange(t *testing.T) {
t.Run("ok_no_range", func(t *testing.T) {
url := "/range/1234"
r, _ := http.NewRequest("GET", url, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
assertStatusCode(t, w, http.StatusOK)
assertHeader(t, w, "ETag", "range1234")
assertHeader(t, w, "Accept-Ranges", "bytes")
assertHeader(t, w, "Content-Length", "1234")
assertContentType(t, w, "text/plain; charset=utf-8")
if len(w.Body.String()) != 1234 {
t.Errorf("expected content length 1234, got %d", len(w.Body.String()))
}
})
t.Run("ok_range", func(t *testing.T) {
url := "/range/100"
r, _ := http.NewRequest("GET", url, nil)
r.Header.Add("Range", "bytes=10-24")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
assertStatusCode(t, w, http.StatusPartialContent)
assertHeader(t, w, "ETag", "range100")
assertHeader(t, w, "Accept-Ranges", "bytes")
assertHeader(t, w, "Content-Length", "15")
assertHeader(t, w, "Content-Range", "bytes 10-24/100")
assertBodyEquals(t, w, "klmnopqrstuvwxy")
})
t.Run("ok_range_first_16_bytes", func(t *testing.T) {
url := "/range/1000"
r, _ := http.NewRequest("GET", url, nil)
r.Header.Add("Range", "bytes=0-15")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
assertStatusCode(t, w, http.StatusPartialContent)
assertHeader(t, w, "ETag", "range1000")
assertHeader(t, w, "Accept-Ranges", "bytes")
assertHeader(t, w, "Content-Length", "16")
assertHeader(t, w, "Content-Range", "bytes 0-15/1000")
assertBodyEquals(t, w, "abcdefghijklmnop")
})
t.Run("ok_range_open_ended_last_6_bytes", func(t *testing.T) {
url := "/range/26"
r, _ := http.NewRequest("GET", url, nil)
r.Header.Add("Range", "bytes=20-")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
assertStatusCode(t, w, http.StatusPartialContent)
assertHeader(t, w, "ETag", "range26")
assertHeader(t, w, "Content-Length", "6")
assertHeader(t, w, "Content-Range", "bytes 20-25/26")
assertBodyEquals(t, w, "uvwxyz")
})
t.Run("ok_range_suffix", func(t *testing.T) {
url := "/range/26"
r, _ := http.NewRequest("GET", url, nil)
r.Header.Add("Range", "bytes=-5")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
t.Logf("headers = %v", w.HeaderMap)
assertStatusCode(t, w, http.StatusPartialContent)
assertHeader(t, w, "ETag", "range26")
assertHeader(t, w, "Content-Length", "5")
assertHeader(t, w, "Content-Range", "bytes 21-25/26")
assertBodyEquals(t, w, "vwxyz")
})
t.Run("err_range_out_of_bounds", func(t *testing.T) {
url := "/range/26"
r, _ := http.NewRequest("GET", url, nil)
r.Header.Add("Range", "bytes=-5")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
assertStatusCode(t, w, http.StatusPartialContent)
assertHeader(t, w, "ETag", "range26")
assertHeader(t, w, "Content-Length", "5")
assertHeader(t, w, "Content-Range", "bytes 21-25/26")
assertBodyEquals(t, w, "vwxyz")
})
// Note: httpbin rejects these requests with invalid range headers, but the
// go stdlib just ignores them.
var badRangeTests = []struct {
url string
rangeHeader string
}{
{"/range/26", "bytes=10-5"},
{"/range/26", "bytes=32-40"},
{"/range/26", "bytes=0-40"},
}
for _, test := range badRangeTests {
t.Run(fmt.Sprintf("ok_bad_range_header/%s", test.rangeHeader), func(t *testing.T) {
r, _ := http.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
assertStatusCode(t, w, http.StatusOK)
assertBodyEquals(t, w, "abcdefghijklmnopqrstuvwxyz")
})
}
var badTests = []struct {
url string
code int
}{
{"/range/1/foo", http.StatusNotFound},
{"/range/", http.StatusBadRequest},
{"/range/foo", http.StatusBadRequest},
{"/range/1.5", http.StatusBadRequest},
{"/range/-1", 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)
})
}
}
...@@ -2,7 +2,9 @@ package httpbin ...@@ -2,7 +2,9 @@ package httpbin
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
...@@ -135,3 +137,49 @@ func parseBoundedDuration(input string, min, max time.Duration) (time.Duration, ...@@ -135,3 +137,49 @@ func parseBoundedDuration(input string, min, max time.Duration) (time.Duration,
} }
return d, err return d, err
} }
// syntheticReadSeeker implements the ReadSeeker interface to allow reading
// arbitrary subsets of bytes up to a maximum size given a function for
// generating the byte at a given offset.
type syntheticReadSeeker struct {
numBytes int64
offset int64
byteFactory func(int64) byte
}
// Read implements the Reader interface for syntheticReadSeeker
func (s *syntheticReadSeeker) Read(p []byte) (int, error) {
start := s.offset
end := start + int64(len(p))
var err error
if end > s.numBytes {
err = io.EOF
end = s.numBytes - start
}
for idx := start; idx < end; idx++ {
p[idx-start] = s.byteFactory(idx)
}
return int(end - start), err
}
// Seek implements the Seeker interface for syntheticReadSeeker
func (s *syntheticReadSeeker) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
s.offset = offset
case io.SeekCurrent:
s.offset += offset
case io.SeekEnd:
s.offset = s.numBytes - offset
default:
return 0, errors.New("Seek: invalid whence")
}
if s.offset < 0 {
return 0, errors.New("Seek: invalid offset")
}
return s.offset, nil
}
...@@ -120,6 +120,7 @@ func (h *HTTPBin) Handler() http.Handler { ...@@ -120,6 +120,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) mux.HandleFunc("/drip", h.Drip)
mux.HandleFunc("/range/", h.Range)
// 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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment