Skip to content

Commit

Permalink
Match Content-Type charset case-insensitively (#440)
Browse files Browse the repository at this point in the history
The connect handler returns http.StatusUnsupportedMediaType when a
request has a `Content-Type: application/json; charset=UTF-8` header.
However, according to [RFC 9110 Section 8.3.2](
https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.2), the charset
parameter value should be treated as case-insensitive.

In this PR, I have modified the charset parameter value for user
requests and acceptable content types of handlers to all be handled in
lowercase.
  • Loading branch information
ichizero authored Jan 26, 2023
1 parent 0a3bfe3 commit 15a843b
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 4 deletions.
2 changes: 1 addition & 1 deletion handler_ext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func TestHandler_ServeHTTP(t *testing.T) {
strings.NewReader("{}"),
)
assert.Nil(t, err)
req.Header.Set("Content-Type", "application/json;Charset=utf-8")
req.Header.Set("Content-Type", "application/json;Charset=Utf-8")
resp, err := client.Do(req)
assert.Nil(t, err)
defer resp.Body.Close()
Expand Down
10 changes: 10 additions & 0 deletions protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,5 +320,15 @@ func canonicalizeContentType(ct string) string {
if err != nil {
return ct
}

// According to RFC 9110 Section 8.3.2, the charset parameter value should be treated as case-insensitive.
// mime.FormatMediaType canonicalizes parameter names, but not parameter values,
// because the case sensitivity of a parameter value depends on its semantics.
// Therefore, the charset parameter value should be canonicalized here.
// ref.) https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.2
if charset, ok := params["charset"]; ok {
params["charset"] = strings.ToLower(charset)
}

return mime.FormatMediaType(base, params)
}
4 changes: 2 additions & 2 deletions protocol_connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ func (*protocolConnect) NewHandler(params *protocolHandlerParams) protocolHandle
contentTypes := make(map[string]struct{})
for _, name := range params.Codecs.Names() {
if params.Spec.StreamType == StreamTypeUnary {
contentTypes[connectUnaryContentTypePrefix+name] = struct{}{}
contentTypes[canonicalizeContentType(connectUnaryContentTypePrefix+name)] = struct{}{}
continue
}
contentTypes[connectStreamingContentTypePrefix+name] = struct{}{}
contentTypes[canonicalizeContentType(connectStreamingContentTypePrefix+name)] = struct{}{}
}
return &connectHandler{
protocolHandlerParams: *params,
Expand Down
2 changes: 1 addition & 1 deletion protocol_grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (g *protocolGRPC) NewHandler(params *protocolHandlerParams) protocolHandler
}
contentTypes := make(map[string]struct{})
for _, name := range params.Codecs.Names() {
contentTypes[prefix+name] = struct{}{}
contentTypes[canonicalizeContentType(prefix+name)] = struct{}{}
}
if params.Codecs.Get(codecNameProto) != nil {
contentTypes[bare] = struct{}{}
Expand Down
40 changes: 40 additions & 0 deletions protocol_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2021-2023 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package connect

import (
"testing"

"github.com/bufbuild/connect-go/internal/assert"
)

func TestCanonicalizeContentType(t *testing.T) {
t.Parallel()
tests := []struct {
name string
arg string
want string
}{
{name: "charset param should be treated as lowercase", arg: "application/json; charset=UTF-8", want: "application/json; charset=utf-8"},
{name: "non charset param should not be changed", arg: "multipart/form-data; boundary=fooBar", want: "multipart/form-data; boundary=fooBar"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, canonicalizeContentType(tt.arg), tt.want)
})
}
}

0 comments on commit 15a843b

Please sign in to comment.