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
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)
 	}