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

[DO NOT MERGE] CXSPA-6939 (PoC) Handling breaking changes related to SSR Error Handling #18789

Open
wants to merge 10 commits into
base: epic/ssr-error-handling
Choose a base branch
from
14 changes: 14 additions & 0 deletions core-libs/setup/ssr/error-handling/enable-ssr-error-handling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { InjectionToken } from '@angular/core';

/**
* Toggle for enabling the SSR error handling.
*/
export const ENABLE_SSR_ERROR_HANDLING = new InjectionToken<boolean>(
'ENABLE_SSR_ERROR_HANDLING'
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
*/

import { Injectable, inject } from '@angular/core';
import { MultiErrorHandler, resolveApplicable } from '@spartacus/core';
import {
FeatureConfigService,
MultiErrorHandler,
resolveApplicable,
} from '@spartacus/core';
import { SERVER_ERROR_RESPONSE_FACTORY } from '../server-error-response-factory';
import { PROPAGATE_SERVER_ERROR_RESPONSE } from '../server-error-response/propagate-server-error-response';

Expand All @@ -25,15 +29,19 @@ export class ServerRespondingErrorHandler implements MultiErrorHandler {
protected propagateServerErrorResponse = inject(
PROPAGATE_SERVER_ERROR_RESPONSE
);
private featureConfigService = inject(FeatureConfigService);

handleError(error: unknown): void {
const cxServerErrorResponse = resolveApplicable(
this.serverErrorResponseFactories,
[error]
)?.create(error);
// Related to CXSPA-6890
if (this.featureConfigService.isEnabled('serverErrorPropagation')) {
const cxServerErrorResponse = resolveApplicable(
this.serverErrorResponseFactories,
[error]
)?.create(error);

if (cxServerErrorResponse) {
this.propagateServerErrorResponse(cxServerErrorResponse);
if (cxServerErrorResponse) {
this.propagateServerErrorResponse(cxServerErrorResponse);
}
}
}
}
9 changes: 8 additions & 1 deletion core-libs/setup/ssr/optimized-engine/rendering-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ export class RenderingCache {
this.renders.delete(this.renders.keys().next().value);
}
}
this.renders.set(key, entry);
// Related to CXSPA-6890
// cache only if cachingStrategyResolver return true
if (
this.options?.cacheStrategyResolver &&
this.options?.cacheStrategyResolver(this.options, entry)
) {
this.renders.set(key, entry);
}
}

get(key: string): RenderingEntry | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,28 @@ export interface SsrOptimizationOptions {
* By default, the DefaultExpressServerLogger is used.
*/
logger?: ExpressServerLogger;

/**
* Related to CXSPA-6890
* WIP: Custom caching strategy resolver.
* By default, the caching strategy is based on the presence of an error.
*/
cacheStrategyResolver?: (
config: SsrOptimizationOptions,
entry: {
error?: Error | unknown;
html?: string;
}
) => boolean;

/**
* Related to CXSPA-6890
* Avoid caching of errors. By default, this value is false.
*
* NOTE: adjust this JSDoc about the consequences, why we suggest to avoid caching errors and inform that this should be treated as a feature toggle
* and eventually will be removed in a future (probably in ~12 months)
*/
avoidCachingErrors?: boolean;
}

export enum RenderingStrategy {
Expand All @@ -150,4 +172,7 @@ export const defaultSsrOptimizationOptions: SsrOptimizationOptions = {
defaultRenderingStrategyResolverOptions
),
logger: new DefaultExpressServerLogger(),
cacheStrategyResolver: (options, entry) =>
!(options.avoidCachingErrors === true && Boolean(entry.error)),
avoidCachingErrors: false,
};
19 changes: 18 additions & 1 deletion feature-libs/asm/core/store/actions/customer.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { CustomerSearchOptions, CustomerSearchPage } from '@spartacus/asm/root';
import { StateUtils, ErrorActionType } from '@spartacus/core';
import { ErrorActionType, StateUtils } from '@spartacus/core';
import {
CUSTOMER_LIST_CUSTOMERS_SEARCH_DATA,
CUSTOMER_SEARCH_DATA,
Expand All @@ -32,9 +32,20 @@ export class CustomerSearch extends StateUtils.LoaderLoadAction {
}
}

// possible ways of handling breaking changes in ALL actions:
// - introduce CustomerSearchFailV2 and deprecate old class
// - overload the constructor of CustomerSearchFail and handle the change there together with deprecating he old one
Comment on lines +35 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm sorry. I don't get the problem - what a breaking change we have? TS Type? Behavior?

And how we're mitigating it?

And if introducing any temporary solutions (e.g. deprecated things), when will we remove them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As we discussed, usage of a new type in the constructor or making the constructor's params required provide breaking changes.
Action points for such cases:

  • revert usage of new ErrorActionType type
  • overload the constructor and deprecate the deprecate version with the optional error/payload param
  • create a fallback if the error passed to the constructor is undefined (assign new Error)


export class CustomerSearchFail extends StateUtils.LoaderFailAction {
readonly type = CUSTOMER_SEARCH_FAIL;
constructor(error: ErrorActionType);
/**
* @deprecated please use constructor(error: ErrorActionType) instead
*/
//eslint-disable-next-line @typescript-eslint/unified-signatures
constructor(error: any);
constructor(public error: ErrorActionType) {
//do we consider changes in NgRx as breaking? if so, we should handle it (somehow)
super(CUSTOMER_SEARCH_DATA, error);
}
}
Expand Down Expand Up @@ -62,6 +73,12 @@ export class CustomerListCustomersSearch extends StateUtils.LoaderLoadAction {

export class CustomerListCustomersSearchFail extends StateUtils.LoaderFailAction {
readonly type = CUSTOMER_LIST_CUSTOMERS_SEARCH_FAIL;
constructor(payload: ErrorActionType);
/**
* @deprecated please use constructor(payload: ErrorActionType) instead
*/
//eslint-disable-next-line @typescript-eslint/unified-signatures
constructor(payload: any);
constructor(public payload: ErrorActionType) {
super(CUSTOMER_LIST_CUSTOMERS_SEARCH_DATA, payload);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export class CartAddEntrySuccess extends StateUtils.EntityProcessesDecrementActi
super(MULTI_CART_DATA, payload.cartId);
}
}

//Related to CXSPA-7198
//might be more actions implementing ErrorAction
export class CartAddEntryFail
extends StateUtils.EntityProcessesDecrementAction
implements ErrorAction
Expand Down
3 changes: 1 addition & 2 deletions projects/core/src/base-core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { ConfigInitializerModule } from './config/config-initializer/config-init
import { ConfigValidatorModule } from './config/config-validator/config-validator.module';
import { ConfigModule } from './config/config.module';
import { ErrorHandlingModule, HttpErrorHandlerModule } from './error-handling';
import { EffectsErrorHandlerModule } from './error-handling/effects-error-handler/effects-error-handler.module';
import { FeaturesConfigModule } from './features-config/features-config.module';
import { GlobalMessageModule } from './global-message/global-message.module';
import { HttpModule } from './http/http.module';
Expand All @@ -24,6 +23,7 @@ import { StateModule } from './state/state.module';

@NgModule({
imports: [
// breaking change - HttpErrorHandlerInterceptor may have featureService inside manipulate the behavior (implemented) OR we can inform in docs that this module has to be imported
Copy link
Contributor

Choose a reason for hiding this comment

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

FeatureToggles for the win. It's easy, simple, documented.
Importing extra modules by customers is an extra overhead. It should be required only in cases when bundlesize of importing this module by default would be considerable.

HttpErrorHandlerModule.forRoot(), // Import this module before any other interceptor to handle HTTP errors efficiently
StateModule.forRoot(),
ConfigModule.forRoot(),
Expand All @@ -40,7 +40,6 @@ import { StateModule } from './state/state.module';
LazyLoadingModule.forRoot(),
HttpModule.forRoot(),
ErrorHandlingModule.forRoot(),
EffectsErrorHandlerModule.forRoot(),
],
})
export class BaseCoreModule {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,35 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { Injectable } from '@angular/core';
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect } from '@ngrx/effects';
import { Observable } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { FeatureConfigService } from '../../features-config';
import { ErrorAction } from '../../model/index';
import { EffectsErrorHandlerService } from './effects-error-handler.service';

@Injectable()
export class CxErrorHandlerEffect {
protected actions$ = inject(Actions);
protected effectsErrorHandlerService = inject(EffectsErrorHandlerService);
protected featureConfigService = inject(FeatureConfigService);

error$: Observable<ErrorAction> = createEffect(
() =>
this.actions$.pipe(
filter(this.effectErrorHandler.filterActions),
tap((errorAction: ErrorAction) =>
this.effectErrorHandler.handleError(errorAction)
)
filter(this.effectsErrorHandlerService.filterActions),
tap((errorAction) => {
// Related to CXSPA-7197
if (
this.featureConfigService.isEnabled(
'strictHttpAndNgrxErrorHandling'
)
) {
this.effectsErrorHandlerService.handleError(errorAction);
}
})
),
{ dispatch: false }
);

constructor(
protected actions$: Actions,
protected effectErrorHandler: EffectsErrorHandlerService
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler, Injectable } from '@angular/core';
import { ErrorHandler, Injectable, inject } from '@angular/core';
import { Action } from '@ngrx/store';
import {
ErrorAction,
Expand All @@ -15,7 +15,7 @@ import {

@Injectable()
export class EffectsErrorHandlerService {
constructor(protected errorHandler: ErrorHandler) {}
protected errorHandler = inject(ErrorHandler);

handleError(action: ErrorAction): void {
const error: ErrorActionType = action.error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
* SPDX-License-Identifier: Apache-2.0
*/

export * from '../../model/error-action';
export * from './effects-error-handler.module';
export * from './effects-error-handler.service';
8 changes: 7 additions & 1 deletion projects/core/src/error-handling/error-handling.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@

import { ErrorHandler, ModuleWithProviders, NgModule } from '@angular/core';
import { CxErrorHandler } from './cx-error-handler';
import { EffectsErrorHandlerModule } from './effects-error-handler';
import { provideMultiErrorHandler } from './multi-error-handler';

@NgModule()
// maybe this module should contain all error handling related parts, including modules providing breaking changes?
// this module is already part of BaseCoreModule
// verify whether importing HttpErrorHandler module here won't break its behavior.
pawelfras marked this conversation as resolved.
Show resolved Hide resolved
@NgModule({
imports: [EffectsErrorHandlerModule.forRoot()],
})
export class ErrorHandlingModule {
static forRoot(): ModuleWithProviders<ErrorHandlingModule> {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { ErrorHandler, Injectable } from '@angular/core';
import { ErrorHandler, Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { FeatureConfigService } from '../../features-config';

/**
* This interceptor forwards all HTTP errors (e.g. 5xx or 4xx status response from backend)
Expand All @@ -24,12 +25,20 @@ import { tap } from 'rxjs/operators';
*/
@Injectable()
export class HttpErrorHandlerInterceptor implements HttpInterceptor {
constructor(protected errorHandler: ErrorHandler) {}
protected errorHandler = inject(ErrorHandler);
protected featureConfigService = inject(FeatureConfigService);

intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
//double-check whether it is good way of handling HTTP errors from api calls
if (
// Related to CXSPA-7197
!this.featureConfigService.isEnabled('strictHttpAndNgrxErrorHandling')
) {
return next.handle(request);
}
return next.handle(request).pipe(
tap({
error: (error) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ export interface FeatureTogglesInterface {
*/
productConfiguratorAttributeTypesV2?: boolean;

/**
* Related to CXSPA-7197
* Enable strict error handling for errors occurred in HTTP calls and in NgRx failure actions.
*/
strictHttpAndNgrxErrorHandling?: boolean;

/**
* Related to CXSPA-6890
* Enable propagating errors occurred during server-side rendering to the server.
*/
serverErrorPropagation?: boolean;

/**
* Adds asterisks to required form fields in all components existing before v2211.20
*/
Expand Down Expand Up @@ -160,6 +172,8 @@ export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
pdfInvoicesSortByInvoiceDate: false,
storeFrontLibCardParagraphTruncated: false,
productConfiguratorAttributeTypesV2: false,
strictHttpAndNgrxErrorHandling: false,
serverErrorPropagation: false,
a11yRequiredAsterisks: false,
a11yQuantityOrderTabbing: false,
a11yNavigationUiKeyboardControls: false,
Expand Down
2 changes: 2 additions & 0 deletions projects/core/src/model/error-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { HttpErrorResponse } from '@angular/common/http';
import { Action } from '@ngrx/store';
import { HttpErrorModel } from './index';

// Related to CXSPA-7198
// revert usage of this interface and eventually remove it from source code.
export type ErrorActionType = HttpErrorResponse | HttpErrorModel | Error;

export interface ErrorAction extends Action {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { Action } from '@ngrx/store';
import { ErrorAction } from '../../../model';
import { EntityId, entityMeta, EntityMeta } from '../entity/entity.action';
import {
failMeta,
Expand Down Expand Up @@ -75,11 +76,28 @@ export class EntityLoadAction implements EntityLoaderAction {
}
}

export class EntityFailAction implements EntityLoaderAction {
export class EntityFailAction implements EntityLoaderAction, ErrorAction {
type = ENTITY_FAIL_ACTION;
readonly meta: EntityLoaderMeta;
constructor(entityType: string, id: EntityId, public error?: any) {
pawelfras marked this conversation as resolved.
Show resolved Hide resolved

error: any;

// Related to CXSPA-7198
/**@deprecated ADD PROPER INFO*/
constructor(entityType: string, id: EntityId);
// eslint-disable-next-line @typescript-eslint/unified-signatures
constructor(entityType: string, id: EntityId, error: any);
constructor(entityType: string, id: EntityId, error?: any) {
this.meta = entityFailMeta(entityType, id, error);
if (error) {
this.error = error;
} else {
// fallback needed only until we make the `error` property a required one
const stringId = Array.isArray(id) ? `[${id.join(',')}]` : id;
this.error = new Error(
`EntityFailAction - entityType: ${entityType}, id: ${stringId}`
);
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions projects/core/src/state/utils/loader/loader.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export class LoaderFailAction implements LoaderAction, ErrorAction {
error: ErrorActionType;
readonly meta: LoaderMeta;

constructor(entityType: string, error: ErrorActionType);
/**
* @deprecated
*/
constructor(entityType: string, error?: any);
constructor(entityType: string, error: ErrorActionType) {
this.meta = failMeta(entityType, error);
this.error = error;
Expand Down