Skip to content

Commit

Permalink
Merge pull request #35 from namecheap/custom404Response
Browse files Browse the repository at this point in the history
  • Loading branch information
Volodymyr Makukha authored Dec 20, 2021
2 parents 8ac20af + 8fe7355 commit 5515a48
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 102 deletions.
25 changes: 8 additions & 17 deletions src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,13 @@ import * as types from './types';
import { IlcIntl } from './IlcIntl';
import defaultIntlAdapter from './defaultIntlAdapter';
import { IIlcAppSdk } from './interfaces/IIlcAppSdk';
import { Render404 } from './interfaces/common';

export * from './types';
export * from './GlobalBrowserApi';
export * from './IlcIntl';
export * from './utils/isSpecialUrl';

/* Using of `Intl` is removed from ILC, probably in the next major version we can remove it here */
/**
* @internal
* @deprecated use `IlcIntl` export instead
*/
export const Intl = IlcIntl;

/**
* @name IlcAppSdk
*/
Expand All @@ -74,19 +68,16 @@ export default class IlcAppSdk implements IIlcAppSdk {
}
/**
* Isomorphic method to render 404 page.
* GLOBAL 404:
* At SSR in processResponse it sets 404 status code to response.
* At CSR it triggers global event which ILC listens and renders 404 page.
*
* CUSTOM 404:
* At SSR in processResponse it sets 404 status code and "X-ILC-Override" header to response.
* At CSR it renders own not found route.
*/
render404 = () => {
if (this.adapter.setStatusCode) {
this.adapter.setStatusCode(404);
} else {
window.dispatchEvent(
new CustomEvent('ilc:404', {
detail: { appId: this.appId },
}),
);
}
render404: Render404 = (withCustomContent) => {
this.adapter.trigger404Page(withCustomContent);
};

unmount() {
Expand Down
6 changes: 0 additions & 6 deletions src/app/interfaces/AppLifecycleFnProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ export interface AppLifecycleFnProps<RegProps = unknown> extends SingleSpaLifecy
* While for `reqUrl = /a/b/c?d=1` & matched route `/a/b/c` base path will be `/a/b/c`.
*/
getCurrentBasePath: () => string;
/**
* Unique application ID, if same app will be rendered twice on a page - it will get different IDs
*
* @deprecated use `appSdk.appId` instead
*/
appId: string;
/**
* App **MUST** use it to propagate all unhandled errors. Usually it's used in app's adapter.
*/
Expand Down
5 changes: 2 additions & 3 deletions src/app/interfaces/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@ export interface AppSdkAdapter {
/** Unique application ID, if same app will be rendered twice on a page - it will get different IDs */
appId: string;
intl: IntlAdapter | null;
setStatusCode: (code: number) => void;
getStatusCode: () => number | undefined;
trigger404Page: (withCustomContent?: boolean) => void;
}

export type Render404 = () => void;
export type Render404 = (withCustomContent?: boolean) => void;

export interface IntlConfig {
locale?: string;
Expand Down
43 changes: 33 additions & 10 deletions src/server/IlcSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { intlSchema } from './IlcProtocol';
import defaultIntlAdapter from '../app/defaultIntlAdapter';
import * as clientTypes from '../app/interfaces/common';
import { IlcSdkLogger } from './IlcSdkLogger';
import AppSdk from '../app';
import * as internalTypes from './internalTypes';

/**
* Entrypoint for SDK that should be used within application server that executes SSR bundle
Expand All @@ -27,7 +29,9 @@ export class IlcSdk {
/**
* Processes incoming request and returns object that can be used to fetch information passed by ILC to the application.
*/
public processRequest<RegistryProps = unknown>(req: IncomingMessage): types.RequestData<RegistryProps> {
public processRequest<RegistryProps = unknown>(
req: IncomingMessage,
): internalTypes.ProcessedRequest<RegistryProps> {
const url = this.parseUrl(req);
const routerProps = this.parseRouterProps(url);
const requestedUrls = this.getRequestUrls(url, routerProps);
Expand Down Expand Up @@ -55,20 +59,31 @@ export class IlcSdk {
originalUri = '/';
}

let statusCode: number | undefined;
const tmpResponseData: internalTypes.SsrContext = {};

return {
const requestData = {
getCurrentReqHost: () => host,
getCurrentReqUrl: () => requestedUrls.requestUrl,
getCurrentBasePath: () => requestedUrls.basePageUrl,
getCurrentReqOriginalUri: () => originalUri,
getCurrentPathProps: () => passedProps,
appId,
intl: this.parseIntl(req),
setStatusCode: (code) => {
statusCode = code;
trigger404Page: (withCustomContent?: boolean) => {
tmpResponseData.code = 404;

if (withCustomContent) {
tmpResponseData.headers = {
['X-ILC-Override']: 'error-page-content',
};
}
},
getStatusCode: () => statusCode,
};

return {
requestData,
appSdk: new AppSdk(requestData),
processResponse: this.processResponse.bind(this, tmpResponseData),
};
}

Expand All @@ -77,10 +92,18 @@ export class IlcSdk {
*
* **WARNING:** this method should be called before response headers were send.
*/
public processResponse(reqData: types.RequestData, res: ServerResponse, data?: types.ResponseData): void {
const statusCode = reqData.getStatusCode();
if (statusCode) {
res.statusCode = statusCode;
private processResponse(
tmpResponseData: internalTypes.SsrContext,
res: ServerResponse,
data?: types.ResponseData,
): void {
if (tmpResponseData.code) {
res.statusCode = tmpResponseData.code;
}
if (tmpResponseData.headers) {
for (const [key, value] of Object.entries(tmpResponseData.headers)) {
res.setHeader(key, value);
}
}

if (!data) {
Expand Down
9 changes: 9 additions & 0 deletions src/server/interfaces/ProcessedRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as types from '../types';
import { ServerResponse } from 'http';
import AppSdk from '../../app';

export interface ProcessedRequest<RegistryProps = unknown> {
requestData: types.RequestData<RegistryProps>;
appSdk: AppSdk;
processResponse: (res: ServerResponse, data?: types.ResponseData) => void;
}
4 changes: 4 additions & 0 deletions src/server/interfaces/SsrContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type SsrContext = {
code?: number;
headers?: Record<string, string>;
};
2 changes: 2 additions & 0 deletions src/server/internalTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './interfaces/ProcessedRequest';
export * from './interfaces/SsrContext';
35 changes: 35 additions & 0 deletions test/app/IlcAppSdk.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import IlcAppSdk from '../../src/app/index';
import { expect } from 'chai';

describe('IlcAppSdk', () => {
it('should throw error due to not provided adapter', () => {
// @ts-ignore
expect(() => new IlcAppSdk()).to.throw('Unable to determine adapter properly...');
});

it('should render404 run trigger404Page method from adapter', () => {
let page404Rendered = false;

const appSdk = new IlcAppSdk({
appId: 'someAppId',
intl: null,
trigger404Page: () => {
page404Rendered = true;
},
});

expect(page404Rendered).to.be.false;
appSdk.render404();
expect(page404Rendered).to.be.true;
});

it('should "unmount" throw error due to not defined set method in adapter', () => {
const appSdk = new IlcAppSdk({
appId: 'someAppId',
intl: null,
trigger404Page: () => {},
});

expect(() => appSdk.unmount()).to.throw("Looks like you're trying to call CSR only method during SSR");
});
});
12 changes: 6 additions & 6 deletions test/server/IlcAppWrapperSdk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ describe('IlcAppWrapperSdk', () => {
const req = new MockReq(merge({}, defReq));
const res = new MockRes();

const pRes = ilcSdk.processRequest(req);
ilcSdk.forwardRequest(pRes, res);
const { requestData } = ilcSdk.processRequest(req);
ilcSdk.forwardRequest(requestData, res);

expect(res.statusCode).to.eql(210);
if (res.writableEnded !== undefined) {
Expand All @@ -43,8 +43,8 @@ describe('IlcAppWrapperSdk', () => {

const testProps = { test: 1 };

const pRes = ilcSdk.processRequest(req);
ilcSdk.forwardRequest(pRes, res, { propsOverride: testProps });
const { requestData } = ilcSdk.processRequest(req);
ilcSdk.forwardRequest(requestData, res, { propsOverride: testProps });
expect(res.getHeader('x-props-override')).to.eq(
Buffer.from(JSON.stringify(testProps), 'utf8').toString('base64'),
);
Expand All @@ -55,8 +55,8 @@ describe('IlcAppWrapperSdk', () => {
const res = new MockRes();
res.end();

const pRes = ilcSdk.processRequest(req);
expect(() => ilcSdk.forwardRequest(pRes, res)).to.throw();
const { requestData } = ilcSdk.processRequest(req);
expect(() => ilcSdk.forwardRequest(requestData, res)).to.throw();
});
});
});
Loading

0 comments on commit 5515a48

Please sign in to comment.