Skip to content

Commit c4bd90d

Browse files
authored
HUB-11880: treat etags as optional in upload (#112)
HUB-11880 (Make etags optional in multipart upload response)
1 parent 51d225f commit c4bd90d

File tree

5 files changed

+39
-38
lines changed

5 files changed

+39
-38
lines changed

.changeset/moody-sides-behave.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@knime/utils": minor
3+
"@knime/hub-features": patch
4+
---
5+
6+
treat etags in upload as optional, adjust docs

eslint-suppressions.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,11 +272,6 @@
272272
"count": 1
273273
}
274274
},
275-
"packages/hub-features/src/useFileUpload/useFileUpload.ts": {
276-
"@typescript-eslint/no-floating-promises": {
277-
"count": 1
278-
}
279-
},
280275
"packages/jsonforms/src/JsonFormsDialog.vue": {
281276
"@typescript-eslint/no-misused-promises": {
282277
"count": 1

packages/hub-features/src/useFileUpload/useFileUpload.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ type UseFileUploadOptions = {
8181
* Once the upload of a single file is complete, the upload manager will trigger the `onFileUploadComplete`
8282
* callback, which then will supply the ids of all the different parts that got uploaded for a given `uploadId`
8383
* Then we need to "complete" the upload by sending a request which provides the `uploadId` and a list of
84-
* the ids of all the parts
84+
* the ids of all the parts. Note that some backends don't return the ids of the file parts after a successful
85+
* upload of a part, in this case empty strings or null can be used instead of the part id.
8586
*
8687
* Cancellation:
8788
* Alternative to completion, you can also cancel an upload. For this, the composable makes a DELETE request
@@ -285,6 +286,8 @@ export const useFileUpload = (options: UseFileUploadOptions = {}) => {
285286
}
286287
});
287288

289+
// accept floating promise, errors are handled inside the function
290+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
288291
useUploadManagerResult.start(parentId, uploadPayload);
289292
} catch (error) {
290293
throw rfcErrors.tryParse(error) ?? error;
@@ -296,7 +299,7 @@ export const useFileUpload = (options: UseFileUploadOptions = {}) => {
296299

297300
cancel: (uploadId: string) => {
298301
useUploadManagerResult.cancel(uploadId);
299-
cancelUpload(uploadId).catch((error) => {
302+
cancelUpload(uploadId).catch((error: unknown) => {
300303
consola.error("There was a problem cancelling the upload", { error });
301304
});
302305
},

packages/utils/src/uploadManager/__tests__/index.test.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("uploadManager", () => {
2323
};
2424

2525
const setup = ({ uploadPartResponse }: Options = {}) => {
26+
consola.mockTypes(() => vi.fn());
2627
const server = newServer({
2728
put: ["/success", uploadPartResponse ?? defaultUploadPartResponse],
2829
});
@@ -126,6 +127,28 @@ describe("uploadManager", () => {
126127
expect(cancelled.length).toBe(0);
127128
expect(resolveFilePartUploadURL).toHaveBeenCalled();
128129
});
130+
131+
it("should succeed when etag is missing from upload part response", async () => {
132+
setup({
133+
uploadPartResponse: (request) => {
134+
request.respond(201, {});
135+
},
136+
});
137+
138+
const { uploadFiles } = createUploadManager({
139+
resolveFilePartUploadURL,
140+
});
141+
142+
const uploadId = getUploadId();
143+
144+
const { completed, failed, cancelled } = await uploadFiles([
145+
{ uploadId, file },
146+
]);
147+
148+
expect(completed).toEqual([uploadId]);
149+
expect(failed.length).toBe(0);
150+
expect(cancelled.length).toBe(0);
151+
});
129152
});
130153

131154
describe("callbacks", () => {
@@ -274,28 +297,5 @@ describe("uploadManager", () => {
274297
expect(cancelled.length).toBe(0);
275298
expect(resolveFilePartUploadURL).toHaveBeenCalled();
276299
});
277-
278-
it("should fail when etag is missing from upload part response", async () => {
279-
setup({
280-
uploadPartResponse: (request) => {
281-
request.respond(200, {});
282-
},
283-
});
284-
285-
const { uploadFiles } = createUploadManager({
286-
resolveFilePartUploadURL,
287-
});
288-
289-
const uploadId = getUploadId();
290-
291-
const { completed, failed, cancelled } = await uploadFiles([
292-
{ uploadId, file },
293-
]);
294-
295-
expect(completed.length).toBe(0);
296-
expect(failed).toEqual([uploadId]);
297-
expect(cancelled.length).toBe(0);
298-
expect(resolveFilePartUploadURL).toHaveBeenCalled();
299-
});
300300
});
301301
});

packages/utils/src/uploadManager/index.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const uploadChunkWithProgress = (params: {
2828
const { method, url, chunk, chunkIndex, onProgress, abortSignal } = params;
2929

3030
const OK = 200;
31+
const CREATED = 201;
3132

3233
return new Promise<{ partId: string }>((resolve, reject) => {
3334
const xhr = new XMLHttpRequest();
@@ -44,17 +45,13 @@ const uploadChunkWithProgress = (params: {
4445
};
4546

4647
xhr.onload = () => {
47-
if (xhr.status === OK) {
48+
if (xhr.status === OK || xhr.status === CREATED) {
4849
const etag = xhr.getResponseHeader("ETag");
4950

50-
if (!etag) {
51-
reject(new Error("Invalid part upload response"));
52-
return;
53-
}
54-
55-
// remove possible doublequotes (") around the etag and/or weak identifier (if either is present)
51+
// 1. note that some backends don't return the etag header, then we just set an empty string
52+
// 2. remove possible doublequotes (") around the etag and/or weak identifier (if either is present)
5653
// https://datatracker.ietf.org/doc/html/rfc7232#section-2.3
57-
resolve({ partId: etag.replace(/^(?:W\/)?"|"$/g, "") });
54+
resolve({ partId: etag?.replace(/^(?:W\/)?"|"$/g, "") ?? "" });
5855
} else {
5956
consola.error("Failed or unexpected XHR response", {
6057
xhrStatus: xhr.status,

0 commit comments

Comments
 (0)