Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom 404 response via headers #35

Merged
merged 9 commits into from
Dec 20, 2021
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