Skip to content

fix: coroutine safe markdown rendering#386

Merged
binaryfire merged 32 commits into
0.4from
fix/coroutine-safe-markdown-rendering
May 13, 2026
Merged

fix: coroutine safe markdown rendering#386
binaryfire merged 32 commits into
0.4from
fix/coroutine-safe-markdown-rendering

Conversation

@binaryfire
Copy link
Copy Markdown
Collaborator

This PR makes Markdown mail rendering safe under concurrent Swoole coroutines. Markdown mail rendering previously changed shared worker state while rendering:

  • EncodedHtmlString::encodeUsing() was temporarily changed and then flushed.
  • The shared view finder had its mail namespace replaced between HTML and text renders.
  • Per-message Markdown themes were applied by mutating the shared Markdown renderer.

That's fine for per-request PHP but unsafe in Hypervel where multiple coroutines can render mail at the same time. Under concurrent HTML and text renders, mail::layout, mail::message, and related components could resolve through the wrong path mid-render, mixing HTML and text mail components.

There was also a separate single-render bug: a Markdown render could clear a boot-time EncodedHtmlString::encodeUsing() encoder even when secured markdown encoding was not active.

Changes

  • Added EncodedHtmlString::withEncoding() for coroutine-local temporary encoder overrides.
  • Updated Markdown render/parse paths to use scoped encoder overrides instead of mutating the global encoder.
  • Added Factory::__clone() so Markdown can render through an isolated view factory/finder clone.
  • Updated Markdown HTML/text rendering to replace the mail namespace only on the cloned factory.
  • Updated <x-mail::...> component compilation to resolve through the current $__env, so mail components stay on the cloned factory. This is a deliberate Laravel divergence; upstream resolves the singleton view factory through the container.
  • Passed message-specific Markdown themes as render parameters instead of mutating the shared Markdown singleton.
  • Updated mailables, notification mail channel, and MailMessage::render() to use the per-render theme path. The markdownRenderer() helpers were removed because they only existed to mutate the singleton.
  • Added regression tests for encoder isolation, view factory cloning, HTML/text namespace isolation, <x-mail::...> resolution, and theme isolation.

Normal view rendering remains on the original fast path. The temporary mail:: namespace swap is now local to just Markdown rendering and does not add overhead to regular package views.

Alternative Approaches

My first iteration put temporary view namespace overrides in CoroutineContext. That fixed the race, but every normal namespaced view lookup (package::view, pagination::default, etc.) had to check for an active override. In package-heavy apps, that adds a lot of unnecessary context lookups to the global view path for a Markdown-specific problem.

I also considered registering separate internal namespaces for HTML and text mail components. That keeps the finder hot path clean, but it changes template semantics and misses dynamic component names like $component = 'mail::message'.

The clone approach keeps the normal view finder unchanged, preserves mail:: semantics, supports dynamic names, and pays the small isolation cost only when Markdown mail is actually rendered.

binaryfire added 30 commits May 13, 2026 02:26
@binaryfire binaryfire merged commit 0a6dd7f into 0.4 May 13, 2026
32 of 34 checks passed
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.

1 participant