Skip to content

Commit 71a7d79

Browse files
VIA-331 MD/SB/DB: Improve error handling and logs when retriving data from S3
1 parent 438e124 commit 71a7d79

File tree

4 files changed

+72
-24
lines changed

4 files changed

+72
-24
lines changed

src/services/content-api/gateway/content-reader-service.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import { S3Client } from "@aws-sdk/client-s3";
55
import mockRsvVaccineJson from "@project/wiremock/__files/rsv-vaccine.json";
66
import { VaccineTypes } from "@src/models/vaccine";
7-
import { _readContentFromCache, getContentForVaccine } from "@src/services/content-api/gateway/content-reader-service";
7+
import { _readContentFromCache,
8+
getContentForVaccine } from "@src/services/content-api/gateway/content-reader-service";
89
import { ContentErrorTypes, GetContentForVaccineResponse } from "@src/services/content-api/types";
910
import { configProvider } from "@src/utils/config";
1011
import { Readable } from "stream";
@@ -59,7 +60,7 @@ describe("Content Reader Service", () => {
5960
mockSend.mockImplementation(() => mockInvalidResponse);
6061

6162
const actualPromise: Promise<string> = _readContentFromCache("s3://bucket", "/file");
62-
await expect(actualPromise).rejects.toThrow("Unexpected response type");
63+
await expect(actualPromise).rejects.toThrow("Error fetching content: unexpected response type");
6364
});
6465

6566
it("throws when remote response has error", async () => {

src/services/content-api/gateway/content-reader-service.ts

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use server";
22

3-
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
3+
import { GetObjectCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
44
import { VaccineTypes } from "@src/models/vaccine";
55
import { VaccineContentPaths, vaccineTypeToPath } from "@src/services/content-api/constants";
66
import { getFilteredContentForVaccine } from "@src/services/content-api/parsers/content-filter-service";
@@ -9,7 +9,7 @@ import {
99
ContentErrorTypes,
1010
GetContentForVaccineResponse,
1111
StyledVaccineContent,
12-
VaccinePageContent,
12+
VaccinePageContent
1313
} from "@src/services/content-api/types";
1414
import { AppConfig, configProvider } from "@src/utils/config";
1515
import { AWS_PRIMARY_REGION } from "@src/utils/constants";
@@ -18,18 +18,24 @@ import { S3_PREFIX, isS3Path } from "@src/utils/path";
1818
import { readFile } from "node:fs/promises";
1919
import { Logger } from "pino";
2020
import { Readable } from "stream";
21+
import {
22+
ReadingS3Error,
23+
S3NoSuchKeyError,
24+
S3HttpStatusError,
25+
} from "@src/services/content-api/gateway/exceptions";
26+
import { HttpStatusCode } from "axios";
2127

2228
const log: Logger = logger.child({ module: "content-reader-service" });
2329

2430
const _readFileS3 = async (bucket: string, key: string): Promise<string> => {
2531
try {
2632
const s3Client: S3Client = new S3Client({
27-
region: AWS_PRIMARY_REGION,
33+
region: AWS_PRIMARY_REGION
2834
});
2935

3036
const getObjectCommand: GetObjectCommand = new GetObjectCommand({
3137
Bucket: bucket,
32-
Key: key,
38+
Key: key
3339
});
3440
const { Body } = await s3Client.send(getObjectCommand);
3541

@@ -41,24 +47,33 @@ const _readFileS3 = async (bucket: string, key: string): Promise<string> => {
4147
Body.on("error", reject);
4248
});
4349
}
44-
} catch (error) {
45-
log.error(`Error reading file from S3: ${error}`);
46-
throw error;
47-
}
50+
} catch (error: unknown) {
51+
if (error instanceof S3ServiceException) {
52+
if (error.name === "NoSuchKey") {
53+
log.error(error, `Error in reading Content API from S3: File not found ${bucket}/${key}`);
54+
throw new S3NoSuchKeyError(`Error in reading Content API from S3`);
55+
}
4856

49-
throw new Error("Unexpected response type");
57+
const statusCode = error.$metadata?.httpStatusCode;
58+
if (statusCode && statusCode >= HttpStatusCode.BadRequest) {
59+
log.error(error, `Error in reading Content API from S3 ${bucket}/${key}`);
60+
throw new S3HttpStatusError(`Error in reading Content API from S3`);
61+
}
62+
63+
log.error(error, `Unhandled error in reading Content API from S3: ${bucket}/${key}`);
64+
throw error;
65+
}
66+
}
67+
log.error("Error fetching content: unexpected response type");
68+
throw new Error("Error fetching content: unexpected response type");
5069
};
5170

5271
const _readContentFromCache = async (cacheLocation: string, cachePath: string): Promise<string> => {
5372
log.info(`Reading file from cache: loc=${cacheLocation}, path=${cachePath}`);
54-
try {
55-
return isS3Path(cacheLocation)
56-
? await _readFileS3(cacheLocation.slice(S3_PREFIX.length), cachePath)
57-
: await readFile(`${cacheLocation}${cachePath}`, { encoding: "utf8" });
58-
} catch (error) {
59-
log.error(`Error reading file from cache: loc=${cacheLocation}: ${error}`);
60-
throw error;
61-
}
73+
74+
return isS3Path(cacheLocation)
75+
? await _readFileS3(cacheLocation.slice(S3_PREFIX.length), cachePath)
76+
: await readFile(`${cacheLocation}${cachePath}`, { encoding: "utf8" });
6277
};
6378

6479
const getContentForVaccine = async (vaccineType: VaccineTypes): Promise<GetContentForVaccineResponse> => {
@@ -77,11 +92,18 @@ const getContentForVaccine = async (vaccineType: VaccineTypes): Promise<GetConte
7792

7893
return { styledVaccineContent, contentError: undefined };
7994
} catch (error) {
80-
log.error(`Error getting content for vaccine: ${error}`);
81-
return {
82-
styledVaccineContent: undefined,
83-
contentError: ContentErrorTypes.CONTENT_LOADING_ERROR,
84-
};
95+
if (error instanceof ReadingS3Error) {
96+
return {
97+
styledVaccineContent: undefined,
98+
contentError: ContentErrorTypes.CONTENT_LOADING_ERROR
99+
};
100+
} else {
101+
log.error(error, `Error getting content for vaccine: ${vaccineType}`);
102+
return {
103+
styledVaccineContent: undefined,
104+
contentError: ContentErrorTypes.CONTENT_LOADING_ERROR
105+
};
106+
}
85107
}
86108
};
87109

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export class ReadingS3Error extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
Object.setPrototypeOf(this, new.target.prototype);
5+
this.name = "ReadingS3Error";
6+
}
7+
}
8+
9+
export class S3HttpStatusError extends ReadingS3Error {
10+
constructor(message: string) {
11+
super(message);
12+
Object.setPrototypeOf(this, new.target.prototype);
13+
this.name = "S3HttpStatusError";
14+
}
15+
}
16+
17+
export class S3NoSuchKeyError extends ReadingS3Error {
18+
constructor(message: string) {
19+
super(message);
20+
Object.setPrototypeOf(this, new.target.prototype);
21+
this.name = "S3NoSuchKeyError";
22+
}
23+
}

src/services/eligibility-api/gateway/exceptions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ export class EligibilityApiError extends Error {
55
this.name = "EligibilityApiError";
66
}
77
}
8+
89
export class EligibilityApiHttpStatusError extends EligibilityApiError {
910
constructor(message: string) {
1011
super(message);
1112
Object.setPrototypeOf(this, new.target.prototype);
1213
this.name = "EligibilityApiHttpStatusError";
1314
}
1415
}
16+
1517
export class EligibilityApiSchemaError extends EligibilityApiError {
1618
constructor(message: string) {
1719
super(message);

0 commit comments

Comments
 (0)