Skip to content

Commit f981f94

Browse files
authored
Encode binary request bodies as bas64 data URLs (#117)
This brings go-httpbin's behavior more in line with original httpbin. Fixes #90.
1 parent c9f5002 commit f981f94

File tree

2 files changed

+90
-5
lines changed

2 files changed

+90
-5
lines changed

httpbin/handlers_test.go

+57-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"compress/gzip"
77
"compress/zlib"
88
"context"
9+
"encoding/base64"
910
"encoding/json"
1011
"errors"
1112
"fmt"
@@ -544,6 +545,7 @@ func testRequestWithBody(t *testing.T, verb, path string) {
544545
testRequestWithBodyMultiPartBody,
545546
testRequestWithBodyQueryParams,
546547
testRequestWithBodyQueryParamsAndBody,
548+
testRequestWithBodyBinaryBody,
547549
}
548550
for _, testFunc := range testFuncs {
549551
testFunc := testFunc
@@ -555,6 +557,57 @@ func testRequestWithBody(t *testing.T, verb, path string) {
555557
}
556558
}
557559

560+
func testRequestWithBodyBinaryBody(t *testing.T, verb string, path string) {
561+
tests := []struct {
562+
contentType string
563+
requestBody string
564+
}{
565+
{"application/octet-stream", "encodeMe"},
566+
{"image/png", "encodeMe-png"},
567+
{"image/webp", "encodeMe-webp"},
568+
{"image/jpeg", "encodeMe-jpeg"},
569+
{"unknown", "encodeMe-unknown"},
570+
}
571+
for _, test := range tests {
572+
test := test
573+
t.Run("content type/"+test.contentType, func(t *testing.T) {
574+
t.Parallel()
575+
576+
testBody := bytes.NewReader([]byte(test.requestBody))
577+
578+
r, _ := http.NewRequest(verb, path, testBody)
579+
r.Header.Set("Content-Type", test.contentType)
580+
w := httptest.NewRecorder()
581+
app.ServeHTTP(w, r)
582+
583+
assertStatusCode(t, w, http.StatusOK)
584+
assertContentType(t, w, jsonContentType)
585+
586+
var resp *bodyResponse
587+
err := json.Unmarshal(w.Body.Bytes(), &resp)
588+
if err != nil {
589+
t.Fatalf("failed to unmarshal body %s from JSON: %s", w.Body, err)
590+
}
591+
592+
expected := "data:" + test.contentType + ";base64," + base64.StdEncoding.EncodeToString([]byte(test.requestBody))
593+
594+
if resp.Data != expected {
595+
t.Fatalf("expected binary encoded response data: %#v got %#v", expected, resp.Data)
596+
}
597+
if resp.JSON != nil {
598+
t.Fatalf("expected nil response json, got %#v", resp.JSON)
599+
}
600+
601+
if len(resp.Args) > 0 {
602+
t.Fatalf("expected no query params, got %#v", resp.Args)
603+
}
604+
if len(resp.Form) > 0 {
605+
t.Fatalf("expected no form data, got %#v", resp.Form)
606+
}
607+
})
608+
}
609+
}
610+
558611
func testRequestWithBodyEmptyBody(t *testing.T, verb string, path string) {
559612
tests := []struct {
560613
contentType string
@@ -681,8 +734,10 @@ func testRequestWithBodyFormEncodedBodyNoContentType(t *testing.T, verb, path st
681734
if len(resp.Form) != 0 {
682735
t.Fatalf("expected no form values, got %d", len(resp.Form))
683736
}
684-
if string(resp.Data) != params.Encode() {
685-
t.Fatalf("response data mismatch, %#v != %#v", string(resp.Data), params.Encode())
737+
// Because we did not set an content type, httpbin will return the base64 encoded data.
738+
expectedBody := "data:application/octet-stream;base64," + base64.StdEncoding.EncodeToString([]byte(params.Encode()))
739+
if string(resp.Data) != expectedBody {
740+
t.Fatalf("response data mismatch, %#v != %#v", string(resp.Data), expectedBody)
686741
}
687742
}
688743

httpbin/helpers.go

+33-3
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,21 @@ func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error
132132
r.Body = io.NopCloser(bytes.NewBuffer(body))
133133

134134
ct := r.Header.Get("Content-Type")
135+
136+
// Strip of charset encoding, if present
137+
if strings.Contains(ct, ";") {
138+
ct = strings.Split(ct, ";")[0]
139+
}
140+
135141
switch {
136-
case strings.HasPrefix(ct, "application/x-www-form-urlencoded"):
142+
// cases where we don't need to parse the body
143+
case strings.HasPrefix(ct, "html/"):
144+
fallthrough
145+
case strings.HasPrefix(ct, "text/"):
146+
// string body is already set above
147+
return nil
148+
149+
case ct == "application/x-www-form-urlencoded":
137150
// r.ParseForm() does not populate r.PostForm for DELETE or GET requests, but
138151
// we need it to for compatibility with the httpbin implementation, so
139152
// we trick it with this ugly hack.
@@ -146,24 +159,41 @@ func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error
146159
return err
147160
}
148161
resp.Form = r.PostForm
149-
case strings.HasPrefix(ct, "multipart/form-data"):
162+
case ct == "multipart/form-data":
150163
// The memory limit here only restricts how many parts will be kept in
151164
// memory before overflowing to disk:
152165
// https://golang.org/pkg/net/http/#Request.ParseMultipartForm
153166
if err := r.ParseMultipartForm(1024); err != nil {
154167
return err
155168
}
156169
resp.Form = r.PostForm
157-
case strings.HasPrefix(ct, "application/json"):
170+
case ct == "application/json":
158171
err := json.NewDecoder(r.Body).Decode(&resp.JSON)
159172
if err != nil && err != io.EOF {
160173
return err
161174
}
175+
176+
default:
177+
// If we don't have a special case for the content type, we'll just return it encoded as base64 data url
178+
// we strip off any charset information, since we will re-encode the body
179+
resp.Data = encodeData(body, ct)
162180
}
163181

164182
return nil
165183
}
166184

185+
// return provided string as base64 encoded data url, with the given content type
186+
func encodeData(body []byte, contentType string) string {
187+
data := base64.StdEncoding.EncodeToString(body)
188+
189+
// If no content type is provided, default to application/octet-stream
190+
if contentType == "" {
191+
contentType = "application/octet-stream"
192+
}
193+
194+
return string("data:" + contentType + ";base64," + data)
195+
}
196+
167197
// parseDuration takes a user's input as a string and attempts to convert it
168198
// into a time.Duration. If not given as a go-style duration string, the input
169199
// is assumed to be seconds as a float.

0 commit comments

Comments
 (0)