Skip to content

feat(types): allow plugin methods to return synchronously#8208

Open
vishwakt wants to merge 1 commit into
apollographql:mainfrom
vishwakt:fix-plugin-method-types-issue-8146
Open

feat(types): allow plugin methods to return synchronously#8208
vishwakt wants to merge 1 commit into
apollographql:mainfrom
vishwakt:fix-plugin-method-types-issue-8146

Conversation

@vishwakt

@vishwakt vishwakt commented May 8, 2026

Copy link
Copy Markdown

Fixes #8146

Description

This PR updates the ApolloServerPlugin and various listener method types to allow synchronous returns by introducing and using a PromiseOrValue<T> helper instead of strictly requiring Promise<T>.

Previously, plugin authors were forced to define their methods as async or explicitly return a resolved Promise, even when their implementation performed no asynchronous operations (e.g., simple logging or stat collection). This created friction, especially when porting existing JavaScript plugins to TypeScript.

This change strictly widens the types, making it purely backward compatible. All internal utilities, such as the invokeHooks methods, already await or cleanly handle non-promise values seamlessly through Promise.all and standard asynchronous loops, so the runtime behavior remains completely unaffected.

Changes Made

  • Introduced export type PromiseOrValue<T> = Promise<T> | T; in packages/server/src/externalTypes/plugins.ts.
  • Replaced Promise<T> with PromiseOrValue<T> on plugin hooks (requestDidStart, serverWillStart, etc.) and event listener methods.
  • Updated invokeHooks.ts internals to correctly type the hooked functions using PromiseOrValue.
  • Added a test in packages/server/src/__tests__/ApolloServer.test.ts to ensure that fully synchronous plugin methods compile and execute correctly.

Testing

  • Ran npm run compile to verify there are no TypeScript errors.
  • Ran npm run test and confirmed all tests pass locally.
  • Added an explicit test suite verifying synchronous plugin execution.

Summary by CodeRabbit

  • New Features

    • Plugin lifecycle hooks now accept either synchronous or Promise-based returns across server, request, and response lifecycles.
  • Tests

    • Added coverage verifying plugins with synchronous hook implementations run correctly through startup, request handling, and shutdown.
  • Documentation

    • Added a changeset entry documenting the plugin hook return-type support for synchronous returns.

@apollo-cla

Copy link
Copy Markdown

@vishwakt: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/

@apollo-librarian

apollo-librarian Bot commented May 8, 2026

Copy link
Copy Markdown

✅ AI Style Review — No Changes Detected

No MDX files were changed in this pull request.

Review Log: View detailed log

This review is AI-generated. Please use common sense when accepting these suggestions, as they may not always be accurate or appropriate for your specific context.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b2b0842d-24b2-4435-83a3-3ff50b02d9b6

📥 Commits

Reviewing files that changed from the base of the PR and between 34d8401 and 69128d3.

📒 Files selected for processing (5)
  • .changeset/sharp-animals-talk.md
  • packages/server/src/__tests__/ApolloServer.test.ts
  • packages/server/src/externalTypes/index.ts
  • packages/server/src/externalTypes/plugins.ts
  • packages/server/src/utils/invokeHooks.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • .changeset/sharp-animals-talk.md
  • packages/server/src/tests/ApolloServer.test.ts
  • packages/server/src/utils/invokeHooks.ts
📜 Recent review details
🔇 Additional comments (3)
packages/server/src/externalTypes/index.ts (1)

31-31: Public API re-export is correct.

This export keeps PromiseOrValue available from the external type barrel, matching the widened plugin hook typing.

packages/server/src/externalTypes/plugins.ts (2)

28-28: PromiseOrValue helper introduction is clean and appropriate.

The alias is minimal, clear, and fits the plugin-author ergonomics goal without narrowing existing behavior.


50-57: Hook/listener return type widening is consistent and well applied.

The Promise<T>PromiseOrValue<T> updates are coherent across server hooks, request lifecycle hooks, and nested listener callbacks, preserving compatibility while allowing synchronous implementations.

Also applies to: 72-75, 82-85, 99-114, 119-119, 124-130, 139-190


📝 Walkthrough

Walkthrough

This PR introduces a new exported PromiseOrValue and widens plugin and listener hook types to accept synchronous or Promise returns, updates hook-invocation utilities to handle both forms, and adds a test verifying synchronous plugin hook behavior.

Changes

Synchronous plugin hook support

Layer / File(s) Summary
Plugin type contracts
packages/server/src/externalTypes/plugins.ts, packages/server/src/externalTypes/index.ts
Introduce PromiseOrValue<T> and change plugin/listener hook return types (e.g., serverWillStart, requestDidStart, serverWillStop, willSendResponse, parsing/validation/execution hooks, LandingPage.html) from Promise<...> to PromiseOrValue<...>.
Hook invocation utilities
packages/server/src/utils/invokeHooks.ts
Update invokeDidStartHook and invokeHooksUntilDefinedAndNonNull signatures to accept hooks that return PromiseOrValue while preserving Promise-based outward behavior and supporting synchronous end-hooks.
Synchronous hook behavior verification
packages/server/src/__tests__/ApolloServer.test.ts
Add a test that registers a plugin whose lifecycle hooks return synchronously (and whose end-hooks are synchronous), runs a query, stops the server, and asserts the synchronous hooks were called.
Changeset
.changeset/sharp-animals-talk.md
Add a Changeset entry documenting the minor release note that plugin methods can return synchronously.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: allowing plugin methods to return synchronously instead of requiring Promise returns.
Linked Issues check ✅ Passed The PR fully addresses issue #8146 by introducing PromiseOrValue type and updating all plugin hook signatures to accept synchronous returns while maintaining backward compatibility.
Out of Scope Changes check ✅ Passed All changes directly support the stated objective of allowing synchronous plugin method returns; no unrelated modifications were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Microsoft Presidio Analyzer (2.2.362)
.changeset/sharp-animals-talk.md

Microsoft Presidio Analyzer failed to scan this file

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codesandbox-ci

codesandbox-ci Bot commented May 8, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/server/src/utils/invokeHooks.ts (1)

17-21: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Async end-hook Promises are silently dropped — didEndHook result must be awaited.

The returned wrapper is declared async, and callers await it (so they expect all end-hook work to have completed). However, the inner didEndHook(...args) call discards its return value. If any collected end hook returns a Promise<void> (now expressible via PromiseOrValue<void>), that promise is silently dropped and the server proceeds before the async work finishes.

This was a pre-existing gap, but this PR widens the type to PromiseOrValue<void> precisely to support async returning hooks, making it more likely that real async end hooks will be silently ignored at runtime.

🐛 Proposed fix — await each end hook in series
   return async (...args: TEndHookArgs) => {
     for (const didEndHook of didEndHooks) {
-      didEndHook(...args);
+      await didEndHook(...args);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/utils/invokeHooks.ts` around lines 17 - 21, The returned
async wrapper currently invokes each didEndHook without awaiting their results,
which drops Promises; update the returned function inside invokeHooks (the async
(...args: TEndHookArgs) => { ... }) to await each didEndHook(...args) so
asynchronous end-hooks complete before returning (i.e., replace
didEndHook(...args) with await didEndHook(...args) while iterating over
didEndHooks to run them in series).
🧹 Nitpick comments (1)
packages/server/src/utils/invokeHooks.ts (1)

4-4: 💤 Low value

AsyncDidEndHook is a misnomer now that the return type is PromiseOrValue<void>.

Since the type union includes plain void, calling it "Async" is misleading. Consider renaming to MaybeAsyncDidEndHook or just DidEndHook to match the widened semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/utils/invokeHooks.ts` at line 4, The type
AsyncDidEndHook<TArgs extends any[]> is misleading because its return is
PromiseOrValue<void>, so rename the type to DidEndHook<TArgs extends any[]> (or
MaybeAsyncDidEndHook) and update all references/usages in this module (e.g.,
functions or arrays that accept AsyncDidEndHook in invokeHooks.ts) to the new
name; adjust any exported type names and import sites accordingly to keep public
API consistent and run a quick compile to fix any remaining identifier
references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/server/src/__tests__/ApolloServer.test.ts`:
- Around line 822-853: The test uses a single syncCalled flag so serverWillStart
immediately makes the final assertion useless; change syncPlugin to track
separate flags (e.g., startedCalled, stoppedCalled, willSendResponseCalled) or a
counter for each hook inside serverWillStart, serverWillStop, requestDidStart,
and willSendResponse, then update the final assertions to verify each specific
flag/counter was incremented (or true) to ensure all three synchronous hooks
(serverWillStart, serverWillStop, requestDidStart->willSendResponse) actually
ran.

In `@packages/server/src/externalTypes/plugins.ts`:
- Line 28: The public export list in externalTypes/index.ts is missing the
PromiseOrValue type (defined in plugins.ts) so plugin authors can't import it;
update the export block that re-exports from './plugins.js' to include
PromiseOrValue alongside ApolloServerPlugin, GraphQLFieldResolverParams, etc.,
so PromiseOrValue becomes part of the package's public API and can be imported
from `@apollo/server` by plugin authors.

---

Outside diff comments:
In `@packages/server/src/utils/invokeHooks.ts`:
- Around line 17-21: The returned async wrapper currently invokes each
didEndHook without awaiting their results, which drops Promises; update the
returned function inside invokeHooks (the async (...args: TEndHookArgs) => { ...
}) to await each didEndHook(...args) so asynchronous end-hooks complete before
returning (i.e., replace didEndHook(...args) with await didEndHook(...args)
while iterating over didEndHooks to run them in series).

---

Nitpick comments:
In `@packages/server/src/utils/invokeHooks.ts`:
- Line 4: The type AsyncDidEndHook<TArgs extends any[]> is misleading because
its return is PromiseOrValue<void>, so rename the type to DidEndHook<TArgs
extends any[]> (or MaybeAsyncDidEndHook) and update all references/usages in
this module (e.g., functions or arrays that accept AsyncDidEndHook in
invokeHooks.ts) to the new name; adjust any exported type names and import sites
accordingly to keep public API consistent and run a quick compile to fix any
remaining identifier references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c4b0c9d-2c30-4692-8842-499bdd5b4919

📥 Commits

Reviewing files that changed from the base of the PR and between 4f15406 and df7b1ea.

📒 Files selected for processing (3)
  • packages/server/src/__tests__/ApolloServer.test.ts
  • packages/server/src/externalTypes/plugins.ts
  • packages/server/src/utils/invokeHooks.ts
📜 Review details
🔇 Additional comments (1)
packages/server/src/externalTypes/plugins.ts (1)

44-190: LGTM — all hook signatures consistently widened.

The PromiseOrValue substitution is uniform across ApolloServerPlugin, GraphQLServerListener, and GraphQLRequestListener. Intentionally excluded hooks (schemaDidLoadOrUpdate, willResolveField) are correctly left untouched.

Comment thread packages/server/src/__tests__/ApolloServer.test.ts
Comment thread packages/server/src/externalTypes/plugins.ts
@vishwakt vishwakt force-pushed the fix-plugin-method-types-issue-8146 branch from df7b1ea to 34d8401 Compare May 8, 2026 04:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.changeset/sharp-animals-talk.md (1)

1-3: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Changeset file is empty and will not trigger a release.

The changeset file contains only the YAML delimiters but is missing the package name, version bump type, and release notes. This will prevent the changes from being included in the next release.

📝 Proposed changeset content
 ---
+"@apollo/server": patch
 ---
-
+
+Plugin and listener method types now accept synchronous returns via `PromiseOrValue<T>`, eliminating the need to mark simple plugin hooks `async` when no asynchronous work is performed.

Note: Consider using minor instead of patch if this is treated as a new feature (exporting PromiseOrValue<T> type). Use patch if it's purely a type fix/improvement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/sharp-animals-talk.md around lines 1 - 3, The changeset file is
empty so it won't trigger a release; open .changeset/sharp-animals-talk.md and
replace the empty YAML with a proper changeset entry including the package
name(s) (e.g., the package that exports PromiseOrValue<T>), the bump type (patch
or minor as appropriate), and a short release note describing the change (e.g.,
"export PromiseOrValue<T> type"); ensure the top-level YAML keys follow
changeset format (packages: or <package-name>: <bump>) and include the summary
under the body so the changeset will be picked up by the release tooling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.changeset/sharp-animals-talk.md:
- Around line 1-3: The changeset file is empty so it won't trigger a release;
open .changeset/sharp-animals-talk.md and replace the empty YAML with a proper
changeset entry including the package name(s) (e.g., the package that exports
PromiseOrValue<T>), the bump type (patch or minor as appropriate), and a short
release note describing the change (e.g., "export PromiseOrValue<T> type");
ensure the top-level YAML keys follow changeset format (packages: or
<package-name>: <bump>) and include the summary under the body so the changeset
will be picked up by the release tooling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7bd2f787-6d2c-4bbf-a399-108ed5fbb4be

📥 Commits

Reviewing files that changed from the base of the PR and between df7b1ea and 34d8401.

📒 Files selected for processing (4)
  • .changeset/sharp-animals-talk.md
  • packages/server/src/__tests__/ApolloServer.test.ts
  • packages/server/src/externalTypes/plugins.ts
  • packages/server/src/utils/invokeHooks.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/server/src/tests/ApolloServer.test.ts
  • packages/server/src/externalTypes/plugins.ts
📜 Review details
🔇 Additional comments (2)
packages/server/src/utils/invokeHooks.ts (2)

2-14: Typing update is aligned and backward compatible.

Allowing PromiseOrValue here cleanly supports sync hook implementations without breaking existing async plugin hooks.


47-50: Good widening of hook return contract.

This change correctly permits synchronous values while preserving existing control flow and return semantics.

Closes apollographql#8146

This updates the ApolloServerPlugin and various listener method types to
return PromiseOrValue<T> instead of Promise<T>. This allows plugin authors
to write synchronous methods without needing to use async/await when they
don't actually perform asynchronous work. Internal utilities like invokeHooks
were also updated to support and correctly type the mixed asynchronous returns.
@vishwakt vishwakt force-pushed the fix-plugin-method-types-issue-8146 branch from 34d8401 to 69128d3 Compare May 8, 2026 04:41
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.

Plugin method types shouldn't force users to return a promise

2 participants