From ac5f06a5b18a902795ab9b31cf86ed7fc36e6b4a Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 12 Sep 2022 18:10:48 -0700 Subject: [PATCH 1/2] Allow and empty body to be valid for HandleCall/Deserialize; allow call/send method to be overridden --- jrpc/jrpcClient.go | 21 ++++++++++++++++++--- jrpc/jrpcServer.go | 11 ++++------- jrpc/jrpc_test.go | 14 +++++++++++++- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/jrpc/jrpcClient.go b/jrpc/jrpcClient.go index 583239f9d..557c6c2f1 100644 --- a/jrpc/jrpcClient.go +++ b/jrpc/jrpcClient.go @@ -71,9 +71,13 @@ type FetchError struct { // Destination is the URL and optional additional headers. type Destination struct { - URL string + URL string + // Default is nil, which means no additional headers. Headers *http.Header + // Default is 0 which means use global timeout. Timeout time.Duration + // Default is "" which will use POST if there is a payload and GET otherwise. + Method string } func (fe *FetchError) Error() string { @@ -129,6 +133,10 @@ func Serialize(obj interface{}) ([]byte, error) { // Deserialize deserializes json as a new object of desired type. func Deserialize[Q any](bytes []byte) (*Q, error) { var result Q + if len(bytes) == 0 { + // Allow empty body to be deserialized as empty object. + return &result, nil + } err := json.Unmarshal(bytes, &result) return &result, err // Will return zero object, not nil upon error } @@ -184,10 +192,17 @@ func Send(dest *Destination, jsonPayload []byte) (int, []byte, error) { var req *http.Request var err error var res []byte + method := dest.Method if len(jsonPayload) > 0 { - req, err = http.NewRequestWithContext(ctx, http.MethodPost, dest.URL, bytes.NewReader(jsonPayload)) + if method == "" { + method = http.MethodPost + } + req, err = http.NewRequestWithContext(ctx, method, dest.URL, bytes.NewReader(jsonPayload)) } else { - req, err = http.NewRequestWithContext(ctx, http.MethodGet, dest.URL, nil) + if method == "" { + method = http.MethodGet + } + req, err = http.NewRequestWithContext(ctx, method, dest.URL, nil) } if err != nil { return -1, res, err diff --git a/jrpc/jrpcServer.go b/jrpc/jrpcServer.go index 35ac90f12..81682abd4 100644 --- a/jrpc/jrpcServer.go +++ b/jrpc/jrpcServer.go @@ -83,14 +83,11 @@ func ReplyError(w http.ResponseWriter, extraMsg string, err error) error { // HandleCall deserializes the expected type from the request body. // Sample usage code: -// ``` // -// req, err := jrpc.HandleCall[Request](w, r) -// if err != nil { -// _ = jrpc.ReplyError(w, "request error", err) -// } -// -// ```. +// req, err := jrpc.HandleCall[Request](w, r) +// if err != nil { +// _ = jrpc.ReplyError(w, "request error", err) +// } func HandleCall[Q any](w http.ResponseWriter, r *http.Request) (*Q, error) { data, err := io.ReadAll(r.Body) if err != nil { diff --git a/jrpc/jrpc_test.go b/jrpc/jrpc_test.go index 7c551f122..9619b3954 100644 --- a/jrpc/jrpc_test.go +++ b/jrpc/jrpc_test.go @@ -143,6 +143,18 @@ func TestJPRC(t *testing.T) { if res.ConcatenatedStrings != "abcd" { t.Errorf("response doesn't contain expected string: %+v", res) } + // OK case: empty POST + dest := &jrpc.Destination{ + URL: url, + Method: http.MethodPost, // force post (default is get when no payload) + } + res, err = jrpc.Fetch[Response](dest, []byte{}) + if err != nil { + t.Errorf("failed Fetch with POST and empty body: %v", err) + } + if res.Error { + t.Errorf("response unexpectedly marked as failed: %+v", res) + } // Error cases // Empty request, using FetchBytes() code, bytes, err := jrpc.FetchBytes(jrpc.NewDestination(url)) @@ -278,7 +290,7 @@ func TestJPRC(t *testing.T) { if err == nil { t.Errorf("error expected %v", res) } - expected = "deserialization error, code 500: unexpected end of JSON input (raw reply: )" + expected = "non ok http result, code 500: (raw reply: )" if err.Error() != expected { t.Errorf("error string expected %q, got %q, %+v", expected, err.Error(), res) } From dfe8ce9ee4fa6768c37905547416985a3883f396 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 12 Sep 2022 18:36:06 -0700 Subject: [PATCH 2/2] prep for 1.37.1 --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d91c699f3..39f946c2f 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,13 @@ You can install from source: The [releases](https://github.com/fortio/fortio/releases) page has binaries for many OS/architecture combinations (see assets). ```shell -curl -L https://github.com/fortio/fortio/releases/download/v1.37.0/fortio-linux_amd64-1.37.0.tgz \ +curl -L https://github.com/fortio/fortio/releases/download/v1.37.1/fortio-linux_amd64-1.37.1.tgz \ | sudo tar -C / -xvzpf - # or the debian package -wget https://github.com/fortio/fortio/releases/download/v1.37.0/fortio_1.37.0_amd64.deb -dpkg -i fortio_1.37.0_amd64.deb +wget https://github.com/fortio/fortio/releases/download/v1.37.1/fortio_1.37.1_amd64.deb +dpkg -i fortio_1.37.1_amd64.deb # or the rpm -rpm -i https://github.com/fortio/fortio/releases/download/v1.37.0/fortio-1.37.0-1.x86_64.rpm +rpm -i https://github.com/fortio/fortio/releases/download/v1.37.1/fortio-1.37.1-1.x86_64.rpm # and more, see assets in release page ``` @@ -68,7 +68,7 @@ On a MacOS you can also install Fortio using [Homebrew](https://brew.sh/): brew install fortio ``` -On Windows, download https://github.com/fortio/fortio/releases/download/v1.37.0/fortio_win_1.37.0.zip and extract `fortio.exe` to any location, then using the Windows Command Prompt: +On Windows, download https://github.com/fortio/fortio/releases/download/v1.37.1/fortio_win_1.37.1.zip and extract `fortio.exe` to any location, then using the Windows Command Prompt: ``` fortio.exe server ``` @@ -116,7 +116,7 @@ Full list of command line flags (`fortio help`):
-Φορτίο 1.37.0 usage:
+Φορτίο 1.37.1 usage:
     fortio command [flags] target
 where command is one of: load (load testing), server (starts ui, rest api,
  http-echo, redirect, proxies, tcp-echo and grpc ping servers), tcp-echo (only