-
-
Notifications
You must be signed in to change notification settings - Fork 614
Fix email template loading and error handling for infrastructure alerts #3059
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -37,7 +37,11 @@ class EmailService { | |||||||||||||||||||
| return EmailService.SERVICE_NAME; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| init = async () => { | ||||||||||||||||||||
| // Initialize template loader and pre-compile frequently used templates. | ||||||||||||||||||||
| // NOTE: This is intentionally synchronous so that templates are available | ||||||||||||||||||||
| // immediately after the constructor returns (prevents race conditions | ||||||||||||||||||||
| // where callers invoke buildEmail before templateLookup is populated). | ||||||||||||||||||||
| init = () => { | ||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Loads an email template from the filesystem. | ||||||||||||||||||||
| * | ||||||||||||||||||||
|
|
@@ -48,6 +52,7 @@ class EmailService { | |||||||||||||||||||
| try { | ||||||||||||||||||||
| const templatePath = this.path.join(__dirname, `../../../templates/${templateName}.mjml`); | ||||||||||||||||||||
| const templateContent = this.fs.readFileSync(templatePath, "utf8"); | ||||||||||||||||||||
| // compile returns a function that accepts a context and returns the rendered mjml string | ||||||||||||||||||||
| return this.compile(templateContent); | ||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||
| this.logger.error({ | ||||||||||||||||||||
|
|
@@ -56,6 +61,8 @@ class EmailService { | |||||||||||||||||||
| method: "loadTemplate", | ||||||||||||||||||||
| stack: error.stack, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| // Return null when template can't be loaded so callers can handle it explicitly | ||||||||||||||||||||
| return null; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -83,16 +90,40 @@ class EmailService { | |||||||||||||||||||
|
|
||||||||||||||||||||
| buildEmail = async (template, context) => { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const mjml = this.templateLookup[template](context); | ||||||||||||||||||||
| const html = await this.mjml2html(mjml); | ||||||||||||||||||||
| return html.html; | ||||||||||||||||||||
| // Ensure the template exists and is a function. Templates are stored in the lookup | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 | Confidence: High The change from silently returning undefined to throwing errors in Code Suggestion: // In settingsController.js, wrap the buildEmail call:
try {
const html = await this.emailService.buildEmail("testEmailTemplate", context);
} catch (error) {
// Handle template error appropriately
throw this.errorService.createServerError("Failed to generate email template.");
}Evidence: path:server/src/controllers/v1/settingsController.js, method:buildEmail |
||||||||||||||||||||
| // with keys like "hardwareIncidentTemplate" mapping to compiled functions. | ||||||||||||||||||||
| let tplFn = this.templateLookup[template]; | ||||||||||||||||||||
| if (typeof tplFn !== "function") { | ||||||||||||||||||||
| // Attempt to lazy-load the template. Templates are named by dropping a trailing | ||||||||||||||||||||
| // "Template" suffix (eg. hardwareIncidentTemplate -> hardwareIncident.mjml). | ||||||||||||||||||||
| const baseName = String(template).replace(/Template$/i, ""); | ||||||||||||||||||||
| tplFn = this.loadTemplate(baseName); | ||||||||||||||||||||
|
Comment on lines
+97
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 | Confidence: Medium The template naming convention (removing "Template" suffix) creates implicit coupling between template lookup keys and filesystem names. This could lead to confusion if new templates don't follow this pattern. The logic is also case-insensitive ( Code Suggestion: const templateMappings = {
hardwareIncidentTemplate: 'hardwareIncident',
// ... other mappings
};
const baseName = templateMappings[template] || template.replace(/Template$/i, ""); |
||||||||||||||||||||
| if (typeof tplFn === "function") { | ||||||||||||||||||||
| // cache the compiled template for future calls | ||||||||||||||||||||
| this.templateLookup[template] = tplFn; | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| this.logger.error({ | ||||||||||||||||||||
| message: `Template not found or failed to compile: ${template}`, | ||||||||||||||||||||
| service: SERVICE_NAME, | ||||||||||||||||||||
| method: "buildEmail", | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| throw new Error(`Template not found or failed to compile: ${template}`); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+104
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 | Confidence: Low The error message doesn't include the resolved template path, making debugging more difficult when templates are missing. Developers would need to manually trace the template resolution logic.
Suggested change
|
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const mjml = tplFn(context); | ||||||||||||||||||||
| const result = this.mjml2html(mjml); | ||||||||||||||||||||
| return (result && result.html) || ""; | ||||||||||||||||||||
|
Comment on lines
+114
to
+116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1 | Confidence: High The
Suggested change
|
||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||
| this.logger.error({ | ||||||||||||||||||||
| message: error.message, | ||||||||||||||||||||
| service: SERVICE_NAME, | ||||||||||||||||||||
| method: "buildEmail", | ||||||||||||||||||||
| stack: error.stack, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| // Re-throw to allow callers to detect templating failures instead of silently | ||||||||||||||||||||
| // sending empty emails. Callers can catch and handle/log as needed. | ||||||||||||||||||||
| throw error; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 | Confidence: Medium
Synchronous file reads (
readFileSync) during initialization could block the event loop, especially with many templates. While the change aims to prevent race conditions, it may impact application startup time and scalability.Code Suggestion: