Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ func Body(body io.Reader) Option {
}
}

// Data sets raw string into the request body.
// Data sets data of request body. It also deduces and sets the Content-Type
// for the input data.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add more details for deducing different content types based on data types:

  1. "application/json": struct, slice(except []byte), and map
  2. auto deduce by [http.DetectContentType]: []byte, io.Reader
  3. "text/plain": others

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

func Data(data any) Option {
return func(opts *Options) {
opts.Data = data
Expand Down
38 changes: 32 additions & 6 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"mime/multipart"
"net/http"
"reflect"

"github.com/Wenchy/requests/internal/auth"
)
Expand Down Expand Up @@ -82,14 +83,16 @@ func request(c *Client, method, url string, opts *Options) (*Response, error) {
func requestData(c *Client, method, url string, opts *Options) (*Response, error) {
body := bytes.NewBuffer(nil)
if opts.Data != nil {
d := fmt.Sprintf("%v", opts.Data)
_, err := body.WriteString(d)
contentType, bytes, err := deduceContentTypeAndBody(opts.Data)
if err != nil {
return nil, err
}
_, err = body.Write(bytes)
if err != nil {
return nil, err
}
opts.Headers.Set("Content-Type", contentType)
}
// TODO: judge content type
// opts.Headers["Content-Type"] = "application/x-www-form-urlencoded"
opts.Body = body
return c.request(method, url, opts, body.Bytes())
}
Expand All @@ -105,7 +108,7 @@ func requestForm(c *Client, method, url string, opts *Options) (*Response, error
return nil, err
}
}
opts.Headers.Set("Content-Type", "application/x-www-form-urlencoded")
opts.Headers.Set("Content-Type", formContentType)
opts.Body = body
return c.request(method, url, opts, body.Bytes())
}
Expand All @@ -123,7 +126,7 @@ func requestJSON(c *Client, method, url string, opts *Options) (*Response, error
return nil, err
}
}
opts.Headers.Set("Content-Type", "application/json")
opts.Headers.Set("Content-Type", jsonContentType)
opts.Body = body
return c.request(method, url, opts, body.Bytes())
}
Expand Down Expand Up @@ -171,3 +174,26 @@ var dispatchers map[bodyType]dispatcher = map[bodyType]dispatcher{
bodyTypeJSON: requestJSON,
bodyTypeFiles: requestFiles,
}

var (
plainTextType = "text/plain; charset=utf-8"
jsonContentType = "application/json"
formContentType = "application/x-www-form-urlencoded"
)

// deduceContentTypeAndBody parses content type and request body from request data
func deduceContentTypeAndBody(data any) (string, []byte, error) {
bodyValue := reflect.Indirect(reflect.ValueOf(data))
switch bodyValue.Kind() {
case reflect.Struct, reflect.Map, reflect.Slice:
// check slice here to differentiate between any slice vs byte slice
if body, ok := data.([]byte); ok {
return http.DetectContentType(body), body, nil
} else {
body, err := json.Marshal(data)
return jsonContentType, body, err
}
default:
return plainTextType, fmt.Appendf(nil, "%v", bodyValue.Interface()), nil
Comment on lines +200 to +201
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the data type is io.Reader, we should also process it well. just like: https://github.com/go-resty/resty/blob/v3/middleware.go#L428C7-L428C16

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. We check if the input data implements io.Reader first before checking its reflect.Kind.

}
}
138 changes: 138 additions & 0 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -776,3 +776,141 @@ func TestInterceptors(t *testing.T) {
})
}
}

func toPtr[T any](v T) *T {
return &v
}

func Test_deduceContentTypeAndBody(t *testing.T) {
type mystruct struct {
A int
B string
}
tests := []struct {
name string
body any
want string
want2 []byte
}{
{
name: "int",
body: 123,
want: plainTextType,
want2: []byte("123"),
},
{
name: "*int",
body: toPtr(123),
want: plainTextType,
want2: []byte("123"),
},
{
name: "string",
body: "abc",
want: plainTextType,
want2: []byte("abc"),
},
{
name: "*string",
body: toPtr("abc"),
want: plainTextType,
want2: []byte("abc"),
},
{
name: "bytes",
body: []byte("abc"),
want: plainTextType,
want2: []byte("abc"),
},
{
name: "struct",
body: mystruct{A: 123, B: "abc"},
want: jsonContentType,
want2: []byte(`{"A":123,"B":"abc"}`),
},
{
name: "*struct",
body: &mystruct{A: 123, B: "abc"},
want: jsonContentType,
want2: []byte(`{"A":123,"B":"abc"}`),
},
{
name: "map",
body: map[int]string{1: "a", 2: "b", 3: "c"},
want: jsonContentType,
want2: []byte(`{"1":"a","2":"b","3":"c"}`),
},
{
name: "[]int",
body: []int{123, 456},
want: jsonContentType,
want2: []byte("[123,456]"),
},
{
name: "[]*int",
body: []*int{toPtr(123), toPtr(456)},
want: jsonContentType,
want2: []byte("[123,456]"),
},
{
name: "[]string",
body: []string{"abc", "def"},
want: jsonContentType,
want2: []byte(`["abc","def"]`),
},
{
name: "[]*string",
body: []*string{toPtr("abc"), toPtr("def")},
want: jsonContentType,
want2: []byte(`["abc","def"]`),
},
{
name: "[]bytes",
body: [][]byte{[]byte("abc"), []byte("def")},
want: jsonContentType,
want2: []byte(`["YWJj","ZGVm"]`),
},
{
name: "[]struct",
body: []mystruct{
{A: 123, B: "abc"},
{A: 456, B: "def"},
},
want: jsonContentType,
want2: []byte(`[{"A":123,"B":"abc"},{"A":456,"B":"def"}]`),
},
{
name: "[]*struct",
body: []*mystruct{
{A: 123, B: "abc"},
{A: 456, B: "def"},
},
want: jsonContentType,
want2: []byte(`[{"A":123,"B":"abc"},{"A":456,"B":"def"}]`),
},
{
name: "[]map",
body: []map[int]string{
{1: "a", 2: "b", 3: "c"},
{4: "d", 5: "e", 6: "f"},
},
want: jsonContentType,
want2: []byte(`[{"1":"a","2":"b","3":"c"},{"4":"d","5":"e","6":"f"}]`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got2, gotErr := deduceContentTypeAndBody(tt.body)
if gotErr != nil {
t.Errorf("detectContentType() failed: %v", gotErr)
return
}
if got != tt.want {
t.Errorf("detectContentType() = %v, want %v", got, tt.want)
}
if string(got2) != string(tt.want2) {
t.Errorf("detectContentType() = %v, want %v", string(got2), string(tt.want2))
}
})
}
}
Loading