Skip to content

Commit

Permalink
feat: add /trailers endpoint (#184)
Browse files Browse the repository at this point in the history
This adds a new `/trailers` endpoint which allows clients to specify
trailer key/value pairs in the query parameters, similar to the existing
`/cookies/set` and `/response-headers` endpoints.

Per discussion on #72,
we'll likely add more useful trailers to some of the existing endpoints
as a follow-up.
  • Loading branch information
mccutchen authored Sep 16, 2024
1 parent 61d7feb commit 24529f4
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 1 deletion.
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

0 comments on commit 24529f4

Please sign in to comment.