TryGhost/Ghost · error · BadRequestError
Email is not valid
Error message
Email is not valid
What it means
A BadRequestError from `MagicLinkService.sendMagicLink` when `isEmail(options.email)` returns false before any token is created or mail sent. It is the first guard in the magic-link/OTC send flow, so an invalid recipient short-circuits immediately. The check uses the `isEmail` validator, not a custom regex.
Source
Thrown at ghost/core/core/server/services/lib/magic-link/magic-link.js:76
this.labsService = options.labsService || undefined;
}
/**
* sendMagicLink
*
* @param {object} options
* @param {string} options.email - The email to send magic link to
* @param {TokenData} options.tokenData - The data for token
* @param {string} [options.type='signin'] - The type to be passed to the url and content generator functions
* @param {string} [options.referrer=null] - The referrer of the request, if exists. The member will be redirected back to this URL after signin.
* @param {boolean} [options.includeOTC=false] - Whether to send a one-time-code in the email.
* @returns {Promise<{token: Token, otcRef: string | null, info: SentMessageInfo}>}
*/
async sendMagicLink(options) {
this.sentry?.captureMessage?.(`[Magic Link] Generating magic link`, {extra: options});
if (!isEmail(options.email)) {
throw new BadRequestError({
message: tpl(messages.invalidEmail)
});
}
const token = await this.tokenProvider.create(options.tokenData);
const type = options.type || 'signin';
const url = this.getSigninURL(token, type, options.referrer);
let otc = null;
if (options.includeOTC) {
try {
otc = await this.getOTCFromToken(token);
} catch (err) {
this.sentry?.captureException?.(err);
otc = null;
}View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Validate and trim the email with the same `isEmail` check on the client before calling `sendMagicLink`.
- Confirm you pass the address under the `email` key (not `address`/`to`).
- Strip whitespace/newlines: `email.trim()`.
- If the member's stored email is invalid, prompt correction in the portal before initiating the magic link.
Example fix
// before
await magicLink.sendMagicLink({email: input, tokenData}); // input may be invalid
// after
const email = String(input ?? '').trim();
if (!isEmail(email)) throw new Error('Enter a valid email');
await magicLink.sendMagicLink({email, tokenData}); Defensive patterns
Strategy: validation
Validate before calling
const {isEmail} = require('@tryghost/validator');
function validateMagicLinkEmail(email) {
const trimmed = String(email ?? '').trim();
if (!isEmail(trimmed)) throw new Error('Email is not valid');
return trimmed;
} Type guard
const isValidEmail = (v) => typeof v === 'string' && isEmail(v.trim());
Try / catch
try {
await magicLink.sendMagicLink({email, tokenData});
} catch (err) {
if (err.type === 'BadRequestError' && /not valid/i.test(err.message)) showEmailError(err.message);
else throw err;
} Prevention
- Trim and isEmail-check the address before calling sendMagicLink.
- Pass the address under the `email` option key.
- Disable the submit button until a valid email is entered.
When it happens
Trigger: Calling `sendMagicLink({email, tokenData, ...})` with an email that is missing, not a string, or fails the `isEmail` RFC check. Also when the field name differs (e.g. `address`) so `options.email` is undefined, or when whitespace/format issues trip the validator.
Common situations: A member types a malformed address in the portal sign-in form; the portal sends an empty string because the input was uncontrolled; an integration passes the member's name field instead of email; copy-paste introduces a trailing space or newline.
Related errors
- Failed to send magic link email
- Failed to update member
- Failed to fetch offer data
- Your email has failed to resubscribe, please try again
- No sign-in email found for ${member.email}
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/689042a10ac3f2a2.
Report an issue: GitHub.