rc: always report an error as JSON

Before this change, the rclone rc command wouldn't actually report the
error as a JSON blob which is inconsitent with what the HTTP API does.

This change make sure we always report a JSON error, making a
synthetic one if necessary.

See: https://forum.rclone.org/t/when-using-rclone-rc-commands-somehow-return-errors-as-parsable-json/41855
Co-authored-by: Fawzib Rojas
This commit is contained in:
Nick Craig-Wood 2023-09-20 10:22:17 +01:00
parent 6072d314e1
commit 8c1e9a2905

View File

@ -168,6 +168,16 @@ func setAlternateFlag(flagName string, output *string) {
}
}
// Format an error and create a synthetic server return from it
func errorf(status int, path string, format string, arg ...any) (out rc.Params, err error) {
err = fmt.Errorf(format, arg...)
out = make(rc.Params)
out["error"] = err.Error()
out["path"] = path
out["status"] = status
return out, err
}
// do a call from (path, in) to (out, err).
//
// if err is set, out may be a valid error return or it may be nil
@ -176,16 +186,16 @@ func doCall(ctx context.Context, path string, in rc.Params) (out rc.Params, err
if loopback {
call := rc.Calls.Get(path)
if call == nil {
return nil, fmt.Errorf("method %q not found", path)
return errorf(http.StatusBadRequest, path, "loopback: method %q not found", path)
}
_, out, err := jobs.NewJob(ctx, call.Fn, in)
if err != nil {
return nil, fmt.Errorf("loopback call failed: %w", err)
return errorf(http.StatusInternalServerError, path, "loopback: call failed: %w", err)
}
// Reshape (serialize then deserialize) the data so it is in the form expected
err = rc.Reshape(&out, out)
if err != nil {
return nil, fmt.Errorf("loopback reshape failed: %w", err)
return errorf(http.StatusInternalServerError, path, "loopback: reshape failed: %w", err)
}
return out, nil
}
@ -195,12 +205,12 @@ func doCall(ctx context.Context, path string, in rc.Params) (out rc.Params, err
url += path
data, err := json.Marshal(in)
if err != nil {
return nil, fmt.Errorf("failed to encode JSON: %w", err)
return errorf(http.StatusBadRequest, path, "failed to encode request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
return errorf(http.StatusInternalServerError, path, "failed to make request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
@ -210,28 +220,24 @@ func doCall(ctx context.Context, path string, in rc.Params) (out rc.Params, err
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("connection failed: %w", err)
return errorf(http.StatusServiceUnavailable, path, "connection failed: %w", err)
}
defer fs.CheckClose(resp.Body, &err)
if resp.StatusCode != http.StatusOK {
var body []byte
body, err = io.ReadAll(resp.Body)
var bodyString string
if err == nil {
bodyString = string(body)
} else {
bodyString = err.Error()
}
bodyString = strings.TrimSpace(bodyString)
return nil, fmt.Errorf("failed to read rc response: %s: %s", resp.Status, bodyString)
// Read response
var body []byte
var bodyString string
body, err = io.ReadAll(resp.Body)
bodyString = strings.TrimSpace(string(body))
if err != nil {
return errorf(resp.StatusCode, "failed to read rc response: %s: %s", resp.Status, bodyString)
}
// Parse output
out = make(rc.Params)
err = json.NewDecoder(resp.Body).Decode(&out)
err = json.NewDecoder(strings.NewReader(bodyString)).Decode(&out)
if err != nil {
return nil, fmt.Errorf("failed to decode JSON: %w", err)
return errorf(resp.StatusCode, path, "failed to decode response: %w: %s", err, bodyString)
}
// Check we got 200 OK