Skip to content

Commit ae15287

Browse files
committed
Release v5.2.0 - Add maintenance mode detection
Add automatic maintenance mode detection via throwOnMaintenanceHeader option. When enabled, the SDK detects maintenance windows by checking the sfdc_maintenance response header and throws MaintenanceError (503) when the value is 'system' or 'site'. Key features: - Opt-in via client configuration (backward compatible) - New MaintenanceError class with detailed error information - Works with all API endpoints - Takes precedence over other error handling - Comprehensive test coverage (100%) Changes: - Add MaintenanceError class and tests - Add throwOnMaintenanceHeader to ClientConfig - Add header check in doFetch() - Export MaintenanceError from helpers - Update README with usage documentation - Update CHANGELOG for v5.2.0 - Bump version to 5.2.0
1 parent cd9df95 commit ae15287

11 files changed

Lines changed: 318 additions & 2 deletions

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# CHANGELOG
22

3+
## v5.2.0
4+
5+
### API Versions
6+
7+
| API Name | API Version |
8+
|----------|-------------|
9+
| shopper-login | 1.46.0 |
10+
| shopper-baskets | 1.11.0 |
11+
| shopper-baskets | 2.5.1 |
12+
| shopper-configurations | 1.2.0 |
13+
| shopper-consents | 1.1.4 |
14+
| shopper-context | 1.1.3 |
15+
| shopper-customers | 1.6.1 |
16+
| shopper-experience | 1.2.1 |
17+
| shopper-gift-certificates | 1.2.0 |
18+
| shopper-orders | 1.12.1 |
19+
| shopper-payments | 1.4.0 |
20+
| shopper-products | 1.3.0 |
21+
| shopper-promotions | 1.2.0 |
22+
| shopper-search | 1.8.0 |
23+
| shopper-seo | 1.0.17 |
24+
| shopper-stores | 1.2.0 |
25+
26+
### Enhancements
27+
28+
- Add automatic maintenance mode detection via `throwOnMaintenanceHeader` client config option. When enabled, the SDK throws `MaintenanceError` (503) if the server responds with `sfdc_maintenance` header set to `'system'` or `'site'`. This feature is opt-in and fully backward compatible.
29+
330
## v5.1.0
431

532
### API Versions

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,40 @@ try {
138138
}
139139
```
140140

141+
#### `throwOnMaintenanceHeader`
142+
143+
When `true`, the SDK automatically detects maintenance mode by checking the `sfdc_maintenance` response header. If the header value is `'system'` or `'site'`, the SDK throws a `MaintenanceError` with status 503. This is useful for handling scheduled maintenance windows and displaying appropriate messaging to users. By default, this flag is `false` for backwards compatibility.
144+
145+
```js
146+
import {ShopperProducts, helpers} from 'commerce-sdk-isomorphic';
147+
const {MaintenanceError} = helpers;
148+
149+
const config = {
150+
throwOnMaintenanceHeader: true,
151+
// rest of the config object...
152+
};
153+
154+
const shopperProducts = new ShopperProducts(config);
155+
156+
// in an async function
157+
try {
158+
const product = await shopperProducts.getProduct({
159+
parameters: {id: 'product-id'},
160+
});
161+
} catch (e) {
162+
if (e instanceof MaintenanceError) {
163+
console.log(`Service in maintenance: ${e.maintenanceType}`); // 'system' or 'site'
164+
console.log(`Status: ${e.status}`); // 503
165+
// Display maintenance page to user
166+
} else {
167+
// Handle other errors
168+
console.error('API error:', e);
169+
}
170+
}
171+
```
172+
173+
**Note:** The maintenance check occurs before other error handling and applies even when using `rawResponse: true`.
174+
141175
#### Additional Config Settings
142176

143177
* `headers`: Headers to include with API requests.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "commerce-sdk-isomorphic",
3-
"version": "5.1.0",
3+
"version": "5.2.0",
44
"private": false,
55
"description": "Salesforce Commerce SDK Isomorphic",
66
"bugs": {

src/static/clientConfig.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('ClientConfig constructor', () => {
2626
proxy: 'https://proxy.com',
2727
transformRequest: ClientConfig.defaults.transformRequest,
2828
throwOnBadResponse: false,
29+
throwOnMaintenanceHeader: false,
2930
fetch: fetch as FetchFunction,
3031
};
3132
expect(new ClientConfig(init)).toEqual({...init});

src/static/clientConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface ClientConfigInit<Params extends BaseUriParameters> {
4141
headers: {[key: string]: string}
4242
) => Required<FetchOptions>['body'];
4343
throwOnBadResponse?: boolean;
44+
throwOnMaintenanceHeader?: boolean;
4445
}
4546

4647
/**
@@ -67,6 +68,8 @@ export default class ClientConfig<Params extends BaseUriParameters>
6768

6869
public throwOnBadResponse: boolean;
6970

71+
public throwOnMaintenanceHeader: boolean;
72+
7073
constructor(config: ClientConfigInit<Params>) {
7174
this.headers = {...config.headers};
7275
this.parameters = {...config.parameters};
@@ -89,6 +92,7 @@ export default class ClientConfig<Params extends BaseUriParameters>
8992
this.proxy = config.proxy;
9093
}
9194
this.throwOnBadResponse = !!config.throwOnBadResponse;
95+
this.throwOnMaintenanceHeader = !!config.throwOnMaintenanceHeader;
9296

9397
this.fetch = config.fetch;
9498
}

src/static/helpers/fetchHelper.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import nock from 'nock';
88
import {Response} from 'node-fetch';
99
import * as environment from './environment';
1010
import ClientConfig from '../clientConfig';
11+
import MaintenanceError from '../maintenanceError';
12+
import ResponseError from '../responseError';
1113
import {doFetch, encodeSCAPISpecialCharacters} from './fetchHelper';
1214

1315
describe('doFetch', () => {
@@ -158,6 +160,140 @@ describe('doFetch', () => {
158160
expect.objectContaining(clientConfig.fetchOptions)
159161
);
160162
});
163+
164+
describe('maintenance header check', () => {
165+
test('throws MaintenanceError when sfdc_maintenance header is "system"', async () => {
166+
nock(basePath)
167+
.post(endpointPath)
168+
.query({siteId: 'site_id'})
169+
.reply(200, responseBody, {sfdc_maintenance: 'system'});
170+
171+
const copyClientConfig = {...clientConfig, throwOnMaintenanceHeader: true};
172+
173+
try {
174+
await doFetch(url, options, copyClientConfig);
175+
fail('Expected MaintenanceError to be thrown');
176+
} catch (error) {
177+
expect(error).toBeInstanceOf(MaintenanceError);
178+
if (error instanceof MaintenanceError) {
179+
expect(error.message).toBe('Service unavailable due to system maintenance');
180+
}
181+
}
182+
});
183+
184+
test('throws MaintenanceError when sfdc_maintenance header is "site"', async () => {
185+
nock(basePath)
186+
.post(endpointPath)
187+
.query({siteId: 'site_id'})
188+
.reply(200, responseBody, {sfdc_maintenance: 'site'});
189+
190+
const copyClientConfig = {...clientConfig, throwOnMaintenanceHeader: true};
191+
192+
try {
193+
await doFetch(url, options, copyClientConfig);
194+
fail('Expected MaintenanceError to be thrown');
195+
} catch (error) {
196+
expect(error).toBeInstanceOf(MaintenanceError);
197+
if (error instanceof MaintenanceError) {
198+
expect(error.message).toBe('Service unavailable due to site maintenance');
199+
}
200+
}
201+
});
202+
203+
test('does not throw when sfdc_maintenance header has different value', async () => {
204+
nock(basePath)
205+
.post(endpointPath)
206+
.query({siteId: 'site_id'})
207+
.reply(200, responseBody, {sfdc_maintenance: 'other'});
208+
209+
const copyClientConfig = {...clientConfig, throwOnMaintenanceHeader: true};
210+
const data = await doFetch(url, options, copyClientConfig);
211+
expect(data).toEqual(responseBody);
212+
});
213+
214+
test('does not throw when throwOnMaintenanceHeader is false', async () => {
215+
nock(basePath)
216+
.post(endpointPath)
217+
.query({siteId: 'site_id'})
218+
.reply(200, responseBody, {sfdc_maintenance: 'system'});
219+
220+
const copyClientConfig = {...clientConfig, throwOnMaintenanceHeader: false};
221+
const data = await doFetch(url, options, copyClientConfig);
222+
expect(data).toEqual(responseBody);
223+
});
224+
225+
test('does not throw when throwOnMaintenanceHeader is not set', async () => {
226+
nock(basePath)
227+
.post(endpointPath)
228+
.query({siteId: 'site_id'})
229+
.reply(200, responseBody, {sfdc_maintenance: 'system'});
230+
231+
const data = await doFetch(url, options, clientConfig);
232+
expect(data).toEqual(responseBody);
233+
});
234+
235+
test('throws MaintenanceError even when rawResponse is true', async () => {
236+
nock(basePath)
237+
.post(endpointPath)
238+
.query({siteId: 'site_id'})
239+
.reply(200, responseBody, {sfdc_maintenance: 'system'});
240+
241+
const copyClientConfig = {...clientConfig, throwOnMaintenanceHeader: true};
242+
243+
try {
244+
await doFetch(url, options, copyClientConfig, true);
245+
fail('Expected MaintenanceError to be thrown');
246+
} catch (error) {
247+
expect(error).toBeInstanceOf(MaintenanceError);
248+
}
249+
});
250+
251+
test('MaintenanceError contains correct properties', async () => {
252+
nock(basePath)
253+
.post(endpointPath)
254+
.query({siteId: 'site_id'})
255+
.reply(200, responseBody, {sfdc_maintenance: 'site'});
256+
257+
const copyClientConfig = {...clientConfig, throwOnMaintenanceHeader: true};
258+
259+
try {
260+
await doFetch(url, options, copyClientConfig);
261+
fail('Expected MaintenanceError to be thrown');
262+
} catch (error) {
263+
expect(error).toBeInstanceOf(MaintenanceError);
264+
if (error instanceof MaintenanceError) {
265+
expect(error.status).toBe(503);
266+
expect(error.maintenanceType).toBe('site');
267+
expect(error.name).toBe('MaintenanceError');
268+
expect(error.response).toBeInstanceOf(Response);
269+
}
270+
}
271+
});
272+
273+
test('throws MaintenanceError before throwOnBadResponse', async () => {
274+
nock(basePath)
275+
.post(endpointPath)
276+
.query({siteId: 'site_id'})
277+
.reply(400, responseBody, {sfdc_maintenance: 'system'});
278+
279+
const copyClientConfig = {
280+
...clientConfig,
281+
throwOnMaintenanceHeader: true,
282+
throwOnBadResponse: true,
283+
};
284+
285+
try {
286+
await doFetch(url, options, copyClientConfig);
287+
fail('Expected MaintenanceError to be thrown');
288+
} catch (error) {
289+
expect(error).toBeInstanceOf(MaintenanceError);
290+
expect(error).not.toBeInstanceOf(ResponseError);
291+
if (error instanceof Error) {
292+
expect(error.message).not.toContain('400 Bad Request');
293+
}
294+
}
295+
});
296+
});
161297
});
162298

163299
describe('encodeSCAPISpecialCharacters', () => {

src/static/helpers/fetchHelper.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {BodyInit} from 'node-fetch';
88
import {BaseUriParameters} from '.';
99
import type {FetchOptions} from '../clientConfig';
1010
import ResponseError from '../responseError';
11+
import MaintenanceError from '../maintenanceError';
1112
import {fetch} from './environment';
1213
import {ClientConfigInit} from '../clientConfig';
1314

@@ -55,6 +56,15 @@ export const doFetch = async <Params extends BaseUriParameters>(
5556
const fetcher = clientConfig?.fetch || fetch;
5657

5758
const response = await fetcher(url, requestOptions);
59+
60+
// Check for maintenance header before processing response
61+
if (clientConfig?.throwOnMaintenanceHeader) {
62+
const maintenanceHeader = response.headers.get('sfdc_maintenance');
63+
if (maintenanceHeader === 'system' || maintenanceHeader === 'site') {
64+
throw new MaintenanceError(response, maintenanceHeader);
65+
}
66+
}
67+
5868
if (rawResponse) {
5969
return response;
6070
}

src/static/helpers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './slasHelper';
1111
export * from './types';
1212
export * from './customApi';
1313
export * from './fetchHelper';
14+
export {default as MaintenanceError} from '../maintenanceError';
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import {Response} from 'node-fetch';
8+
import MaintenanceError from './maintenanceError';
9+
10+
describe('MaintenanceError', () => {
11+
test('creates error with system maintenance type', () => {
12+
const response = new Response('{}', {
13+
status: 200,
14+
headers: {sfdc_maintenance: 'system'},
15+
});
16+
17+
const error = new MaintenanceError(response, 'system');
18+
19+
expect(error).toBeInstanceOf(Error);
20+
expect(error).toBeInstanceOf(MaintenanceError);
21+
expect(error.name).toBe('MaintenanceError');
22+
expect(error.message).toBe('Service unavailable due to system maintenance');
23+
expect(error.status).toBe(503);
24+
expect(error.maintenanceType).toBe('system');
25+
expect(error.response).toBe(response);
26+
});
27+
28+
test('creates error with site maintenance type', () => {
29+
const response = new Response('{}', {
30+
status: 200,
31+
headers: {sfdc_maintenance: 'site'},
32+
});
33+
34+
const error = new MaintenanceError(response, 'site');
35+
36+
expect(error).toBeInstanceOf(Error);
37+
expect(error).toBeInstanceOf(MaintenanceError);
38+
expect(error.name).toBe('MaintenanceError');
39+
expect(error.message).toBe('Service unavailable due to site maintenance');
40+
expect(error.status).toBe(503);
41+
expect(error.maintenanceType).toBe('site');
42+
expect(error.response).toBe(response);
43+
});
44+
45+
test('error can be caught and properties accessed', () => {
46+
const response = new Response('{}', {status: 200});
47+
const error = new MaintenanceError(response, 'system');
48+
49+
try {
50+
throw error;
51+
} catch (caught) {
52+
expect(caught).toBeInstanceOf(MaintenanceError);
53+
if (caught instanceof MaintenanceError) {
54+
expect(caught.status).toBe(503);
55+
expect(caught.maintenanceType).toBe('system');
56+
expect(caught.response).toBe(response);
57+
}
58+
}
59+
});
60+
61+
test('error has correct property types', () => {
62+
const response = new Response('{}', {status: 200});
63+
const error = new MaintenanceError(response, 'system');
64+
65+
expect(error.status).toBe(503);
66+
expect(typeof error.status).toBe('number');
67+
expect(error.maintenanceType).toBe('system');
68+
expect(['system', 'site']).toContain(error.maintenanceType);
69+
});
70+
});

src/static/maintenanceError.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
/**
9+
* Error thrown when the sfdc_maintenance header is detected in a response
10+
* and throwOnMaintenanceHeader is enabled in client configuration.
11+
*
12+
* @class MaintenanceError
13+
* @extends Error
14+
*/
15+
export default class MaintenanceError extends Error {
16+
public readonly response: Response | import('node-fetch').Response;
17+
18+
public readonly maintenanceType: 'system' | 'site';
19+
20+
public readonly status = 503;
21+
22+
constructor(
23+
response: Response | import('node-fetch').Response,
24+
maintenanceType: 'system' | 'site'
25+
) {
26+
super(`Service unavailable due to ${maintenanceType} maintenance`);
27+
this.name = 'MaintenanceError';
28+
this.response = response;
29+
this.maintenanceType = maintenanceType;
30+
}
31+
}

0 commit comments

Comments
 (0)