-
Notifications
You must be signed in to change notification settings - Fork 465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
json handler: return full ratelimit service response as json #148
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package server_test | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"strings" | ||
"testing" | ||
|
||
pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" | ||
|
||
"github.com/envoyproxy/ratelimit/src/server" | ||
mock_v2 "github.com/envoyproxy/ratelimit/test/mocks/rls" | ||
"github.com/golang/mock/gomock" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func assertHttpResponse(t *testing.T, | ||
handler http.HandlerFunc, | ||
requestBody string, | ||
expectedStatusCode int, | ||
expectedContentType string, | ||
expectedResponseBody string) { | ||
|
||
t.Helper() | ||
assert := assert.New(t) | ||
|
||
req := httptest.NewRequest("METHOD_NOT_CHECKED", "/path_not_checked", strings.NewReader(requestBody)) | ||
w := httptest.NewRecorder() | ||
handler(w, req) | ||
|
||
resp := w.Result() | ||
actualBody, _ := ioutil.ReadAll(resp.Body) | ||
assert.Equal(expectedContentType, resp.Header.Get("Content-Type")) | ||
assert.Equal(expectedStatusCode, resp.StatusCode) | ||
assert.Equal(expectedResponseBody, string(actualBody)) | ||
} | ||
|
||
func TestJsonHandler(t *testing.T) { | ||
controller := gomock.NewController(t) | ||
defer controller.Finish() | ||
|
||
rls := mock_v2.NewMockRateLimitServiceServer(controller) | ||
handler := server.NewJsonHandler(rls) | ||
|
||
// Missing request body | ||
assertHttpResponse(t, handler, "", 400, "text/plain; charset=utf-8", "EOF\n") | ||
|
||
// Request body is not valid json | ||
assertHttpResponse(t, handler, "}", 400, "text/plain; charset=utf-8", "invalid character '}' looking for beginning of value\n") | ||
|
||
// Unknown response code | ||
rls.EXPECT().ShouldRateLimit(nil, &pb.RateLimitRequest{ | ||
Domain: "foo", | ||
}).Return(&pb.RateLimitResponse{}, nil) | ||
assertHttpResponse(t, handler, `{"domain": "foo"}`, 500, "application/json", "{}") | ||
|
||
// ratelimit service error | ||
rls.EXPECT().ShouldRateLimit(nil, &pb.RateLimitRequest{ | ||
Domain: "foo", | ||
}).Return(nil, fmt.Errorf("some error")) | ||
assertHttpResponse(t, handler, `{"domain": "foo"}`, 400, "text/plain; charset=utf-8", "some error\n") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: I kinda think this sort of error from ShouldRateLimit should result in a 500 rather than a 400, but changing that is not in scope for this PR. |
||
|
||
// json unmarshaling error | ||
rls.EXPECT().ShouldRateLimit(nil, &pb.RateLimitRequest{ | ||
Domain: "foo", | ||
}).Return(nil, nil) | ||
assertHttpResponse(t, handler, `{"domain": "foo"}`, 500, "text/plain; charset=utf-8", "error marshaling proto3 to json: Marshal called with nil\n") | ||
|
||
// successful request, not rate limited | ||
rls.EXPECT().ShouldRateLimit(nil, &pb.RateLimitRequest{ | ||
Domain: "foo", | ||
}).Return(&pb.RateLimitResponse{ | ||
OverallCode: pb.RateLimitResponse_OK, | ||
}, nil) | ||
assertHttpResponse(t, handler, `{"domain": "foo"}`, 200, "application/json", `{"overallCode":"OK"}`) | ||
|
||
// successful request, rate limited | ||
rls.EXPECT().ShouldRateLimit(nil, &pb.RateLimitRequest{ | ||
Domain: "foo", | ||
}).Return(&pb.RateLimitResponse{ | ||
OverallCode: pb.RateLimitResponse_OVER_LIMIT, | ||
}, nil) | ||
assertHttpResponse(t, handler, `{"domain": "foo"}`, 429, "application/json", `{"overallCode":"OVER_LIMIT"}`) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this actually happen? If not can you assert it instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is that there probably couldn't be a serialization error but probably could be an I/O error if the remote peer closes the TCP connection, in which case the fmt.Fprintf() would fail anyway.
I could split the serialization from the writing to the socket and do a assert/panic and recover dance to print the stack trace if a serialization error happens. If that's the route, I'd kind of rather just do a normal log.Error() instead of an assert because I don't think the extra code to deal with panic()s to get a stack trace will provide much value.
Up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine either way, I mostly just want to avoid error handling and just crash if there are things that should never happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My sense is:
More generally on the topic of when to crash the process, it seems like crashing a process makes sense when:
My sense is that a single unknown json serialization error in isolation probably doesn't justify a crash, vs something like a resource allocation issue potentially could justify a crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The guiding principle is whether you can test this or not. I don't think you can test this branch, as such I would use https://github.com/envoyproxy/ratelimit/blob/master/src/assert/assert.go and not handle the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added unit tests for all the cases. For the serialization failure I used a 'nil' return value from ShouldRateLimit(). A non-nil serialization problem would theoretically be possible if the ShouldRateLimit() API ever evolves to include
Any
types, but I don't think there areAny
types in the spec todayThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, thanks. Will take a look.