[インデックス 18811] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/rpc/jsonrpc
パッケージにおける、JSON-RPCレスポンスの構造に関するバグ修正です。具体的には、レスポンスにおいて result
と error
のどちらか一方のみが非nullとなるように修正し、JSON-RPCの仕様に厳密に準拠させることを目的としています。
コミット
commit 7718c626926beb0e5fed152d8d1a86be22570a34
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Mar 7 16:59:11 2014 -0800
net/rpc/jsonrpc: set exactly one of result or error to non-null in response
Fixes #7442
LGTM=gri
R=golang-codereviews, gri
CC=golang-codereviews
https://golang.org/cl/72570044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7718c626926beb0e5fed152d8d1a86be22570a34
元コミット内容
net/rpc/jsonrpc: set exactly one of result or error to non-null in response
このコミットメッセージは、net/rpc/jsonrpc
パッケージにおいて、JSON-RPCのレスポンスで result
フィールドまたは error
フィールドのいずれか一方のみが非nullになるように設定するという変更の意図を明確に示しています。これはJSON-RPC 2.0の仕様に準拠するための重要な修正です。
Fixes #7442
この行は、このコミットがGoのIssueトラッカーで報告されたIssue 7442を修正するものであることを示しています。Issue 7442は、JSON-RPCレスポンスの result
フィールドと error
フィールドの扱いに関するバグ、特にエラー発生時に result
がnullにならないという問題に関連していると推測されます。
LGTM=gri
, R=golang-codereviews, gri
, CC=golang-codereviews
これらはコードレビューに関する情報です。LGTM
(Looks Good To Me) は、gri
(Andrew Gerrand) がこの変更を承認したことを示します。R
はレビュー担当者、CC
はカーボンコピーの対象を示しており、golang-codereviews
はGoプロジェクトのコードレビューメーリングリストを指します。
https://golang.org/cl/72570044
これは、このコミットに対応するGerritのチェンジリスト(Change List)へのリンクです。Goプロジェクトでは、GitHubにプッシュされる前にGerritでコードレビューが行われます。
変更の背景
この変更の背景には、JSON-RPC 2.0の仕様への厳密な準拠があります。JSON-RPC 2.0の仕様では、レスポンスオブジェクトには result
または error
のどちらか一方のメンバーのみを含めるべきであると明確に規定されています。
従来の net/rpc/jsonrpc
の実装では、エラーが発生した場合でも result
フィールドが null
ではなく、何らかの値(例えば、ゼロ値や以前の値)を持つ可能性がありました。これは、JSON-RPCクライアントがレスポンスを正しくパースする際に混乱を招く可能性があり、特に厳密なJSON-RPCパーサーを使用している場合に問題となります。
Issue 7442は、この仕様不適合を指摘したものであり、このコミットはその問題を解決するために導入されました。具体的には、TestServerErrorHasNullResult
というテストが以前はスキップされていた(t.Skip("Known failing test; Issue 7442")
)ことから、この問題が認識されており、修正が待たれていたことがわかります。
前提知識の解説
JSON-RPC
JSON-RPCは、リモートプロシージャコール(RPC)プロトコルの一種で、データフォーマットとしてJSONを使用します。HTTPまたはTCP/IPなどのトランスポート層プロトコル上で動作し、クライアントがサーバー上のメソッドを呼び出し、その結果をJSON形式で受け取ることを可能にします。
JSON-RPC 2.0の仕様は、そのシンプルさと柔軟性から広く採用されています。主要な概念は以下の通りです。
- Request Object: クライアントがサーバーに送信するリクエスト。
jsonrpc
(バージョン),method
(呼び出すメソッド名),params
(メソッドの引数),id
(リクエストID) を含みます。 - Response Object: サーバーがクライアントに返すレスポンス。
jsonrpc
(バージョン),result
(成功時の結果),error
(エラー時の情報),id
(対応するリクエストID) を含みます。 - Error Object: エラーが発生した場合に
error
フィールドに含まれるオブジェクト。code
(エラーコード),message
(エラーメッセージ),data
(追加のエラー情報) を含みます。
JSON-RPC 2.0 仕様における result
と error
JSON-RPC 2.0の仕様(https://www.jsonrpc.org/specification)では、レスポンスオブジェクトについて以下のように規定されています。
A Response object MUST contain one of either:
result
: This member is REQUIRED on success.error
: This member is REQUIRED on error.The Response object MUST NOT contain both a
result
member anderror
member.
つまり、成功レスポンスでは result
が必須であり error
は存在しない、エラーレスポンスでは error
が必須であり result
は存在しない、という排他的な関係が求められます。このコミットは、この「どちらか一方のみが存在する」というルールを厳密に適用するためのものです。
Go言語の net/rpc
および net/rpc/jsonrpc
パッケージ
net/rpc
: Go言語の標準ライブラリに含まれるRPCパッケージです。Goの構造体とメソッドをRPCサービスとして公開し、クライアントから呼び出すための基本的なフレームワークを提供します。エンコーディングには通常、Go独自のgobエンコーディングが使用されます。net/rpc/jsonrpc
:net/rpc
パッケージのサブパッケージで、JSON-RPCプロトコルを介してRPCサービスを提供するためのコーデック(エンコーダ/デコーダ)を実装しています。これにより、GoのRPCサービスをJSON-RPCクライアントから利用できるようになります。ServerCodec
とClientCodec
が主要なインターフェースです。
技術的詳細
このコミットは、net/rpc/jsonrpc
パッケージ内の server.go
ファイルにある serverCodec
型の WriteResponse
メソッドのロジックを変更しています。このメソッドは、RPC呼び出しの結果をJSON-RPCレスポンスとしてクライアントに書き込む役割を担っています。
変更前は、serverResponse
構造体のインスタンスを宣言し、Id
と Result
を設定した後、エラーの有無に応じて Error
フィールドを設定していました。この際、r.Error == ""
(エラーがない場合) に resp.Error = nil
と設定していましたが、r.Error != ""
(エラーがある場合) には resp.Result
が明示的に null
に設定されるロジックがありませんでした。これにより、エラーレスポンスにもかかわらず result
フィールドが何らかの値を持つ可能性がありました。
変更後は、serverResponse
構造体の初期化時に Id
のみを設定し、その後の条件分岐で result
または error
のどちらか一方のみを設定するようにロジックが修正されています。
具体的には、以下のようになります。
resp := serverResponse{Id: b}
:serverResponse
構造体を初期化し、Id
フィールドのみを設定します。この時点ではResult
とError
はGoのゼロ値(ポインタ型の場合はnil
)です。if r.Error == ""
:- エラーがない場合(成功レスポンス)。
resp.Result = x
:result
フィールドに実際のRPC呼び出し結果x
を設定します。このときresp.Error
は初期値のnil
のままです。
else
:- エラーがある場合(エラーレスポンス)。
resp.Error = r.Error
:error
フィールドにエラーメッセージr.Error
を設定します。このときresp.Result
は初期値のnil
のままです。
この修正により、JSON-RPC 2.0の仕様で求められる「result
と error
のどちらか一方のみが非nullである」という条件が満たされるようになります。
また、all_test.go
の変更は、この修正が正しく機能することを確認するためのテストの有効化です。TestServerErrorHasNullResult
というテストが以前はスキップされていましたが、このコミットによってスキップが解除され、テストが実行されるようになります。このテストは、サーバーエラーが発生した場合にレスポンスの result
フィールドが null
であることを検証するものです。
コアとなるコードの変更箇所
diff --git a/src/pkg/net/rpc/jsonrpc/all_test.go b/src/pkg/net/rpc/jsonrpc/all_test.go
index 07913572aa..a433a365e8 100644
--- a/src/pkg/net/rpc/jsonrpc/all_test.go
+++ b/src/pkg/net/rpc/jsonrpc/all_test.go
@@ -205,7 +205,6 @@ func TestMalformedOutput(t *testing.T) {
}
func TestServerErrorHasNullResult(t *testing.T) {
- t.Skip("Known failing test; Issue 7442")
var out bytes.Buffer
sc := NewServerCodec(struct {
io.Reader
diff --git a/src/pkg/net/rpc/jsonrpc/server.go b/src/pkg/net/rpc/jsonrpc/server.go
index 16ec0fe9ad..e6d37cfa64 100644
--- a/src/pkg/net/rpc/jsonrpc/server.go
+++ b/src/pkg/net/rpc/jsonrpc/server.go
@@ -100,7 +100,6 @@ func (c *serverCodec) ReadRequestBody(x interface{}) error {
var null = json.RawMessage([]byte("null"))
func (c *serverCodec) WriteResponse(r *rpc.Response, x interface{}) error {
- var resp serverResponse
c.mutex.Lock()
tb, ok := c.pending[r.Seq]
if !ok {
@@ -114,10 +113,9 @@ func (c *serverCodec) WriteResponse(r *rpc.Response, x interface{}) error {
// Invalid request so no id. Use JSON null.
tb = &null
}
- resp.Id = tb
- resp.Result = x
+ resp := serverResponse{Id: tb}
if r.Error == "" {
- resp.Error = nil
+ resp.Result = x
} else {
resp.Error = r.Error
}
コアとなるコードの解説
src/pkg/net/rpc/jsonrpc/all_test.go
の変更
func TestServerErrorHasNullResult(t *testing.T) {
- t.Skip("Known failing test; Issue 7442")
var out bytes.Buffer
sc := NewServerCodec(struct {
io.Reader
この変更は、TestServerErrorHasNullResult
というテスト関数の先頭にあった t.Skip("Known failing test; Issue 7442")
の行を削除しています。これは、このテストが以前は既知のバグ(Issue 7442)のためにスキップされていたが、今回の修正によってそのバグが解消されたため、テストを有効化して実行できるようにしたことを意味します。このテストは、JSON-RPCサーバーがエラーレスポンスを返す際に、result
フィールドが正しく null
になることを検証するものです。
src/pkg/net/rpc/jsonrpc/server.go
の変更
func (c *serverCodec) WriteResponse(r *rpc.Response, x interface{}) error {
- var resp serverResponse
c.mutex.Lock()
tb, ok := c.pending[r.Seq]
if !ok {
@@ -114,10 +113,9 @@ func (c *serverCodec) WriteResponse(r *rpc.Response, x interface{}) error {
// Invalid request so no id. Use JSON null.
tb = &null
}
- resp.Id = tb
- resp.Result = x
+ resp := serverResponse{Id: tb}
if r.Error == "" {
- resp.Error = nil
+ resp.Result = x
} else {
resp.Error = r.Error
}
この部分が変更の核心です。
変更前:
var resp serverResponse
// ...
resp.Id = tb
resp.Result = x // ここで常にResultが設定される
if r.Error == "" {
resp.Error = nil
} else {
resp.Error = r.Error // エラーがあってもResultはxのまま
}
変更前は、まず serverResponse
型の変数 resp
を宣言し、その後に resp.Id = tb
と resp.Result = x
を設定していました。resp.Result = x
の行は、RPC呼び出しの結果 x
を resp.Result
に代入しています。その後、r.Error == ""
の条件でエラーの有無をチェックし、エラーがない場合は resp.Error = nil
と設定していました。しかし、エラーがある場合 (else
ブロック) では resp.Error = r.Error
と設定するだけで、resp.Result
は x
の値がそのまま残ってしまいます。これが、エラーレスポンスにもかかわらず result
フィールドが null
にならない原因でした。
変更後:
resp := serverResponse{Id: tb} // Idのみを設定して初期化
// ...
if r.Error == "" {
resp.Result = x // 成功時のみResultを設定
} else {
resp.Error = r.Error // エラー時のみErrorを設定
}
変更後では、resp
の宣言と初期化が resp := serverResponse{Id: tb}
となっています。これにより、resp
は Id
フィールドのみが設定され、Result
と Error
フィールドはGoのゼロ値(この場合は nil
)で初期化されます。
その後の if r.Error == ""
の条件分岐で、エラーがない場合は resp.Result = x
と Result
フィールドに結果を代入します。このとき Error
フィールドは初期値の nil
のままです。
一方、エラーがある場合は else
ブロックに入り、resp.Error = r.Error
と Error
フィールドにエラーメッセージを代入します。このとき Result
フィールドは初期値の nil
のままです。
この修正により、result
と error
のどちらか一方のみが設定され、もう一方は nil
のままとなるため、JSON-RPC 2.0の仕様に厳密に準拠したレスポンスが生成されるようになります。
関連リンク
- Go Issue 7442: https://github.com/golang/go/issues/7442 (このコミットが修正したIssue)
- Go Gerrit Change-Id:
I72570044
(このコミットに対応するGerritチェンジリスト) - Go
net/rpc/jsonrpc
パッケージドキュメント: https://pkg.go.dev/net/rpc/jsonrpc
参考にした情報源リンク
- JSON-RPC 2.0 Specification: https://www.jsonrpc.org/specification
- Go
net/rpc
パッケージドキュメント: https://pkg.go.dev/net/rpc - Go言語のコードレビュープロセス (Gerritについて): https://go.dev/doc/contribute#code_reviews
- Go言語のIssueトラッカー: https://go.dev/issue
bytes.Buffer
のドキュメント: https://pkg.go.dev/bytes#Bufferio.Reader
およびio.Writer
インターフェース: https://pkg.go.dev/iojson.RawMessage
のドキュメント: https://pkg.go.dev/encoding/json#RawMessagetesting.T.Skip
のドキュメント: https://pkg.go.dev/testing#T.Skip- Go言語におけるゼロ値: https://go.dev/tour/basics/12