Repository: creachadair/jrpc2 (go)

Issue:
Allow "Content-Type: application/json; charset=utf-8"
Currently it looks like only "application/json" is allowed by jrpc2's handler - https://github.com/creachadair/jrpc2/blob/main/jhttp/bridge.go#L60

We would like to allow "application/json; charset=utf-8" too since some of our partners use that request type.

Pull request description:
jhttp: allow content-type to include a charset
Accept "utf8" and "utf-8".

Fixes #110.


Patch:
diff --git a/jhttp/bridge.go b/jhttp/bridge.go
index a36c13d..df512c9 100644
--- a/jhttp/bridge.go
+++ b/jhttp/bridge.go
@@ -8,6 +8,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"mime"
 	"net/http"
 
 	"github.com/creachadair/jrpc2"
@@ -57,8 +58,12 @@ func (b Bridge) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 			w.WriteHeader(http.StatusMethodNotAllowed)
 			return
 		}
-		if req.Header.Get("Content-Type") != "application/json" {
-			w.WriteHeader(http.StatusUnsupportedMediaType)
+		mt, params, _ := mime.ParseMediaType(req.Header.Get("Content-Type"))
+		if mt != "application/json" {
+			http.Error(w, "content-type must be application/json", http.StatusUnsupportedMediaType)
+			return
+		} else if cs, ok := params["charset"]; ok && cs != "utf-8" && cs != "utf8" {
+			http.Error(w, "invalid content-type charset", http.StatusUnsupportedMediaType)
 			return
 		}
 	}


Test patch:
diff --git a/jhttp/jhttp_test.go b/jhttp/jhttp_test.go
index 747d01b..84b6417 100644
--- a/jhttp/jhttp_test.go
+++ b/jhttp/jhttp_test.go
@@ -40,22 +40,24 @@ func TestBridge(t *testing.T) {
 
 	// Verify that a valid POST request succeeds.
 	t.Run("PostOK", func(t *testing.T) {
-		got := mustPost(t, hsrv.URL, `{
+		for _, charset := range []string{"", "utf8", "utf-8"} {
+			got := mustPost(t, hsrv.URL, charset, `{
 		  "jsonrpc": "2.0",
 		  "id": 1,
 		  "method": "Test1",
 		  "params": ["a", "foolish", "consistency", "is", "the", "hobgoblin"]
 		}`, http.StatusOK)
 
-		const want = `{"jsonrpc":"2.0","id":1,"result":6}`
-		if got != want {
-			t.Errorf("POST body: got %#q, want %#q", got, want)
+			const want = `{"jsonrpc":"2.0","id":1,"result":6}`
+			if got != want {
+				t.Errorf("POST body: got %#q, want %#q", got, want)
+			}
 		}
 	})
 
 	// Verify that the bridge will accept a batch.
 	t.Run("PostBatchOK", func(t *testing.T) {
-		got := mustPost(t, hsrv.URL, `[
+		got := mustPost(t, hsrv.URL, "", `[
 		  {"jsonrpc":"2.0", "id": 3, "method": "Test1", "params": ["first"]},
 		  {"jsonrpc":"2.0", "id": 7, "method": "Test1", "params": ["among", "equals"]}
 		]`, http.StatusOK)
@@ -88,9 +90,18 @@ func TestBridge(t *testing.T) {
 		}
 	})
 
+	// Verify that the charset, if provided, is utf-8.
+	t.Run("PostInvalidCharset", func(t *testing.T) {
+		got := mustPost(t, hsrv.URL, "iso-8859-1", "{}", http.StatusUnsupportedMediaType)
+		const want = "invalid content-type charset\n"
+		if got != want {
+			t.Errorf("POST response body: got %q, want %q", got, want)
+		}
+	})
+
 	// Verify that a POST that generates a JSON-RPC error succeeds.
 	t.Run("PostErrorReply", func(t *testing.T) {
-		got := mustPost(t, hsrv.URL, `{
+		got := mustPost(t, hsrv.URL, "utf-8", `{
 		  "id": 1,
 		  "jsonrpc": "2.0"
 		}`, http.StatusOK)
@@ -103,7 +114,7 @@ func TestBridge(t *testing.T) {
 
 	// Verify that an invalid ID is not swallowed by the remapping process (see #80).
 	t.Run("PostInvalidID", func(t *testing.T) {
-		got := mustPost(t, hsrv.URL, `{
+		got := mustPost(t, hsrv.URL, "", `{
         "jsonrpc": "2.0",
         "id": ["this is totally bogus"],
         "method": "Test1"
@@ -117,7 +128,7 @@ func TestBridge(t *testing.T) {
 
 	// Verify that a notification returns an empty success.
 	t.Run("PostNotification", func(t *testing.T) {
-		got := mustPost(t, hsrv.URL, `{
+		got := mustPost(t, hsrv.URL, "", `{
 		  "jsonrpc": "2.0",
 		  "method": "TakeNotice",
 		  "params": []
@@ -224,7 +235,7 @@ func TestBridge_parseGETRequest(t *testing.T) {
 	})
 	t.Run("POST", func(t *testing.T) {
 		const req = `{"jsonrpc":"2.0", "id":1, "method":"str/eq", "params":["foo","foo"]}`
-		got := mustPost(t, hsrv.URL, req, http.StatusOK)
+		got := mustPost(t, hsrv.URL, "", req, http.StatusOK)
 
 		const want = `{"jsonrpc":"2.0","id":1,"result":true}`
 		if got != want {
@@ -302,9 +313,13 @@ func checkClose(t *testing.T, c io.Closer) {
 	}
 }
 
-func mustPost(t *testing.T, url, req string, code int) string {
+func mustPost(t *testing.T, url, charset, req string, code int) string {
 	t.Helper()
-	rsp, err := http.Post(url, "application/json", strings.NewReader(req))
+	ctype := "application/json"
+	if charset != "" {
+		ctype += "; charset=" + charset
+	}
+	rsp, err := http.Post(url, ctype, strings.NewReader(req))
 	if err != nil {
 		t.Fatalf("POST request failed: %v", err)
 	} else if got := rsp.StatusCode; got != code {