Skip to content

Commit

Permalink
Compatibility immprovements (rakyll#89)
Browse files Browse the repository at this point in the history
- Handle bodies for GET requests to the /anything endpoint
- Do not encode HTML tags in serialized JSON
  • Loading branch information
anuraaga authored Nov 11, 2022
1 parent 4ec6c82 commit 683ef7f
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 27 deletions.
49 changes: 29 additions & 20 deletions httpbin/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ func (h *HTTPBin) Get(w http.ResponseWriter, r *http.Request) {
Origin: getClientIP(r),
URL: getURL(r).String(),
}
body, _ := json.Marshal(resp)
body, _ := jsonMarshalNoEscape(resp)
writeJSON(w, body, http.StatusOK)
}

// Anything returns anything that is passed to request.
func (h *HTTPBin) Anything(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET", "HEAD":
case "HEAD":
h.Get(w, r)
default:
h.RequestWithBody(w, r)
Expand All @@ -83,7 +83,7 @@ func (h *HTTPBin) RequestWithBody(w http.ResponseWriter, r *http.Request) {
return
}

body, _ := json.Marshal(resp)
body, _ := jsonMarshalNoEscape(resp)
writeJSON(w, body, http.StatusOK)
}

Expand All @@ -94,7 +94,7 @@ func (h *HTTPBin) Gzip(w http.ResponseWriter, r *http.Request) {
Origin: getClientIP(r),
Gzipped: true,
}
body, _ := json.Marshal(resp)
body, _ := jsonMarshalNoEscape(resp)

buf := &bytes.Buffer{}
gzw := gzip.NewWriter(buf)
Expand All @@ -114,7 +114,7 @@ func (h *HTTPBin) Deflate(w http.ResponseWriter, r *http.Request) {
Origin: getClientIP(r),
Deflated: true,
}
body, _ := json.Marshal(resp)
body, _ := jsonMarshalNoEscape(resp)

buf := &bytes.Buffer{}
w2 := zlib.NewWriter(buf)
Expand All @@ -129,23 +129,23 @@ func (h *HTTPBin) Deflate(w http.ResponseWriter, r *http.Request) {

// IP echoes the IP address of the incoming request
func (h *HTTPBin) IP(w http.ResponseWriter, r *http.Request) {
body, _ := json.Marshal(&ipResponse{
body, _ := jsonMarshalNoEscape(&ipResponse{
Origin: getClientIP(r),
})
writeJSON(w, body, http.StatusOK)
}

// UserAgent echoes the incoming User-Agent header
func (h *HTTPBin) UserAgent(w http.ResponseWriter, r *http.Request) {
body, _ := json.Marshal(&userAgentResponse{
body, _ := jsonMarshalNoEscape(&userAgentResponse{
UserAgent: r.Header.Get("User-Agent"),
})
writeJSON(w, body, http.StatusOK)
}

// Headers echoes the incoming request headers
func (h *HTTPBin) Headers(w http.ResponseWriter, r *http.Request) {
body, _ := json.Marshal(&headersResponse{
body, _ := jsonMarshalNoEscape(&headersResponse{
Headers: getRequestHeaders(r),
})
writeJSON(w, body, http.StatusOK)
Expand Down Expand Up @@ -175,7 +175,7 @@ func (h *HTTPBin) Status(w http.ResponseWriter, r *http.Request) {
"Location": "/redirect/1",
},
}
notAcceptableBody, _ := json.Marshal(map[string]interface{}{
notAcceptableBody, _ := jsonMarshalNoEscape(map[string]interface{}{
"message": "Client did not request a supported media type",
"accept": acceptedMediaTypes,
})
Expand Down Expand Up @@ -302,7 +302,7 @@ func (h *HTTPBin) ResponseHeaders(w http.ResponseWriter, r *http.Request) {
w.Header().Add(k, v)
}
}
body, _ := json.Marshal(args)
body, _ := jsonMarshalNoEscape(args)
if contentType := w.Header().Get("Content-Type"); contentType == "" {
w.Header().Set("Content-Type", jsonContentType)
}
Expand Down Expand Up @@ -400,7 +400,7 @@ func (h *HTTPBin) Cookies(w http.ResponseWriter, r *http.Request) {
for _, c := range r.Cookies() {
resp[c.Name] = c.Value
}
body, _ := json.Marshal(resp)
body, _ := jsonMarshalNoEscape(resp)
writeJSON(w, body, http.StatusOK)
}

Expand Down Expand Up @@ -455,7 +455,7 @@ func (h *HTTPBin) BasicAuth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", `Basic realm="Fake Realm"`)
}

body, _ := json.Marshal(&authResponse{
body, _ := jsonMarshalNoEscape(&authResponse{
Authorized: authorized,
User: givenUser,
})
Expand All @@ -481,7 +481,7 @@ func (h *HTTPBin) HiddenBasicAuth(w http.ResponseWriter, r *http.Request) {
return
}

body, _ := json.Marshal(&authResponse{
body, _ := jsonMarshalNoEscape(&authResponse{
Authorized: authorized,
User: givenUser,
})
Expand Down Expand Up @@ -517,9 +517,8 @@ func (h *HTTPBin) Stream(w http.ResponseWriter, r *http.Request) {
f := w.(http.Flusher)
for i := 0; i < n; i++ {
resp.ID = i
line, _ := json.Marshal(resp)
line, _ := jsonMarshalNoEscape(resp)
w.Write(line)
w.Write([]byte("\n"))
f.Flush()
}
}
Expand Down Expand Up @@ -728,7 +727,7 @@ func (h *HTTPBin) ETag(w http.ResponseWriter, r *http.Request) {
Origin: getClientIP(r),
URL: getURL(r).String(),
}
body, _ := json.Marshal(resp)
body, _ := jsonMarshalNoEscape(resp)

// Let http.ServeContent deal with If-None-Match and If-Match headers:
// https://golang.org/pkg/net/http/#ServeContent
Expand Down Expand Up @@ -954,7 +953,7 @@ func (h *HTTPBin) DigestAuth(w http.ResponseWriter, r *http.Request) {
return
}

resp, _ := json.Marshal(&authResponse{
resp, _ := jsonMarshalNoEscape(&authResponse{
Authorized: true,
User: user,
})
Expand All @@ -963,7 +962,7 @@ func (h *HTTPBin) DigestAuth(w http.ResponseWriter, r *http.Request) {

// UUID - responds with a generated UUID
func (h *HTTPBin) UUID(w http.ResponseWriter, r *http.Request) {
resp, _ := json.Marshal(&uuidResponse{
resp, _ := jsonMarshalNoEscape(&uuidResponse{
UUID: uuidv4(),
})
writeJSON(w, resp, http.StatusOK)
Expand Down Expand Up @@ -1007,7 +1006,7 @@ func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
return
}
body, _ := json.Marshal(&bearerResponse{
body, _ := jsonMarshalNoEscape(&bearerResponse{
Authenticated: true,
Token: tokenFields[1],
})
Expand All @@ -1016,8 +1015,18 @@ func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) {

// Hostname - returns the hostname.
func (h *HTTPBin) Hostname(w http.ResponseWriter, r *http.Request) {
body, _ := json.Marshal(hostnameResponse{
body, _ := jsonMarshalNoEscape(hostnameResponse{
Hostname: h.hostname,
})
writeJSON(w, body, http.StatusOK)
}

// json.Marshal escapes HTML in strings while httpbin does not, so
// we need to set up the encoder manually to reproduce that behavior.
func jsonMarshalNoEscape(value interface{}) ([]byte, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(value)
return buffer.Bytes(), err
}
26 changes: 22 additions & 4 deletions httpbin/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ func TestAnything(t *testing.T) {
t.Parallel()
var (
verbsWithReqBodies = []string{
"GET",
"DELETE",
"PATCH",
"POST",
Expand All @@ -494,10 +495,6 @@ func TestAnything(t *testing.T) {
for _, verb := range verbsWithReqBodies {
testRequestWithBody(t, verb, path)
}
// also test GET requests for each path
t.Run("GET "+path, func(t *testing.T) {
testRequestWithoutBody(t, path, nil, nil, http.StatusOK)
})
}
}

Expand Down Expand Up @@ -527,6 +524,7 @@ func testRequestWithBody(t *testing.T, verb, path string) {
testRequestWithBodyInvalidFormEncodedBody,
testRequestWithBodyInvalidJSON,
testRequestWithBodyInvalidMultiPartBody,
testRequestWithBodyHTML,
testRequestWithBodyJSON,
testRequestWithBodyMultiPartBody,
testRequestWithBodyQueryParams,
Expand Down Expand Up @@ -623,6 +621,26 @@ func testRequestWithBodyFormEncodedBody(t *testing.T, verb, path string) {
}
}

func testRequestWithBodyHTML(t *testing.T, verb, path string) {
data := "<html><body><h1>hello world</h1></body></html>"

r, _ := http.NewRequest(verb, path, strings.NewReader(data))
r.Header.Set("Content-Type", htmlContentType)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)

assertStatusCode(t, w, http.StatusOK)
assertContentType(t, w, jsonContentType)

// We do not use json.Unmarshal here which would unescape any escaped characters.
// For httpbin compatibility, we need to verify the data is returned as-is without
// escaping.
respBody := w.Body.String()
if !strings.Contains(respBody, data) {
t.Fatalf("response data mismatch, %#v != %#v", respBody, data)
}
}

func testRequestWithBodyFormEncodedBodyNoContentType(t *testing.T, verb, path string) {
params := url.Values{}
params.Set("foo", "foo")
Expand Down
7 changes: 4 additions & 3 deletions httpbin/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,13 @@ func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error
ct := r.Header.Get("Content-Type")
switch {
case strings.HasPrefix(ct, "application/x-www-form-urlencoded"):
// r.ParseForm() does not populate r.PostForm for DELETE requests, but
// r.ParseForm() does not populate r.PostForm for DELETE or GET requests, but
// we need it to for compatibility with the httpbin implementation, so
// we trick it with this ugly hack.
if r.Method == http.MethodDelete {
if r.Method == http.MethodDelete || r.Method == http.MethodGet {
originalMethod := r.Method
r.Method = http.MethodPost
defer func() { r.Method = http.MethodDelete }()
defer func() { r.Method = originalMethod }()
}
if err := r.ParseForm(); err != nil {
return err
Expand Down

0 comments on commit 683ef7f

Please sign in to comment.