Skip to content
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

Merged
merged 5 commits into from
Jul 9, 2018

Conversation

lyuxuan
Copy link
Contributor

@lyuxuan lyuxuan commented May 16, 2018

  • 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

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thing here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

func TestServerMaxHeaderListSizeClientUserViolation(t *testing.T) {
defer leakcheck.Check(t)
for _, e := range listTestEnv() {
if e.httpHandler == true {
Copy link
Contributor

Choose a reason for hiding this comment

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

if e.httpHandler {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -41,6 +41,10 @@ import (
"google.golang.org/grpc/status"
)

const (
defaultClientMaxHeaderListSize = uint32(16 << 20)
)
Copy link
Contributor

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

Copy link
Contributor Author

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

}
return
state := decodeState{serverSide: true}
if err := state.decodeResponseHeader(frame); err != nil {
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

// 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")
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@@ -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{
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

c.mu.Unlock()
return false, c.err
}
if f != nil {
Copy link
Contributor

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.

@dfawley dfawley assigned lyuxuan and unassigned lyuxuan and MakMukhi Jun 28, 2018
@lyuxuan lyuxuan force-pushed the header_list_size branch 3 times, most recently from bd73ad3 to 6eb6e04 Compare July 2, 2018 20:55
@lyuxuan lyuxuan merged commit 264daa2 into grpc:master Jul 9, 2018
@dfawley dfawley changed the title Set and respect HTTP/2 SETTINGS_MAX_HEADER_LIST_SIZE transport: set and respect HTTP/2 SETTINGS_MAX_HEADER_LIST_SIZE Jul 12, 2018
@dfawley dfawley added the Type: Feature New features or improvements in behavior label Jul 12, 2018
@dfawley dfawley added this to the 1.14 Release milestone Jul 12, 2018
@lock lock bot locked as resolved and limited conversation to collaborators Jan 8, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Type: Feature New features or improvements in behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

gRPC should set and respect HTTP/2 SETTINGS_MAX_HEADER_LIST_SIZE
3 participants