Skip to content

Commit 24529f4

Browse files
authored
feat: add /trailers endpoint (#184)
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.
1 parent 61d7feb commit 24529f4

File tree

4 files changed

+100
-1
lines changed

4 files changed

+100
-1
lines changed

httpbin/handlers.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ func (h *HTTPBin) Anything(w http.ResponseWriter, r *http.Request) {
7070
h.RequestWithBody(w, r)
7171
}
7272

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

552+
// set of keys that may not be specified in trailers, per
553+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer#directives
554+
var forbiddenTrailers = map[string]struct{}{
555+
http.CanonicalHeaderKey("Authorization"): {},
556+
http.CanonicalHeaderKey("Cache-Control"): {},
557+
http.CanonicalHeaderKey("Content-Encoding"): {},
558+
http.CanonicalHeaderKey("Content-Length"): {},
559+
http.CanonicalHeaderKey("Content-Range"): {},
560+
http.CanonicalHeaderKey("Content-Type"): {},
561+
http.CanonicalHeaderKey("Host"): {},
562+
http.CanonicalHeaderKey("Max-Forwards"): {},
563+
http.CanonicalHeaderKey("Set-Cookie"): {},
564+
http.CanonicalHeaderKey("TE"): {},
565+
http.CanonicalHeaderKey("Trailer"): {},
566+
http.CanonicalHeaderKey("Transfer-Encoding"): {},
567+
}
568+
569+
// Trailers adds the header keys and values specified in the request's query
570+
// parameters as HTTP trailers in the response.
571+
//
572+
// Trailers are returned in canonical form. Any forbidden trailer will result
573+
// in an error.
574+
func (h *HTTPBin) Trailers(w http.ResponseWriter, r *http.Request) {
575+
q := r.URL.Query()
576+
// ensure all requested trailers are allowed
577+
for k := range q {
578+
if _, found := forbiddenTrailers[http.CanonicalHeaderKey(k)]; found {
579+
writeError(w, http.StatusBadRequest, fmt.Errorf("forbidden trailer: %s", k))
580+
return
581+
}
582+
}
583+
for k := range q {
584+
w.Header().Add("Trailer", k)
585+
}
586+
h.RequestWithBody(w, r)
587+
w.(http.Flusher).Flush() // force chunked transfer encoding even when no trailers are given
588+
for k, vs := range q {
589+
for _, v := range vs {
590+
w.Header().Set(k, v)
591+
}
592+
}
593+
}
594+
551595
// Delay waits for a given amount of time before responding, where the time may
552596
// be specified as a golang-style duration or seconds in floating point.
553597
func (h *HTTPBin) Delay(w http.ResponseWriter, r *http.Request) {

httpbin/handlers_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,6 +1844,59 @@ func TestStream(t *testing.T) {
18441844
}
18451845
}
18461846

1847+
func TestTrailers(t *testing.T) {
1848+
t.Parallel()
1849+
1850+
testCases := []struct {
1851+
url string
1852+
wantStatus int
1853+
wantTrailers http.Header
1854+
}{
1855+
{
1856+
"/trailers",
1857+
http.StatusOK,
1858+
nil,
1859+
},
1860+
{
1861+
"/trailers?test-trailer-1=v1&Test-Trailer-2=v2",
1862+
http.StatusOK,
1863+
// note that response headers are canonicalized
1864+
http.Header{"Test-Trailer-1": {"v1"}, "Test-Trailer-2": {"v2"}},
1865+
},
1866+
{
1867+
"/trailers?test-trailer-1&Authorization=Bearer",
1868+
http.StatusBadRequest,
1869+
nil,
1870+
},
1871+
}
1872+
for _, tc := range testCases {
1873+
tc := tc
1874+
t.Run(tc.url, func(t *testing.T) {
1875+
t.Parallel()
1876+
1877+
req := newTestRequest(t, "GET", tc.url)
1878+
resp := must.DoReq(t, client, req)
1879+
1880+
assert.StatusCode(t, resp, tc.wantStatus)
1881+
if tc.wantStatus != http.StatusOK {
1882+
return
1883+
}
1884+
1885+
// trailers only sent w/ chunked transfer encoding
1886+
assert.DeepEqual(t, resp.TransferEncoding, []string{"chunked"}, "expected Transfer-Encoding: chunked")
1887+
1888+
// must read entire body to get trailers
1889+
body := must.ReadAll(t, resp.Body)
1890+
1891+
// don't really care about the contents, as long as body can be
1892+
// unmarshaled into the correct type
1893+
must.Unmarshal[bodyResponse](t, strings.NewReader(body))
1894+
1895+
assert.DeepEqual(t, resp.Trailer, tc.wantTrailers, "trailers mismatch")
1896+
})
1897+
}
1898+
}
1899+
18471900
func TestDelay(t *testing.T) {
18481901
t.Parallel()
18491902

httpbin/httpbin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ func (h *HTTPBin) Handler() http.Handler {
181181
mux.HandleFunc("/status/{code}", h.Status)
182182
mux.HandleFunc("/stream-bytes/{numBytes}", h.StreamBytes)
183183
mux.HandleFunc("/stream/{numLines}", h.Stream)
184+
mux.HandleFunc("/trailers", h.Trailers)
184185
mux.HandleFunc("/unstable", h.Unstable)
185186
mux.HandleFunc("/user-agent", h.UserAgent)
186187
mux.HandleFunc("/uuid", h.UUID)

httpbin/static/index.html.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
<li><a href="{{.Prefix}}/status/418"><code>{{.Prefix}}/status/:code</code></a> Returns given HTTP Status code.</li>
114114
<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>
115115
<li><a href="{{.Prefix}}/stream/20"><code>{{.Prefix}}/stream/:n</code></a> Streams <em>min(n, 100)</em> lines.</li>
116+
<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>
116117
<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>
117118
<li><a href="{{.Prefix}}/user-agent"><code>{{.Prefix}}/user-agent</code></a> Returns user-agent.</li>
118119
<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>

0 commit comments

Comments
 (0)