Skip to content

fix(types): allow send() to accept PromiseLike payloads - #6526

Closed
skdas20 wants to merge 2 commits into
fastify:mainfrom
skdas20:fix/send-accepts-promise-payload
Closed

skdas20 wants to merge 2 commits into
fastify:mainfrom
skdas20:fix/send-accepts-promise-payload

Conversation

@skdas20

@skdas20 skdas20 commented Feb 22, 2026 •

Copy link
Copy Markdown

What this does

Two things, which are separable:

1. Makes reply.send(promise) actually work at runtime. SendArgs is widened to accept PromiseLike<ReplyType>, and Reply.prototype.send routes a thenable payload through the existing wrap-thenable path instead of serializing the promise object itself.

2. Stops a dropped thenable from crashing the process. When send() bails out through the this.sent === true branch, the payload is discarded. If that payload is a promise that later rejects, nothing has attached a handler to it, so it surfaces as an unhandled rejection — fatal by default on modern Node. send() now attaches a handler that logs it instead. This one is a robustness fix that applies regardless of whether (1) is wanted, since a JS user can pass a promise today whether or not the types permit it.

Behaviour

  • reply.send(Promise.resolve(x)) sends x; a rejection is sent through the error path.
  • With two promise payloads, the one that settles first wins, not the one send() was called for first — the payload isn't known until settlement. Asserted explicitly in calling reply.send(promise) twice is settle-ordered, not call-ordered, since it differs from the plain-value case.

Tests

test/internals/reply.test.js covers resolve, reject, return reply.send(promise) from a sync handler, send() inside an async handler, double-send with already-resolved promises, double-send settling out of order, and the dropped-rejection case. Type tests updated in reply.tst.ts, type-provider.tst.ts, instance.tst.ts.

Locally: 89 unit tests in reply.test.js, tstyche 1284 assertions across 16 files, lint clean.

Status

Opened originally as a types-only change. It grew into the above because the widened type would otherwise have described behaviour the runtime did not implement. See the discussion below on whether the feature is wanted at all.

@github-actions github-actions Bot added the typescript TypeScript related label Feb 22, 2026
mcollina
mcollina previously approved these changes Feb 22, 2026

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@jean-michelet

Copy link
Copy Markdown
Member

Can you plz fix the lint error?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Fastify’s TypeScript types to allow reply.send() to accept PromiseLike<T> payloads and adds type-level tests asserting the new behavior.

Changes:

  • Widen SendArgs<ReplyType> to include PromiseLike<ReplyType> in all branches (optional/required payload cases).
  • Add/adjust tsd tests to validate reply.send(Promise.resolve(...)) is accepted and wrong resolved types are rejected.
  • Add a note in the SendArgs JSDoc claiming runtime support via wrap-thenable.js.

Reviewed changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated 1 comment.

File Description
types/type-provider.d.ts Broadens SendArgs to allow PromiseLike payloads and updates comment explaining the rationale.
test/types/reply.test-d.ts Updates tsd expectations and adds new type tests for reply.send() with Promise payloads.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread test/types/reply.test-d.ts Resolved
@Eomm Eomm added the bugfix Issue or PR that should land as semver patch label Jul 5, 2026
@Eomm

Eomm commented Jul 5, 2026

Copy link
Copy Markdown
Member

@skdas20 would you like to finalize this PR?

@skdas20
skdas20 force-pushed the fix/send-accepts-promise-payload branch from 77d8eb5 to badaa91 Compare July 26, 2026 05:12
@skdas20

skdas20 commented Jul 26, 2026

Copy link
Copy Markdown
Author

Force-pushed a rebase + fix for the issues raised here:

  • Rebased onto current main to resolve the merge conflict, and fixed the lint error (trailing whitespace) @jean-michelet flagged.
  • Addressed the Copilot review comment: the type change alone claimed send() accepts a PromiseLike<T>, but runtime never actually resolved one — it would have just serialized the Promise object itself. Added the missing runtime support in Reply.prototype.send (reusing the existing wrap-thenable path that already handles this for handler return values), plus a couple of runtime tests (test/internals/reply.test.js) covering resolve and reject.
  • Updated the other type tests (type-provider.tst.ts, instance.tst.ts) whose expected send() signatures were affected by the widened type, and adjusted the test-typescript fixtures for the tst.ts rename that landed on main since this PR was opened.

All type tests (tstyche) and the full unit suite pass locally.

Comment thread test/internals/reply.test.js Resolved
@skdas20

skdas20 commented Jul 27, 2026

Copy link
Copy Markdown
Author

Thanks @Eomm — added all four, plus one more that writing them turned up.

Behaviour of the four cases (all now asserted):

  1. return reply.send(Promise.resolve('asd')) from a sync handler → asd
  2. reply.send(Promise.resolve('asd')) inside an async handler → asd
  3. send(Promise.resolve('one')) then send(Promise.resolve('two')) → one, second warns FST_ERR_REP_ALREADY_SENT (same as double-send with plain values)
  4. send(wait(1000) → 'one') then send(wait(500) → 'two') → two

Case 4 is worth a maintainer opinion: with promise payloads the winner is whichever settles first, not whichever send() was called first, because the payload isn't known until settlement. I think that's the only coherent semantic here (and it matches how wrap-thenable already behaves for returned promises), but it is a genuine difference from the plain-value case, so I've asserted it explicitly rather than left it implicit.

The extra fix: case 3/4 exposed that when send() bails out via the this.sent === true branch, a thenable payload was dropped with no handler ever attached — so if it later rejected, that surfaced as an unhandled rejection, which is fatal by default on modern Node. Since this PR makes promises a supported payload type, that path is now reachable through normal use, so send() now attaches a handler that logs the late rejection instead. Test included (a promise rejecting after the reply was sent is logged, not left unhandled).

Full unit suite green locally (2064 passing), lint clean. Happy to split the unhandled-rejection fix into its own PR if you'd rather keep this one narrow.

@Tony133

Tony133 commented Jul 30, 2026

Copy link
Copy Markdown
Member

@skdas20 As soon as you have time, can you resolve conflicts?

skdas20 added 2 commits August 1, 2026 06:20
Widens SendArgs so send() accepts a PromiseLike<ReplyType>, and adds the
matching runtime support in Reply.prototype.send(): a thenable payload is
now resolved (reusing the existing wrap-thenable path used for handler
return values) and its value sent, or its rejection reason sent as an
error.

Previously the type-only version of this change would have shipped a
type that didn't match runtime behavior: calling send() with a Promise
would have just serialized the Promise object itself. Flagged in review
by Copilot on the original PR.

Resolves merge conflicts and lint/CI failures from the original branch,
and updates the other type tests (type-provider, instance) whose
expected send() signatures were widened by this change.

Signed-off-by: Sumit Kumar Das <[email protected]>
Adds the cases @Eomm asked for: returning reply.send(promise) from a sync
handler, reply.send(promise) inside an async handler, and calling send()
twice with promises (both already-resolved and settling out of order).

Writing them surfaced a real hazard: when send() bails because the reply
was already sent, a thenable payload was dropped without any handler
attached, so an eventual rejection became an unhandled rejection (fatal
by default on modern Node). It now attaches a handler that logs it.

Signed-off-by: skdas20 <[email protected]>
@skdas20
skdas20 force-pushed the fix/send-accepts-promise-payload branch from bdaecef to e35ddb1 Compare August 1, 2026 06:23
@skdas20

skdas20 commented Aug 1, 2026

Copy link
Copy Markdown
Author

@Tony133 done — rebased onto main, conflicts resolved.

The only conflict was in test/internals/reply.test.js, where #6879's new quoted-charset= test and my promise-payload tests were both appended at the end of the file. Both are kept.

Green locally after the rebase: 89 unit tests in reply.test.js, tstyche 1284 assertions across 16 files, lint clean.

@Eomm the question from my earlier comment is still open whenever you have a moment: with two promise payloads, the one that settles first wins rather than the one send() was called for first (asserted in calling reply.send(promise) twice is settle-ordered, not call-ordered). It's the only coherent semantic I could find, since the payload isn't known until settlement, but it does differ from the plain-value case, so I'd rather it be a deliberate decision than an accident of implementation.

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@Tony133
Tony133 requested a review from Eomm August 1, 2026 08:08
@mcollina

mcollina commented Aug 2, 2026

Copy link
Copy Markdown
Member

Can you change the PR description?

This is a full implementation of that feature.

I'm not exactly sure we should land this, because what this wants to achieve is already supported with a slightly different API: return the thenable from the handler. Why adding it?

@skdas20

skdas20 commented Aug 2, 2026

Copy link
Copy Markdown
Author

Description rewritten — it described the original types-only diff, which was stale. It now states plainly that this is a feature implementation plus a separable robustness fix.

On whether it should land: your instinct is right, and I don't have a user-facing justification to offer. There is no linked issue and no reported problem behind this PR. It began as a types-only widening, and the runtime work exists only because that widened type would otherwise have described behaviour send() did not implement — the type would have been lying. That's a reason the two halves can't ship apart; it is not a reason the feature is needed.

Since return thenable is the supported idiom and already covers the use case, I think the honest options are:

  1. Close it. return is the API, send() stays value-only, and nothing is owed. I'd have no objection — a second path to the same outcome is API surface and maintenance for no new capability, plus it forces you to take a position on the settle-order semantics above.
  2. Take only the unhandled-rejection fix, as a small standalone PR. That one stands on its own: a JS user can pass a promise to send() today regardless of the types, and if the reply was already sent, the payload is dropped with no handler attached, so a later rejection takes the process down. That's a defect independent of whether send(promise) is ever a supported API.
  3. Land the whole thing, if you see a case I don't.

I'd suggest (2). Happy to open it as a separate PR against a clean branch and close this one — just say the word rather than my assuming.

@skdas20

skdas20 commented Aug 10, 2026

Copy link
Copy Markdown
Author

@mcollina I've opened #6936 with just the unhandled-rejection half — option (2) from my previous comment. It's four lines plus a test and doesn't touch SendArgs or add any API, so it can be judged without deciding anything about send(promise).

Leaving this PR open for now in case you want the feature, but I have no attachment to it — if your answer is that return thenable is the API, closing this is the right outcome and #6936 covers the part that's a defect either way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Issue or PR that should land as semver patch typescript TypeScript related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants