From c657c299b3659bb2f9db0af5de894c53ad956921 Mon Sep 17 00:00:00 2001 From: Vadim Grem Date: Thu, 18 Jun 2026 12:09:58 +0000 Subject: [PATCH 1/3] fix: handle JSON file download from OneDrive /content endpoint The OData response processor relies solely on Content-Type header to decide whether to parse the response body as OData JSON. For OneDrive's /content endpoint, a .json file returns application/json, causing the client to incorrectly parse the file contents as an OData response instead of returning raw bytes. Fix: add _is_content_download() check that skips JSON parsing for FunctionQuery operations named 'content' (OneDrive) or '$value' (SharePoint), which always represent raw file content downloads. Fixes #944, #598 --- office365/runtime/odata/request.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/office365/runtime/odata/request.py b/office365/runtime/odata/request.py index 6f3e4980e..93223d970 100644 --- a/office365/runtime/odata/request.py +++ b/office365/runtime/odata/request.py @@ -77,7 +77,9 @@ def process_response(self, response: Response, query: ClientQuery) -> None: if isinstance(return_type, ClientObject): return_type.clear_state() - if response.headers.get("Content-Type", "").lower().split(";")[0] != "application/json": + content_type = response.headers.get("Content-Type", "").lower().split(";")[0] + + if content_type != "application/json" or self._is_content_download(query): if isinstance(return_type, ClientResult): return_type.set_property("__value", response.content) else: @@ -87,6 +89,14 @@ def process_response(self, response: Response, query: ClientQuery) -> None: self.map_json(response.json(), return_type, json_format) + @staticmethod + def _is_content_download(query: ClientQuery) -> bool: + """Check if the query is a raw content download (file content, not OData metadata).""" + if isinstance(query, FunctionQuery): + name = query.name + return name in ("content", "$value") + return False + def map_json( self, json: Any, From b5962ddf14902b4d164e91001bcfd55c91960188 Mon Sep 17 00:00:00 2001 From: Vadim Grem Date: Thu, 18 Jun 2026 14:52:22 +0000 Subject: [PATCH 2/3] refactor: use return_raw_content flag instead of hardcoded name check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace _is_content_download name matching with a explicit return_raw_content flag on FunctionQuery. Callers set the flag at construction time — cleaner and extensible. --- office365/onedrive/driveitems/driveItem.py | 2 +- office365/runtime/odata/request.py | 5 +---- office365/runtime/queries/function.py | 9 +++++++++ office365/sharepoint/files/file.py | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/office365/onedrive/driveitems/driveItem.py b/office365/onedrive/driveitems/driveItem.py index 0c31c628b..2c81a9f73 100644 --- a/office365/onedrive/driveitems/driveItem.py +++ b/office365/onedrive/driveitems/driveItem.py @@ -427,7 +427,7 @@ def get_content(self, format_name: str | None = None) -> ClientResult[AnyStr]: action_name = "content" if format_name is not None: action_name = action_name + rf"?format={format_name}" - qry = FunctionQuery(self, action_name, None, return_type) + qry = FunctionQuery(self, action_name, None, return_type, return_raw_content=True) self.context.add_query(qry) return return_type diff --git a/office365/runtime/odata/request.py b/office365/runtime/odata/request.py index 93223d970..861ab9b72 100644 --- a/office365/runtime/odata/request.py +++ b/office365/runtime/odata/request.py @@ -92,10 +92,7 @@ def process_response(self, response: Response, query: ClientQuery) -> None: @staticmethod def _is_content_download(query: ClientQuery) -> bool: """Check if the query is a raw content download (file content, not OData metadata).""" - if isinstance(query, FunctionQuery): - name = query.name - return name in ("content", "$value") - return False + return bool(getattr(query, "return_raw_content", False)) def map_json( self, diff --git a/office365/runtime/queries/function.py b/office365/runtime/queries/function.py index 4c1911b3d..726ef9a8f 100644 --- a/office365/runtime/queries/function.py +++ b/office365/runtime/queries/function.py @@ -15,6 +15,7 @@ def __init__( method_name: str | None = None, method_params: list | dict | ClientValue | None = None, return_type: ReturnT | None = None, + return_raw_content: bool = False, ) -> None: """Initialize a function query. @@ -23,9 +24,17 @@ def __init__( method_name: The name of the method to call method_params: Parameters for the method call return_type: The expected return type + return_raw_content: When True, the response body is treated as raw content + (e.g. file download) rather than parsed as OData JSON. """ super().__init__(binding_type.context, binding_type, None, None, return_type) self._path = ServiceOperationPath(method_name or "", method_params, binding_type.resource_path) + self._return_raw_content = return_raw_content + + @property + def return_raw_content(self) -> bool: + """Whether the response should be treated as raw content, not OData JSON.""" + return self._return_raw_content def __repr__(self) -> str: return f"FunctionQuery(name={self.path.name})" diff --git a/office365/sharepoint/files/file.py b/office365/sharepoint/files/file.py index 6ba5c0f4a..219d310de 100644 --- a/office365/sharepoint/files/file.py +++ b/office365/sharepoint/files/file.py @@ -140,7 +140,7 @@ def _file_loaded(): def get_content(self) -> ClientResult[bytes]: """Downloads a file content""" return_type = ClientResult(self.context, bytes()) - qry = FunctionQuery(self, "$value", return_type=return_type) + qry = FunctionQuery(self, "$value", return_type=return_type, return_raw_content=True) self.context.add_query(qry) return return_type From 688dc3f184b68d60858c27f4dbea21b8be2d8dfa Mon Sep 17 00:00:00 2001 From: Vadim Grem Date: Thu, 18 Jun 2026 14:54:19 +0000 Subject: [PATCH 3/3] refactor: check return_raw_content via isinstance + property Replace getattr hack in _is_content_download with a proper isinstance + property check directly on the query object. --- office365/runtime/odata/request.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/office365/runtime/odata/request.py b/office365/runtime/odata/request.py index 861ab9b72..d5bc43fbc 100644 --- a/office365/runtime/odata/request.py +++ b/office365/runtime/odata/request.py @@ -79,7 +79,7 @@ def process_response(self, response: Response, query: ClientQuery) -> None: content_type = response.headers.get("Content-Type", "").lower().split(";")[0] - if content_type != "application/json" or self._is_content_download(query): + if content_type != "application/json" or self._is_raw_content_query(query): if isinstance(return_type, ClientResult): return_type.set_property("__value", response.content) else: @@ -90,9 +90,9 @@ def process_response(self, response: Response, query: ClientQuery) -> None: self.map_json(response.json(), return_type, json_format) @staticmethod - def _is_content_download(query: ClientQuery) -> bool: - """Check if the query is a raw content download (file content, not OData metadata).""" - return bool(getattr(query, "return_raw_content", False)) + def _is_raw_content_query(query: ClientQuery) -> bool: + """Check if the query represents a raw content retrieval (e.g. file download).""" + return isinstance(query, FunctionQuery) and query.return_raw_content def map_json( self,