Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/jss-10377-scClient-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sitecore-content-sdk/content': patch
---

Avoid throwing errors in scClient calls when context ID in browser is missing.
* Replaces the GraphQL client used by scClient with no-op fallback in browser context, when public content ID is missing, preventing `getPage()`, `getDictionary()` and other GraphQL-bound method from throwing.
* Adds `edgeInitialized` to scClient to indicate the status of Sitecore Edge connectivity. `false` in browser when public Edge context ID is missing.
* Adds a guard into `getHeadLinks()` call to return empty results when both server and client context IDs are missing, instead of throwing an error.
2 changes: 2 additions & 0 deletions packages/content/api/content-sdk-content.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,8 @@ export class SitecoreClient implements BaseSitecoreClient {
// (undocumented)
protected dictionaryService: DictionaryService;
// (undocumented)
edgeInitialized: boolean;
// (undocumented)
protected editingService: EditingService;
// (undocumented)
protected errorPagesService: ErrorPagesService;
Expand Down
61 changes: 61 additions & 0 deletions packages/content/src/client/sitecore-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@sitecore-content-sdk/core/tools';

import { ErrorPage, SitecoreClient } from './sitecore-client';
import { NoOpGraphQLClient } from './utils';
import { LayoutKind, DesignLibraryMode } from '../../src/editing';
import { LayoutServiceData } from '../../layout';
import { LayoutServicePageState } from '../layout';
Expand Down Expand Up @@ -112,6 +113,46 @@ describe('SitecoreClient', () => {
(sitecoreClient as any).sitemapXmlService = sitemapXmlServiceStub;
});

describe('constructor', () => {
const originalWindow = global.window;

afterEach(() => {
global.window = originalWindow;
});

it('should set edgeInitialized when instantiated with a context id', () => {
const client = new SitecoreClient(defaultInitOptions);

expect(client.edgeInitialized).to.be.true;
});

it('should set edgeInitialized to false when both server and client context ids are missing', () => {
const client = new SitecoreClient({
...defaultInitOptions,
api: {
...defaultInitOptions.api,
edge: { edgeUrl: defaultInitOptions.api.edge.edgeUrl },
},
});

expect(client.edgeInitialized).to.be.false;
});

it('should use a NoOpGraphQLClient in the browser when the browser context id is not provided', () => {
const warnStub = sandbox.stub(console, 'warn');
// Simulate a browser bundle with no Edge/local configuration
(global as any).window = {};

const client = new SitecoreClient({
...defaultInitOptions,
api: { edge: { edgeUrl: defaultInitOptions.api.edge.edgeUrl } },
});

expect((client as any).graphQLClient).to.be.instanceOf(NoOpGraphQLClient);
expect(warnStub.called).to.be.true;
});
});

describe('getData', () => {
let requestStub: sinon.SinonStub;

Expand Down Expand Up @@ -1554,6 +1595,26 @@ describe('SitecoreClient', () => {

expect(result).to.deep.equal([]);
});

it('should log and return an empty array when both server and client context ids are missing', () => {
const warnStub = sandbox.stub(console, 'warn');
const clientWithoutContext = new SitecoreClient({
...defaultInitOptions,
api: {
...defaultInitOptions.api,
edge: { edgeUrl: defaultInitOptions.api.edge.edgeUrl },
},
});

const result = clientWithoutContext.getHeadLinks(layoutData);

expect(result).to.deep.equal([]);
expect(
warnStub.calledWith(
'Both client and server context IDs are absent. Cannot retrieve head links.'
)
).to.be.true;
});
});

describe('getSiteMap', () => {
Expand Down
12 changes: 11 additions & 1 deletion packages/content/src/client/sitecore-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ export interface BaseServiceOptions {
* @public
*/
export class SitecoreClient implements BaseSitecoreClient {
public edgeInitialized = true;
Comment thread
art-alexeyenko marked this conversation as resolved.
protected layoutService: LayoutService;
protected dictionaryService: DictionaryService;
protected editingService: EditingService;
Expand All @@ -315,6 +316,10 @@ export class SitecoreClient implements BaseSitecoreClient {
this.graphQLClient = this.clientFactory({
debugger: debug.http,
});
const { contextId: serverContextId, clientContextId } = this.initOptions.api.edge;
if (!serverContextId && !clientContextId) {
this.edgeInitialized = false;
}

const baseServiceOptions = this.getBaseServiceOptions();

Expand Down Expand Up @@ -420,18 +425,23 @@ export class SitecoreClient implements BaseSitecoreClient {
): HTMLLink[] {
const { enableStyles = true, enableThemes = true } = options;
const { contextId: serverContextId, clientContextId } = this.initOptions.api.edge;
if (!this.edgeInitialized) {
console.warn('Both client and server context IDs are absent. Cannot retrieve head links.');
return [];
}

const headLinks: HTMLLink[] = [];

// Stylesheets are requested by the browser through <link> elements, so the context id cannot be
// moved into the x-sitecore-contextid header. Prefer the client context id to keep the
// server-side one out of publicly rendered markup.
const contextId = clientContextId || serverContextId;

if (!clientContextId) {
debug.common(
'clientContextId is not configured, falling back to the server context id for stylesheet links'
);
}

const edgeUrlForStyles = resolveEdgeUrlForStaticFiles(this.initOptions.api.edge.edgeUrl);

if (enableStyles) {
Expand Down
19 changes: 18 additions & 1 deletion packages/content/src/client/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { GraphQLRequestClient } from '@sitecore-content-sdk/core';
import { createGraphQLClientFactory } from './utils';
import { createGraphQLClientFactory, NoOpGraphQLClient } from './utils';

describe('createGraphQLClientFactory', () => {
const originalWindow = global.window;
Expand Down Expand Up @@ -92,6 +92,23 @@ describe('createGraphQLClientFactory', () => {
).to.not.throw();
});

it('returns a NoOpGraphQLClient factory in the browser when no IDs are configured', () => {
const warnStub = sinon.stub(console, 'warn');
const createClientFactorySpy = sinon.spy(GraphQLRequestClient, 'createClientFactory');

// Browser bundle with no Edge/local configuration
(global as any).window = {};

const factory = createGraphQLClientFactory({ api: {} });
const client = factory();

expect(client).to.be.instanceOf(NoOpGraphQLClient);
// Should not fall through to the real client factory
expect(createClientFactorySpy.called).to.be.false;
// Should warn that client-side requests will fail
expect(warnStub.calledOnce).to.be.true;
});

it('throws error on server when no valid configuration is provided', () => {
// Server (no window)
delete (global as any).window;
Expand Down
20 changes: 18 additions & 2 deletions packages/content/src/client/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {
FetchOptions,
GraphQLClient,
GraphQLRequestClient,
GraphQLRequestClientFactory,
GraphQLRequestClientFactoryConfig,
debug,
} from '@sitecore-content-sdk/core';
import { SitecoreConfigInput } from '../config';
import { getEdgeProxyContentUrl } from './edge-proxy';
Expand All @@ -12,6 +15,19 @@ import { getEdgeProxyContentUrl } from './edge-proxy';
*/
export type GraphQLClientOptions = Pick<SitecoreConfigInput, 'api'> & FetchOptions;

/**
* No op client to be used in browser context, when client API settings and env variables are missing
* @internal
*/
export class NoOpGraphQLClient implements GraphQLClient {
Comment thread
art-alexeyenko marked this conversation as resolved.
request<T>(): Promise<T> {
debug.common(
'GraphQL client was initialized without a valid context ID in browser context. Using fallback client that returns empty results.'
);
return Promise.resolve({} as T);
}
}

/**
* Creates a new GraphQLRequestClientFactory instance
* @param {GraphQLClientOptions} options content sdk config
Expand Down Expand Up @@ -47,9 +63,9 @@ export const createGraphQLClientFactory = (options: GraphQLClientOptions) => {
// Browser bundle has no IDs – initialise a dummy client and warn
/* eslint-disable no-console */
console.warn(
'GraphQL client initialised in the browser without Edge or local API configuration; client-side requests may fail.'
'GraphQL client initialized in the browser without Edge or local API configuration; client-side requests will return empty results.'
);
clientConfig = { endpoint: '/api/graphql' };
return (() => new NoOpGraphQLClient()) as unknown as GraphQLRequestClientFactory;
} else {
throw new Error(
`GraphQL client misconfigured.
Expand Down
Loading