Skip to content

fix(mailer): retry the configured template after a default fallback - #2911

Open
mansur-akhmadi wants to merge 1 commit into
supabase:masterfrom
mansur-akhmadi:fix/template-default-fallback-retry
Open

mansur-akhmadi wants to merge 1 commit into
supabase:masterfrom
mansur-akhmadi:fix/template-default-fallback-retry

Conversation

@mansur-akhmadi

Copy link
Copy Markdown

What kind of change does this PR introduce?

Bug fix. Fixes #2910

What is the current behavior?

If the first fetch of a template fails and nothing is cached yet, the cache stores the default template. Cache.get treats that entry like a normal one and serves it until TemplateMaxAge (10m) runs out. TemplateRetryInterval (10s) only kicks in after that. With template reloading disabled (the default) one failed fetch at a bad moment means ten minutes of default emails.

The background reloader already handles this: reloadAt skips the max age check when ent.def is set.

What is the new behavior?

Cache.get skips the max age check for default fallback entries too, so the configured template is retried after TemplateRetryInterval. Nothing else changes:

  • a successfully fetched template is still cached for TemplateMaxAge;
  • if a refresh fails after a good fetch, the previous custom template is kept (def stays false on the copy);
  • while the endpoint stays down, the default is retried at most once per retry interval per template type, the same as for an expired custom template.

How this change was tested

New TestCacheGet in internal/mailer/templatemailer/template_test.go uses a fake clock and an httptest server that counts requests. It covers the cases above, plus the background reloader path. DefaultFallbackIsRetriedAfterRetryInterval fails on master (no retry happens, request count stays at 1) and passes with the fix.

go test -race -count=20 ./internal/mailer/..., go vet ./..., make static sec and make check-format pass locally.

Additional context

The tradeoff: if the template endpoint is down for a long time, mail requests for that type now try the fetch every TemplateRetryInterval rather than every TemplateMaxAge. That's the same retry behaviour expired custom templates already get, and what the background reloader already does for the fallback.

AI disclosure

AI assistance was used for the investigation and for drafting the test. I reviewed every line and reproduced the fail-before / pass-after locally.

When the first load of a template fails and there is no previous entry,
the cache stores the default template. Cache.get treated that entry like
any other and served it until TemplateMaxAge (10m by default), so a short
outage of the template endpoint at the wrong moment meant default emails
for the next ten minutes.

The background reloader already skips the max age check for default
entries and retries them after TemplateRetryInterval. Do the same in get,
which is the only path when template reloading is disabled (the default).
@mansur-akhmadi
mansur-akhmadi requested a review from a team as a code owner November 6, 2026 15:00

This branch has not been deployed

No deployments
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.

Default email template is served for 10 minutes after one failed template fetch

1 participant