From 35a590d0477943e6d069647c6c2c75427ff8ed33 Mon Sep 17 00:00:00 2001 From: Adam Brauer <400763+ambrauer@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:03:02 -0500 Subject: [PATCH] [Next.js] Deployment Protection Bypass support for Editing (#1634) * Added support for deployment protection bypass query string parameters (or any query string parameters, really) on the editing/render endpoint. Currently, this is setup for Sitecore and Vercel protection bypass query params, but could easily be extended to handle others. * updated CHANGELOG --- CHANGELOG.md | 2 +- .../src/editing/constants.ts | 3 + .../editing/editing-data-middleware.test.ts | 4 +- .../src/editing/editing-data-middleware.ts | 2 +- .../src/editing/editing-data-service.test.ts | 58 +++++- .../src/editing/editing-data-service.ts | 34 +++- .../editing/editing-render-middleware.test.ts | 182 ++++++++++-------- .../src/editing/editing-render-middleware.ts | 55 +++++- 8 files changed, 237 insertions(+), 103 deletions(-) create mode 100644 packages/sitecore-jss-nextjs/src/editing/constants.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 425b2b90f6..f901e388db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ Our versioning strategy is as follows: ### 🎉 New Features & Improvements * `[templates/nextjs]` Enable client-only BYOC component imports. Client-only components can be imported through src/byoc-imports/index.client.ts. Hybrid (server render with client hydration) components can be imported through src/byoc-imports/index.hybrid.ts. BYOC scaffold logic is also moved from nextjs-sxa addon into base template ([#1628](https://github.com/Sitecore/jss/pull/1628)) - * `[templates/nextjs]` Import SitecoreForm component into sample nextjs app ([#1628](https://github.com/Sitecore/jss/pull/1628)) +* `[sitecore-jss-nextjs]` (Vercel/Sitecore) Deployment Protection Bypass support for editing integration. ([#1634](https://github.com/Sitecore/jss/pull/1634)) ### 🐛 Bug Fixes diff --git a/packages/sitecore-jss-nextjs/src/editing/constants.ts b/packages/sitecore-jss-nextjs/src/editing/constants.ts new file mode 100644 index 0000000000..908e4d1fa8 --- /dev/null +++ b/packages/sitecore-jss-nextjs/src/editing/constants.ts @@ -0,0 +1,3 @@ +export const QUERY_PARAM_EDITING_SECRET = 'secret'; +export const QUERY_PARAM_PROTECTION_BYPASS_SITECORE = 'x-sitecore-protection-bypass'; +export const QUERY_PARAM_PROTECTION_BYPASS_VERCEL = 'x-vercel-protection-bypass'; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts index 8ff5fdfdd6..bdb7bb2946 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.test.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { expect, use } from 'chai'; import { NextApiRequest, NextApiResponse } from 'next'; -import { QUERY_PARAM_EDITING_SECRET } from './editing-data-service'; +import { QUERY_PARAM_EDITING_SECRET } from './constants'; import { EditingData } from './editing-data'; import { EditingDataCache } from './editing-data-cache'; import { EditingDataMiddleware } from './editing-data-middleware'; @@ -42,7 +42,7 @@ const mockCache = (data?: EditingData) => { const cache = {} as EditingDataCache; cache.set = spy(); cache.get = spy(() => { - return data; + return Promise.resolve(data); }); return cache; }; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts index 7f53003880..0af412c141 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-middleware.ts @@ -1,7 +1,7 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { EditingDataCache, editingDataDiskCache } from './editing-data-cache'; import { EditingData, isEditingData } from './editing-data'; -import { QUERY_PARAM_EDITING_SECRET } from './editing-data-service'; +import { QUERY_PARAM_EDITING_SECRET } from './constants'; import { getJssEditingSecret } from '../utils/utils'; export interface EditingDataMiddlewareConfig { diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-service.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-service.test.ts index baa6b63892..7342a4a2b6 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-service.test.ts @@ -8,9 +8,9 @@ import { EditingDataCache } from './editing-data-cache'; import { ServerlessEditingDataService, BasicEditingDataService, - QUERY_PARAM_EDITING_SECRET, generateKey, } from './editing-data-service'; +import { QUERY_PARAM_EDITING_SECRET } from './constants'; import sinonChai from 'sinon-chai'; import { spy } from 'sinon'; @@ -93,6 +93,29 @@ describe('ServerlessEditingDataService', () => { }); }); + it('should invoke PUT request with extra params and return in preview data', async () => { + const data = { + path: '/styleguide', + } as EditingData; + const key = '1234key'; + const serverUrl = 'https://test.com'; + const paramFoo = 'foo'; + const paramBar = 'bar'; + const params = { paramFoo, paramBar }; + const expectedUrl = `${serverUrl}/api/editing/data/${key}?${QUERY_PARAM_EDITING_SECRET}=${secret}¶mFoo=${paramFoo}¶mBar=${paramBar}`; + + const fetcher = mockFetcher(); + + const service = new ServerlessEditingDataService({ dataFetcher: fetcher }); + service['generateKey'] = () => key; + + return service.setEditingData(data, serverUrl, params).then((previewData) => { + expect(previewData.params).to.equal(params); + expect(fetcher.put).to.have.been.calledOnce; + expect(fetcher.put).to.have.been.calledWithExactly(expectedUrl, data); + }); + }); + it('should use custom apiRoute', async () => { const data = { layoutData: { sitecore: { route: { itemId: 'd6ac9d26-9474-51cf-982d-4f8d44951229' } } }, @@ -115,24 +138,23 @@ describe('ServerlessEditingDataService', () => { }); }); - it('should URI encode secret', async () => { + it('should URI encode secret and params', async () => { const superSecret = ';,/?:@&=+$'; + const encodedSecret = encodeURIComponent(superSecret); process.env.JSS_EDITING_SECRET = superSecret; const data = { layoutData: { sitecore: { route: { itemId: 'd6ac9d26-9474-51cf-982d-4f8d44951229' } } }, } as EditingData; const key = '1234key'; const serverUrl = 'https://test.com'; - const expectedUrl = `${serverUrl}/api/editing/data/${key}?${QUERY_PARAM_EDITING_SECRET}=${encodeURIComponent( - superSecret - )}`; + const expectedUrl = `${serverUrl}/api/editing/data/${key}?${QUERY_PARAM_EDITING_SECRET}=${encodedSecret}¶m=${encodedSecret}`; const fetcher = mockFetcher(); const service = new ServerlessEditingDataService({ dataFetcher: fetcher }); service['generateKey'] = () => key; - return service.setEditingData(data, serverUrl).then(() => { + return service.setEditingData(data, serverUrl, { param: superSecret }).then(() => { expect(fetcher.put).to.have.been.calledOnce; expect(fetcher.put).to.have.been.calledWithExactly(expectedUrl, data); }); @@ -159,6 +181,28 @@ describe('ServerlessEditingDataService', () => { expect(fetcher.get).to.have.been.calledWith(expectedUrl); }); + it('should invoke GET request with extra params', async () => { + const data = { + path: '/styleguide', + } as EditingData; + const key = '1234key'; + const serverUrl = 'https://test.com'; + const paramFoo = 'foo'; + const paramBar = 'bar'; + const params = { paramFoo, paramBar }; + const expectedUrl = `${serverUrl}/api/editing/data/${key}?${QUERY_PARAM_EDITING_SECRET}=${secret}¶mFoo=${paramFoo}¶mBar=${paramBar}`; + + const fetcher = mockFetcher(data); + + const service = new ServerlessEditingDataService({ dataFetcher: fetcher }); + service['generateKey'] = () => key; + + const editingData = await service.getEditingData({ key, serverUrl, params }); + expect(editingData).to.equal(data); + expect(fetcher.get).to.have.been.calledOnce; + expect(fetcher.get).to.have.been.calledWith(expectedUrl); + }); + it('should return undefined if serverUrl missing', async () => { const data = { path: '/styleguide', @@ -181,7 +225,7 @@ describe('BasicEditingDataService', () => { const cache = {} as EditingDataCache; cache.set = spy(); cache.get = spy(() => { - return data; + return Promise.resolve(data); }); return cache; }; diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-data-service.ts b/packages/sitecore-jss-nextjs/src/editing/editing-data-service.ts index d6a51f9b0e..b03a577342 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-data-service.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-data-service.ts @@ -1,17 +1,17 @@ +import { QUERY_PARAM_EDITING_SECRET } from './constants'; import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss'; import { EditingData } from './editing-data'; import { EditingDataCache, editingDataDiskCache } from './editing-data-cache'; import { getJssEditingSecret } from '../utils/utils'; import { PreviewData } from 'next'; -export const QUERY_PARAM_EDITING_SECRET = 'secret'; - /** * Data for Next.js Preview (Editing) mode */ export interface EditingPreviewData { key: string; serverUrl?: string; + params?: { [key: string]: string }; } /** @@ -24,7 +24,11 @@ export interface EditingDataService { * @param {string} serverUrl The server url e.g. which can be used for further API requests * @returns The {@link EditingPreviewData} containing the information to use for retrieval */ - setEditingData(data: EditingData, serverUrl: string): Promise<EditingPreviewData>; + setEditingData( + data: EditingData, + serverUrl: string, + params?: { [key: string]: string } + ): Promise<EditingPreviewData>; /** * Retrieves Sitecore editor payload data * @param {PreviewData} previewData Editing preview data containing the information to use for retrieval @@ -149,13 +153,18 @@ export class ServerlessEditingDataService implements EditingDataService { * @param {string} serverUrl The server url to use for subsequent data API requests * @returns {Promise} The {@link EditingPreviewData} containing the generated key and serverUrl to use for retrieval */ - async setEditingData(data: EditingData, serverUrl: string): Promise<EditingPreviewData> { + async setEditingData( + data: EditingData, + serverUrl: string, + params?: { [key: string]: string } + ): Promise<EditingPreviewData> { const key = this.generateKey(data); - const url = this.getUrl(serverUrl, key); + const url = this.getUrl(serverUrl, key, params); const previewData = { key, serverUrl, + params, } as EditingPreviewData; debug.editing('storing editing data for %o: %o', previewData, data); @@ -174,7 +183,11 @@ export class ServerlessEditingDataService implements EditingDataService { if (!editingPreviewData?.serverUrl) { return undefined; } - const url = this.getUrl(editingPreviewData.serverUrl, editingPreviewData.key); + const url = this.getUrl( + editingPreviewData.serverUrl, + editingPreviewData.key, + editingPreviewData.params + ); debug.editing('retrieving editing data for %o', previewData); return this.dataFetcher.get<EditingData>(url).then((response: { data: EditingData }) => { @@ -182,12 +195,19 @@ export class ServerlessEditingDataService implements EditingDataService { }); } - protected getUrl(serverUrl: string, key: string): string { + protected getUrl(serverUrl: string, key: string, params?: { [key: string]: string }): string { // Example URL format: // http://localhost:3000/api/editing/data/52961eea-bafd-5287-a532-a72e36bd8a36-qkb4e3fv5x?secret=1234secret const apiRoute = this.apiRoute?.replace('[key]', key); const url = new URL(apiRoute, serverUrl); url.searchParams.append(QUERY_PARAM_EDITING_SECRET, getJssEditingSecret()); + if (params) { + for (const key in params) { + if ({}.hasOwnProperty.call(params, key)) { + url.searchParams.append(key, params[key]); + } + } + } return url.toString(); } } diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts index 846ff8ef64..7bade6fc70 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.test.ts @@ -3,11 +3,12 @@ import { expect, use } from 'chai'; import { NextApiRequest, NextApiResponse } from 'next'; import { AxiosDataFetcher } from '@sitecore-jss/sitecore-jss'; +import { EditingDataService, EditingPreviewData } from './editing-data-service'; import { - EditingDataService, - EditingPreviewData, QUERY_PARAM_EDITING_SECRET, -} from './editing-data-service'; + QUERY_PARAM_PROTECTION_BYPASS_SITECORE, + QUERY_PARAM_PROTECTION_BYPASS_VERCEL, +} from './constants'; import { EE_PATH, EE_LANGUAGE, @@ -63,8 +64,7 @@ const mockResponse = () => { const mockFetcher = (html?: string) => { const fetcher = {} as AxiosDataFetcher; - fetcher.get = spy<any>((url, config) => { - console.log(url, config); + fetcher.get = spy<any>(() => { return Promise.resolve({ data: html ?? '' }); }); return fetcher; @@ -204,9 +204,8 @@ describe('EditingRenderMiddleware', () => { expect(res.status).to.have.been.calledOnce; expect(res.status).to.have.been.calledWith(500); expect(res.json).to.have.been.calledOnce; - expect(res.json).to.have.been.calledWith({ - html: - '<html><body>Error: Failed to render component for http://localhost:3000/test/path</body></html>', + expect(res.json).to.have.been.calledWithMatch({ + html: '<html><body>Error: Failed to render component for /test/path</body></html>', }); }); @@ -415,83 +414,116 @@ describe('EditingRenderMiddleware', () => { await handler(req, res); expect(fetcher.get).to.have.been.calledWithMatch('https://vercel.com'); + }); - it('should use custom resolveServerUrl', async () => { - const html = '<html><body>Something amazing</body></html>'; - const fetcher = mockFetcher(html); - const dataService = mockDataService(); - const query = {} as Query; - query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(EE_BODY, query); - const res = mockResponse(); - - const serverUrl = 'https://test.com'; - - const middleware = new EditingRenderMiddleware({ - dataFetcher: fetcher, - editingDataService: dataService, - resolveServerUrl: () => { - return serverUrl; - }, - }); - const handler = middleware.getHandler(); + it('should use custom resolveServerUrl', async () => { + const html = '<html><body>Something amazing</body></html>'; + const fetcher = mockFetcher(html); + const dataService = mockDataService(); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const req = mockRequest(EE_BODY, query); + const res = mockResponse(); - await handler(req, res); + const serverUrl = 'https://test.com'; - expect(fetcher.get).to.have.been.calledWithMatch(serverUrl); + const middleware = new EditingRenderMiddleware({ + dataFetcher: fetcher, + editingDataService: dataService, + resolveServerUrl: () => { + return serverUrl; + }, }); + const handler = middleware.getHandler(); - it('should use custom resolvePageUrl', async () => { - const html = '<html><body>Something amazing</body></html>'; - const fetcher = mockFetcher(html); - const dataService = mockDataService(); - const query = {} as Query; - query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(EE_BODY, query); - const res = mockResponse(); - - const serverUrl = 'https://test.com'; - const resolvePageUrl = spy((serverUrl: string, itemPath: string) => { - return `${serverUrl}/some/path${itemPath}`; - }); - - const middleware = new EditingRenderMiddleware({ - dataFetcher: fetcher, - editingDataService: dataService, - resolvePageUrl: resolvePageUrl, - resolveServerUrl: () => { - return serverUrl; - }, - }); - const handler = middleware.getHandler(); + await handler(req, res); + + expect(fetcher.get).to.have.been.calledWithMatch(serverUrl); + }); + + it('should use custom resolvePageUrl', async () => { + const html = '<html><body>Something amazing</body></html>'; + const fetcher = mockFetcher(html); + const dataService = mockDataService(); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const req = mockRequest(EE_BODY, query); + const res = mockResponse(); + + const serverUrl = 'https://test.com'; + const expectedPageUrl = `${serverUrl}/some/path${EE_PATH}`; + const resolvePageUrl = spy((serverUrl: string, itemPath: string) => { + return `${serverUrl}/some/path${itemPath}`; + }); + + const middleware = new EditingRenderMiddleware({ + dataFetcher: fetcher, + editingDataService: dataService, + resolvePageUrl: resolvePageUrl, + resolveServerUrl: () => { + return serverUrl; + }, + }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(resolvePageUrl).to.have.been.calledOnce; + expect(resolvePageUrl).to.have.been.calledWith(serverUrl, EE_PATH); + expect(fetcher.get).to.have.been.calledOnce; + expect(fetcher.get).to.have.been.calledWithMatch(expectedPageUrl); + }); - await handler(req, res); + it('should respondWith 500 if rendered html empty', async () => { + const fetcher = mockFetcher(''); + const dataService = mockDataService(); + const query = {} as Query; + query[QUERY_PARAM_EDITING_SECRET] = secret; + const req = mockRequest(EE_BODY, query); + const res = mockResponse(); - expect(resolvePageUrl).to.have.been.calledOnce; - expect(resolvePageUrl).to.have.been.calledWith(EE_PATH); - expect(fetcher.get).to.have.been.calledWithMatch(serverUrl); + const middleware = new EditingRenderMiddleware({ + dataFetcher: fetcher, + editingDataService: dataService, }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(res.status).to.have.been.calledOnce; + expect(res.status).to.have.been.calledWith(500); + expect(res.json).to.have.been.calledOnce; + }); + + it('should pass along protection bypass query parameters', async () => { + const html = '<html phkey="test1"><body phkey="test2">Something amazing</body></html>'; + const query = {} as Query; + const bypassTokenSitecore = 'token1234Sitecore'; + const bypassTokenVercel = 'token1234Vercel'; + query[QUERY_PARAM_EDITING_SECRET] = secret; + query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = bypassTokenSitecore; + query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = bypassTokenVercel; + const previewData = { key: 'key1234' } as EditingPreviewData; + + const fetcher = mockFetcher(html); + const dataService = mockDataService(previewData); + const req = mockRequest(EE_BODY, query); + const res = mockResponse(); - it('should respondWith 500 if rendered html empty', async () => { - const fetcher = mockFetcher(''); - const dataService = mockDataService(); - const query = {} as Query; - query[QUERY_PARAM_EDITING_SECRET] = secret; - const req = mockRequest(EE_BODY, query); - const res = mockResponse(); - - const middleware = new EditingRenderMiddleware({ - dataFetcher: fetcher, - editingDataService: dataService, - }); - const handler = middleware.getHandler(); - - await handler(req, res); - - expect(res.status).to.have.been.calledOnce; - expect(res.status).to.have.been.calledWith(500); - expect(res.json).to.have.been.calledOnce; + const middleware = new EditingRenderMiddleware({ + dataFetcher: fetcher, + editingDataService: dataService, }); + const handler = middleware.getHandler(); + + await handler(req, res); + + expect(dataService.setEditingData, 'stash editing data').to.have.been.called; + expect(res.setPreviewData, 'set preview mode w/ data').to.have.been.calledWith(previewData); + expect(fetcher.get).to.have.been.calledOnce; + expect(fetcher.get, 'pass along protection bypass query params').to.have.been.calledWithMatch( + `http://localhost:3000/test/path?${QUERY_PARAM_PROTECTION_BYPASS_SITECORE}=${bypassTokenSitecore}&${QUERY_PARAM_PROTECTION_BYPASS_VERCEL}=${bypassTokenVercel}×tamp` + ); }); describe('extractEditingData', () => { diff --git a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts index e3bc72f213..022b1d0fc3 100644 --- a/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/editing/editing-render-middleware.ts @@ -4,11 +4,12 @@ import { AxiosDataFetcher, debug } from '@sitecore-jss/sitecore-jss'; import { EDITING_COMPONENT_ID, RenderingType } from '@sitecore-jss/sitecore-jss/layout'; import { parse } from 'node-html-parser'; import { EditingData } from './editing-data'; +import { EditingDataService, editingDataService } from './editing-data-service'; import { - EditingDataService, - editingDataService, QUERY_PARAM_EDITING_SECRET, -} from './editing-data-service'; + QUERY_PARAM_PROTECTION_BYPASS_SITECORE, + QUERY_PARAM_PROTECTION_BYPASS_VERCEL, +} from './constants'; import { getJssEditingSecret } from '../utils/utils'; export interface EditingRenderMiddlewareConfig { @@ -75,6 +76,28 @@ export class EditingRenderMiddleware { return this.handler; } + /** + * Gets query parameters that should be passed along to subsequent requests + * @param query Object of query parameters from incoming URL + * @returns Object of approved query parameters + */ + protected getQueryParamsForPropagation = ( + query: Partial<{ [key: string]: string | string[] }> + ): { [key: string]: string } => { + const params: { [key: string]: string } = {}; + if (query[QUERY_PARAM_PROTECTION_BYPASS_SITECORE]) { + params[QUERY_PARAM_PROTECTION_BYPASS_SITECORE] = query[ + QUERY_PARAM_PROTECTION_BYPASS_SITECORE + ] as string; + } + if (query[QUERY_PARAM_PROTECTION_BYPASS_VERCEL]) { + params[QUERY_PARAM_PROTECTION_BYPASS_VERCEL] = query[ + QUERY_PARAM_PROTECTION_BYPASS_VERCEL + ] as string; + } + return params; + }; + private handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => { const { method, query, body, headers } = req; @@ -115,10 +138,17 @@ export class EditingRenderMiddleware { // Resolve server URL const serverUrl = this.resolveServerUrl(req); + // Get query string parameters to propagate on subsequent requests (e.g. for deployment protection bypass) + const params = this.getQueryParamsForPropagation(query); + // Stash for use later on (i.e. within getStatic/ServerSideProps). // Note we can't set this directly in setPreviewData since it's stored as a cookie (2KB limit) // https://nextjs.org/docs/advanced-features/preview-mode#previewdata-size-limits) - const previewData = await this.editingDataService.setEditingData(editingData, serverUrl); + const previewData = await this.editingDataService.setEditingData( + editingData, + serverUrl, + params + ); // Enable Next.js Preview Mode, passing our preview data (i.e. editingData cache key) res.setPreviewData(previewData); @@ -126,13 +156,18 @@ export class EditingRenderMiddleware { // Grab the Next.js preview cookies to send on to the render request const cookies = res.getHeader('Set-Cookie') as string[]; - // Make actual render request for page route, passing on preview cookies. + // Make actual render request for page route, passing on preview cookies as well as any approved query string parameters. // Note timestamp effectively disables caching the request in Axios (no amount of cache headers seemed to do it) - const requestUrl = this.resolvePageUrl(serverUrl, editingData.path); debug.editing('fetching page route for %s', editingData.path); - const queryStringCharacter = requestUrl.indexOf('?') === -1 ? '?' : '&'; + const requestUrl = new URL(this.resolvePageUrl(serverUrl, editingData.path)); + for (const key in params) { + if ({}.hasOwnProperty.call(params, key)) { + requestUrl.searchParams.append(key, params[key]); + } + } + requestUrl.searchParams.append('timestamp', Date.now().toString()); const pageRes = await this.dataFetcher - .get<string>(`${requestUrl}${queryStringCharacter}timestamp=${Date.now()}`, { + .get<string>(requestUrl.toString(), { headers: { Cookie: cookies.join(';'), }, @@ -149,7 +184,7 @@ export class EditingRenderMiddleware { let html = pageRes.data; if (!html || html.length === 0) { - throw new Error(`Failed to render html for ${requestUrl}`); + throw new Error(`Failed to render html for ${editingData.path}`); } // replace phkey attribute with key attribute so that newly added renderings @@ -168,7 +203,7 @@ export class EditingRenderMiddleware { // Handle component rendering. Extract component markup only html = parse(html).getElementById(EDITING_COMPONENT_ID)?.innerHTML; - if (!html) throw new Error(`Failed to render component for ${requestUrl}`); + if (!html) throw new Error(`Failed to render component for ${editingData.path}`); } const body = { html };