-
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
transport: set and respect HTTP/2 SETTINGS_MAX_HEADER_LIST_SIZE #2084
Conversation
clientconn.go
Outdated
// header list that the client is prepared to accept. | ||
func WithMaxHeaderListSize(s uint32) DialOption { | ||
return func(o *dialOptions) { | ||
o.copts.MaxHeaderListSize = new(uint32) |
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.
How about if we just take address of s, since it's already a copy?
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.
done
server.go
Outdated
// of header list that the server is prepared to accept. | ||
func MaxHeaderListSize(s uint32) ServerOption { | ||
return func(o *options) { | ||
o.maxHeaderListSize = new(uint32) |
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.
Same thing here.
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.
done
test/end2end_test.go
Outdated
func TestServerMaxHeaderListSizeClientUserViolation(t *testing.T) { | ||
defer leakcheck.Check(t) | ||
for _, e := range listTestEnv() { | ||
if e.httpHandler == true { |
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 e.httpHandler {
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.
done
transport/http2_client.go
Outdated
@@ -41,6 +41,10 @@ import ( | |||
"google.golang.org/grpc/status" | |||
) | |||
|
|||
const ( | |||
defaultClientMaxHeaderListSize = uint32(16 << 20) | |||
) |
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.
Should we put all connection-level defaults together? They all live here right now https://github.com/grpc/grpc-go/blob/master/transport/flowcontrol.go#L50 but I don't think some of them belong in a file called flowcontrol.go
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.
Yeah, I was considering whether the default should be inside flowcontol.go, and decided that probably not. How about I go ahead and make a defaults.go to have all the defaults in one place?
hdrFrame := it.(*headerFrame) | ||
var sz int64 | ||
for _, f := range hdrFrame.hf { | ||
if sz += int64(f.Size()); sz > int64(*t.maxSendHeaderListSize) { |
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.
Should we update createHeaderFields
to record the total size so that we don't have to loop over all fields again?
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.
It will benefit users that their peers enforce a header list size limit, but will add overhead to the case that peers don't have a limit.
transport/http2_server.go
Outdated
} | ||
return | ||
state := decodeState{serverSide: true} | ||
if err := state.decodeResponseHeader(frame); err != nil { |
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.
decodeResponseHeader
is supposed to be used on the client side. Perhaps, rename the function to something more appropriate since it's being used on the server as well.
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.
Alternatively, add the if frame.Truncated
check right at the beginning of this function itself.
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.
Changed to decodeHeader
.
Tried to move if frame.Truncated
, but it seems to only save an declaration for a decodeState variable, but complicates the code substantially, e.g. add redundant code for error handling, have to specialize for server and client.
transport/http_util.go
Outdated
// frame.Truncated is set to true when framer detects that the current header | ||
// list size hits MaxHeaderListSize limit. | ||
if frame.Truncated { | ||
return streamErrorf(codes.Internal, "peer header list size exceeded 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.
Is it better to return an error or merely log it and still try and parse the header that we got?
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 header would be incomplete and I am not sure what will happen if we return an incomplete header to grpc and the user.
transport/http2_server.go
Outdated
@@ -714,7 +756,7 @@ func (t *http2Server) WriteHeader(s *Stream, md metadata.MD) error { | |||
headerFields = append(headerFields, hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)}) | |||
} | |||
} | |||
t.controlBuf.put(&headerFrame{ | |||
success, err := t.controlBuf.executeAndPut(t.checkForHeaderListSize, &headerFrame{ |
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.
Should we also do this check while writing trailer?
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.
Yes, I think we should. I was under the wrong impression that WriteHeader is the centralized space for both headers and trailers.
if t.maxSendHeaderListSize == nil { | ||
return true | ||
} | ||
hdrFrame := it.(*headerFrame) |
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.
Same thing; perhaps the headerFrame
should have a field to record size.
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.
Adding a size field would add overhead to non limit case.
if config.MaxHeaderListSize != nil { | ||
maxHeaderListSize = *config.MaxHeaderListSize | ||
} | ||
framer := newFramer(conn, writeBufSize, readBufSize, maxHeaderListSize) |
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.
Only relevant to the server side: Should Framer.MaxHeaderListSize
be set only after fist settings' ack is received?
The other side might start with an unlimited MaxHeaderListSize(https://tools.ietf.org/html/rfc7540#section-6.5.2) and start sending header frames before our settings frame is received by it.
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.
Yes, I would like to do that in a separate PR, as things like max concurrent streams may also need this behavior.
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.
Offline discussion decides that at current stage, enforcement after ack will not be necessary and add complexity to the code.
91aa1ba
to
c7e89b1
Compare
c7e89b1
to
d2a7132
Compare
transport/controlbuf.go
Outdated
c.mu.Unlock() | ||
return false, c.err | ||
} | ||
if f != nil { |
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.
executeAndPut
has this check because put
calls it with a nil argument.
bd73ad3
to
6eb6e04
Compare
Add DialOption(
WithMaxHeaderListSize
) and ServerOption(MaxHeaderListSize
) to set the max header list size that peer should respect. Configuring these two options results in sending the SETTINGS_MAX_HEADER_LIST_SIZE setting inside the initial settings frame. And at the same time, the framer is configured to omit header fields after peer's header list size hitting the limit.Note: framer uses the max header list size to configure hpack decoder max string length( link). While a stream exceeding the header list size limit leads to reset stream, exceeding the max string length for hpack decoder will lead to connection close.
Issues:
8KB defualt limit will potentially break some users, however, other languages implementation all enforce this limit. Our current implicit setting is 16MB (by golang http2 package), and I am setting that explicitly now.
As suggested here, https://http2.github.io/http2-spec/#rfc.section.10.5.1, it is better to send a HTTP 431 code rather than a reset stream, as peer will know better about what is happening.
A potential behavior change for user who sends >16MB metadata: before we return truncated metadata silently, now we will fail the RPC. It's a bug fix.
fix #1291