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: CXSPA-6578 Introduce PROPAGATE_SERVER_ERROR_RESPONSE and default ExpressJS handlers #18753

Merged
merged 13 commits into from
Apr 29, 2024

Conversation

pawelfras
Copy link
Contributor

@pawelfras pawelfras commented Apr 22, 2024

This PR introduces the PROPAGATE_SERVER_ERROR_RESPONSE injection token which helps to propagate errors that occur during server-side rendering. This covers Angular's loophole in dealing with errors that occur during SSR - by default, Angular returns HTML with a status of 200, regardless of the errors that occur, which can malformed the output.
Thanks to this propagator, we can catch errors in the wrapper for CommonEngine responsible for rendering and return them instead of the malformed rendered view.
Together with this PR, we provide defaultServerErrorResponseHandlers function for ExpressJS, which helps to deal with caught errors:

  • falls back to the CSR with the status 404:
    image
  • falls back to the CSR with the status 500:
    image

Customer can also provide their handlers and use them inside the server.js file instead of the ones we provided.

QA steps:

  1. Tests in storefront app:
  • call HTTP error that may be caught during server-side rendering e.g use HTTP proxy OR
    • modify pages property in default-cms-config.ts to generate CmsPageNotFoundServerError
    • modify components property in default-cms-config.ts to generate UnknownServerError
  • run npm run dev:ssr
  • open the application in the browser
  • verify whether the app fell back to CSR with the proper status code:
  • index.html returned with status 404 if pages endpoint has been modified in default-cms-config.ts
  • index.html returned with status 500 if components endpoint has been modified in default-cms-config.ts
  1. Tests in customer's app:
  • build and deploy Spartacus packages locally:
    npx ts-node ./tools/schematics/testing.ts in the SPA root folder
  • generate fresh ng17 application:
    npx @angular/cli new my-app --standalone=false --ssr=false --style=scss --routing=false
  • go to the project's directory and install Spartacus with SSR from local packages:
    npx @angular/cli add @spartacus/schematics@latest --ssr
  • use HTTP proxy or modify config for CMS endpoints to cause an error when rendering:
provideConfig(<OccConfig>{
      backend: {
        occ: {
          endpoints: {
            // components: 'cms/components2', FOR STATUS 500
            pages: 'cms/pages2', // FOR STATUS 404
          },
        },
      },
    }),
  • in server.ts inside app function use handlers provided by Spartacus:
  const indexHtmlContent = readFileSync(indexHtml, 'utf-8');

  /** */

  server.use(defaultServerErrorResponseHandlers(indexHtmlContent));
  • run npm run build and npm run serve:ssr:my-app to verify if server error responses have been handled as in test case 1
  • create a custom handler for errors (you can use the below example):
    • npm i -d @types/ejs ejs
    • handle-page-not-found.ts:
    import { ErrorRequestHandler } from 'express';
    import ejs from 'ejs';
    import { CmsPageNotFoundServerErrorResponse } from '@spartacus/setup/ssr';
    
    export const handlePageNotFound: ErrorRequestHandler = async (
      err,
      _req,
      res,
      next,
    ) => {
      if (err instanceof CmsPageNotFoundServerErrorResponse) {
        const html = await ejs.renderFile('./404.ejs', {
          title: 'OPS! Page not found',
          text: 'The page you are looking for does not exist',
        });
        res.status(404).send(html);
      } else {
        next(err);
      }
    };
    • 404.ejs:
    <!DOCTYPE HTML>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Page Not Found</title>
      </head>
      <body>
        <h1><%= title %></h1>
        <p><%= text %></p>
      </body>
    </html>
    
    • in server.ts, use handler inside app function:
     server.use(handlePageNotFound)
  • run npm run build and npm run serve:ssr:my-app to verify if the custom handler works

closes CXSPA-6578

@pawelfras pawelfras requested a review from a team as a code owner April 22, 2024 10:55
Comment on lines 69 to 71
server.use(handleCmsPageNotFoundErrorResponse(indexHtmlContent));
server.use(handleUnknownServerErrorResponse(indexHtmlContent));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strategic question: how do we plan to distribute our error handlers to customers?

  • will we be adding them to their server.ts with schematics?
    • if yes, do we plan to put them one by one or somehow wrap them in one uber-error-handler, a.k.a. server.use(defaultServerErrorResponseHandlers()) // import defaultServerErrorResponseHandlers from @spartacus/setup/ssr?
      • thanks to this, over time we will be able to introduce changes/additions if needed - in existing apps. For example, we can easily add a temporaryRedirectErrorHandler (sending 302) if we really want to

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the idea is to provide it to fresh SPA apps via schematics. uber-error-handler is a tempting idea giving us possibilities for easier adjustments. The reason why I decided to introduce separate handlers was the question what if the customer wants to have our handler for unknown server errors, but they custom handler with an error page for 404?
Actually, now I have an answer... the custom handler can be provided before ours. What do you think about that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO the most frequent use cases should be handled with a simple config, or super simple API. Ideally enabled implicitly by default. Medium-frequent use cases should require a tiny bit of coding, but proportionally to how often it is a use case. And for edge cases it's acceptable that customers might need to give up many OOTB default APIs and possibly write a lot of copy-pasted code + some customizations. (https://blog.platis.dev/open-source)

And in this case, in my opinion, default error handlers are good enough for most people. So, it should be a super simple and ergonomic API to use. Ideally implicit, but unfortunately, it's not possible because you have to put it explictly into the server.ts file.

If someone wants to add their error handlers - well, you nicely noticed that they can add their own above ours!! Nice catch!

If someone would like to remove one of our handlers at all, then they might want to remove our pack of error handlers. But for ergonomics, let's expose in our public APi all individuall handlers - so customers can pick and mix whatever they want, explicitly.

@@ -38,23 +38,34 @@ describe('CmsPageNotFoundServerErrorResponse', () => {
expect(cmsPageNotFoundServerErrorResponse.getPriority()).toBe(Priority.LOW);
});

it('should match if error is an instance of HttpErrorResponse and URL starts with proper OCC endpoint including /cms/pages', () => {
it('should match if error is an instance of HttpErrorResponse and, status is 404 and URL starts with proper OCC endpoint including /cms/pages', () => {
Copy link
Contributor

@Platonn Platonn Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('should match if error is an instance of HttpErrorResponse and, status is 404 and URL starts with proper OCC endpoint including /cms/pages', () => {
it('should match if: error is an instance of HttpErrorResponse and status is 404 and URL starts with proper OCC endpoint including /cms/pages', () => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether I like the proposal more 🤔

pawelfras and others added 5 commits April 26, 2024 16:09
Co-authored-by: Krzysztof Platis <platonn.git@gmail.com>
…gate-server-error-response.ts

Co-authored-by: Krzysztof Platis <platonn.git@gmail.com>
Co-authored-by: Krzysztof Platis <platonn.git@gmail.com>
@pawelfras pawelfras merged commit e578194 into epic/ssr-error-handling Apr 29, 2024
9 checks passed
@pawelfras pawelfras deleted the feat/CXSPA-6578 branch April 29, 2024 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants