chore(server): temporary diagnostic logging for empty verification email body (#21628)
## What Adds **temporary** diagnostic logging to `EmailVerificationService.sendVerificationEmail` so we can capture the real error behind the empty verification email body in deployed environments. ## Why Verification emails are delivered with an **empty body** (subject is fine). The body is `<!DOCTYPE html …><!--$!--><template></template><!--/$-->` — an **errored React Suspense boundary**. `@react-email/render`'s `render()` wraps the email in `<Suspense>` and streams via `renderToReadableStream` **without an `onError` handler**, so any throw during SSR is swallowed into the errored boundary and the body ships empty. In production React also strips the error text from the markup, so the cause is invisible. This could **not** be reproduced locally on `main` (renders fine in dev, in the production-focused `yarn workspaces focus --production` install layout, and on the React 18 + react-email 6 dep set), so we need the error from a deployed environment. ## What it logs When the rendered html is empty or contains `<!--$!-->`, it logs (prefix `EMAIL_VERIFICATION_RENDER_DEBUG`): - locale, trigger, html length, and the first 400 chars of the html; - the **real error + stack**, obtained by re-rendering synchronously with `renderToStaticMarkup` (which re-throws instead of swallowing). No behavior change on the happy path — the block only runs when rendering already failed. ## How to use Deploy, trigger a verification email (sign up / resend), then: ``` grep -i "EMAIL_VERIFICATION_RENDER_DEBUG" <twenty-server logs> ``` ## Revert Remove this block once the root cause is identified.
This commit is contained in:
+33
-1
@@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { SendEmailVerificationLinkEmail } from 'twenty-emails';
|
||||
@@ -31,6 +32,8 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class EmailVerificationService {
|
||||
private readonly logger = new Logger(EmailVerificationService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AppTokenEntity)
|
||||
private readonly appTokenRepository: Repository<AppTokenEntity>,
|
||||
@@ -95,6 +98,35 @@ export class EmailVerificationService {
|
||||
const emailTemplate = SendEmailVerificationLinkEmail(emailData);
|
||||
|
||||
const html = await render(emailTemplate);
|
||||
|
||||
// TEMPORARY DIAGNOSTIC (remove once the empty verification email body is root-caused):
|
||||
// render() wraps the tree in <Suspense> and uses renderToReadableStream WITHOUT an
|
||||
// onError, so any SSR throw is swallowed into an errored boundary and the body ships
|
||||
// as `<!DOCTYPE ...><!--$!--><template></template><!--/$-->`. Detect that and re-render
|
||||
// synchronously to surface the real error + stack (production strips it from the markup).
|
||||
if (html.length === 0 || html.includes('<!--$!-->')) {
|
||||
this.logger.error(
|
||||
`[EMAIL_VERIFICATION_RENDER_DEBUG] verification email rendered empty/errored (locale=${locale}, trigger=${verificationTrigger}, htmlLength=${html.length})`,
|
||||
);
|
||||
this.logger.error(
|
||||
`[EMAIL_VERIFICATION_RENDER_DEBUG] html start: ${html.slice(0, 400)}`,
|
||||
);
|
||||
try {
|
||||
renderToStaticMarkup(emailTemplate);
|
||||
this.logger.error(
|
||||
'[EMAIL_VERIFICATION_RENDER_DEBUG] renderToStaticMarkup did not throw — likely an async/Suspense rejection rather than a synchronous throw',
|
||||
);
|
||||
} catch (renderError) {
|
||||
this.logger.error(
|
||||
`[EMAIL_VERIFICATION_RENDER_DEBUG] underlying render error: ${
|
||||
renderError instanceof Error
|
||||
? `${renderError.message}\n${renderError.stack}`
|
||||
: String(renderError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const text = await render(emailTemplate, {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user