Skip to content

Commit 15a843b

Browse files
authored
Match Content-Type charset case-insensitively (connectrpc#440)
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.
1 parent 0a3bfe3 commit 15a843b

File tree

5 files changed

+54
-4
lines changed

5 files changed

+54
-4
lines changed

handler_ext_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func TestHandler_ServeHTTP(t *testing.T) {
9595
strings.NewReader("{}"),
9696
)
9797
assert.Nil(t, err)
98-
req.Header.Set("Content-Type", "application/json;Charset=utf-8")
98+
req.Header.Set("Content-Type", "application/json;Charset=Utf-8")
9999
resp, err := client.Do(req)
100100
assert.Nil(t, err)
101101
defer resp.Body.Close()

protocol.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,5 +320,15 @@ func canonicalizeContentType(ct string) string {
320320
if err != nil {
321321
return ct
322322
}
323+
324+
// According to RFC 9110 Section 8.3.2, the charset parameter value should be treated as case-insensitive.
325+
// mime.FormatMediaType canonicalizes parameter names, but not parameter values,
326+
// because the case sensitivity of a parameter value depends on its semantics.
327+
// Therefore, the charset parameter value should be canonicalized here.
328+
// ref.) https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.2
329+
if charset, ok := params["charset"]; ok {
330+
params["charset"] = strings.ToLower(charset)
331+
}
332+
323333
return mime.FormatMediaType(base, params)
324334
}

protocol_connect.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ func (*protocolConnect) NewHandler(params *protocolHandlerParams) protocolHandle
5656
contentTypes := make(map[string]struct{})
5757
for _, name := range params.Codecs.Names() {
5858
if params.Spec.StreamType == StreamTypeUnary {
59-
contentTypes[connectUnaryContentTypePrefix+name] = struct{}{}
59+
contentTypes[canonicalizeContentType(connectUnaryContentTypePrefix+name)] = struct{}{}
6060
continue
6161
}
62-
contentTypes[connectStreamingContentTypePrefix+name] = struct{}{}
62+
contentTypes[canonicalizeContentType(connectStreamingContentTypePrefix+name)] = struct{}{}
6363
}
6464
return &connectHandler{
6565
protocolHandlerParams: *params,

protocol_grpc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func (g *protocolGRPC) NewHandler(params *protocolHandlerParams) protocolHandler
8585
}
8686
contentTypes := make(map[string]struct{})
8787
for _, name := range params.Codecs.Names() {
88-
contentTypes[prefix+name] = struct{}{}
88+
contentTypes[canonicalizeContentType(prefix+name)] = struct{}{}
8989
}
9090
if params.Codecs.Get(codecNameProto) != nil {
9191
contentTypes[bare] = struct{}{}

protocol_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2021-2023 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package connect
16+
17+
import (
18+
"testing"
19+
20+
"github.com/bufbuild/connect-go/internal/assert"
21+
)
22+
23+
func TestCanonicalizeContentType(t *testing.T) {
24+
t.Parallel()
25+
tests := []struct {
26+
name string
27+
arg string
28+
want string
29+
}{
30+
{name: "charset param should be treated as lowercase", arg: "application/json; charset=UTF-8", want: "application/json; charset=utf-8"},
31+
{name: "non charset param should not be changed", arg: "multipart/form-data; boundary=fooBar", want: "multipart/form-data; boundary=fooBar"},
32+
}
33+
for _, tt := range tests {
34+
tt := tt
35+
t.Run(tt.name, func(t *testing.T) {
36+
t.Parallel()
37+
assert.Equal(t, canonicalizeContentType(tt.arg), tt.want)
38+
})
39+
}
40+
}

0 commit comments

Comments
 (0)