Repository: plone/plone.restapi (language: python)

Issue:
tus endpoint via ++api++ doesn't give api url in location
The @tus-upload endpoint creates a temporary upload url which is returned in the header. A follow up PATCH request is made to that url.

If the original @tus-upload POST is made via the ++api++ url, the resulting location in the response header does not include  ++api++ in the path and so the follow up post must be made directly to the plone instances.

This breaks for situations where it is being used in volto.

The code in question is https://github.com/plone/plone.restapi/blob/master/src/plone/restapi/services/content/tus.py#L122


Discussion / hints:

Fix patch:
diff --git a/news/1570.bugfix b/news/1570.bugfix
new file mode 100644
index 0000000000..37f023d218
--- /dev/null
+++ b/news/1570.bugfix
@@ -0,0 +1,2 @@
+Use incoming request to produce location for @tus-upload
+[instification]
diff --git a/src/plone/restapi/services/content/tus.py b/src/plone/restapi/services/content/tus.py
index cd85343321..10cd47cdf2 100644
--- a/src/plone/restapi/services/content/tus.py
+++ b/src/plone/restapi/services/content/tus.py
@@ -117,9 +117,14 @@ def reply(self):
         tus_upload = TUSUpload(uuid4().hex, metadata=metadata)
 
         self.request.response.setStatus(201)
+        url = self.request.getURL()
+        # Regardless of @tus-upload or @tus-replace the response should return
+        # a Location of @tus-upload
+        if url.endswith("@tus-replace"):
+            url = url.replace("@tus-replace", "@tus-upload")
         self.request.response.setHeader(
             "Location",
-            f"{self.context.absolute_url()}/@tus-upload/{tus_upload.uid}",
+            f"{url}/{tus_upload.uid}",
         )
         self.request.response.setHeader("Upload-Expires", tus_upload.expires())
         self.request.response.setHeader("Tus-Resumable", "1.0.0")


Test patch:
diff --git a/src/plone/restapi/tests/test_tus.py b/src/plone/restapi/tests/test_tus.py
index bece6c679b..838c935a2c 100644
--- a/src/plone/restapi/tests/test_tus.py
+++ b/src/plone/restapi/tests/test_tus.py
@@ -541,6 +541,35 @@ def record_event(event):
         sm.unregisterHandler(record_event, (IObjectAddedEvent,))
         sm.unregisterHandler(record_event, (IObjectModifiedEvent,))
 
+    def test_tus_with_api(self):
+        self.upload_url = (
+            f"{self.portal.absolute_url()}/++api++/{self.folder.id}/@tus-upload"
+        )
+        metadata = _prepare_metadata(UPLOAD_FILENAME, UPLOAD_MIMETYPE)
+        response = self.api_session.post(
+            self.upload_url,
+            headers={
+                "Tus-Resumable": "1.0.0",
+                "Upload-Length": str(UPLOAD_LENGTH),
+                "Upload-Metadata": metadata,
+            },
+        )
+        self.assertEqual(response.status_code, 201)
+        location = response.headers["Location"]
+        self.assertEqual("++api++" in location, True)
+
+        # upload the data with PATCH
+        response = self.api_session.patch(
+            location,
+            headers={
+                "Content-Type": "application/offset+octet-stream",
+                "Upload-Offset": "0",
+                "Tus-Resumable": "1.0.0",
+            },
+            data=BytesIO(UPLOAD_DATA),
+        )
+        self.assertEqual(response.status_code, 204)
+
     def tearDown(self):
         self.api_session.close()
         client_home = os.environ.get("CLIENT_HOME")