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). // 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 // 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 { if loopback {
call := rc.Calls.Get(path) call := rc.Calls.Get(path)
if call == nil { 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) _, out, err := jobs.NewJob(ctx, call.Fn, in)
if err != nil { 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 // Reshape (serialize then deserialize) the data so it is in the form expected
err = rc.Reshape(&out, out) err = rc.Reshape(&out, out)
if err != nil { 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 return out, nil
} }
@ -195,12 +205,12 @@ func doCall(ctx context.Context, path string, in rc.Params) (out rc.Params, err
url += path url += path
data, err := json.Marshal(in) data, err := json.Marshal(in)
if err != nil { 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)) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(data))
if err != nil { 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") 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) resp, err := client.Do(req)
if err != nil { 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) defer fs.CheckClose(resp.Body, &err)
if resp.StatusCode != http.StatusOK { // Read response
var body []byte var body []byte
body, err = io.ReadAll(resp.Body) var bodyString string
var bodyString string body, err = io.ReadAll(resp.Body)
if err == nil { bodyString = strings.TrimSpace(string(body))
bodyString = string(body) if err != nil {
} else { return errorf(resp.StatusCode, "failed to read rc response: %s: %s", resp.Status, bodyString)
bodyString = err.Error()
}
bodyString = strings.TrimSpace(bodyString)
return nil, fmt.Errorf("failed to read rc response: %s: %s", resp.Status, bodyString)
} }
// Parse output // Parse output
out = make(rc.Params) out = make(rc.Params)
err = json.NewDecoder(resp.Body).Decode(&out) err = json.NewDecoder(strings.NewReader(bodyString)).Decode(&out)
if err != nil { 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 // Check we got 200 OK