Skip to content

Commit 6558cc5

Browse files
committed
refactoring http errors for webdav
1 parent e0e1903 commit 6558cc5

11 files changed

Lines changed: 100 additions & 51 deletions

File tree

packages/b2c-cli/src/commands/webdav/get.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,12 @@ export default class WebDavGet extends WebDavCommand<typeof WebDavGet> {
5858
size: buffer.length,
5959
};
6060

61-
if (!this.jsonEnabled()) {
62-
this.log(
63-
t('commands.webdav.get.success', 'Downloaded {{size}} bytes to {{path}}', {
64-
size: result.size,
65-
path: result.localPath,
66-
}),
67-
);
68-
}
61+
this.log(
62+
t('commands.webdav.get.success', 'Downloaded {{size}} bytes to {{path}}', {
63+
size: result.size,
64+
path: result.localPath,
65+
}),
66+
);
6967

7068
return result;
7169
}

packages/b2c-cli/src/commands/webdav/mkdir.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ export default class WebDavMkdir extends WebDavCommand<typeof WebDavMkdir> {
3535

3636
const fullPath = this.buildPath(this.args.path);
3737

38-
this.log(t('commands.webdav.mkdir.creating', 'Creating directory {{path}}...', {path: fullPath}));
39-
4038
// Create all parent directories and the target directory
4139
await this.createDirectoryPath(fullPath);
4240

@@ -45,9 +43,7 @@ export default class WebDavMkdir extends WebDavCommand<typeof WebDavMkdir> {
4543
created: true,
4644
};
4745

48-
if (!this.jsonEnabled()) {
49-
this.log(t('commands.webdav.mkdir.success', 'Created directory: {{path}}', {path: fullPath}));
50-
}
46+
this.log(t('commands.webdav.mkdir.success', 'Created: {{path}}', {path: fullPath}));
5147

5248
return result;
5349
}

packages/b2c-cli/src/commands/webdav/put.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,12 @@ export default class WebDavPut extends WebDavCommand<typeof WebDavPut> {
107107
contentType,
108108
};
109109

110-
if (!this.jsonEnabled()) {
111-
this.log(
112-
t('commands.webdav.put.success', 'Uploaded {{size}} bytes to {{path}}', {
113-
size: result.size,
114-
path: result.remotePath,
115-
}),
116-
);
117-
}
110+
this.log(
111+
t('commands.webdav.put.success', 'Uploaded {{size}} bytes to {{path}}', {
112+
size: result.size,
113+
path: result.remotePath,
114+
}),
115+
);
118116

119117
return result;
120118
}

packages/b2c-cli/src/commands/webdav/rm.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,14 @@ export default class WebDavRm extends WebDavCommand<typeof WebDavRm> {
7474
}
7575
}
7676

77-
this.log(t('commands.webdav.rm.deleting', 'Deleting {{path}}...', {path: fullPath}));
78-
7977
await this.instance.webdav.delete(fullPath);
8078

8179
const result: RmResult = {
8280
path: fullPath,
8381
deleted: true,
8482
};
8583

86-
if (!this.jsonEnabled()) {
87-
this.log(t('commands.webdav.rm.success', 'Deleted {{path}}', {path: fullPath}));
88-
}
84+
this.log(t('commands.webdav.rm.success', 'Deleted: {{path}}', {path: fullPath}));
8985

9086
return result;
9187
}

packages/b2c-cli/src/commands/webdav/unzip.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ export default class WebDavUnzip extends WebDavCommand<typeof WebDavUnzip> {
6161
extractPath,
6262
};
6363

64-
if (!this.jsonEnabled()) {
65-
this.log(t('commands.webdav.unzip.success', 'Extracted to: {{path}}', {path: extractPath}));
66-
}
64+
this.log(t('commands.webdav.unzip.success', 'Extracted to: {{path}}', {path: extractPath}));
6765

6866
return result;
6967
}

packages/b2c-cli/src/commands/webdav/zip.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default class WebDavZip extends WebDavCommand<typeof WebDavZip> {
3737
const fullPath = this.buildPath(this.args.path);
3838
const archivePath = `${fullPath}.zip`;
3939

40-
this.log(t('commands.webdav.zip.zipping', 'Creating zip archive of {{path}}...', {path: fullPath}));
40+
this.log(t('commands.webdav.zip.zipping', 'Zipping {{path}}...', {path: fullPath}));
4141

4242
const response = await this.instance.webdav.request(fullPath, {
4343
method: 'POST',
@@ -57,9 +57,7 @@ export default class WebDavZip extends WebDavCommand<typeof WebDavZip> {
5757
archivePath,
5858
};
5959

60-
if (!this.jsonEnabled()) {
61-
this.log(t('commands.webdav.zip.success', 'Created archive: {{path}}', {path: archivePath}));
62-
}
60+
this.log(t('commands.webdav.zip.success', 'Created archive: {{path}}', {path: archivePath}));
6361

6462
return result;
6563
}

packages/b2c-tooling-sdk/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,17 @@
133133
"types": "./dist/cjs/logging/index.d.ts",
134134
"default": "./dist/cjs/logging/index.js"
135135
}
136+
},
137+
"./errors": {
138+
"development": "./src/errors/index.ts",
139+
"import": {
140+
"types": "./dist/esm/errors/index.d.ts",
141+
"default": "./dist/esm/errors/index.js"
142+
},
143+
"require": {
144+
"types": "./dist/cjs/errors/index.d.ts",
145+
"default": "./dist/cjs/errors/index.js"
146+
}
136147
}
137148
},
138149
"main": "./dist/cjs/index.js",

packages/b2c-tooling-sdk/src/auth/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
export interface AuthStrategy {
77
/**
8-
* Performs a fetch request.
8+
* Performs a fetch request with authentication.
99
* Implementations MUST handle header injection and 401 retries (token refresh) internally.
1010
*/
1111
fetch(url: string, init?: RequestInit): Promise<Response>;

packages/b2c-tooling-sdk/src/clients/webdav.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
import {parseStringPromise} from 'xml2js';
1515
import type {AuthStrategy} from '../auth/types.js';
16+
import {HTTPError} from '../errors/http-error.js';
1617
import {getLogger} from '../logging/logger.js';
1718

1819
/**
@@ -66,8 +67,11 @@ export class WebDavClient {
6667

6768
/**
6869
* Builds the full URL for a WebDAV path.
70+
*
71+
* @param path - Path relative to /webdav/Sites/
72+
* @returns Full URL
6973
*/
70-
private buildUrl(path: string): string {
74+
buildUrl(path: string): string {
7175
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
7276
return `${this.baseUrl}/${cleanPath}`;
7377
}
@@ -167,8 +171,7 @@ export class WebDavClient {
167171

168172
// 201 = created, 405 = already exists (acceptable)
169173
if (!response.ok && response.status !== 405) {
170-
const text = await response.text();
171-
throw new Error(`MKCOL failed: ${response.status} ${response.statusText} - ${text}`);
174+
throw new HTTPError(`MKCOL failed: ${response.status} ${response.statusText}`, response, 'MKCOL');
172175
}
173176
}
174177

@@ -188,15 +191,10 @@ export class WebDavClient {
188191
headers['Content-Type'] = contentType;
189192
}
190193

191-
const response = await this.request(path, {
192-
method: 'PUT',
193-
headers,
194-
body: content,
195-
});
194+
const response = await this.request(path, {method: 'PUT', headers, body: content});
196195

197196
if (!response.ok) {
198-
const text = await response.text();
199-
throw new Error(`PUT failed: ${response.status} ${response.statusText} - ${text}`);
197+
throw new HTTPError(`PUT failed: ${response.status} ${response.statusText}`, response, 'PUT');
200198
}
201199
}
202200

@@ -213,7 +211,7 @@ export class WebDavClient {
213211
const response = await this.request(path, {method: 'GET'});
214212

215213
if (!response.ok) {
216-
throw new Error(`GET failed: ${response.status} ${response.statusText}`);
214+
throw new HTTPError(`GET failed: ${response.status} ${response.statusText}`, response, 'GET');
217215
}
218216

219217
return response.arrayBuffer();
@@ -223,17 +221,16 @@ export class WebDavClient {
223221
* Deletes a file or directory.
224222
*
225223
* @param path - Path to delete
224+
* @throws Error if the path doesn't exist (404) or deletion fails
226225
*
227226
* @example
228227
* await client.delete('Cartridges/v1/old-cartridge');
229228
*/
230229
async delete(path: string): Promise<void> {
231230
const response = await this.request(path, {method: 'DELETE'});
232231

233-
// 404 is acceptable (already deleted)
234-
if (!response.ok && response.status !== 404) {
235-
const text = await response.text();
236-
throw new Error(`DELETE failed: ${response.status} ${response.statusText} - ${text}`);
232+
if (!response.ok) {
233+
throw new HTTPError(`DELETE failed: ${response.status} ${response.statusText}`, response, 'DELETE');
237234
}
238235
}
239236

@@ -270,8 +267,7 @@ export class WebDavClient {
270267
});
271268

272269
if (!response.ok) {
273-
const text = await response.text();
274-
throw new Error(`PROPFIND failed: ${response.status} ${response.statusText} - ${text}`);
270+
throw new HTTPError(`PROPFIND failed: ${response.status} ${response.statusText}`, response, 'PROPFIND');
275271
}
276272

277273
const xml = await response.text();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
/**
7+
* Custom error class for HTTP errors from API clients.
8+
*
9+
* Wraps the Response object directly for full access to response details.
10+
*
11+
* @example
12+
* try {
13+
* await webdav.delete('some/path');
14+
* } catch (error) {
15+
* if (error instanceof HTTPError && error.response.status === 404) {
16+
* // Handle not found
17+
* }
18+
* throw error;
19+
* }
20+
*
21+
* @module errors/http-error
22+
*/
23+
24+
/**
25+
* Error thrown when an HTTP request fails.
26+
*
27+
* Wraps the original Response for access to status, headers, and body.
28+
*/
29+
export class HTTPError extends Error {
30+
/**
31+
* The original Response object from the failed request.
32+
* Note: Body can only be read once via response.text(), response.json(), etc.
33+
*/
34+
readonly response: Response;
35+
36+
/**
37+
* HTTP method used for the request (GET, POST, PUT, DELETE, etc.).
38+
*/
39+
readonly method: string;
40+
41+
constructor(message: string, response: Response, method: string) {
42+
super(message);
43+
this.name = 'HTTPError';
44+
this.response = response;
45+
this.method = method;
46+
}
47+
}

0 commit comments

Comments
 (0)