diff --git a/.changeset/fix-error-output.md b/.changeset/fix-error-output.md
new file mode 100644
index 00000000..8ecead91
--- /dev/null
+++ b/.changeset/fix-error-output.md
@@ -0,0 +1,8 @@
+---
+'@salesforce/b2c-cli': patch
+'@salesforce/b2c-tooling-sdk': patch
+---
+
+Fix HTML response bodies appearing in ERROR log lines. When API requests fail with non-JSON responses (like HTML error pages), error messages now show the HTTP status code (e.g., "HTTP 521 Web Server Is Down") instead of serializing the entire response body.
+
+Added `getApiErrorMessage(error, response)` utility that extracts clean error messages from ODS, OCAPI, and SCAPI error patterns with HTTP status fallback.
diff --git a/.claude/skills/api-client-development/SKILL.md b/.claude/skills/api-client-development/SKILL.md
index c61118b8..13162723 100644
--- a/.claude/skills/api-client-development/SKILL.md
+++ b/.claude/skills/api-client-development/SKILL.md
@@ -380,6 +380,62 @@ it('fetches endpoints', async () => {
---
+## Error Handling
+
+When API requests fail, use `getApiErrorMessage()` to extract clean, user-friendly error messages. This utility handles multiple error formats and ensures HTML response bodies (like error pages from stopped sandboxes) are never shown to users.
+
+### Using getApiErrorMessage
+
+```typescript
+import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk/clients';
+
+const {data, error, response} = await client.GET('/sites', {...});
+
+if (error) {
+ // Returns structured error message or "HTTP 521 Web Server Is Down"
+ const message = getApiErrorMessage(error, response);
+ this.error(`Failed to fetch sites: ${message}`);
+}
+```
+
+### Supported Error Patterns
+
+The utility extracts messages from these patterns in priority order:
+
+| API | Error Structure | Message Location |
+|-----|-----------------|------------------|
+| ODS/SLAS | `{ error: { message } }` | `error.error.message` |
+| OCAPI | `{ fault: { message } }` | `error.fault.message` |
+| SCAPI/Problem+JSON | `{ title, detail }` | `error.detail` or `error.title` |
+| Standard Error | `{ message }` | `error.message` |
+| Fallback | Any | `HTTP {status} {statusText}` |
+
+### Why This Matters
+
+**Without `getApiErrorMessage`:**
+```
+ERROR: Failed to fetch sites:
521 - Sandbox Down...
+```
+
+**With `getApiErrorMessage`:**
+```
+ERROR: Failed to fetch sites: HTTP 521 Web Server Is Down
+```
+
+### Important: Always Destructure `response`
+
+When making API calls, always destructure the `response` object alongside `error`:
+
+```typescript
+// GOOD: Include response for error handling
+const {data, error, response} = await client.GET('/endpoint', {...});
+
+// BAD: Missing response - can't get clean error message
+const {data, error} = await client.GET('/endpoint', {...});
+```
+
+---
+
## Checklist: New SCAPI Client
1. Add OpenAPI spec to `specs/`
diff --git a/.claude/skills/cli-command-development/SKILL.md b/.claude/skills/cli-command-development/SKILL.md
index 8a8f1779..9368120d 100644
--- a/.claude/skills/cli-command-development/SKILL.md
+++ b/.claude/skills/cli-command-development/SKILL.md
@@ -57,6 +57,7 @@ import { InstanceCommand, CartridgeCommand, OdsCommand } from '@salesforce/b2c-t
*/
import {Args, Flags} from '@oclif/core';
import {InstanceCommand} from '@salesforce/b2c-tooling-sdk/cli';
+import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../i18n/index.js';
interface MyCommandResponse {
@@ -105,27 +106,27 @@ export default class MyCommand extends InstanceCommand {
this.log(t('commands.topic.mycommand.working', 'Working on {{name}}...', {name}));
// Implementation
- const result = await this.instance.ocapi.GET('/some/endpoint');
+ const {data, error, response} = await this.instance.ocapi.GET('/some/endpoint');
- if (!result.data) {
+ if (error) {
this.error(t('commands.topic.mycommand.error', 'Failed: {{message}}', {
- message: result.response?.statusText || 'Unknown error',
+ message: getApiErrorMessage(error, response),
}));
}
- const response: MyCommandResponse = {
+ const result: MyCommandResponse = {
success: true,
- data: result.data,
+ data,
};
// JSON mode returns the object directly (oclif handles serialization)
if (this.jsonEnabled()) {
- return response;
+ return result;
}
// Human-readable output
this.log('Success!');
- return response;
+ return result;
}
}
```
@@ -305,14 +306,21 @@ this.error('Config file not found', {
// Warning (continues execution)
this.warn('Deprecated flag used');
-// Structured API errors
-if (result.error) {
+// API errors - use getApiErrorMessage for clean messages
+import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
+
+const {data, error, response} = await this.instance.ocapi.GET('/sites', {...});
+if (error) {
this.error(t('commands.topic.cmd.apiError', 'API error: {{message}}', {
- message: formatApiError(result.error),
+ message: getApiErrorMessage(error, response),
}));
}
```
+**Important:** Always destructure `response` alongside `error` when making API calls. The `getApiErrorMessage` utility extracts clean messages from ODS, OCAPI, and SCAPI error patterns, and falls back to HTTP status (e.g., "HTTP 521 Web Server Is Down") for non-JSON responses like HTML error pages.
+
+See [API Client Development](../api-client-development/SKILL.md#error-handling) for supported error patterns.
+
## Creating a Command Checklist
1. Create file at `packages/b2c-cli/src/commands//.ts`
diff --git a/packages/b2c-cli/src/commands/ods/create.ts b/packages/b2c-cli/src/commands/ods/create.ts
index d98cb570..a8dc2409 100644
--- a/packages/b2c-cli/src/commands/ods/create.ts
+++ b/packages/b2c-cli/src/commands/ods/create.ts
@@ -6,7 +6,7 @@
import {Flags, ux} from '@oclif/core';
import cliui from 'cliui';
import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
-import type {OdsComponents} from '@salesforce/b2c-tooling-sdk';
+import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../i18n/index.js';
type SandboxModel = OdsComponents['schemas']['SandboxModel'];
@@ -139,11 +139,9 @@ export default class OdsCreate extends OdsCommand {
});
if (!result.data?.data) {
- const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined;
- const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error';
this.error(
t('commands.ods.create.error', 'Failed to create sandbox: {{message}}', {
- message: errorMessage,
+ message: getApiErrorMessage(result.error, result.response),
}),
);
}
diff --git a/packages/b2c-cli/src/commands/ods/delete.ts b/packages/b2c-cli/src/commands/ods/delete.ts
index 091e1e9a..86223221 100644
--- a/packages/b2c-cli/src/commands/ods/delete.ts
+++ b/packages/b2c-cli/src/commands/ods/delete.ts
@@ -6,7 +6,7 @@
import * as readline from 'node:readline';
import {Args, Flags} from '@oclif/core';
import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
-import type {OdsComponents} from '@salesforce/b2c-tooling-sdk';
+import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../i18n/index.js';
/**
@@ -92,11 +92,9 @@ export default class OdsDelete extends OdsCommand {
});
if (result.response.status !== 202) {
- const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined;
- const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error';
this.error(
t('commands.ods.delete.error', 'Failed to delete sandbox: {{message}}', {
- message: errorMessage,
+ message: getApiErrorMessage(result.error, result.response),
}),
);
}
diff --git a/packages/b2c-cli/src/commands/ods/list.ts b/packages/b2c-cli/src/commands/ods/list.ts
index 57a9b783..430157c6 100644
--- a/packages/b2c-cli/src/commands/ods/list.ts
+++ b/packages/b2c-cli/src/commands/ods/list.ts
@@ -5,7 +5,7 @@
*/
import {Flags} from '@oclif/core';
import {OdsCommand, TableRenderer, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli';
-import type {OdsComponents} from '@salesforce/b2c-tooling-sdk';
+import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../i18n/index.js';
type SandboxModel = OdsComponents['schemas']['SandboxModel'];
@@ -142,11 +142,9 @@ export default class OdsList extends OdsCommand {
});
if (result.error) {
- const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined;
- const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error';
this.error(
t('commands.ods.list.error', 'Failed to fetch sandboxes: {{message}}', {
- message: errorMessage,
+ message: getApiErrorMessage(result.error, result.response),
}),
);
}
diff --git a/packages/b2c-cli/src/commands/ods/restart.ts b/packages/b2c-cli/src/commands/ods/restart.ts
index b6667369..44a51b51 100644
--- a/packages/b2c-cli/src/commands/ods/restart.ts
+++ b/packages/b2c-cli/src/commands/ods/restart.ts
@@ -5,7 +5,7 @@
*/
import {Args} from '@oclif/core';
import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
-import type {OdsComponents} from '@salesforce/b2c-tooling-sdk';
+import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../i18n/index.js';
type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
@@ -45,11 +45,9 @@ export default class OdsRestart extends OdsCommand {
});
if (!result.data?.data) {
- const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined;
- const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error';
this.error(
t('commands.ods.restart.error', 'Failed to restart sandbox: {{message}}', {
- message: errorMessage,
+ message: getApiErrorMessage(result.error, result.response),
}),
);
}
diff --git a/packages/b2c-cli/src/commands/ods/start.ts b/packages/b2c-cli/src/commands/ods/start.ts
index 2a1aa8ae..fc3c0698 100644
--- a/packages/b2c-cli/src/commands/ods/start.ts
+++ b/packages/b2c-cli/src/commands/ods/start.ts
@@ -5,7 +5,7 @@
*/
import {Args} from '@oclif/core';
import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
-import type {OdsComponents} from '@salesforce/b2c-tooling-sdk';
+import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../i18n/index.js';
type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
@@ -45,11 +45,9 @@ export default class OdsStart extends OdsCommand {
});
if (!result.data?.data) {
- const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined;
- const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error';
this.error(
t('commands.ods.start.error', 'Failed to start sandbox: {{message}}', {
- message: errorMessage,
+ message: getApiErrorMessage(result.error, result.response),
}),
);
}
diff --git a/packages/b2c-cli/src/commands/ods/stop.ts b/packages/b2c-cli/src/commands/ods/stop.ts
index 3160bb05..78d4c053 100644
--- a/packages/b2c-cli/src/commands/ods/stop.ts
+++ b/packages/b2c-cli/src/commands/ods/stop.ts
@@ -5,7 +5,7 @@
*/
import {Args} from '@oclif/core';
import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
-import type {OdsComponents} from '@salesforce/b2c-tooling-sdk';
+import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../i18n/index.js';
type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
@@ -45,11 +45,9 @@ export default class OdsStop extends OdsCommand {
});
if (!result.data?.data) {
- const errorResponse = result.error as OdsComponents['schemas']['ErrorResponse'] | undefined;
- const errorMessage = errorResponse?.error?.message || result.response?.statusText || 'Unknown error';
this.error(
t('commands.ods.stop.error', 'Failed to stop sandbox: {{message}}', {
- message: errorMessage,
+ message: getApiErrorMessage(result.error, result.response),
}),
);
}
diff --git a/packages/b2c-cli/src/commands/scapi/custom/status.ts b/packages/b2c-cli/src/commands/scapi/custom/status.ts
index e8ed0df6..cecb3e67 100644
--- a/packages/b2c-cli/src/commands/scapi/custom/status.ts
+++ b/packages/b2c-cli/src/commands/scapi/custom/status.ts
@@ -5,7 +5,12 @@
*/
import {Command, Flags, ux} from '@oclif/core';
import {OAuthCommand, TableRenderer, type ColumnDef} from '@salesforce/b2c-tooling-sdk/cli';
-import {createCustomApisClient, toOrganizationId, type CustomApisComponents} from '@salesforce/b2c-tooling-sdk';
+import {
+ createCustomApisClient,
+ getApiErrorMessage,
+ toOrganizationId,
+ type CustomApisComponents,
+} from '@salesforce/b2c-tooling-sdk';
import {t} from '../../../i18n/index.js';
type CustomApiEndpoint = CustomApisComponents['schemas']['CustomApiEndpoint'];
@@ -242,7 +247,11 @@ export default class ScapiCustomStatus extends ScapiCustomCommand {
this.log(t('commands.sites.list.fetching', 'Fetching sites from {{hostname}}...', {hostname}));
- const {data, error} = await this.instance.ocapi.GET('/sites', {
+ const {data, error, response} = await this.instance.ocapi.GET('/sites', {
params: {query: {select: '(**)'}},
});
if (error) {
- this.error(t('commands.sites.list.error', 'Failed to fetch sites: {{message}}', {message: String(error)}));
+ this.error(
+ t('commands.sites.list.error', 'Failed to fetch sites: {{message}}', {
+ message: getApiErrorMessage(error, response),
+ }),
+ );
}
const sites = data as Sites;
diff --git a/packages/b2c-cli/src/commands/slas/client/create.ts b/packages/b2c-cli/src/commands/slas/client/create.ts
index f57e233c..7e6caaa1 100644
--- a/packages/b2c-cli/src/commands/slas/client/create.ts
+++ b/packages/b2c-cli/src/commands/slas/client/create.ts
@@ -178,7 +178,7 @@ export default class SlasClientCreate extends SlasClientCommand extends OAuthC
if (!isTenantNotFound) {
this.error(
t('commands.slas.client.create.tenantError', 'Failed to check tenant: {{message}}', {
- message: formatApiError(error),
+ message: formatApiError(error, response),
}),
);
}
@@ -149,7 +149,7 @@ export abstract class SlasClientCommand extends OAuthC
this.log(t('commands.slas.client.create.creatingTenant', 'Creating SLAS tenant {{tenantId}}...', {tenantId}));
}
- const {error: createError} = await slasClient.PUT('/tenants/{tenantId}', {
+ const {error: createError, response: createResponse} = await slasClient.PUT('/tenants/{tenantId}', {
params: {
path: {tenantId},
},
@@ -166,7 +166,7 @@ export abstract class SlasClientCommand extends OAuthC
if (createError) {
this.error(
t('commands.slas.client.create.tenantCreateError', 'Failed to create tenant: {{message}}', {
- message: formatApiError(createError),
+ message: formatApiError(createError, createResponse),
}),
);
}
diff --git a/packages/b2c-tooling-sdk/src/clients/error-utils.ts b/packages/b2c-tooling-sdk/src/clients/error-utils.ts
new file mode 100644
index 00000000..ed57363e
--- /dev/null
+++ b/packages/b2c-tooling-sdk/src/clients/error-utils.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2025, Salesforce, Inc.
+ * SPDX-License-Identifier: Apache-2
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
+ */
+/**
+ * Utilities for extracting error messages from API responses.
+ *
+ * @module clients/error-utils
+ */
+
+/**
+ * Extract a clean error message from an API error response.
+ *
+ * Handles multiple API error patterns and falls back to HTTP status.
+ * This ensures that HTML response bodies (like error pages) are never
+ * included in user-facing error messages.
+ *
+ * Supported error patterns:
+ * - ODS/SLAS: `{ error: { message: '...' } }`
+ * - OCAPI: `{ fault: { message: '...' } }`
+ * - SCAPI/Problem+JSON: `{ detail: '...', title: '...' }`
+ * - Standard Error: `{ message: '...' }`
+ *
+ * @param error - The error object from an API response
+ * @param response - The HTTP response (for status code fallback)
+ * @returns A clean, human-readable error message
+ *
+ * @example
+ * ```typescript
+ * const {data, error, response} = await client.GET('/sites', {...});
+ * if (error) {
+ * const message = getApiErrorMessage(error, response);
+ * // Returns structured error message or "HTTP 521 Web Server Is Down"
+ * }
+ * ```
+ */
+export function getApiErrorMessage(error: unknown, response: Response | {status: number; statusText: string}): string {
+ if (error && typeof error === 'object') {
+ const err = error as Record;
+
+ // ODS/SLAS pattern: { error: { message: '...' } }
+ if (err.error && typeof err.error === 'object') {
+ const nested = err.error as Record;
+ if (typeof nested.message === 'string' && nested.message) {
+ return nested.message;
+ }
+ }
+
+ // OCAPI fault pattern: { fault: { message: '...' } }
+ if (err.fault && typeof err.fault === 'object') {
+ const fault = err.fault as Record;
+ if (typeof fault.message === 'string' && fault.message) {
+ return fault.message;
+ }
+ }
+
+ // SCAPI/Problem+JSON pattern: { detail: '...', title: '...' }
+ if (typeof err.detail === 'string' && err.detail) {
+ return err.detail;
+ }
+ if (typeof err.title === 'string' && err.title) {
+ return err.title;
+ }
+
+ // Standard Error pattern: { message: '...' }
+ if (typeof err.message === 'string' && err.message) {
+ return err.message;
+ }
+ }
+
+ // Fallback to HTTP status
+ return `HTTP ${response.status} ${response.statusText}`;
+}
diff --git a/packages/b2c-tooling-sdk/src/clients/index.ts b/packages/b2c-tooling-sdk/src/clients/index.ts
index 591d3615..688b7195 100644
--- a/packages/b2c-tooling-sdk/src/clients/index.ts
+++ b/packages/b2c-tooling-sdk/src/clients/index.ts
@@ -195,3 +195,5 @@ export type {
paths as ScapiSchemasPaths,
components as ScapiSchemasComponents,
} from './scapi-schemas.js';
+
+export {getApiErrorMessage} from './error-utils.js';
diff --git a/packages/b2c-tooling-sdk/src/index.ts b/packages/b2c-tooling-sdk/src/index.ts
index 8b2e5923..4970ef19 100644
--- a/packages/b2c-tooling-sdk/src/index.ts
+++ b/packages/b2c-tooling-sdk/src/index.ts
@@ -74,6 +74,7 @@ export {
toOrganizationId,
toTenantId,
buildTenantScope,
+ getApiErrorMessage,
ORGANIZATION_ID_PREFIX,
SCAPI_TENANT_SCOPE_PREFIX,
CUSTOM_APIS_DEFAULT_SCOPES,
diff --git a/packages/b2c-tooling-sdk/test/clients/error-utils.test.ts b/packages/b2c-tooling-sdk/test/clients/error-utils.test.ts
new file mode 100644
index 00000000..fc51b5e7
--- /dev/null
+++ b/packages/b2c-tooling-sdk/test/clients/error-utils.test.ts
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2025, Salesforce, Inc.
+ * SPDX-License-Identifier: Apache-2
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
+ */
+import {expect} from 'chai';
+import {getApiErrorMessage} from '../../src/clients/error-utils.js';
+
+describe('getApiErrorMessage', () => {
+ // Mock response object for testing
+ const mockResponse = (status: number, statusText: string) => ({
+ status,
+ statusText,
+ });
+
+ describe('ODS/SLAS error pattern', () => {
+ it('extracts message from { error: { message } } structure', () => {
+ const error = {error: {message: 'Sandbox not found'}};
+ const response = mockResponse(404, 'Not Found');
+ expect(getApiErrorMessage(error, response)).to.equal('Sandbox not found');
+ });
+
+ it('ignores error.error if message is not a string', () => {
+ const error = {error: {message: 123}};
+ const response = mockResponse(500, 'Internal Server Error');
+ expect(getApiErrorMessage(error, response)).to.equal('HTTP 500 Internal Server Error');
+ });
+
+ it('ignores error.error if message is empty', () => {
+ const error = {error: {message: ''}};
+ const response = mockResponse(500, 'Internal Server Error');
+ expect(getApiErrorMessage(error, response)).to.equal('HTTP 500 Internal Server Error');
+ });
+ });
+
+ describe('OCAPI fault pattern', () => {
+ it('extracts message from { fault: { message } } structure', () => {
+ const error = {fault: {type: 'NotFoundException', message: 'Site not found'}};
+ const response = mockResponse(404, 'Not Found');
+ expect(getApiErrorMessage(error, response)).to.equal('Site not found');
+ });
+
+ it('ignores fault if message is not a string', () => {
+ const error = {fault: {type: 'Error', message: null}};
+ const response = mockResponse(500, 'Internal Server Error');
+ expect(getApiErrorMessage(error, response)).to.equal('HTTP 500 Internal Server Error');
+ });
+ });
+
+ describe('SCAPI/Problem+JSON pattern', () => {
+ it('extracts detail from { detail } structure', () => {
+ const error = {type: 'NotFound', title: 'Resource Not Found', detail: 'The requested schema was not found'};
+ const response = mockResponse(404, 'Not Found');
+ expect(getApiErrorMessage(error, response)).to.equal('The requested schema was not found');
+ });
+
+ it('falls back to title if detail is missing', () => {
+ const error = {type: 'NotFound', title: 'Resource Not Found'};
+ const response = mockResponse(404, 'Not Found');
+ expect(getApiErrorMessage(error, response)).to.equal('Resource Not Found');
+ });
+
+ it('ignores detail if it is empty', () => {
+ const error = {title: 'Some Title', detail: ''};
+ const response = mockResponse(400, 'Bad Request');
+ expect(getApiErrorMessage(error, response)).to.equal('Some Title');
+ });
+ });
+
+ describe('standard Error pattern', () => {
+ it('extracts message from { message } structure', () => {
+ const error = {message: 'Something went wrong'};
+ const response = mockResponse(500, 'Internal Server Error');
+ expect(getApiErrorMessage(error, response)).to.equal('Something went wrong');
+ });
+
+ it('works with actual Error objects', () => {
+ const error = new Error('Connection refused');
+ const response = mockResponse(503, 'Service Unavailable');
+ expect(getApiErrorMessage(error, response)).to.equal('Connection refused');
+ });
+ });
+
+ describe('HTTP status fallback', () => {
+ it('returns HTTP status when error is null', () => {
+ const response = mockResponse(521, 'Web Server Is Down');
+ expect(getApiErrorMessage(null, response)).to.equal('HTTP 521 Web Server Is Down');
+ });
+
+ it('returns HTTP status when error is undefined', () => {
+ const response = mockResponse(502, 'Bad Gateway');
+ expect(getApiErrorMessage(undefined, response)).to.equal('HTTP 502 Bad Gateway');
+ });
+
+ it('returns HTTP status when error is not an object', () => {
+ const response = mockResponse(500, 'Internal Server Error');
+ expect(getApiErrorMessage('some string error', response)).to.equal('HTTP 500 Internal Server Error');
+ expect(getApiErrorMessage(123, response)).to.equal('HTTP 500 Internal Server Error');
+ });
+
+ it('returns HTTP status when error object has no recognized fields', () => {
+ const error = {someField: 'value', html: '...'};
+ const response = mockResponse(521, 'Sandbox Down');
+ expect(getApiErrorMessage(error, response)).to.equal('HTTP 521 Sandbox Down');
+ });
+
+ it('does not include HTML content in the message', () => {
+ const error = {body: 'Error page'};
+ const response = mockResponse(521, 'Web Server Is Down');
+ const message = getApiErrorMessage(error, response);
+ expect(message).to.equal('HTTP 521 Web Server Is Down');
+ expect(message).to.not.include('');
+ });
+ });
+
+ describe('priority order', () => {
+ it('prioritizes error.error.message over other fields', () => {
+ const error = {
+ error: {message: 'ODS message'},
+ fault: {message: 'OCAPI message'},
+ detail: 'SCAPI detail',
+ message: 'Standard message',
+ };
+ const response = mockResponse(500, 'Error');
+ expect(getApiErrorMessage(error, response)).to.equal('ODS message');
+ });
+
+ it('prioritizes fault.message over SCAPI and standard message', () => {
+ const error = {
+ fault: {message: 'OCAPI message'},
+ detail: 'SCAPI detail',
+ message: 'Standard message',
+ };
+ const response = mockResponse(500, 'Error');
+ expect(getApiErrorMessage(error, response)).to.equal('OCAPI message');
+ });
+
+ it('prioritizes detail over title and standard message', () => {
+ const error = {
+ title: 'Error Title',
+ detail: 'Error Detail',
+ message: 'Standard message',
+ };
+ const response = mockResponse(500, 'Error');
+ expect(getApiErrorMessage(error, response)).to.equal('Error Detail');
+ });
+
+ it('prioritizes title over standard message', () => {
+ const error = {
+ title: 'Error Title',
+ message: 'Standard message',
+ };
+ const response = mockResponse(500, 'Error');
+ expect(getApiErrorMessage(error, response)).to.equal('Error Title');
+ });
+ });
+
+ describe('works with real Response objects', () => {
+ it('accepts a Response-like object', () => {
+ // Simulate what openapi-fetch returns
+ const error = null;
+ const response = {
+ status: 521,
+ statusText: 'Web Server Is Down',
+ ok: false,
+ headers: new Headers(),
+ };
+ expect(getApiErrorMessage(error, response as Response)).to.equal('HTTP 521 Web Server Is Down');
+ });
+ });
+});