soundcloud-api-ts 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -293,6 +293,57 @@ import { getSoundCloudWidgetUrl } from "soundcloud-api-ts";
293
293
  const widgetUrl = getSoundCloudWidgetUrl(trackId);
294
294
  ```
295
295
 
296
+ ## Error Handling
297
+
298
+ All API errors throw a `SoundCloudError` with structured properties:
299
+
300
+ ```ts
301
+ import { SoundCloudError } from "soundcloud-api-ts";
302
+
303
+ try {
304
+ await sc.tracks.getTrack(999);
305
+ } catch (err) {
306
+ if (err instanceof SoundCloudError) {
307
+ console.log(err.status); // 404
308
+ console.log(err.statusText); // "Not Found"
309
+ console.log(err.message); // "404 - Not Found" (from SC's response)
310
+ console.log(err.errorCode); // "invalid_client" (on auth errors)
311
+ console.log(err.errors); // ["404 - Not Found"] (individual error messages)
312
+ console.log(err.docsLink); // "https://developers.soundcloud.com/docs/api/explorer/open-api"
313
+ console.log(err.body); // full parsed response body
314
+
315
+ // Convenience getters
316
+ if (err.isNotFound) { /* handle 404 */ }
317
+ if (err.isRateLimited) { /* handle 429 */ }
318
+ if (err.isUnauthorized) { /* handle 401 */ }
319
+ if (err.isForbidden) { /* handle 403 */ }
320
+ if (err.isServerError) { /* handle 5xx */ }
321
+ }
322
+ }
323
+ ```
324
+
325
+ Error messages are parsed directly from SoundCloud's API response format, giving you the most useful message available.
326
+
327
+ ## Rate Limiting & Retries
328
+
329
+ The client automatically retries on **429 Too Many Requests** and **5xx Server Errors** with exponential backoff:
330
+
331
+ ```ts
332
+ const sc = new SoundCloudClient({
333
+ clientId: "...",
334
+ clientSecret: "...",
335
+ maxRetries: 3, // default: 3
336
+ retryBaseDelay: 1000, // default: 1000ms
337
+ onDebug: (msg) => console.log(msg), // optional retry logging
338
+ });
339
+ ```
340
+
341
+ - **429 responses** respect the `Retry-After` header when present
342
+ - **5xx responses** (500, 502, 503, 504) are retried with exponential backoff
343
+ - **4xx errors** (except 429) are NOT retried — they throw immediately
344
+ - **401 errors** trigger `onTokenRefresh` (if configured) instead of retry
345
+ - Backoff formula: `baseDelay × 2^attempt` with jitter
346
+
296
347
  ## Requirements
297
348
 
298
349
  - Node.js 20+ (uses native `fetch`)
@@ -0,0 +1,57 @@
1
+ // src/errors.ts
2
+ var SoundCloudError = class extends Error {
3
+ /** HTTP status code of the failed response (e.g. 401, 404, 429) */
4
+ status;
5
+ /** HTTP status text of the failed response (e.g. "Unauthorized", "Not Found") */
6
+ statusText;
7
+ /** Machine-readable error code from SoundCloud (e.g. "invalid_client"), if present */
8
+ errorCode;
9
+ /** Link to SoundCloud API documentation, if included in the error response */
10
+ docsLink;
11
+ /** Individual error messages extracted from the response body's `errors` array */
12
+ errors;
13
+ /** The full parsed error response body, if available */
14
+ body;
15
+ /**
16
+ * Creates a new SoundCloudError.
17
+ *
18
+ * @param status - HTTP status code
19
+ * @param statusText - HTTP status text
20
+ * @param body - Parsed JSON error response body from SoundCloud, if available
21
+ */
22
+ constructor(status, statusText, body) {
23
+ const msg = body?.message || body?.error_description || body?.error_code || body?.errors?.[0]?.error_message || body?.error || `${status} ${statusText}`;
24
+ super(msg);
25
+ this.name = "SoundCloudError";
26
+ this.status = status;
27
+ this.statusText = statusText;
28
+ this.errorCode = body?.error_code ?? void 0;
29
+ this.docsLink = body?.link ?? void 0;
30
+ this.errors = body?.errors?.map((e) => e.error_message).filter((m) => !!m) ?? [];
31
+ this.body = body;
32
+ }
33
+ /** True if status is 401 Unauthorized (invalid or expired token) */
34
+ get isUnauthorized() {
35
+ return this.status === 401;
36
+ }
37
+ /** True if status is 403 Forbidden (insufficient permissions) */
38
+ get isForbidden() {
39
+ return this.status === 403;
40
+ }
41
+ /** True if status is 404 Not Found (resource does not exist) */
42
+ get isNotFound() {
43
+ return this.status === 404;
44
+ }
45
+ /** True if status is 429 Too Many Requests (rate limit exceeded) */
46
+ get isRateLimited() {
47
+ return this.status === 429;
48
+ }
49
+ /** True if status is 5xx (SoundCloud server error) */
50
+ get isServerError() {
51
+ return this.status >= 500 && this.status < 600;
52
+ }
53
+ };
54
+
55
+ export { SoundCloudError };
56
+ //# sourceMappingURL=chunk-ALTJSKVN.mjs.map
57
+ //# sourceMappingURL=chunk-ALTJSKVN.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"names":[],"mappings":";AA6DO,IAAM,eAAA,GAAN,cAA8B,KAAA,CAAM;AAAA;AAAA,EAEhC,MAAA;AAAA;AAAA,EAEA,UAAA;AAAA;AAAA,EAEA,SAAA;AAAA;AAAA,EAEA,QAAA;AAAA;AAAA,EAEA,MAAA;AAAA;AAAA,EAEA,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAST,WAAA,CAAY,MAAA,EAAgB,UAAA,EAAoB,IAAA,EAA4B;AAE1E,IAAA,MAAM,MACJ,IAAA,EAAM,OAAA,IACN,IAAA,EAAM,iBAAA,IACN,MAAM,UAAA,IACN,IAAA,EAAM,MAAA,GAAS,CAAC,GAAG,aAAA,IACnB,IAAA,EAAM,SACN,CAAA,EAAG,MAAM,IAAI,UAAU,CAAA,CAAA;AAEzB,IAAA,KAAA,CAAM,GAAG,CAAA;AACT,IAAA,IAAA,CAAK,IAAA,GAAO,iBAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,SAAA,GAAY,MAAM,UAAA,IAAc,MAAA;AACrC,IAAA,IAAA,CAAK,QAAA,GAAW,MAAM,IAAA,IAAQ,MAAA;AAC9B,IAAA,IAAA,CAAK,SACH,IAAA,EAAM,MAAA,EACF,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,aAAa,CAAA,CAC3B,MAAA,CAAO,CAAC,CAAA,KAAmB,CAAC,CAAC,CAAC,KAAK,EAAC;AACzC,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAA,GAA0B;AAC5B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,WAAA,GAAuB;AACzB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,UAAA,GAAsB;AACxB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,IAAA,CAAK,MAAA,IAAU,GAAA,IAAO,IAAA,CAAK,MAAA,GAAS,GAAA;AAAA,EAC7C;AACF","file":"chunk-ALTJSKVN.mjs","sourcesContent":["/**\n * Shape of error response bodies returned by the SoundCloud API.\n *\n * The API returns varying combinations of these fields depending on the endpoint\n * and error type. All fields are optional.\n *\n * @example\n * ```json\n * {\n * \"code\": 401,\n * \"message\": \"invalid_client\",\n * \"link\": \"https://developers.soundcloud.com/docs/api/explorer/open-api\",\n * \"status\": \"401 - Unauthorized\",\n * \"errors\": [{\"error_message\": \"invalid_client\"}],\n * \"error\": null,\n * \"error_code\": \"invalid_client\"\n * }\n * ```\n *\n * @see https://developers.soundcloud.com/docs/api/explorer/open-api\n */\nexport interface SoundCloudErrorBody {\n /** HTTP status code echoed in the response body */\n code?: number;\n /** Error message from SoundCloud (e.g. \"invalid_client\", \"404 - Not Found\") */\n message?: string;\n /** Human-readable status string (e.g. \"401 - Unauthorized\") */\n status?: string;\n /** Link to SoundCloud API documentation */\n link?: string;\n /** Array of individual error detail objects */\n errors?: Array<{ error_message?: string }>;\n /** Generic error field — typically null in SoundCloud responses */\n error?: string | null;\n /** Machine-readable error code (e.g. \"invalid_client\") */\n error_code?: string;\n /** OAuth error description, present in `/oauth2/token` error responses */\n error_description?: string;\n}\n\n/**\n * Error class thrown when a SoundCloud API request fails.\n *\n * Provides structured access to HTTP status, error codes, and convenience\n * getters for common error categories.\n *\n * @example\n * ```ts\n * import { SoundCloudError } from 'tsd-soundcloud';\n *\n * try {\n * await sc.tracks.getTrack(999999999);\n * } catch (err) {\n * if (err instanceof SoundCloudError) {\n * if (err.isNotFound) console.log('Track not found');\n * if (err.isRateLimited) console.log('Rate limited, retry later');\n * console.log(err.status, err.message);\n * }\n * }\n * ```\n */\nexport class SoundCloudError extends Error {\n /** HTTP status code of the failed response (e.g. 401, 404, 429) */\n readonly status: number;\n /** HTTP status text of the failed response (e.g. \"Unauthorized\", \"Not Found\") */\n readonly statusText: string;\n /** Machine-readable error code from SoundCloud (e.g. \"invalid_client\"), if present */\n readonly errorCode?: string;\n /** Link to SoundCloud API documentation, if included in the error response */\n readonly docsLink?: string;\n /** Individual error messages extracted from the response body's `errors` array */\n readonly errors: string[];\n /** The full parsed error response body, if available */\n readonly body?: SoundCloudErrorBody;\n\n /**\n * Creates a new SoundCloudError.\n *\n * @param status - HTTP status code\n * @param statusText - HTTP status text\n * @param body - Parsed JSON error response body from SoundCloud, if available\n */\n constructor(status: number, statusText: string, body?: SoundCloudErrorBody) {\n // Build the most useful message we can from SC's response\n const msg =\n body?.message ||\n body?.error_description ||\n body?.error_code ||\n body?.errors?.[0]?.error_message ||\n body?.error ||\n `${status} ${statusText}`;\n\n super(msg);\n this.name = \"SoundCloudError\";\n this.status = status;\n this.statusText = statusText;\n this.errorCode = body?.error_code ?? undefined;\n this.docsLink = body?.link ?? undefined;\n this.errors =\n body?.errors\n ?.map((e) => e.error_message)\n .filter((m): m is string => !!m) ?? [];\n this.body = body;\n }\n\n /** True if status is 401 Unauthorized (invalid or expired token) */\n get isUnauthorized(): boolean {\n return this.status === 401;\n }\n\n /** True if status is 403 Forbidden (insufficient permissions) */\n get isForbidden(): boolean {\n return this.status === 403;\n }\n\n /** True if status is 404 Not Found (resource does not exist) */\n get isNotFound(): boolean {\n return this.status === 404;\n }\n\n /** True if status is 429 Too Many Requests (rate limit exceeded) */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /** True if status is 5xx (SoundCloud server error) */\n get isServerError(): boolean {\n return this.status >= 500 && this.status < 600;\n }\n}\n"]}
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var SoundCloudError = class extends Error {
5
+ /** HTTP status code of the failed response (e.g. 401, 404, 429) */
6
+ status;
7
+ /** HTTP status text of the failed response (e.g. "Unauthorized", "Not Found") */
8
+ statusText;
9
+ /** Machine-readable error code from SoundCloud (e.g. "invalid_client"), if present */
10
+ errorCode;
11
+ /** Link to SoundCloud API documentation, if included in the error response */
12
+ docsLink;
13
+ /** Individual error messages extracted from the response body's `errors` array */
14
+ errors;
15
+ /** The full parsed error response body, if available */
16
+ body;
17
+ /**
18
+ * Creates a new SoundCloudError.
19
+ *
20
+ * @param status - HTTP status code
21
+ * @param statusText - HTTP status text
22
+ * @param body - Parsed JSON error response body from SoundCloud, if available
23
+ */
24
+ constructor(status, statusText, body) {
25
+ const msg = body?.message || body?.error_description || body?.error_code || body?.errors?.[0]?.error_message || body?.error || `${status} ${statusText}`;
26
+ super(msg);
27
+ this.name = "SoundCloudError";
28
+ this.status = status;
29
+ this.statusText = statusText;
30
+ this.errorCode = body?.error_code ?? void 0;
31
+ this.docsLink = body?.link ?? void 0;
32
+ this.errors = body?.errors?.map((e) => e.error_message).filter((m) => !!m) ?? [];
33
+ this.body = body;
34
+ }
35
+ /** True if status is 401 Unauthorized (invalid or expired token) */
36
+ get isUnauthorized() {
37
+ return this.status === 401;
38
+ }
39
+ /** True if status is 403 Forbidden (insufficient permissions) */
40
+ get isForbidden() {
41
+ return this.status === 403;
42
+ }
43
+ /** True if status is 404 Not Found (resource does not exist) */
44
+ get isNotFound() {
45
+ return this.status === 404;
46
+ }
47
+ /** True if status is 429 Too Many Requests (rate limit exceeded) */
48
+ get isRateLimited() {
49
+ return this.status === 429;
50
+ }
51
+ /** True if status is 5xx (SoundCloud server error) */
52
+ get isServerError() {
53
+ return this.status >= 500 && this.status < 600;
54
+ }
55
+ };
56
+
57
+ exports.SoundCloudError = SoundCloudError;
58
+ //# sourceMappingURL=chunk-DZRZLIAH.js.map
59
+ //# sourceMappingURL=chunk-DZRZLIAH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"names":[],"mappings":";;;AA6DO,IAAM,eAAA,GAAN,cAA8B,KAAA,CAAM;AAAA;AAAA,EAEhC,MAAA;AAAA;AAAA,EAEA,UAAA;AAAA;AAAA,EAEA,SAAA;AAAA;AAAA,EAEA,QAAA;AAAA;AAAA,EAEA,MAAA;AAAA;AAAA,EAEA,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAST,WAAA,CAAY,MAAA,EAAgB,UAAA,EAAoB,IAAA,EAA4B;AAE1E,IAAA,MAAM,MACJ,IAAA,EAAM,OAAA,IACN,IAAA,EAAM,iBAAA,IACN,MAAM,UAAA,IACN,IAAA,EAAM,MAAA,GAAS,CAAC,GAAG,aAAA,IACnB,IAAA,EAAM,SACN,CAAA,EAAG,MAAM,IAAI,UAAU,CAAA,CAAA;AAEzB,IAAA,KAAA,CAAM,GAAG,CAAA;AACT,IAAA,IAAA,CAAK,IAAA,GAAO,iBAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,SAAA,GAAY,MAAM,UAAA,IAAc,MAAA;AACrC,IAAA,IAAA,CAAK,QAAA,GAAW,MAAM,IAAA,IAAQ,MAAA;AAC9B,IAAA,IAAA,CAAK,SACH,IAAA,EAAM,MAAA,EACF,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,aAAa,CAAA,CAC3B,MAAA,CAAO,CAAC,CAAA,KAAmB,CAAC,CAAC,CAAC,KAAK,EAAC;AACzC,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAA,GAA0B;AAC5B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,WAAA,GAAuB;AACzB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,UAAA,GAAsB;AACxB,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,KAAK,MAAA,KAAW,GAAA;AAAA,EACzB;AAAA;AAAA,EAGA,IAAI,aAAA,GAAyB;AAC3B,IAAA,OAAO,IAAA,CAAK,MAAA,IAAU,GAAA,IAAO,IAAA,CAAK,MAAA,GAAS,GAAA;AAAA,EAC7C;AACF","file":"chunk-DZRZLIAH.js","sourcesContent":["/**\n * Shape of error response bodies returned by the SoundCloud API.\n *\n * The API returns varying combinations of these fields depending on the endpoint\n * and error type. All fields are optional.\n *\n * @example\n * ```json\n * {\n * \"code\": 401,\n * \"message\": \"invalid_client\",\n * \"link\": \"https://developers.soundcloud.com/docs/api/explorer/open-api\",\n * \"status\": \"401 - Unauthorized\",\n * \"errors\": [{\"error_message\": \"invalid_client\"}],\n * \"error\": null,\n * \"error_code\": \"invalid_client\"\n * }\n * ```\n *\n * @see https://developers.soundcloud.com/docs/api/explorer/open-api\n */\nexport interface SoundCloudErrorBody {\n /** HTTP status code echoed in the response body */\n code?: number;\n /** Error message from SoundCloud (e.g. \"invalid_client\", \"404 - Not Found\") */\n message?: string;\n /** Human-readable status string (e.g. \"401 - Unauthorized\") */\n status?: string;\n /** Link to SoundCloud API documentation */\n link?: string;\n /** Array of individual error detail objects */\n errors?: Array<{ error_message?: string }>;\n /** Generic error field — typically null in SoundCloud responses */\n error?: string | null;\n /** Machine-readable error code (e.g. \"invalid_client\") */\n error_code?: string;\n /** OAuth error description, present in `/oauth2/token` error responses */\n error_description?: string;\n}\n\n/**\n * Error class thrown when a SoundCloud API request fails.\n *\n * Provides structured access to HTTP status, error codes, and convenience\n * getters for common error categories.\n *\n * @example\n * ```ts\n * import { SoundCloudError } from 'tsd-soundcloud';\n *\n * try {\n * await sc.tracks.getTrack(999999999);\n * } catch (err) {\n * if (err instanceof SoundCloudError) {\n * if (err.isNotFound) console.log('Track not found');\n * if (err.isRateLimited) console.log('Rate limited, retry later');\n * console.log(err.status, err.message);\n * }\n * }\n * ```\n */\nexport class SoundCloudError extends Error {\n /** HTTP status code of the failed response (e.g. 401, 404, 429) */\n readonly status: number;\n /** HTTP status text of the failed response (e.g. \"Unauthorized\", \"Not Found\") */\n readonly statusText: string;\n /** Machine-readable error code from SoundCloud (e.g. \"invalid_client\"), if present */\n readonly errorCode?: string;\n /** Link to SoundCloud API documentation, if included in the error response */\n readonly docsLink?: string;\n /** Individual error messages extracted from the response body's `errors` array */\n readonly errors: string[];\n /** The full parsed error response body, if available */\n readonly body?: SoundCloudErrorBody;\n\n /**\n * Creates a new SoundCloudError.\n *\n * @param status - HTTP status code\n * @param statusText - HTTP status text\n * @param body - Parsed JSON error response body from SoundCloud, if available\n */\n constructor(status: number, statusText: string, body?: SoundCloudErrorBody) {\n // Build the most useful message we can from SC's response\n const msg =\n body?.message ||\n body?.error_description ||\n body?.error_code ||\n body?.errors?.[0]?.error_message ||\n body?.error ||\n `${status} ${statusText}`;\n\n super(msg);\n this.name = \"SoundCloudError\";\n this.status = status;\n this.statusText = statusText;\n this.errorCode = body?.error_code ?? undefined;\n this.docsLink = body?.link ?? undefined;\n this.errors =\n body?.errors\n ?.map((e) => e.error_message)\n .filter((m): m is string => !!m) ?? [];\n this.body = body;\n }\n\n /** True if status is 401 Unauthorized (invalid or expired token) */\n get isUnauthorized(): boolean {\n return this.status === 401;\n }\n\n /** True if status is 403 Forbidden (insufficient permissions) */\n get isForbidden(): boolean {\n return this.status === 403;\n }\n\n /** True if status is 404 Not Found (resource does not exist) */\n get isNotFound(): boolean {\n return this.status === 404;\n }\n\n /** True if status is 429 Too Many Requests (rate limit exceeded) */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /** True if status is 5xx (SoundCloud server error) */\n get isServerError(): boolean {\n return this.status >= 500 && this.status < 600;\n }\n}\n"]}