@dotcms/client 26.9.24-1 → 26.9.24-1-next.2772

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/index.cjs.js CHANGED
@@ -4,7 +4,7 @@ var consola = require('consola');
4
4
  var types = require('@dotcms/types');
5
5
  var internal = require('./internal.cjs.js');
6
6
 
7
- const SDK_VERSION = "26.9.24-1";
7
+ const SDK_VERSION = "26.9.24-1-next.2772";
8
8
 
9
9
  const DOTCMS_VERSION_HEADER = 'x-dotcms-version';
10
10
  const DOTCMS_MIN_SDK_HEADER = 'x-dotcms-min-sdk';
@@ -93,25 +93,23 @@ const checkSdkCompatibility = (headers, ownVersion) => {
93
93
  *
94
94
  * Extends BaseHttpClient to provide a standard interface for making HTTP requests.
95
95
  * This implementation uses the native Fetch API and handles:
96
- * - JSON and non-JSON response parsing
96
+ * - JSON response parsing
97
97
  * - HTTP error response parsing and conversion to DotHttpError
98
98
  * - Network error handling and wrapping
99
- * - Content-Type detection for proper response handling
99
+ * - Diagnosing misconfiguration: a response in a format other than JSON, or a request
100
+ * that was redirected to another origin, is named as such in the error message
101
+ *
102
+ * Every SDK caller talks to a dotCMS JSON endpoint, so a successful response with a
103
+ * non-JSON content type is rejected rather than returned.
100
104
  *
101
105
  * @example
102
106
  * ```typescript
103
107
  * const client = new FetchHttpClient();
104
108
  *
105
- * // JSON request
106
109
  * const data = await client.request<MyType>('/api/data', {
107
110
  * method: 'GET',
108
111
  * headers: { 'Authorization': 'Bearer token' }
109
112
  * });
110
- *
111
- * // Non-JSON request (e.g., file download)
112
- * const response = await client.request<Response>('/api/file.pdf', {
113
- * method: 'GET'
114
- * });
115
113
  * ```
116
114
  */
117
115
  class FetchHttpClient extends types.BaseHttpClient {
@@ -122,12 +120,13 @@ class FetchHttpClient extends types.BaseHttpClient {
122
120
  * Automatically handles response parsing based on Content-Type headers and converts
123
121
  * HTTP errors to standardized DotHttpError instances.
124
122
  *
125
- * @template T - The expected response type. For JSON responses, T should be the parsed object type.
126
- * For non-JSON responses, T should be Response or the expected response type.
123
+ * @template T - The parsed JSON body type.
127
124
  * @param url - The URL to send the request to.
128
125
  * @param options - Optional fetch options including method, headers, body, etc.
129
- * @returns Promise that resolves with the parsed response data or the Response object for non-JSON.
126
+ * @returns Promise that resolves with the parsed JSON body, or the Response object when the
127
+ * response carries no content type at all.
130
128
  * @throws {DotHttpError} - Throws DotHttpError for HTTP errors (4xx/5xx status codes).
129
+ * @throws {DotHttpError} - Throws DotHttpError (status 502) for a successful response whose content type is not JSON.
131
130
  * @throws {DotHttpError} - Throws DotHttpError for network errors (connection issues, timeouts).
132
131
  *
133
132
  * @example
@@ -144,11 +143,6 @@ class FetchHttpClient extends types.BaseHttpClient {
144
143
  * headers: { 'Content-Type': 'application/json' },
145
144
  * body: JSON.stringify({ name: 'John', email: 'john@example.com' })
146
145
  * });
147
- *
148
- * // File download (non-JSON response)
149
- * const response = await client.request<Response>('/api/files/document.pdf', {
150
- * method: 'GET'
151
- * });
152
146
  * ```
153
147
  */
154
148
  async request(url, options) {
@@ -160,35 +154,62 @@ class FetchHttpClient extends types.BaseHttpClient {
160
154
  // e.g. an older server) and never throws, so this can't affect the actual
161
155
  // request/response handling below.
162
156
  checkSdkCompatibility(response.headers, SDK_VERSION);
157
+ const contentType = response.headers.get('content-type');
158
+ // application/json and the structured-syntax variants (problem+json,
159
+ // graphql-response+json, ...), in any case.
160
+ const isJson = /^application\/([\w.-]+\+)?json\b/i.test(contentType ?? '');
163
161
  if (!response.ok) {
164
162
  // Parse response body for error context
165
163
  let errorBody;
166
164
  try {
167
- const contentType = response.headers.get('content-type');
168
- if (contentType?.includes('application/json')) {
169
- errorBody = await response.json();
170
- }
171
- else {
172
- errorBody = await response.text();
173
- }
165
+ errorBody = isJson ? await response.json() : await response.text();
174
166
  }
175
167
  catch {
176
168
  errorBody = response.statusText;
177
169
  }
178
- // Convert headers to plain object
179
- const headers = {};
180
- response.headers.forEach((value, key) => {
181
- headers[key] = value;
182
- });
183
- throw this.createHttpError(response.status, response.statusText, headers, errorBody);
170
+ // The status alone hides the cause, so name what else went wrong: the redirect
171
+ // first (it is usually the root cause), then the unexpected format.
172
+ const hints = [
173
+ describeCrossOriginRedirect(url, response),
174
+ contentType && !isJson
175
+ ? `The response was '${contentType}', not JSON, so it may not have come from the dotCMS API: a proxy, a load balancer or another server can answer this way.`
176
+ : undefined
177
+ ].filter(Boolean);
178
+ const status = `HTTP ${response.status}: ${response.statusText}`;
179
+ const message = hints.length ? `${status}. ${hints.join(' ')}` : status;
180
+ throw this.createHttpError(response.status, response.statusText, toPlainHeaders(response.headers), errorBody, message);
184
181
  }
185
- // Handle different response types
186
- const contentType = response.headers.get('content-type');
187
- if (contentType?.includes('application/json')) {
182
+ if (isJson) {
188
183
  return response.json();
189
184
  }
190
- // For non-JSON responses, return the response object
191
- // Sub-clients can handle specific response types as needed
185
+ // Every SDK caller talks to a JSON endpoint, so a successful answer in another
186
+ // format means something other than the dotCMS API answered (a login page, a
187
+ // proxy, the wrong port). Returning the Response would only move the failure
188
+ // downstream.
189
+ if (contentType) {
190
+ let body;
191
+ try {
192
+ body = await response.text();
193
+ }
194
+ catch {
195
+ body = undefined;
196
+ }
197
+ const crossOriginRedirect = describeCrossOriginRedirect(url, response);
198
+ const sameOriginRedirect = !crossOriginRedirect && response.redirected && response.url
199
+ ? ` after a redirect to '${response.url}'`
200
+ : '';
201
+ const message = [
202
+ `Expected a JSON response from '${url}' but received '${contentType}' (HTTP ${response.status})${sameOriginRedirect}, which is not JSON. dotCMS API endpoints answer in JSON, so something other than the dotCMS API answered (a login page, a proxy or another server): check that dotcmsUrl points at your dotCMS instance.`,
203
+ crossOriginRedirect
204
+ ]
205
+ .filter(Boolean)
206
+ .join(' ');
207
+ // Reported as 502 Bad Gateway, not the real 2xx: callers forward error.status
208
+ // as their own response status, and an error must never read as success. The
209
+ // real status is in the message and the body in data.
210
+ throw this.createHttpError(502, 'Bad Gateway', toPlainHeaders(response.headers), body, message);
211
+ }
212
+ // No content type at all: hand back the Response untouched.
192
213
  return response;
193
214
  }
194
215
  catch (error) {
@@ -200,6 +221,46 @@ class FetchHttpClient extends types.BaseHttpClient {
200
221
  }
201
222
  }
202
223
  }
224
+ /**
225
+ * Explains a redirect that took the request to a different origin, or returns undefined
226
+ * when there was no redirect, it stayed on the same origin, or either URL can't be parsed.
227
+ *
228
+ * A changed origin almost always means `dotcmsUrl` names a scheme or host the server moves
229
+ * away from (e.g. `http://` answered with a 301 to `https://`), and the hop itself breaks the
230
+ * request: fetch drops the Authorization header on a cross-origin redirect, and a 301, 302 or
231
+ * 303 turns a POST into a GET without its body.
232
+ *
233
+ * @param requestUrl - The URL the SDK asked for.
234
+ * @param response - The response fetch ended on after following redirects.
235
+ * @returns A sentence naming both URLs and `dotcmsUrl`, or undefined.
236
+ */
237
+ function describeCrossOriginRedirect(requestUrl, response) {
238
+ if (!response.redirected || !response.url) {
239
+ return undefined;
240
+ }
241
+ try {
242
+ if (new URL(requestUrl).origin === new URL(response.url).origin) {
243
+ return undefined;
244
+ }
245
+ }
246
+ catch {
247
+ return undefined;
248
+ }
249
+ return `The request to '${requestUrl}' was redirected to '${response.url}', a different origin. Check that dotcmsUrl uses the exact scheme and host your dotCMS instance serves: a cross-origin redirect drops the Authorization header, and a 301, 302 or 303 turns a POST into a GET without its body.`;
250
+ }
251
+ /**
252
+ * Copies fetch Headers into a plain object, the shape createHttpError expects.
253
+ *
254
+ * @param headers - The response headers.
255
+ * @returns The headers keyed by lower-case name.
256
+ */
257
+ function toPlainHeaders(headers) {
258
+ const plain = {};
259
+ headers.forEach((value, key) => {
260
+ plain[key] = value;
261
+ });
262
+ return plain;
263
+ }
203
264
 
204
265
  /*! *****************************************************************************
205
266
  Copyright (c) Microsoft Corporation.
package/index.esm.js CHANGED
@@ -2,7 +2,7 @@ import { consola } from 'consola';
2
2
  import { BaseHttpClient, DISTANCE_FUNCTIONS, DotHttpError, DotErrorAISearch, DotErrorContent, DotErrorNavigation, UVE_MODE, DotErrorPage } from '@dotcms/types';
3
3
  import { graphqlToPageEntity } from './internal.esm.js';
4
4
 
5
- const SDK_VERSION = "26.9.24-1";
5
+ const SDK_VERSION = "26.9.24-1-next.2772";
6
6
 
7
7
  const DOTCMS_VERSION_HEADER = 'x-dotcms-version';
8
8
  const DOTCMS_MIN_SDK_HEADER = 'x-dotcms-min-sdk';
@@ -91,25 +91,23 @@ const checkSdkCompatibility = (headers, ownVersion) => {
91
91
  *
92
92
  * Extends BaseHttpClient to provide a standard interface for making HTTP requests.
93
93
  * This implementation uses the native Fetch API and handles:
94
- * - JSON and non-JSON response parsing
94
+ * - JSON response parsing
95
95
  * - HTTP error response parsing and conversion to DotHttpError
96
96
  * - Network error handling and wrapping
97
- * - Content-Type detection for proper response handling
97
+ * - Diagnosing misconfiguration: a response in a format other than JSON, or a request
98
+ * that was redirected to another origin, is named as such in the error message
99
+ *
100
+ * Every SDK caller talks to a dotCMS JSON endpoint, so a successful response with a
101
+ * non-JSON content type is rejected rather than returned.
98
102
  *
99
103
  * @example
100
104
  * ```typescript
101
105
  * const client = new FetchHttpClient();
102
106
  *
103
- * // JSON request
104
107
  * const data = await client.request<MyType>('/api/data', {
105
108
  * method: 'GET',
106
109
  * headers: { 'Authorization': 'Bearer token' }
107
110
  * });
108
- *
109
- * // Non-JSON request (e.g., file download)
110
- * const response = await client.request<Response>('/api/file.pdf', {
111
- * method: 'GET'
112
- * });
113
111
  * ```
114
112
  */
115
113
  class FetchHttpClient extends BaseHttpClient {
@@ -120,12 +118,13 @@ class FetchHttpClient extends BaseHttpClient {
120
118
  * Automatically handles response parsing based on Content-Type headers and converts
121
119
  * HTTP errors to standardized DotHttpError instances.
122
120
  *
123
- * @template T - The expected response type. For JSON responses, T should be the parsed object type.
124
- * For non-JSON responses, T should be Response or the expected response type.
121
+ * @template T - The parsed JSON body type.
125
122
  * @param url - The URL to send the request to.
126
123
  * @param options - Optional fetch options including method, headers, body, etc.
127
- * @returns Promise that resolves with the parsed response data or the Response object for non-JSON.
124
+ * @returns Promise that resolves with the parsed JSON body, or the Response object when the
125
+ * response carries no content type at all.
128
126
  * @throws {DotHttpError} - Throws DotHttpError for HTTP errors (4xx/5xx status codes).
127
+ * @throws {DotHttpError} - Throws DotHttpError (status 502) for a successful response whose content type is not JSON.
129
128
  * @throws {DotHttpError} - Throws DotHttpError for network errors (connection issues, timeouts).
130
129
  *
131
130
  * @example
@@ -142,11 +141,6 @@ class FetchHttpClient extends BaseHttpClient {
142
141
  * headers: { 'Content-Type': 'application/json' },
143
142
  * body: JSON.stringify({ name: 'John', email: 'john@example.com' })
144
143
  * });
145
- *
146
- * // File download (non-JSON response)
147
- * const response = await client.request<Response>('/api/files/document.pdf', {
148
- * method: 'GET'
149
- * });
150
144
  * ```
151
145
  */
152
146
  async request(url, options) {
@@ -158,35 +152,62 @@ class FetchHttpClient extends BaseHttpClient {
158
152
  // e.g. an older server) and never throws, so this can't affect the actual
159
153
  // request/response handling below.
160
154
  checkSdkCompatibility(response.headers, SDK_VERSION);
155
+ const contentType = response.headers.get('content-type');
156
+ // application/json and the structured-syntax variants (problem+json,
157
+ // graphql-response+json, ...), in any case.
158
+ const isJson = /^application\/([\w.-]+\+)?json\b/i.test(contentType ?? '');
161
159
  if (!response.ok) {
162
160
  // Parse response body for error context
163
161
  let errorBody;
164
162
  try {
165
- const contentType = response.headers.get('content-type');
166
- if (contentType?.includes('application/json')) {
167
- errorBody = await response.json();
168
- }
169
- else {
170
- errorBody = await response.text();
171
- }
163
+ errorBody = isJson ? await response.json() : await response.text();
172
164
  }
173
165
  catch {
174
166
  errorBody = response.statusText;
175
167
  }
176
- // Convert headers to plain object
177
- const headers = {};
178
- response.headers.forEach((value, key) => {
179
- headers[key] = value;
180
- });
181
- throw this.createHttpError(response.status, response.statusText, headers, errorBody);
168
+ // The status alone hides the cause, so name what else went wrong: the redirect
169
+ // first (it is usually the root cause), then the unexpected format.
170
+ const hints = [
171
+ describeCrossOriginRedirect(url, response),
172
+ contentType && !isJson
173
+ ? `The response was '${contentType}', not JSON, so it may not have come from the dotCMS API: a proxy, a load balancer or another server can answer this way.`
174
+ : undefined
175
+ ].filter(Boolean);
176
+ const status = `HTTP ${response.status}: ${response.statusText}`;
177
+ const message = hints.length ? `${status}. ${hints.join(' ')}` : status;
178
+ throw this.createHttpError(response.status, response.statusText, toPlainHeaders(response.headers), errorBody, message);
182
179
  }
183
- // Handle different response types
184
- const contentType = response.headers.get('content-type');
185
- if (contentType?.includes('application/json')) {
180
+ if (isJson) {
186
181
  return response.json();
187
182
  }
188
- // For non-JSON responses, return the response object
189
- // Sub-clients can handle specific response types as needed
183
+ // Every SDK caller talks to a JSON endpoint, so a successful answer in another
184
+ // format means something other than the dotCMS API answered (a login page, a
185
+ // proxy, the wrong port). Returning the Response would only move the failure
186
+ // downstream.
187
+ if (contentType) {
188
+ let body;
189
+ try {
190
+ body = await response.text();
191
+ }
192
+ catch {
193
+ body = undefined;
194
+ }
195
+ const crossOriginRedirect = describeCrossOriginRedirect(url, response);
196
+ const sameOriginRedirect = !crossOriginRedirect && response.redirected && response.url
197
+ ? ` after a redirect to '${response.url}'`
198
+ : '';
199
+ const message = [
200
+ `Expected a JSON response from '${url}' but received '${contentType}' (HTTP ${response.status})${sameOriginRedirect}, which is not JSON. dotCMS API endpoints answer in JSON, so something other than the dotCMS API answered (a login page, a proxy or another server): check that dotcmsUrl points at your dotCMS instance.`,
201
+ crossOriginRedirect
202
+ ]
203
+ .filter(Boolean)
204
+ .join(' ');
205
+ // Reported as 502 Bad Gateway, not the real 2xx: callers forward error.status
206
+ // as their own response status, and an error must never read as success. The
207
+ // real status is in the message and the body in data.
208
+ throw this.createHttpError(502, 'Bad Gateway', toPlainHeaders(response.headers), body, message);
209
+ }
210
+ // No content type at all: hand back the Response untouched.
190
211
  return response;
191
212
  }
192
213
  catch (error) {
@@ -198,6 +219,46 @@ class FetchHttpClient extends BaseHttpClient {
198
219
  }
199
220
  }
200
221
  }
222
+ /**
223
+ * Explains a redirect that took the request to a different origin, or returns undefined
224
+ * when there was no redirect, it stayed on the same origin, or either URL can't be parsed.
225
+ *
226
+ * A changed origin almost always means `dotcmsUrl` names a scheme or host the server moves
227
+ * away from (e.g. `http://` answered with a 301 to `https://`), and the hop itself breaks the
228
+ * request: fetch drops the Authorization header on a cross-origin redirect, and a 301, 302 or
229
+ * 303 turns a POST into a GET without its body.
230
+ *
231
+ * @param requestUrl - The URL the SDK asked for.
232
+ * @param response - The response fetch ended on after following redirects.
233
+ * @returns A sentence naming both URLs and `dotcmsUrl`, or undefined.
234
+ */
235
+ function describeCrossOriginRedirect(requestUrl, response) {
236
+ if (!response.redirected || !response.url) {
237
+ return undefined;
238
+ }
239
+ try {
240
+ if (new URL(requestUrl).origin === new URL(response.url).origin) {
241
+ return undefined;
242
+ }
243
+ }
244
+ catch {
245
+ return undefined;
246
+ }
247
+ return `The request to '${requestUrl}' was redirected to '${response.url}', a different origin. Check that dotcmsUrl uses the exact scheme and host your dotCMS instance serves: a cross-origin redirect drops the Authorization header, and a 301, 302 or 303 turns a POST into a GET without its body.`;
248
+ }
249
+ /**
250
+ * Copies fetch Headers into a plain object, the shape createHttpError expects.
251
+ *
252
+ * @param headers - The response headers.
253
+ * @returns The headers keyed by lower-case name.
254
+ */
255
+ function toPlainHeaders(headers) {
256
+ const plain = {};
257
+ headers.forEach((value, key) => {
258
+ plain[key] = value;
259
+ });
260
+ return plain;
261
+ }
201
262
 
202
263
  /*! *****************************************************************************
203
264
  Copyright (c) Microsoft Corporation.
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@dotcms/client",
3
- "version": "26.9.24-1",
3
+ "version": "26.9.24-1-next.2772",
4
4
  "description": "Official JavaScript library for interacting with DotCMS REST APIs.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/dotCMS/core.git#main"
8
8
  },
9
9
  "peerDependencies": {
10
- "@dotcms/types": "26.9.24-1"
10
+ "@dotcms/types": "26.9.24-1-next.2772"
11
11
  },
12
12
  "dependencies": {
13
13
  "consola": "^3.4.2"
14
14
  },
15
15
  "devDependencies": {
16
- "@dotcms/types": "26.9.24-1"
16
+ "@dotcms/types": "26.9.24-1-next.2772"
17
17
  },
18
18
  "keywords": [
19
19
  "dotCMS",
@@ -4,25 +4,23 @@ import { BaseHttpClient, DotRequestOptions } from '@dotcms/types';
4
4
  *
5
5
  * Extends BaseHttpClient to provide a standard interface for making HTTP requests.
6
6
  * This implementation uses the native Fetch API and handles:
7
- * - JSON and non-JSON response parsing
7
+ * - JSON response parsing
8
8
  * - HTTP error response parsing and conversion to DotHttpError
9
9
  * - Network error handling and wrapping
10
- * - Content-Type detection for proper response handling
10
+ * - Diagnosing misconfiguration: a response in a format other than JSON, or a request
11
+ * that was redirected to another origin, is named as such in the error message
12
+ *
13
+ * Every SDK caller talks to a dotCMS JSON endpoint, so a successful response with a
14
+ * non-JSON content type is rejected rather than returned.
11
15
  *
12
16
  * @example
13
17
  * ```typescript
14
18
  * const client = new FetchHttpClient();
15
19
  *
16
- * // JSON request
17
20
  * const data = await client.request<MyType>('/api/data', {
18
21
  * method: 'GET',
19
22
  * headers: { 'Authorization': 'Bearer token' }
20
23
  * });
21
- *
22
- * // Non-JSON request (e.g., file download)
23
- * const response = await client.request<Response>('/api/file.pdf', {
24
- * method: 'GET'
25
- * });
26
24
  * ```
27
25
  */
28
26
  export declare class FetchHttpClient extends BaseHttpClient {
@@ -33,12 +31,13 @@ export declare class FetchHttpClient extends BaseHttpClient {
33
31
  * Automatically handles response parsing based on Content-Type headers and converts
34
32
  * HTTP errors to standardized DotHttpError instances.
35
33
  *
36
- * @template T - The expected response type. For JSON responses, T should be the parsed object type.
37
- * For non-JSON responses, T should be Response or the expected response type.
34
+ * @template T - The parsed JSON body type.
38
35
  * @param url - The URL to send the request to.
39
36
  * @param options - Optional fetch options including method, headers, body, etc.
40
- * @returns Promise that resolves with the parsed response data or the Response object for non-JSON.
37
+ * @returns Promise that resolves with the parsed JSON body, or the Response object when the
38
+ * response carries no content type at all.
41
39
  * @throws {DotHttpError} - Throws DotHttpError for HTTP errors (4xx/5xx status codes).
40
+ * @throws {DotHttpError} - Throws DotHttpError (status 502) for a successful response whose content type is not JSON.
42
41
  * @throws {DotHttpError} - Throws DotHttpError for network errors (connection issues, timeouts).
43
42
  *
44
43
  * @example
@@ -55,11 +54,6 @@ export declare class FetchHttpClient extends BaseHttpClient {
55
54
  * headers: { 'Content-Type': 'application/json' },
56
55
  * body: JSON.stringify({ name: 'John', email: 'john@example.com' })
57
56
  * });
58
- *
59
- * // File download (non-JSON response)
60
- * const response = await client.request<Response>('/api/files/document.pdf', {
61
- * method: 'GET'
62
- * });
63
57
  * ```
64
58
  */
65
59
  request<T = unknown>(url: string, options?: DotRequestOptions): Promise<T>;