Skip to content

Commit

Permalink
IMN-736 - pt 1 - API GW Agreement errors without passthrough / bubble…
Browse files Browse the repository at this point in the history
…-up (#1009)
  • Loading branch information
ecamellini authored Oct 2, 2024
1 parent 8c71502 commit 76e7bfb
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 91 deletions.
16 changes: 12 additions & 4 deletions packages/api-clients/src/axiosLogInterceptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,21 @@ export function configureAxiosLogInterceptors(
prefixText: getPrefix(response.config.headers, clientName),
}
) as AxiosResponse,
(error: AxiosError): Promise<AxiosError> =>
AxiosLogger.errorLogger(
(error: AxiosError): Promise<AxiosError> => {
const prefixText = getPrefix(error.config?.headers, clientName);
if ("errors" in error && Array.isArray(error.errors)) {
error.errors.forEach((err) =>
genericLogger.error(`[${prefixText}] ${err}`)
);
}

return AxiosLogger.errorLogger(
error as Parameters<typeof AxiosLogger.errorLogger>[0],
{
logger: genericLogger.error,
prefixText: getPrefix(error.config?.headers, clientName),
prefixText,
}
) as Promise<AxiosError>
) as Promise<AxiosError>;
}
);
}
6 changes: 4 additions & 2 deletions packages/api-gateway/src/api/agreementApiConverter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { agreementApi, apiGatewayApi } from "pagopa-interop-api-clients";
import { Logger } from "pagopa-interop-commons";
import { assertAgreementStateNotDraft } from "../services/validators.js";

const allowedAgreementStates: apiGatewayApi.AgreementState[] = [
Expand All @@ -10,9 +11,10 @@ const allowedAgreementStates: apiGatewayApi.AgreementState[] = [
];

export function toApiGatewayAgreementIfNotDraft(
agreement: agreementApi.Agreement
agreement: agreementApi.Agreement,
logger: Logger
): apiGatewayApi.Agreement {
assertAgreementStateNotDraft(agreement.state, agreement.id);
assertAgreementStateNotDraft(agreement.state, agreement.id, logger);

return {
id: agreement.id,
Expand Down
40 changes: 40 additions & 0 deletions packages/api-gateway/src/clients/catchClientError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { z } from "zod";
import { ApiError, genericError } from "pagopa-interop-models";
import { ErrorCodes } from "../models/errors.js";

const CatchableCodes = z.enum(["404", "403"]);
type CatchableCodes = z.infer<typeof CatchableCodes>;
const ClientError = z.object({
message: z.string(),
response: z.object({
status: z.coerce.string().pipe(CatchableCodes),
}),
});

type StatusCodeErrorsMap = Record<
CatchableCodes,
ApiError<ErrorCodes> | undefined
>;
export function clientStatusCodeToError(
clientResult: unknown,
statusCodesErrorMap: Partial<StatusCodeErrorsMap>
): Error {
const clientError = ClientError.safeParse(clientResult);

if (clientError.success) {
const error = statusCodesErrorMap[clientError.data.response.status];
if (error) {
return error;
}
}

if (clientResult instanceof Error) {
return clientResult;
// Returning the original error as it is,
// so that it can be thrown, handled and logged
}

return genericError(
`Unexpected error response from API Client: ${JSON.stringify(clientResult)}`
);
}
40 changes: 27 additions & 13 deletions packages/api-gateway/src/models/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
catalogApi,
purposeApi,
} from "pagopa-interop-api-clients";
import { Logger } from "pagopa-interop-commons";
import { ApiError, makeApiProblemBuilder } from "pagopa-interop-models";

export const errorCodes = {
invalidAgreementState: "0001",
agreementNotFound: "0001",
producerAndConsumerParamMissing: "0002",
missingActivePurposeVersion: "0003",
activeAgreementByEserviceAndConsumerNotFound: "0004",
Expand All @@ -21,18 +22,10 @@ export const errorCodes = {

export type ErrorCodes = keyof typeof errorCodes;

export const makeApiProblem = makeApiProblemBuilder(errorCodes);

export function invalidAgreementState(
state: agreementApi.AgreementState,
agreementId: agreementApi.Agreement["id"]
): ApiError<ErrorCodes> {
return new ApiError({
detail: `Cannot retrieve agreement in ${state} state - id: ${agreementId}`,
code: "invalidAgreementState",
title: "Invalid agreement state",
});
}
export const makeApiProblem = makeApiProblemBuilder(
errorCodes,
false // API Gateway shall not let Problem errors from other services to pass through
);

export function producerAndConsumerParamMissing(): ApiError<ErrorCodes> {
return new ApiError({
Expand Down Expand Up @@ -123,3 +116,24 @@ export function keyNotFound(kId: string): ApiError<ErrorCodes> {
title: "Key not found",
});
}

export function agreementNotFound(
agreementId: agreementApi.Agreement["id"]
): ApiError<ErrorCodes> {
return new ApiError({
detail: `Agreement ${agreementId} not found`,
code: "agreementNotFound",
title: "Agreement not found",
});
}

export function invalidAgreementState(
agreementId: agreementApi.Agreement["id"],
logger: Logger
): ApiError<ErrorCodes> {
const error = agreementNotFound(agreementId);
logger.warn(
`Root cause for Error "${error.title}": cannot retrieve agreement in DRAFT state`
);
return error;
}
12 changes: 10 additions & 2 deletions packages/api-gateway/src/routers/apiGatewayRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ const apiGatewayRouter = (

return res.status(200).send(attributes);
} catch (error) {
const errorRes = makeApiProblem(error, emptyErrorMapper, ctx.logger);
const errorRes = makeApiProblem(
error,
getAgreementErrorMapper,
ctx.logger
);
return res.status(errorRes.status).send(errorRes);
}
}
Expand All @@ -170,7 +174,11 @@ const apiGatewayRouter = (

return res.status(200).send(purposes);
} catch (error) {
const errorRes = makeApiProblem(error, emptyErrorMapper, ctx.logger);
const errorRes = makeApiProblem(
error,
getAgreementErrorMapper,
ctx.logger
);
return res.status(errorRes.status).send(errorRes);
}
}
Expand Down
71 changes: 45 additions & 26 deletions packages/api-gateway/src/services/agreementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import {
} from "../clients/clientsProvider.js";
import { ApiGatewayAppContext } from "../utilities/context.js";
import { toApiGatewayAgreementIfNotDraft } from "../api/agreementApiConverter.js";
import { producerAndConsumerParamMissing } from "../models/errors.js";
import {
agreementNotFound,
producerAndConsumerParamMissing,
} from "../models/errors.js";
import { toAgreementProcessGetAgreementsQueryParams } from "../api/agreementApiConverter.js";
import { toApiGatewayAgreementAttributes } from "../api/attributeApiConverter.js";
import { clientStatusCodeToError } from "../clients/catchClientError.js";
import { getAllPurposes } from "./purposeService.js";

export async function getAllAgreements(
agreementProcessClient: AgreementProcessClient,
headers: ApiGatewayAppContext["headers"],
{ headers, logger }: WithLogger<ApiGatewayAppContext>,
queryParams: apiGatewayApi.GetAgreementsQueryParams
): Promise<apiGatewayApi.Agreements> {
const getAgreementsQueryParams =
Expand All @@ -31,9 +35,31 @@ export async function getAllAgreements(
},
})
);
return { agreements: agreements.map(toApiGatewayAgreementIfNotDraft) };
return {
agreements: agreements.map((agreement) =>
toApiGatewayAgreementIfNotDraft(agreement, logger)
),
};
}

const retrieveAgreement = (
agreementProcessClient: AgreementProcessClient,
headers: ApiGatewayAppContext["headers"],
agreementId: agreementApi.Agreement["id"]
): Promise<agreementApi.Agreement> =>
agreementProcessClient
.getAgreementById({
headers,
params: {
agreementId,
},
})
.catch((res) => {
throw clientStatusCodeToError(res, {
404: agreementNotFound(agreementId),
});
});

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function agreementServiceBuilder(
agreementProcessClient: AgreementProcessClient,
Expand All @@ -42,40 +68,35 @@ export function agreementServiceBuilder(
) {
return {
getAgreements: async (
{ logger, headers }: WithLogger<ApiGatewayAppContext>,
ctx: WithLogger<ApiGatewayAppContext>,
queryParams: apiGatewayApi.GetAgreementsQueryParams
): Promise<apiGatewayApi.Agreements> => {
const { producerId, consumerId, eserviceId, descriptorId, states } =
queryParams;

logger.info(
ctx.logger.info(
`Retrieving agreements for producerId ${producerId} consumerId ${consumerId} eServiceId ${eserviceId} descriptorId ${descriptorId} states ${states}`
);

if (producerId === undefined && consumerId === undefined) {
throw producerAndConsumerParamMissing();
}

return await getAllAgreements(
agreementProcessClient,
headers,
queryParams
);
return await getAllAgreements(agreementProcessClient, ctx, queryParams);
},

getAgreementById: async (
{ logger, headers }: WithLogger<ApiGatewayAppContext>,
agreementId: agreementApi.Agreement["id"]
): Promise<apiGatewayApi.Agreement> => {
logger.info(`Retrieving agreement by id = ${agreementId}`);
const agreement = await agreementProcessClient.getAgreementById({
const agreement = await retrieveAgreement(
agreementProcessClient,
headers,
params: {
agreementId,
},
});
agreementId
);

return toApiGatewayAgreementIfNotDraft(agreement);
return toApiGatewayAgreementIfNotDraft(agreement, logger);
},

getAgreementAttributes: async (
Expand All @@ -84,12 +105,11 @@ export function agreementServiceBuilder(
): Promise<apiGatewayApi.Attributes> => {
logger.info(`Retrieving Attributes for Agreement ${agreementId}`);

const agreement = await agreementProcessClient.getAgreementById({
const agreement = await retrieveAgreement(
agreementProcessClient,
headers,
params: {
agreementId,
},
});
agreementId
);

const tenant = await tenantProcessClient.tenant.getTenant({
headers,
Expand All @@ -107,12 +127,11 @@ export function agreementServiceBuilder(
): Promise<apiGatewayApi.Purposes> => {
logger.info(`Retrieving Purposes for Agreement ${agreementId}`);

const agreement = await agreementProcessClient.getAgreementById({
const agreement = await retrieveAgreement(
agreementProcessClient,
headers,
params: {
agreementId,
},
});
agreementId
);

return await getAllPurposes(purposeProcessClient, headers, {
eserviceId: agreement.eserviceId,
Expand Down
8 changes: 4 additions & 4 deletions packages/api-gateway/src/services/purposeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,20 @@ export function purposeServiceBuilder(
},

getAgreementByPurpose: async (
{ logger, headers }: WithLogger<ApiGatewayAppContext>,
ctx: WithLogger<ApiGatewayAppContext>,
purposeId: purposeApi.Purpose["id"]
): Promise<apiGatewayApi.Agreement> => {
logger.info(`Retrieving agreement by purpose ${purposeId}`);
ctx.logger.info(`Retrieving agreement by purpose ${purposeId}`);
const purpose = await purposeProcessClient.getPurpose({
headers,
headers: ctx.headers,
params: {
id: purposeId,
},
});

const { agreements } = await getAllAgreements(
agreementProcessClient,
headers,
ctx,
{
consumerId: purpose.consumerId,
eserviceId: purpose.eserviceId,
Expand Down
6 changes: 4 additions & 2 deletions packages/api-gateway/src/services/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
purposeApi,
} from "pagopa-interop-api-clients";
import { operationForbidden, TenantId } from "pagopa-interop-models";
import { Logger } from "pagopa-interop-commons";
import {
activeAgreementByEserviceAndConsumerNotFound,
attributeNotFoundInRegistry,
Expand All @@ -19,10 +20,11 @@ import { NonDraftCatalogApiDescriptor } from "../api/catalogApiConverter.js";

export function assertAgreementStateNotDraft(
agreementState: agreementApi.AgreementState,
agreementId: agreementApi.Agreement["id"]
agreementId: agreementApi.Agreement["id"],
logger: Logger
): asserts agreementState is apiGatewayApi.AgreementState {
if (agreementState === agreementApi.AgreementState.Values.DRAFT) {
throw invalidAgreementState(agreementState, agreementId);
throw invalidAgreementState(agreementId, logger);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/api-gateway/src/utilities/errorMappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const {

export const getAgreementErrorMapper = (error: ApiError<ErrorCodes>): number =>
match(error.code)
.with("invalidAgreementState", () => HTTP_STATUS_NOT_FOUND)
.with("agreementNotFound", () => HTTP_STATUS_NOT_FOUND)
.otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR);

export const getAgreementsErrorMapper = (error: ApiError<ErrorCodes>): number =>
Expand Down
Loading

0 comments on commit 76e7bfb

Please sign in to comment.