http: support additional content type#903
Conversation
Support `application/x-www-form-urlencoded` as an additional content type for `AtomicLevel.ServeHTTP`. This is the default content type for `curl -X POST`. With this change, interacting with the HTTP endpoint is a bit more user friendly: ``` curl -X PUT localhost:8080/log/level -d level=debug ``` Additionally, the unit tests for the HTTP handler are transformed to a table driven approach. fixes uber-go#902
Codecov Report
@@ Coverage Diff @@
## master #903 +/- ##
=======================================
Coverage 98.22% 98.23%
=======================================
Files 44 44
Lines 1915 1925 +10
=======================================
+ Hits 1881 1891 +10
Misses 27 27
Partials 7 7
Continue to review full report at Codecov.
|
abhinav
left a comment
There was a problem hiding this comment.
Thanks for the change! The core logic looks mostly good. I have some suggestions around style.
| // request could look like this: | ||
| // | ||
| // curl -X PUT localhost:8080/log/level -d level=debug | ||
| // | ||
| // For any other content type, the payload is expected to be JSON encoded and | ||
| // look like: | ||
| // | ||
| // {"level":"info"} | ||
| // | ||
| // An example curl request could look like this: | ||
| // | ||
| // curl -X PUT localhost:8080/log/level -H "Content-Type: application/json" -d '{"level":"debug"}' |
There was a problem hiding this comment.
This is great documentation, thank you!
| } | ||
| if req.Level == nil { | ||
| return "Must specify a logging level." | ||
| requestedLvl, err := func(body io.Reader) (*zapcore.Level, error) { |
There was a problem hiding this comment.
This appears to be an effort to reduce error handling duplication. That's a good call but can we perhaps move this to its own unexported method?
requestedLevel, err := lvl.decodePutRequest(r)
Additionally, since zapcore.Level is just an int8, we don't need to return its address:
func (lvl AtomicLevel) decodePutRequest(*http.Request) (zapcore.Level, error)
It's a pointer in payload to detect the absence of the level in the JSON payload. So we can validate it's non-nil on the JSON path and dereference it.
There was a problem hiding this comment.
Good point. I tried to follow the previous style, but your suggestion is more maintainable and readable IMO.
| if err := l.UnmarshalText([]byte(lvl)); err != nil { | ||
| return nil, err | ||
| } | ||
| return &l, nil |
There was a problem hiding this comment.
Optionally, this can be shortened.
| if err := l.UnmarshalText([]byte(lvl)); err != nil { | |
| return nil, err | |
| } | |
| return &l, nil | |
| err := l.UnmarshalText([]byte(lvl)) | |
| return &l, err |
There was a problem hiding this comment.
I prefer being explicit here. Especially after switching to value type.
It is not immediately obvious whether the returned value is pre- or post-UnmarshalText without knowing that part of the spec by hart. (TBH I would need to look that up)
| tests := map[string]struct { | ||
| Method string | ||
| ContentType string | ||
| Body string | ||
| ExpectedCode int | ||
| ExpectedLevel zapcore.Level | ||
| }{ |
There was a problem hiding this comment.
Thanks for making this a table test. If you wouldn't mind, we have a preference for slice-based table tests rather than maps:
tests := []struct {
desc string
method string
contentType string
...
}{
...
}
for _, tt := range tests {
..
(See also https://github.com/uber-go/guide/blob/master/style.md#test-tables)
| ts := httptest.NewServer(lvl) | ||
| defer ts.Close() | ||
|
|
||
| req, err := http.NewRequest(test.Method, ts.URL, strings.NewReader(test.Body)) |
There was a problem hiding this comment.
can we name the test server srv or server?
| ts := httptest.NewServer(lvl) | |
| defer ts.Close() | |
| req, err := http.NewRequest(test.Method, ts.URL, strings.NewReader(test.Body)) | |
| server := httptest.NewServer(lvl) | |
| defer server.Close() | |
| req, err := http.NewRequest(test.Method, server.URL, strings.NewReader(test.Body)) |
| }, | ||
| } | ||
|
|
||
| for name, test := range tests { |
There was a problem hiding this comment.
For table tests, consider renaming the test cases to tt.
for _, tt := range tests {
| enc.Encode(errorResponse{Error: err.Error()}) | ||
| return | ||
| } | ||
|
|
||
| lvl.SetLevel(*req.Level) | ||
| enc.Encode(req) | ||
|
|
||
| lvl.SetLevel(*requestedLvl) | ||
| enc.Encode(payload{Level: requestedLvl}) |
There was a problem hiding this comment.
Do we want a JSON output even for non-JSON input?
Maybe this is okay and we can add a plain text output later if we need it and the client has an Accepts: text/plain?
CC @prashantv
There was a problem hiding this comment.
If we want to return non-JSON in the response, I would definitively go for content type negotiation through the Accepts header.
In any case, I would vote for this to be part of a follow-up PR if it is desired.
|
Thanks for the swift feedback. I hope I get around to address the comments this week. |
These methods do not have need of the atomic level. Turn them into functions to avoid accidentally manipulating or accessing it.
| if contentType == "application/x-www-form-urlencoded" { | ||
| return lvl.decodePutURL(body) | ||
| } | ||
| return lvl.decodePutJSON(body) |
There was a problem hiding this comment.
Minor: It looks like these methods could be top-level functions since they don't do anything with the AtomicLevel. I'm gonna change that.
|
Sorry for the delay in reviewing. This looks great! |
prashantv
left a comment
There was a problem hiding this comment.
Change looks good, although one change we can make to simplify, but also provide an easier way to set the level: use ParseForm() and FormValue(..) to get the level field if it exists. It would simplify the code, while also allowing URL parameters to be used. Thoughts?
|
@prashantv nice. Did not know that exists. Should I also add tests for the URL params encoding? |
Yes, that would be great if you could add a test for passing the parameter in a URL (and mention it in the docs). Thanks! |
|
Done. |
prashantv
left a comment
There was a problem hiding this comment.
Thanks for the contribution!
Support `application/x-www-form-urlencoded` as an additional content type for `AtomicLevel.ServeHTTP`. This is the default content type for `curl -X POST`. With this change, interacting with the HTTP endpoint is a bit more user friendly: ``` curl -X PUT localhost:8080/log/level -d level=debug ``` Additionally, the unit tests for the HTTP handler are transformed to a table driven approach. fixes #902 Co-authored-by: Abhinav Gupta <abg@uber.com>
Support `application/x-www-form-urlencoded` as an additional content type for `AtomicLevel.ServeHTTP`. This is the default content type for `curl -X POST`. With this change, interacting with the HTTP endpoint is a bit more user friendly: ``` curl -X PUT localhost:8080/log/level -d level=debug ``` Additionally, the unit tests for the HTTP handler are transformed to a table driven approach. fixes uber-go#902 Co-authored-by: Abhinav Gupta <abg@uber.com>
Support
application/x-www-form-urlencodedas an additional contenttype for
AtomicLevel.ServeHTTP.This is the default content type for
curl -X POST.With this change, interacting with the HTTP endpoint is a bit more
user friendly:
Additionally, the unit tests for the HTTP handler are transformed to
a table driven approach.
fixes #902
This change is