Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add /trailers endpoint #184

Merged
merged 1 commit into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion httpbin/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ func (h *HTTPBin) Anything(w http.ResponseWriter, r *http.Request) {
h.RequestWithBody(w, r)
}

// RequestWithBody handles POST, PUT, and PATCH requests
// RequestWithBody handles POST, PUT, and PATCH requests by responding with a
// JSON representation of the incoming request.
func (h *HTTPBin) RequestWithBody(w http.ResponseWriter, r *http.Request) {
resp := &bodyResponse{
Args: r.URL.Query(),
Expand Down Expand Up @@ -548,6 +549,49 @@ func (h *HTTPBin) Stream(w http.ResponseWriter, r *http.Request) {
}
}

// set of keys that may not be specified in trailers, per
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer#directives
var forbiddenTrailers = map[string]struct{}{
http.CanonicalHeaderKey("Authorization"): {},
http.CanonicalHeaderKey("Cache-Control"): {},
http.CanonicalHeaderKey("Content-Encoding"): {},
http.CanonicalHeaderKey("Content-Length"): {},
http.CanonicalHeaderKey("Content-Range"): {},
http.CanonicalHeaderKey("Content-Type"): {},
http.CanonicalHeaderKey("Host"): {},
http.CanonicalHeaderKey("Max-Forwards"): {},
http.CanonicalHeaderKey("Set-Cookie"): {},
http.CanonicalHeaderKey("TE"): {},
http.CanonicalHeaderKey("Trailer"): {},
http.CanonicalHeaderKey("Transfer-Encoding"): {},
}

// Trailers adds the header keys and values specified in the request's query
// parameters as HTTP trailers in the response.
//
// Trailers are returned in canonical form. Any forbidden trailer will result
// in an error.
func (h *HTTPBin) Trailers(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
// ensure all requested trailers are allowed
for k := range q {
if _, found := forbiddenTrailers[http.CanonicalHeaderKey(k)]; found {
writeError(w, http.StatusBadRequest, fmt.Errorf("forbidden trailer: %s", k))
return
}
}
for k := range q {
w.Header().Add("Trailer", k)
}
h.RequestWithBody(w, r)
w.(http.Flusher).Flush() // force chunked transfer encoding even when no trailers are given
for k, vs := range q {
for _, v := range vs {
w.Header().Set(k, v)
}
}
}

// Delay waits for a given amount of time before responding, where the time may
// be specified as a golang-style duration or seconds in floating point.
func (h *HTTPBin) Delay(w http.ResponseWriter, r *http.Request) {
Expand Down
53 changes: 53 additions & 0 deletions httpbin/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,59 @@ func TestStream(t *testing.T) {
}
}

func TestTrailers(t *testing.T) {
t.Parallel()

testCases := []struct {
url string
wantStatus int
wantTrailers http.Header
}{
{
"/trailers",
http.StatusOK,
nil,
},
{
"/trailers?test-trailer-1=v1&Test-Trailer-2=v2",
http.StatusOK,
// note that response headers are canonicalized
http.Header{"Test-Trailer-1": {"v1"}, "Test-Trailer-2": {"v2"}},
},
{
"/trailers?test-trailer-1&Authorization=Bearer",
http.StatusBadRequest,
nil,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.url, func(t *testing.T) {
t.Parallel()

req := newTestRequest(t, "GET", tc.url)
resp := must.DoReq(t, client, req)

assert.StatusCode(t, resp, tc.wantStatus)
if tc.wantStatus != http.StatusOK {
return
}

// trailers only sent w/ chunked transfer encoding
assert.DeepEqual(t, resp.TransferEncoding, []string{"chunked"}, "expected Transfer-Encoding: chunked")

// must read entire body to get trailers
body := must.ReadAll(t, resp.Body)

// don't really care about the contents, as long as body can be
// unmarshaled into the correct type
must.Unmarshal[bodyResponse](t, strings.NewReader(body))

assert.DeepEqual(t, resp.Trailer, tc.wantTrailers, "trailers mismatch")
})
}
}

func TestDelay(t *testing.T) {
t.Parallel()

Expand Down
1 change: 1 addition & 0 deletions httpbin/httpbin.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ func (h *HTTPBin) Handler() http.Handler {
mux.HandleFunc("/status/{code}", h.Status)
mux.HandleFunc("/stream-bytes/{numBytes}", h.StreamBytes)
mux.HandleFunc("/stream/{numLines}", h.Stream)
mux.HandleFunc("/trailers", h.Trailers)
mux.HandleFunc("/unstable", h.Unstable)
mux.HandleFunc("/user-agent", h.UserAgent)
mux.HandleFunc("/uuid", h.UUID)
Expand Down
1 change: 1 addition & 0 deletions httpbin/static/index.html.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
<li><a href="{{.Prefix}}/status/418"><code>{{.Prefix}}/status/:code</code></a> Returns given HTTP Status code.</li>
<li><a href="{{.Prefix}}/stream-bytes/1024"><code>{{.Prefix}}/stream-bytes/:n</code></a> Streams <em>n</em> random bytes of binary data, accepts optional <em>seed</em> and <em>chunk_size</em> integer parameters.</li>
<li><a href="{{.Prefix}}/stream/20"><code>{{.Prefix}}/stream/:n</code></a> Streams <em>min(n, 100)</em> lines.</li>
<li><a href="{{.Prefix}}/trailers?trailer1=value1&amp;trailer2=value2"><code>{{.Prefix}}/trailers?key=val</code></a> Returns JSON response with query params added as HTTP Trailers.</li>
<li><a href="{{.Prefix}}/unstable"><code>{{.Prefix}}/unstable</code></a> Fails half the time, accepts optional <em>failure_rate</em> float and <em>seed</em> integer parameters.</li>
<li><a href="{{.Prefix}}/user-agent"><code>{{.Prefix}}/user-agent</code></a> Returns user-agent.</li>
<li><a href="{{.Prefix}}/uuid"><code>{{.Prefix}}/uuid</code></a> Generates a <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier">UUIDv4</a> value.</li>
Expand Down
Loading