Skip to content

Conversation

@Retr0-XD
Copy link

@Retr0-XD Retr0-XD commented Nov 9, 2025

Enhancements to the EmailService improve the loading of email templates and error handling. The changes ensure that when a template fails to load or compile, the error is logged and re-thrown, preventing the sending of empty emails. This addresses the issue of empty infrastructure alert emails.

Fixes #3054

Summary by CodeRabbit

  • Improvements
    • Enhanced email template reliability with improved error handling and reporting.
    • Optimized email template initialization for faster performance.
    • Added template caching and lazy-loading for consistent email delivery.

@coderabbitai
Copy link

coderabbitai bot commented Nov 9, 2025

Note

.coderabbit.yml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'release_notes'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

The EmailService class was refactored to use synchronous initialization with template preloading instead of asynchronous initialization. A new loadTemplate() method loads and compiles templates, while buildEmail() now supports lazy-loading and caching of templates with enhanced error handling for templating failures.

Changes

Cohort / File(s) Summary
Template initialization and caching
server/src/service/v1/infrastructure/emailService.js
Changed init method from async to synchronous with preloaded templates; added loadTemplate() method for loading and compiling templates; introduced templateLookup object for caching compiled templates; enhanced buildEmail() with lazy-loading fallback and template validation; improved error handling with descriptive logging and re-throws

Sequence Diagram

sequenceDiagram
    participant Client
    participant EmailService
    participant TemplateSystem
    
    Note over Client,TemplateSystem: Initialization Phase
    Client->>EmailService: new EmailService()
    EmailService->>EmailService: init() [synchronous]
    EmailService->>TemplateSystem: loadTemplate() for each
    TemplateSystem-->>EmailService: compiled functions cached
    Note over EmailService: templateLookup populated
    
    Note over Client,TemplateSystem: Email Building Phase
    Client->>EmailService: buildEmail(templateName, ...)
    EmailService->>EmailService: Check templateLookup
    alt Template Found
        EmailService->>TemplateSystem: Use cached template
    else Template Not Found
        EmailService->>TemplateSystem: loadTemplate() with fallback logic
        TemplateSystem-->>EmailService: compiled function or null
        alt Load Success
            EmailService->>EmailService: Cache in templateLookup
        else Load Fails
            EmailService->>EmailService: Log & throw error
            EmailService-->>Client: Error
        end
    end
    EmailService->>TemplateSystem: Generate MJML → HTML
    EmailService-->>Client: HTML or empty string
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Areas requiring attention:
    • Logic in buildEmail() for template fallback resolution and lazy-loading behavior
    • Error handling paths and their impact on caller expectations
    • Verify all templates that should be preloaded are covered in init()
    • Confirm synchronous initialization doesn't block the application startup

Poem

🐰 Templates now load without a wait,
No empty emails, their proper fate!
Preloaded and cached, they're ready to go,
Infrastructure alerts will always show! 📧✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides context and includes the issue reference, but it lacks details on verification steps and doesn't follow the provided template with required checklist items. Add a completed checklist from the template showing local deployment, self-review, and testing verification to confirm the fix resolves the empty email issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing email template loading and error handling for infrastructure alerts, which directly addresses the linked issue.
Linked Issues check ✅ Passed The code changes directly address all key requirements from issue #3054: fixing template loading errors, preventing empty emails, and improving error handling through synchronous initialization and lazy-loading with caching.
Out of Scope Changes check ✅ Passed All changes are focused on the EmailService template loading mechanism and error handling, directly scoped to fixing the infrastructure alert email issue with no unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
server/src/service/v1/infrastructure/emailService.js (1)

51-67: Consider caching template load failures to avoid repeated disk I/O.

When loadTemplate returns null (line 65), preloaded templates that fail will be null in templateLookup. Later, when buildEmail encounters a null template, it will attempt to lazy-load it again (line 100). If the template file is genuinely missing or has a persistent compilation error, this results in repeated filesystem reads and failed compilations on every call.

Consider one of these approaches:

Option 1: Cache failures explicitly

 this.loadTemplate = (templateName) => {
   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({
       message: error.message,
       service: SERVICE_NAME,
       method: "loadTemplate",
       stack: error.stack,
     });
-    // Return null when template can't be loaded so callers can handle it explicitly
-    return null;
+    // Return false to distinguish cached failures from missing entries
+    return false;
   }
 };

Then in buildEmail (line 96), check for false:

-if (typeof tplFn !== "function") {
+if (tplFn === false) {
+  // Template previously failed to load, don't retry
+  throw new Error(`Template not found or failed to compile: ${template}`);
+}
+if (typeof tplFn !== "function") {
   const baseName = String(template).replace(/Template$/i, "");
   tplFn = this.loadTemplate(baseName);
-  if (typeof tplFn === "function") {
+  if (tplFn === false) {
+    this.templateLookup[template] = false; // cache the failure
+    throw new Error(`Template not found or failed to compile: ${template}`);
+  } else if (typeof tplFn === "function") {
     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}`);
   }
 }

Option 2: Fail-fast during initialization

If any of the preloaded templates are critical, consider throwing an error during init() when a required template fails to load.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb9851 and d8e57a4.

📒 Files selected for processing (1)
  • server/src/service/v1/infrastructure/emailService.js (4 hunks)
🔇 Additional comments (3)
server/src/service/v1/infrastructure/emailService.js (3)

40-44: LGTM: Synchronous initialization is appropriate.

The change from async to sync initialization is correct since loadTemplate uses synchronous file operations (readFileSync). The detailed comments clearly explain the rationale for preventing race conditions.


91-128: Excellent error handling! Addresses the PR objective of preventing empty emails.

The enhanced error handling in buildEmail correctly throws errors when templates fail to load or compile (line 126), preventing the system from silently sending empty emails. This directly addresses the issue described in #3054.

The lazy-loading mechanism (lines 97-112) with caching is well-implemented and provides good fallback support for templates not preloaded during initialization.

Minor observation about line 116:

The fallback to empty string || "" might hide edge cases where mjml2html returns an unexpected value (e.g., {html: null}). However, since any error during template execution or MJML conversion is caught and re-thrown (line 126), this fallback should only apply to successful conversions with falsy HTML, which is likely acceptable.

Template naming convention (line 99):

The logic assumes template keys in templateLookup end with "Template" suffix (e.g., hardwareIncidentTemplate) while the actual .mjml files use the base name (e.g., hardwareIncident.mjml). This is consistent with the preloaded templates (lines 74-83) and should work correctly. Consider documenting this convention in the JSDoc for buildEmail to help future maintainers.


74-83: All template files verified as present.

Verification confirms that all 8 template files referenced in the templateLookup initialization exist in the server/src/templates/ directory. No issues found.

Copy link

@llamapreview llamapreview bot left a comment

Choose a reason for hiding this comment

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

AI Code Review by LlamaPReview

🎯 TL;DR & Recommendation

Recommendation: Request Changes

This PR enhances email template reliability but introduces a breaking change in error handling that could crash existing callers, along with an async/await mismatch that may return Promises instead of HTML strings.

📄 Documentation Diagram

This diagram documents the refactored email template loading and rendering flow with lazy-loading capabilities.

sequenceDiagram
    participant C as Caller
    participant E as EmailService
    participant T as Template Loader
    participant M as MJML Compiler
    C->>E: buildEmail(template, context)
    E->>E: Check templateLookup[template]
    alt template is function
        E->>E: tplFn(context)
    else template not found
        note over E: PR #35;3059 added lazy-loading logic
        E->>T: loadTemplate(baseName)
        T->>T: readFileSync and compile
        alt template loaded
            E->>E: Cache template in lookup
        else template error
            E->>E: Log error and return null
        end
    end
    E->>M: mjml2html(mjml)
    M-->>E: result
    E-->>C: html or error
Loading

🌟 Strengths

  • Addresses the core issue of empty emails by improving error handling and logging.
  • Implements lazy-loading and caching for better performance and consistency.
Priority File Category Impact Summary Anchors
P1 server/src/.../emailService.js Architecture Breaking change crashes callers without try-catch method:buildEmail, path:settingsController
P1 server/src/.../emailService.js Bug Async/await mismatch returns Promise instead of HTML
P2 server/src/.../emailService.js Performance Synchronous file reads may block event loop
P2 server/src/.../emailService.js Maintainability Implicit template naming could cause confusion
P2 server/src/.../emailService.js Documentation Error message lacks template path for debugging

🔍 Notable Themes

  • Error Handling Changes: The shift from silent failures to throwing errors requires all callers to handle exceptions, potentially breaking existing integrations.
  • Asynchronous Consistency: Inconsistent use of async/await could lead to type errors and unexpected behavior in the email rendering pipeline.

📈 Risk Diagram

This diagram illustrates the breaking change in error handling and async/await mismatch risks introduced by the PR.

sequenceDiagram
    participant C as Caller
    participant E as EmailService
    C->>E: buildEmail(template, context)
    E->>E: Check if template is function
    alt template not function
        E->>E: Lazy load template
        alt load fails
            note over E: R1(P1): Throws error, breaking callers without try-catch
            E-->>C: Error thrown
        end
    end
    E->>E: Render MJML without await
    note over E: R2(P1): May return Promise if mjml2html is async
    E-->>C: Potential Promise instead of HTML
Loading

💡 Have feedback? We'd love to hear it in our GitHub Discussions.
✨ This review was generated by LlamaPReview Advanced, which is free for all open-source projects. Learn more.

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
Copy link

Choose a reason for hiding this comment

The 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 buildEmail creates a breaking change for all callers. The related context shows settingsController.js calls buildEmail without try-catch, meaning template errors will now bubble up and crash the email test endpoint. This impacts method:buildEmail because existing callers expect graceful failure but will now receive unhandled exceptions.

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

Comment on lines +114 to +116
const mjml = tplFn(context);
const result = this.mjml2html(mjml);
return (result && result.html) || "";
Copy link

Choose a reason for hiding this comment

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

P1 | Confidence: High

The mjml2html function is called without await, but the method is marked async and the previous implementation used await. If mjml2html returns a Promise (asynchronous MJML compilation), this will return a Promise instead of the HTML string, breaking the email rendering pipeline and causing downstream type errors.

Suggested change
const mjml = tplFn(context);
const result = this.mjml2html(mjml);
return (result && result.html) || "";
const mjml = tplFn(context);
const result = await this.mjml2html(mjml);
return (result && result.html) || "";

// 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 = () => {
Copy link

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:

this.initialized = false;
init = async () => {
	// ... async file operations
	this.initialized = true;
};
// In buildEmail, check initialization status

Comment on lines +97 to +100
// 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);
Copy link

Choose a reason for hiding this comment

The 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 (/Template$/i) which might mask naming inconsistencies.

Code Suggestion:

const templateMappings = {
	hardwareIncidentTemplate: 'hardwareIncident',
	// ... other mappings
};
const baseName = templateMappings[template] || template.replace(/Template$/i, "");

Comment on lines +104 to +111
} 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}`);
}
Copy link

Choose a reason for hiding this comment

The 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
} 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}`);
}
throw new Error(`Template not found or failed to compile: ${template} (resolved to: ${baseName}.mjml)`);

Copy link
Collaborator

@ajhollid ajhollid left a comment

Choose a reason for hiding this comment

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

Hi @Retr0-XD

This looks like a good addition, however the behavior of buildEmail has chagned to now throw, so please go through the application and make sure that all callers of buildEmail are properly handling the new behavior.

Thanks!

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.

Infrastructure alerts emails are empty

2 participants