From a12278f8d78a10f5f63d426a35e915274bd75fa4 Mon Sep 17 00:00:00 2001 From: StillGreen-san <40620628+StillGreen-san@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:23:52 +0100 Subject: [PATCH 1/8] remove incorrect GraphApiError parsing --- .../onedrive/GraphApiErrorException.java | 68 +++---------------- 1 file changed, 10 insertions(+), 58 deletions(-) diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java index 80e56c3a..93a8cfc2 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java @@ -2,15 +2,10 @@ import okhttp3.Response; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONException; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; /** * an exception representing a microsoft graph api error @@ -18,23 +13,16 @@ public class GraphApiErrorException extends Exception { private static final String ERROR_OBJ_KEY = "error"; private static final String CODE_STR_KEY = "code"; - private static final String INNERERROR_OBJ_KEY = "innererror"; private static final String MESSAGE_STR_KEY = "message"; - private static final String DETAILS_ARR_KEY = "details"; - /** status code of the response or -1 if not available */ + /** status code of the response */ public final int statusCode; /** an error code string for the error that occurred */ public final String errorCode; /** a developer ready message about the error that occurred. this shouldn't be displayed to the user directly */ public final String errorMessage; - /** optional list of additional error objects that might be more specific than the top-level error */ - public final List innerErrors; - /** - * optional list of additional error objects that might provide a breakdown of multiple errors encountered - * while processing the request - */ - public final List details; + /** the full error object */ + public final JSONObject errorObject; /** * create the exception from a response @@ -59,56 +47,20 @@ public GraphApiErrorException(int statusCode, @NotNull String responseBody) { this(statusCode, new JSONObject(responseBody).getJSONObject(ERROR_OBJ_KEY)); } - private static List parseInnerErrors(@Nullable JSONObject innerErrors) { - List list = new ArrayList<>(); - while (innerErrors != null) { - String errorCode = innerErrors.optString(CODE_STR_KEY); - if (errorCode != null) { - list.add(errorCode); - } - innerErrors = innerErrors.optJSONObject(INNERERROR_OBJ_KEY); - } - return list; - } - - private static List parseDetails(@Nullable JSONArray details) { - if (details == null) { - return new ArrayList<>(); - } - List list = new ArrayList<>(details.length()); - for (int detailIdx = 0; detailIdx < details.length(); detailIdx++) { - list.add(new GraphApiErrorException(-1, details.getJSONObject(detailIdx).getJSONObject(ERROR_OBJ_KEY))); - } - return list; - } - private GraphApiErrorException(int statusCode, @NotNull JSONObject error) { - this(statusCode, error.getString(CODE_STR_KEY), error.getString(MESSAGE_STR_KEY), - parseInnerErrors(error.optJSONObject(INNERERROR_OBJ_KEY)), - parseDetails(error.optJSONArray(DETAILS_ARR_KEY))); + this(statusCode, error.getString(CODE_STR_KEY), error.getString(MESSAGE_STR_KEY), error); } - private static String toMessage(int statusCode, String errorCode, String errorMessage, List innerErrors, - List details) { - String format = "%d %s : \"%s\"%s%s"; - String inner = String.join("\", \"", innerErrors); - String detail = details.stream().map(GraphApiErrorException::getMessage).collect(Collectors.joining(" }, { ")); - if (!inner.isEmpty()) { - inner = " inner:[ \"" + inner + "\" ]"; - } - if (!detail.isEmpty()) { - detail = " details:[ { " + detail + " } ]"; - } - return String.format(format, statusCode, errorCode, errorMessage, inner, detail); + private static String toMessage(int statusCode, String errorCode, String errorMessage, JSONObject errorObject) { + String format = "%d %s : \"%s\"\n%s"; + return String.format(format, statusCode, errorCode, errorMessage, errorObject.toString(2)); } - private GraphApiErrorException(int statusCode, String errorCode, String errorMessage, List innerErrors, - List details) { - super(toMessage(statusCode, errorCode, errorMessage, innerErrors, details)); + private GraphApiErrorException(int statusCode, String errorCode, String errorMessage, JSONObject errorObject) { + super(toMessage(statusCode, errorCode, errorMessage, errorObject)); this.statusCode = statusCode; this.errorCode = errorCode; this.errorMessage = errorMessage; - this.innerErrors = innerErrors; - this.details = details; + this.errorObject = errorObject; } } From 076e963e848271264b922c11895cd3b7790b57cf Mon Sep 17 00:00:00 2001 From: StillGreen-san <40620628+StillGreen-san@users.noreply.github.com> Date: Tue, 11 Nov 2025 13:16:26 +0100 Subject: [PATCH 2/8] prevent exceptions during GraphApiErrorException creation --- .../onedrive/GraphApiErrorException.java | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java index 93a8cfc2..97041e0b 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java @@ -2,6 +2,7 @@ import okhttp3.Response; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.json.JSONObject; import org.json.JSONException; @@ -18,11 +19,11 @@ public class GraphApiErrorException extends Exception { /** status code of the response */ public final int statusCode; /** an error code string for the error that occurred */ - public final String errorCode; + public final @NotNull String errorCode; /** a developer ready message about the error that occurred. this shouldn't be displayed to the user directly */ - public final String errorMessage; + public final @NotNull String errorMessage; /** the full error object */ - public final JSONObject errorObject; + public final @Nullable JSONObject errorObject; /** * create the exception from a response @@ -33,34 +34,64 @@ public class GraphApiErrorException extends Exception { * @throws JSONException if the body does not contain the expected json values */ public GraphApiErrorException(@NotNull Response response) throws IOException { - this(response.code(), new JSONObject(response.body().string()).getJSONObject(ERROR_OBJ_KEY)); + this(response.code(), response.body().string()); } /** * create the exception from a status code and response body * - * @param statusCode of the response - * @param responseBody of the response + * @param statusCode of the response + * @param jsonResponse string of the response body * @throws JSONException if the body does not contain the expected json values */ - public GraphApiErrorException(int statusCode, @NotNull String responseBody) { - this(statusCode, new JSONObject(responseBody).getJSONObject(ERROR_OBJ_KEY)); + public GraphApiErrorException(int statusCode, @NotNull String jsonResponse) { + this(statusCode, new ParsedError(jsonResponse)); } - private GraphApiErrorException(int statusCode, @NotNull JSONObject error) { - this(statusCode, error.getString(CODE_STR_KEY), error.getString(MESSAGE_STR_KEY), error); + /** parsing logic that needs to happen before calling this/super constructor */ + private static class ParsedError { + public final @NotNull String errorCode; + public final @NotNull String errorMessage; + public final @Nullable JSONObject errorObject; + + public ParsedError(@NotNull String responseBody) { + JSONObject errorResponse; + try { + errorResponse = new JSONObject(responseBody); + } catch (JSONException jsonException) { + this.errorCode = "invalidErrorResponse"; + this.errorMessage = String.valueOf(jsonException.getMessage()); + this.errorObject = null; + return; + } + JSONObject errorObject = errorResponse.optJSONObject(ERROR_OBJ_KEY); + if (errorObject == null) { + this.errorCode = "invalidErrorResponse"; + this.errorMessage = "error response does not contain an json object 'error'"; + this.errorObject = null; + return; + } + + this.errorCode = errorObject.optString(CODE_STR_KEY, "invalid/missing member 'code'"); + this.errorMessage = errorObject.optString(MESSAGE_STR_KEY, "invalid/missing member 'message'"); + this.errorObject = errorObject; + } } - private static String toMessage(int statusCode, String errorCode, String errorMessage, JSONObject errorObject) { - String format = "%d %s : \"%s\"\n%s"; - return String.format(format, statusCode, errorCode, errorMessage, errorObject.toString(2)); + private static String toMessage(int statusCode, @NotNull ParsedError error) { + String format = "%d %s : \"%s\""; + if (error.errorObject == null) { + return String.format(format, statusCode, error.errorCode, error.errorMessage); + } + return String.format(format + "\n%s", statusCode, error.errorCode, error.errorMessage, + error.errorObject.toString(2)); } - private GraphApiErrorException(int statusCode, String errorCode, String errorMessage, JSONObject errorObject) { - super(toMessage(statusCode, errorCode, errorMessage, errorObject)); + private GraphApiErrorException(int statusCode, @NotNull ParsedError error) { + super(toMessage(statusCode, error)); this.statusCode = statusCode; - this.errorCode = errorCode; - this.errorMessage = errorMessage; - this.errorObject = errorObject; + this.errorCode = error.errorCode; + this.errorMessage = error.errorMessage; + this.errorObject = error.errorObject; } } From 47974676c9d70b4d95c333763511ffeeadc193d5 Mon Sep 17 00:00:00 2001 From: StillGreen-san <40620628+StillGreen-san@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:17:09 +0100 Subject: [PATCH 3/8] remove Response constructor from GraphApiErrorException --- .../onedrive/GraphApiErrorException.java | 15 ------ .../uploaders/onedrive/OneDriveUploader.java | 52 +++++++++++++------ 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java index 97041e0b..f82a90dd 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java @@ -1,13 +1,10 @@ package ratismal.drivebackup.uploaders.onedrive; -import okhttp3.Response; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.JSONObject; import org.json.JSONException; -import java.io.IOException; - /** * an exception representing a microsoft graph api error */ @@ -25,18 +22,6 @@ public class GraphApiErrorException extends Exception { /** the full error object */ public final @Nullable JSONObject errorObject; - /** - * create the exception from a response - * - * @param response to parse error from its body - * @throws IOException if the body string could not be loaded - * @throws NullPointerException if the body could not be loaded - * @throws JSONException if the body does not contain the expected json values - */ - public GraphApiErrorException(@NotNull Response response) throws IOException { - this(response.code(), response.body().string()); - } - /** * create the exception from a status code and response body * diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java index 28ef6b81..47fdfa7f 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java @@ -87,6 +87,7 @@ private void retrieveNewAccessToken() throws Exception { .post(requestBody) .build(); try (Response response = DriveBackup.httpClient.newCall(request).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) JSONObject parsedResponse = new JSONObject(response.body().string()); if (!response.isSuccessful()) { String error = parsedResponse.optString("error"); @@ -263,10 +264,12 @@ private FQID createFolder(@NotNull FQID root, @NotNull String folder) throws IOE .post(requestBody) .build(); try (Response response = DriveBackup.httpClient.newCall(request).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String jsonResponse = response.body().string(); if (response.code() != 201) { - throw new GraphApiErrorException(response); + throw new GraphApiErrorException(response.code(), jsonResponse); } - JSONObject parsedResponse = new JSONObject(response.body().string()); + JSONObject parsedResponse = new JSONObject(jsonResponse); String driveId = parsedResponse.getJSONObject("parentReference").getString("driveId"); String itemId = parsedResponse.getString("id"); return new FQID(driveId, itemId); @@ -295,10 +298,12 @@ private FQID createRootFolder(@NotNull String folder) throws IOException, GraphA .post(requestBody) .build(); try (Response response = DriveBackup.httpClient.newCall(request).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String responseBody = response.body().string(); if (response.code() != 201) { - throw new GraphApiErrorException(response); + throw new GraphApiErrorException(response.code(), responseBody); } - JSONObject parsedResponse = new JSONObject(response.body().string()); + JSONObject parsedResponse = new JSONObject(responseBody); String driveId = parsedResponse.getJSONObject("parentReference").getString("driveId"); String itemId = parsedResponse.getString("id"); return new FQID(driveId, itemId); @@ -322,13 +327,15 @@ private FQID getRootFolder(@NotNull String folder) throws IOException, GraphApiE .build(); JSONObject parsedResponse; try (Response response = DriveBackup.httpClient.newCall(request).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String responseBody = response.body().string(); if (response.code() == 404) { return null; } if (!response.isSuccessful()) { - throw new GraphApiErrorException(response); + throw new GraphApiErrorException(response.code(), responseBody); } - parsedResponse = new JSONObject(response.body().string()); + parsedResponse = new JSONObject(responseBody); } if (parsedResponse.has("remoteItem")) { parsedResponse = parsedResponse.getJSONObject("remoteItem"); @@ -386,10 +393,12 @@ private List getChildren(@NotNull FQID folder, @NotNull String query .url(targetUrl) .build(); try (Response response = DriveBackup.httpClient.newCall(request).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String responseBody = response.body().string(); if (!response.isSuccessful()) { - throw new GraphApiErrorException(response); + throw new GraphApiErrorException(response.code(), responseBody); } - JSONObject parsedResponse = new JSONObject(response.body().string()); + JSONObject parsedResponse = new JSONObject(responseBody); JSONArray someChildren = parsedResponse.getJSONArray("value"); allChildren.ensureCapacity(parsedResponse.optInt("@odata.count", someChildren.length())); for (int i = 0; i < someChildren.length(); i++) { @@ -419,7 +428,8 @@ private void recycleItem(@NotNull String driveId, @NotNull String itemId) throws .build(); try (Response response = DriveBackup.httpClient.newCall(delteRequest).execute()) { if (response.code() != 204 && response.code() != 404) { - throw new GraphApiErrorException(response); + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + throw new GraphApiErrorException(response.code(), response.body().string()); } } } @@ -441,10 +451,12 @@ private FQID uploadSmallFile(@NotNull File file, @NotNull FQID destinationFolder .put(RequestBody.create(file, textMediaType)) .build(); try (Response response = DriveBackup.httpClient.newCall(uploadRequest).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String responseBody = response.body().string(); if (response.code() != 201) { - throw new GraphApiErrorException(response); + throw new GraphApiErrorException(response.code(), responseBody); } - JSONObject parsedResponse = new JSONObject(response.body().string()); + JSONObject parsedResponse = new JSONObject(responseBody); return new FQID(destinationFolder.driveId, parsedResponse.getString("id")); } } @@ -468,10 +480,12 @@ private String createUploadSession(@NotNull String fileName, @NotNull FQID desti .post(RequestBody.create("{}", jsonMediaType)) .build(); try (Response response = DriveBackup.httpClient.newCall(request).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String responseBody = response.body().string(); if (!response.isSuccessful()) { - throw new GraphApiErrorException(response); + throw new GraphApiErrorException(response.code(), responseBody); } - return new JSONObject(response.body().string()).getString("uploadUrl"); + return new JSONObject(responseBody).getString("uploadUrl"); } } @@ -501,8 +515,10 @@ private void uploadToSession(@NotNull String uploadURL, @NotNull RandomAccessFil .put(RequestBody.create(bytesToUpload, zipMediaType)) .build(); try (Response uploadResponse = DriveBackup.httpClient.newCall(uploadRequest).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String uploadResponseBody = uploadResponse.body().string(); if (uploadResponse.code() == 202) { - JSONObject responseObject = new JSONObject(uploadResponse.body().string()); + JSONObject responseObject = new JSONObject(uploadResponseBody); JSONArray expectedRanges = responseObject.getJSONArray("nextExpectedRanges"); range = new Range(expectedRanges.getString(0), UPLOAD_CHUNK_SIZE); exponentialBackoffMillis = EXPONENTIAL_BACKOFF_MILLIS_DEFAULT; @@ -513,13 +529,15 @@ private void uploadToSession(@NotNull String uploadURL, @NotNull RandomAccessFil if (retryCount > MAX_RETRY_ATTEMPTS || uploadResponse.code() == 409) { Request cancelRequest = new Request.Builder().url(uploadURL).delete().build(); DriveBackup.httpClient.newCall(cancelRequest).execute().close(); - throw new GraphApiErrorException(uploadResponse); + throw new GraphApiErrorException(uploadResponse.code(), uploadResponseBody); } else if (uploadResponse.code() == 404) { - throw new GraphApiErrorException(uploadResponse); + throw new GraphApiErrorException(uploadResponse.code(), uploadResponseBody); } else if (uploadResponse.code() == 416) { Request statusRequest = new Request.Builder().url(uploadURL).build(); try (Response statusResponse = DriveBackup.httpClient.newCall(statusRequest).execute()) { - JSONObject responseObject = new JSONObject(statusResponse.body().string()); + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String statusResponseBody = statusResponse.body().string(); + JSONObject responseObject = new JSONObject(statusResponseBody); JSONArray expectedRanges = responseObject.getJSONArray("nextExpectedRanges"); range = new Range(expectedRanges.getString(0), UPLOAD_CHUNK_SIZE); } From 42ed82962e82993678b1ddbe21b81f51a1037cad Mon Sep 17 00:00:00 2001 From: StillGreen-san <40620628+StillGreen-san@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:38:01 +0100 Subject: [PATCH 4/8] add JsonUtil for case-insensitive JSON lookup --- .../ratismal/drivebackup/util/JsonUtil.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 DriveBackup/src/main/java/ratismal/drivebackup/util/JsonUtil.java diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/util/JsonUtil.java b/DriveBackup/src/main/java/ratismal/drivebackup/util/JsonUtil.java new file mode 100644 index 00000000..034d4c3a --- /dev/null +++ b/DriveBackup/src/main/java/ratismal/drivebackup/util/JsonUtil.java @@ -0,0 +1,58 @@ +package ratismal.drivebackup.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +import java.util.Iterator; + +public class JsonUtil { + /** + * tries to get the {@link JSONObject} associated with a key, ignoring case differences + * + * @param json object to lookup in + * @param key string to compare against + * @return {@link JSONObject} or null if the value was not found or is not a {@link JSONObject} + */ + @Nullable + public static JSONObject optJsonObjectIgnoreCase(@NotNull JSONObject json, @NotNull String key) { + return json.optJSONObject(findKeyIgnoreCase(json, key)); + } + + /** + * tries to get the {@link String} associated with a key, ignoring case differences. if the value is not a string + * and is not null, then it is converted to a string + * + * @param json object to lookup in + * @param key string to compare against + * @param alt string to return if not found + * @return {@link String} or alt if the value was not found + */ + @NotNull + public static String optStringIgnoreCase(@NotNull JSONObject json, @NotNull String key, @NotNull String alt) { + return json.optString(findKeyIgnoreCase(json, key), alt); + } + + /** + * tries to find a key in json that matches the given key, ignoring case differences. an exact match is preferred. + * otherwise any case-insensitive match is returned, or null if nothing is found. + * + * @param json object to search in + * @param key string to search for + * @return the matching key if found, or null + */ + @Nullable + public static String findKeyIgnoreCase(@NotNull JSONObject json, @NotNull String key) { + if (json.has(key)) { + return key; + } + Iterator keyIt = json.keys(); + while (keyIt.hasNext()) { + String member = keyIt.next(); + if (member.equalsIgnoreCase(key)) { + return member; + } + } + return null; + } +} From 5d6f22e5e9a228191ee571f235a48a4640ae60bd Mon Sep 17 00:00:00 2001 From: StillGreen-san <40620628+StillGreen-san@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:42:46 +0100 Subject: [PATCH 5/8] use case-insensitive JSON parsing in GraphApiErrorException --- .../uploaders/onedrive/GraphApiErrorException.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java index f82a90dd..01c356bb 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java @@ -5,6 +5,9 @@ import org.json.JSONObject; import org.json.JSONException; +import static ratismal.drivebackup.util.JsonUtil.optJsonObjectIgnoreCase; +import static ratismal.drivebackup.util.JsonUtil.optStringIgnoreCase; + /** * an exception representing a microsoft graph api error */ @@ -49,7 +52,7 @@ public ParsedError(@NotNull String responseBody) { this.errorObject = null; return; } - JSONObject errorObject = errorResponse.optJSONObject(ERROR_OBJ_KEY); + JSONObject errorObject = optJsonObjectIgnoreCase(errorResponse, ERROR_OBJ_KEY); if (errorObject == null) { this.errorCode = "invalidErrorResponse"; this.errorMessage = "error response does not contain an json object 'error'"; @@ -57,8 +60,8 @@ public ParsedError(@NotNull String responseBody) { return; } - this.errorCode = errorObject.optString(CODE_STR_KEY, "invalid/missing member 'code'"); - this.errorMessage = errorObject.optString(MESSAGE_STR_KEY, "invalid/missing member 'message'"); + this.errorCode = optStringIgnoreCase(errorObject, CODE_STR_KEY, "invalid/missing member 'code'"); + this.errorMessage = optStringIgnoreCase(errorObject, MESSAGE_STR_KEY, "invalid/missing member 'message'"); this.errorObject = errorObject; } } From e6927bd766d0a2f9425d23fa4c76668270329b3f Mon Sep 17 00:00:00 2001 From: StillGreen-san <40620628+StillGreen-san@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:02:48 +0100 Subject: [PATCH 6/8] add missing success check in OneDriveUploader --- .../drivebackup/uploaders/onedrive/OneDriveUploader.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java index 47fdfa7f..06d368b4 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java @@ -537,6 +537,9 @@ private void uploadToSession(@NotNull String uploadURL, @NotNull RandomAccessFil try (Response statusResponse = DriveBackup.httpClient.newCall(statusRequest).execute()) { //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) String statusResponseBody = statusResponse.body().string(); + if (!statusResponse.isSuccessful()) { + throw new GraphApiErrorException(statusResponse.code(), statusResponseBody); + } JSONObject responseObject = new JSONObject(statusResponseBody); JSONArray expectedRanges = responseObject.getJSONArray("nextExpectedRanges"); range = new Range(expectedRanges.getString(0), UPLOAD_CHUNK_SIZE); From 074d341a9a34a2bd58b654cabf1de3a8e8b86f22 Mon Sep 17 00:00:00 2001 From: StillGreen-san <40620628+StillGreen-san@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:51:03 +0100 Subject: [PATCH 7/8] OneDrive cleanup --- .../onedrive/GraphApiErrorException.java | 12 ++- .../uploaders/onedrive/OneDriveUploader.java | 95 ++++++++++++------- 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java index 01c356bb..8fd5bfbc 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java @@ -66,13 +66,17 @@ public ParsedError(@NotNull String responseBody) { } } - private static String toMessage(int statusCode, @NotNull ParsedError error) { + /** + * constructs a formatted error message string using the provided status code and error details. + * if ParsedError.errorObject is non-null, its included as pretty-printed json. + */ + private static @NotNull String toMessage(int statusCode, @NotNull ParsedError error) { String format = "%d %s : \"%s\""; + String common = String.format(format, statusCode, error.errorCode, error.errorMessage); if (error.errorObject == null) { - return String.format(format, statusCode, error.errorCode, error.errorMessage); + return common; } - return String.format(format + "\n%s", statusCode, error.errorCode, error.errorMessage, - error.errorObject.toString(2)); + return common + '\n' + error.errorObject.toString(2); } private GraphApiErrorException(int statusCode, @NotNull ParsedError error) { diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java index 06d368b4..a4236aad 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/OneDriveUploader.java @@ -503,57 +503,71 @@ private String createUploadSession(@NotNull String fileName, @NotNull FQID desti */ private void uploadToSession(@NotNull String uploadURL, @NotNull RandomAccessFile randomAccessFile) throws IOException, GraphApiErrorException, InterruptedException { - int exponentialBackoffMillis = EXPONENTIAL_BACKOFF_MILLIS_DEFAULT; - int retryCount = 0; - Range range = new Range(0, UPLOAD_CHUNK_SIZE); + UploadSessionState state = new UploadSessionState(); while (true) { - byte[] bytesToUpload = getChunk(randomAccessFile, range); + byte[] bytesToUpload = getChunk(randomAccessFile, state.range); Request uploadRequest = new Request.Builder() .addHeader("Content-Range", String.format("bytes %d-%d/%d", - range.start, range.start + bytesToUpload.length - 1, randomAccessFile.length())) + state.range.start, state.range.start + bytesToUpload.length - 1, randomAccessFile.length())) .url(uploadURL) .put(RequestBody.create(bytesToUpload, zipMediaType)) .build(); try (Response uploadResponse = DriveBackup.httpClient.newCall(uploadRequest).execute()) { //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) - String uploadResponseBody = uploadResponse.body().string(); - if (uploadResponse.code() == 202) { - JSONObject responseObject = new JSONObject(uploadResponseBody); - JSONArray expectedRanges = responseObject.getJSONArray("nextExpectedRanges"); - range = new Range(expectedRanges.getString(0), UPLOAD_CHUNK_SIZE); - exponentialBackoffMillis = EXPONENTIAL_BACKOFF_MILLIS_DEFAULT; - retryCount = 0; - } else if (uploadResponse.code() == 201 || uploadResponse.code() == 200) { + String responseBody = uploadResponse.body().string(); + int statusCode = uploadResponse.code(); + if (statusCode == 202) { + uploadAccepted(state, responseBody); + } else if (statusCode == 201 || statusCode == 200) { break; } else { - if (retryCount > MAX_RETRY_ATTEMPTS || uploadResponse.code() == 409) { - Request cancelRequest = new Request.Builder().url(uploadURL).delete().build(); - DriveBackup.httpClient.newCall(cancelRequest).execute().close(); - throw new GraphApiErrorException(uploadResponse.code(), uploadResponseBody); - } else if (uploadResponse.code() == 404) { - throw new GraphApiErrorException(uploadResponse.code(), uploadResponseBody); - } else if (uploadResponse.code() == 416) { - Request statusRequest = new Request.Builder().url(uploadURL).build(); - try (Response statusResponse = DriveBackup.httpClient.newCall(statusRequest).execute()) { - //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) - String statusResponseBody = statusResponse.body().string(); - if (!statusResponse.isSuccessful()) { - throw new GraphApiErrorException(statusResponse.code(), statusResponseBody); - } - JSONObject responseObject = new JSONObject(statusResponseBody); - JSONArray expectedRanges = responseObject.getJSONArray("nextExpectedRanges"); - range = new Range(expectedRanges.getString(0), UPLOAD_CHUNK_SIZE); - } - } else if (uploadResponse.code() >= 500 && uploadResponse.code() < 600) { - TimeUnit.MILLISECONDS.sleep(exponentialBackoffMillis); - exponentialBackoffMillis *= EXPONENTIAL_BACKOFF_FACTOR; - } - retryCount++; + uploadRetryOrFailure(state, uploadURL, statusCode, responseBody); } } } } + /** + * handles 202:Accepted upload responses + */ + private void uploadAccepted(@NotNull UploadSessionState state, @NotNull String uploadResponseBody) { + JSONObject responseObject = new JSONObject(uploadResponseBody); + JSONArray expectedRanges = responseObject.getJSONArray("nextExpectedRanges"); + state.range = new Range(expectedRanges.getString(0), UPLOAD_CHUNK_SIZE); + state.exponentialBackoffMillis = EXPONENTIAL_BACKOFF_MILLIS_DEFAULT; + state.retryCount = 0; + } + + /** + * handles non-success upload responses + */ + private void uploadRetryOrFailure(@NotNull UploadSessionState state, @NotNull String uploadURL, int statusCode, + @NotNull String uploadResponseBody) throws IOException, GraphApiErrorException, InterruptedException { + if (state.retryCount > MAX_RETRY_ATTEMPTS || statusCode == 409) { + Request cancelRequest = new Request.Builder().url(uploadURL).delete().build(); + DriveBackup.httpClient.newCall(cancelRequest).execute().close(); + throw new GraphApiErrorException(statusCode, uploadResponseBody); + } else if (statusCode == 404) { + throw new GraphApiErrorException(statusCode, uploadResponseBody); + } else if (statusCode == 416) { + Request statusRequest = new Request.Builder().url(uploadURL).build(); + try (Response statusResponse = DriveBackup.httpClient.newCall(statusRequest).execute()) { + //noinspection DataFlowIssue (response.body() is non-null after Call.execute()) + String statusResponseBody = statusResponse.body().string(); + if (!statusResponse.isSuccessful()) { + throw new GraphApiErrorException(statusResponse.code(), statusResponseBody); + } + JSONObject responseObject = new JSONObject(statusResponseBody); + JSONArray expectedRanges = responseObject.getJSONArray("nextExpectedRanges"); + state.range = new Range(expectedRanges.getString(0), UPLOAD_CHUNK_SIZE); + } + } else if (statusCode >= 500 && statusCode < 600) { + TimeUnit.MILLISECONDS.sleep(state.exponentialBackoffMillis); + state.exponentialBackoffMillis *= EXPONENTIAL_BACKOFF_FACTOR; + } + state.retryCount++; + } + /** * Deletes the oldest files in the specified folder past the number to retain from the authenticated user's OneDrive. *

@@ -585,6 +599,15 @@ private void pruneBackups(@NotNull FQID parent) throws IOException, GraphApiErro } } + /** + * upload session state + */ + private static final class UploadSessionState { + private int exponentialBackoffMillis = EXPONENTIAL_BACKOFF_MILLIS_DEFAULT; + private int retryCount = 0; + private Range range = new Range(0, UPLOAD_CHUNK_SIZE); + } + /** * A range of bytes */ From e9144c0fab3212e114632117c374e9c4ce66a5f5 Mon Sep 17 00:00:00 2001 From: StillGreen-san <40620628+StillGreen-san@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:35:46 +0100 Subject: [PATCH 8/8] OneDrive cleanup --- .../uploaders/onedrive/GraphApiErrorException.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java index 8fd5bfbc..6dcb0707 100644 --- a/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java +++ b/DriveBackup/src/main/java/ratismal/drivebackup/uploaders/onedrive/GraphApiErrorException.java @@ -55,13 +55,13 @@ public ParsedError(@NotNull String responseBody) { JSONObject errorObject = optJsonObjectIgnoreCase(errorResponse, ERROR_OBJ_KEY); if (errorObject == null) { this.errorCode = "invalidErrorResponse"; - this.errorMessage = "error response does not contain an json object 'error'"; + this.errorMessage = String.format("error response has no json object '%s'", ERROR_OBJ_KEY); this.errorObject = null; return; } - this.errorCode = optStringIgnoreCase(errorObject, CODE_STR_KEY, "invalid/missing member 'code'"); - this.errorMessage = optStringIgnoreCase(errorObject, MESSAGE_STR_KEY, "invalid/missing member 'message'"); + this.errorCode = optStringIgnoreCase(errorObject, CODE_STR_KEY, "null"); + this.errorMessage = optStringIgnoreCase(errorObject, MESSAGE_STR_KEY, "null"); this.errorObject = errorObject; } }