Skip to content

fix(worker): never report an empty handler rejection reason - #14

Merged
proggeramlug merged 2 commits into
mainfrom
fix/rejection-reason-diagnosability
Aug 27, 2026
Merged

fix(worker): never report an empty handler rejection reason#14
proggeramlug merged 2 commits into
mainfrom
fix/rejection-reason-diagnosability

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Problem

A rejected handler promise is reported as:

ERROR deployment dispatch failed deployment=bench-001
      error=Runtime(invoking deployment HTTP handler: handler promise rejected: )

Nothing after the colon. On a production 500 that names neither the failure nor
where to look.

Cause

let reason_str = read_perry_string(reason)
    .unwrap_or_else(|| format!("0x{:016x}", reason.to_bits()));

The hex fallback only fires when read_perry_string returns None, which it
does only for a value that is not a string at all. A rejection carrying an
Error object reads back here as a zero-length string, so it takes the
Some("") path and prints nothing — the fallback that exists for exactly this
case never runs.

Change

Distinguish the three outcomes and never render an empty reason:

  • a non-empty string -> the string, as before
  • an empty string -> <empty string> (raw 0x…)
  • not a string -> <non-string rejection value> (raw 0x…)

The raw NaN-boxed bits are always included now, so a bug report has something to
correlate even when the value cannot be rendered.

Why now

Found while investigating PerryTS/perry#8546: two Next.js applications hosted
in_process in one daemon leave the second serving 500 with exactly this empty
reason. Both applications load and initialize; the failure is at dispatch. With
the reason blank there is nothing to attribute, and the remaining suspects are a
list of about seven process-global registries — this converts that guesswork into
a named failure.

Testing

cargo check -p coop-worker — clean.

Deliberately does not include the rusqlite 0.37 -> 0.39 bump needed to
build against current Perry main; that has to land atomically with a
perry-main.lock pin bump (bumping it alone breaks against the current pin — I
verified this, cargo refuses with the same libsqlite3-sys links conflict in the
opposite direction). Separate PR.

Summary by CodeRabbit

  • Bug Fixes
    • Improved promise rejection error messages.
    • Empty and non-string rejection reasons now include descriptive context and diagnostic details instead of appearing blank or unlabeled.
    • Non-string rejection values are converted to readable text when possible.
    • Added safer handling for invalid or unexpected rejection values.

A rejected handler promise renders as

    deployment dispatch failed error=Runtime(invoking deployment HTTP
    handler: handler promise rejected: )

with nothing after the colon. That names neither the failure nor where to
look, and it is what an operator actually sees on a 500.

read_perry_string returns None only when the value is not a string, so a
rejection carrying an Error object -- which reads back here as a
zero-length string -- took the Some("") path and printed nothing. Empty
string and non-string are now distinguished, and the raw NaN-boxed bits
are always included so a bug report has something to correlate.

Found while investigating PerryTS/perry#8546, where two Next.js
applications in one in_process daemon leave the second serving 500 with
exactly this empty reason.
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: da9f3d94-ed86-4615-b5dc-7ae00030f420

📥 Commits

Reviewing files that changed from the base of the PR and between e659dfa and f887099.

📒 Files selected for processing (2)
  • crates/coop-worker/src/plugin_host.rs
  • crates/coop-worker/src/runtime_libraries.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/coop-worker/src/plugin_host.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The plugin host now validates Perry string values and formats promise rejection reasons by type. Runtime initialization provides JavaScript-value stringification for non-string rejection values.

Changes

Promise rejection formatting

Layer / File(s) Summary
Wire runtime string conversion
crates/coop-worker/src/runtime_libraries.rs
RuntimeApi now exposes js_jsvalue_to_string, and runtime initialization resolves the corresponding Perry symbol.
Format rejection reasons
crates/coop-worker/src/plugin_host.rs
Non-empty strings remain unchanged. Empty strings and non-string values include descriptive labels and raw NaN-box bits. String decoding validates NaN-box tags before reading string headers.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to f8870

The PR makes rejected handler failures report a non-empty diagnostic with raw value details; no actionable merge-blocking risk remains after normal checks.


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/settings/billing.

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

…the real error

read_perry_string trusted a contract that does not exist. Its comment says
js_get_string_pointer_unified 'Returns 0 if not a string', but that function
deliberately returns the payload for a POINTER_TAG (0x7ffd) value too --
'used for cross-module returns' (perry value/nanbox.rs). Handing it an Error
object yields a non-null pointer that is NOT a StringHeader, and reading
through it is a wild read: it fabricated a one-character reason ("\t") out
of unrelated heap bytes, which then took the success path and printed as if
it were the rejection reason.

Check the NaN-box tag before dereferencing, and stringify a non-string
rejection with js_jsvalue_to_string so an Error reports its message.

On the case that motivated this (PerryTS/perry#8546, two Next.js apps in one
in_process daemon), the reason goes from an empty string to

    handler promise rejected: TypeError: value is not a function

which is the documented signature of a GC rooting failure -- turning an
unattributable 500 into a named defect class.
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