-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
status: fix/improve status handling #6662
Conversation
API-layer: 1. If the user sets a status code outside the bounds defined in the `codes` package 0-16 (inclusive), set the status code to `codes.Unknown`. This impacts statuses created locally as well as statuses received in RPC response trailers. See grpc/grpc-java#10568 for evidence this may be happening in the wild. Client-side: 1. When receiving a `grpc-status-details-bin` trailer: - If there is 1 value and it deserializes into a `google.rpc.Status`, ensure the code field matches the `grpc-status` header's code. If it does not match, convert the code to `codes.Internal` and set a message indicating the mismatch. If it does, the status will contain the full details of the `grpc-status-details-bin` proto. (Note that `grpc-message` will not be checked against the proto's message field, and will be silently discarded if there is a mismatch.) - Otherwise, the status returned to the application will use the `grpc-status` and `grpc-message` values only. - In all cases, the raw `grpc-status-details-bin` trailer will be visible to the application via `metadata.FromIncomingContext(ctx)["grpc-status-details-bin"]`. Server-side: 1. If the user manually sets `grpc-status-details-bin` in the trailers: - If the status returned by the method handler _does not_ include details (see `status.(*Status).WithDetails`), the transport will send the user's `grpc-status-details-bin` trailer(s) directly. - If the status returned by the method handler _does_ include details, the transport will disregard the user's trailer(s) and replace them with a `google.rpc.Status` proto version of the returned status.
status/status_ext_test.go
Outdated
trailerGot := metadata.MD{} | ||
_, errGot := ss.Client.UnaryCall(ctx, &testpb.SimpleRequest{}, grpc.Trailer(&trailerGot)) | ||
gsdb := trailerGot["grpc-status-details-bin"] | ||
if !reflect.DeepEqual(gsdb, tc.trailerWant) { |
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.
Any reason for this to not use cmp.Equal
?
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'll flip it around: why prefer cmp.Equal
over reflect.DeepEqual
if there are no special equality options needed?
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.
why prefer
cmp.Equal
overreflect.DeepEqual
?
Because it is the recommended way of doing comparisons in tests.
if there are no special equality options needed?
We shouldn't have to think about whether special comparison options are required before making the decision between the two, every time we have to make a comparison in our tests. reflect.DeepEqual
is sensitive to changes in unexported fields. Agreed that we are only comparing a slice of strings here, but I feel more usages of reflect.DeepEqual
will lead to more of them in our codebase (saying I'm using it to maintain consistency, or oh that test does it and why shouldn't this one do?)
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.
Because it is the recommended way of doing comparisons in tests.
The rationale cites "reflect.DeepEqual compares unexported fields <which presumably shouldn't ever be important to your tests I guess?>"...but if unexported fields is what you want isn't reflect.DeepEqual
much easier to use? In this case it's comparing []string
so if it isn't good enough then I don't understand why.
If cmp
is preferred why isn't it in the stdlib? 🤷
I switched it but don't really see the rationale of always reaching for something that's often more complicated and harder to use by default over something that for simple use cases is easier to use and understand.
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.
If
cmp
is preferred why isn't it in the stdlib? 🤷
Don't know why it is not part of the stdlib. But they do mention that it is maintained by the Go team.
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 switched it but don't really see the rationale of always reaching for something that's often more complicated and harder to use by default over something that for simple use cases is easier to use and understand.
I don't see it switched yet. But why is cmp.Equal(gsdb, tc.trailerWant)
more difficult to use or understand here when compared to reflect.DeepEqual(gsdb, tc.trailerWant)
? Am I missing something?
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 see that commit where you switched, but for some reason github doesn't show it in the full diff.
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.
But why is
cmp.Equal(gsdb, tc.trailerWant)
more difficult to use or understand here when compared toreflect.DeepEqual(gsdb, tc.trailerWant)
?
Here it's equivalent. But there are other places where, e.g. if you want to compare the unexported fields, it can be more complicated. For diffs and complex comparisons, it's great, but for simple things, reflect.DeepEqual
matches with its own simplicity.
I see that commit where you switched, but for some reason github doesn't show it in the full diff.
That's strange. It shows up in the "Files Changed" tab for me as expected.
status/status_ext_test.go
Outdated
trailerGot := metadata.MD{} | ||
_, errGot := ss.Client.UnaryCall(ctx, &testpb.SimpleRequest{}, grpc.Trailer(&trailerGot)) | ||
gsdb := trailerGot["grpc-status-details-bin"] | ||
if !reflect.DeepEqual(gsdb, tc.trailerWant) { |
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.
why prefer
cmp.Equal
overreflect.DeepEqual
?
Because it is the recommended way of doing comparisons in tests.
if there are no special equality options needed?
We shouldn't have to think about whether special comparison options are required before making the decision between the two, every time we have to make a comparison in our tests. reflect.DeepEqual
is sensitive to changes in unexported fields. Agreed that we are only comparing a slice of strings here, but I feel more usages of reflect.DeepEqual
will lead to more of them in our codebase (saying I'm using it to maintain consistency, or oh that test does it and why shouldn't this one do?)
status/status_ext_test.go
Outdated
trailerGot := metadata.MD{} | ||
_, errGot := ss.Client.UnaryCall(ctx, &testpb.SimpleRequest{}, grpc.Trailer(&trailerGot)) | ||
gsdb := trailerGot["grpc-status-details-bin"] | ||
if !reflect.DeepEqual(gsdb, tc.trailerWant) { |
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.
If
cmp
is preferred why isn't it in the stdlib? 🤷
Don't know why it is not part of the stdlib. But they do mention that it is maintained by the Go team.
status/status_ext_test.go
Outdated
trailerGot := metadata.MD{} | ||
_, errGot := ss.Client.UnaryCall(ctx, &testpb.SimpleRequest{}, grpc.Trailer(&trailerGot)) | ||
gsdb := trailerGot["grpc-status-details-bin"] | ||
if !reflect.DeepEqual(gsdb, tc.trailerWant) { |
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 switched it but don't really see the rationale of always reaching for something that's often more complicated and harder to use by default over something that for simple use cases is easier to use and understand.
I don't see it switched yet. But why is cmp.Equal(gsdb, tc.trailerWant)
more difficult to use or understand here when compared to reflect.DeepEqual(gsdb, tc.trailerWant)
? Am I missing something?
status/status_ext_test.go
Outdated
trailerGot := metadata.MD{} | ||
_, errGot := ss.Client.UnaryCall(ctx, &testpb.SimpleRequest{}, grpc.Trailer(&trailerGot)) | ||
gsdb := trailerGot["grpc-status-details-bin"] | ||
if !reflect.DeepEqual(gsdb, tc.trailerWant) { |
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 see that commit where you switched, but for some reason github doesn't show it in the full diff.
This reverts commit 0772ed7.
Fixes #5485
cc @ejona86 @markdroth FYI / to review the behavior to confirm it conforms to the grpc spec or the intentions of the spec, since there is unfortunately no written spec for
grpc-status-details-bin
.API-layer:
If the user sets a status code outside the bounds defined in the
codes
package 0-16 (inclusive), set the status code tocodes.Unknown
. This impacts statuses created locally as well as statuses received in RPC response trailers. See grpc/grpc-java#10568 for evidence this may be happening in the wild.Previous behavior: allowed any value to be used and set on the wire (server) or read from the wire (client).
Client-side:
When receiving a
grpc-status-details-bin
trailer:If there is 1 value and it deserializes into a
google.rpc.Status
, ensure the code field matches thegrpc-status
header's code. If it does not match, convert the code tocodes.Internal
and set a message indicating the mismatch. If it does, the status will contain the full details of thegrpc-status-details-bin
proto. (Note thatgrpc-message
will not be checked against the proto's message field, and will be silently discarded if there is a mismatch.)Otherwise, the status returned to the application will use the
grpc-status
andgrpc-message
values only.In all cases, the raw
grpc-status-details-bin
trailer will be visible to the application viatrailer["grpc-status-details-bin"]
(wheretrailer
is either obtained via thegrpc.Trailer()
CallOption
orgrpc.ClientStream.Trailer()
).Previous behavior: use the
grpc-status-details-bin
, if present, directly, ignoringgrpc-status
andgrpc-message
, and returning a different error to the application if it could not be parsed as agoogle.rpc.Status
.Server-side:
If the user manually sets
grpc-status-details-bin
in the trailers:If the status returned by the method handler does not include details (see
status.(*Status).WithDetails
), the transport will send the user'sgrpc-status-details-bin
trailer(s) directly.If the status returned by the method handler does include details, the transport will disregard the user's trailer(s) and replace them with a serialized
google.rpc.Status
proto version of the returned status.Previous behavior: ignore any
grpc-status-details-bin
set by the user and set it automatically as a proto if details were present in the returned status.Internal
This change also adds a way to use the stub server with the handler transport.
RELEASE NOTES:
code.Code
; other values will be converted tocodes.Unknown
AND server: allow applications to send arbitrary data in thegrpc-status-details-bin
trailer AND client: validategrpc-status-details-bin
trailer and pass through the trailer to the application directly