diff --git a/.changeset/fastify-actionable-version-error.md b/.changeset/fastify-actionable-version-error.md new file mode 100644 index 00000000000..6de00fd51cf --- /dev/null +++ b/.changeset/fastify-actionable-version-error.md @@ -0,0 +1,5 @@ +--- +'@clerk/fastify': patch +--- + +Registering `clerkPlugin` on Fastify <5 now throws an actionable error that tells you exactly how to resolve the mismatch — either upgrade to `fastify@^5` or pin `@clerk/fastify@^1` to stay on Fastify 4 LTS — instead of the generic `FST_ERR_PLUGIN_VERSION_MISMATCH` you would previously see during registration. diff --git a/packages/fastify/README.md b/packages/fastify/README.md index eadfd363f36..6f410f2f607 100644 --- a/packages/fastify/README.md +++ b/packages/fastify/README.md @@ -35,6 +35,8 @@ - Node.js `>=20.9.0` or later - An existing Clerk application. [Create your account for free](https://dashboard.clerk.com/sign-up?utm_source=github&utm_medium=clerk_fastify). +> **Still on Fastify 4 (LTS)?** Pin `@clerk/fastify@^1`. The current major requires Fastify 5; registering it on Fastify 4 throws at plugin registration time with an actionable error explaining your options. + ### Installation The fastest way to get started with Clerk is by following the [Fastify Quickstart](https://clerk.com/docs/quickstarts/fastify?utm_source=github&utm_medium=clerk_fastify). diff --git a/packages/fastify/src/__tests__/clerkPlugin.test.ts b/packages/fastify/src/__tests__/clerkPlugin.test.ts index 6adc90af05f..c93479b979f 100644 --- a/packages/fastify/src/__tests__/clerkPlugin.test.ts +++ b/packages/fastify/src/__tests__/clerkPlugin.test.ts @@ -59,4 +59,31 @@ describe('clerkPlugin()', () => { expect(fastify.decorateRequest).toHaveBeenCalledWith('clerk', null); expect(doneFn).toHaveBeenCalled(); }); + + test.each(['4.28.0', '3.29.5', '2.0.0'])( + 'throws an actionable error when registered on fastify@%s', + version => { + const doneFn = vi.fn(); + const fastify = createFastifyInstanceMock({ version }); + + expect(() => { + clerkPlugin(fastify, {}, doneFn); + }).toThrowError(new RegExp(`requires fastify@>=5 but is being registered on fastify@${version.replace(/\./g, '\\.')}`)); + expect(() => { + clerkPlugin(fastify, {}, doneFn); + }).toThrowError(/pin @clerk\/fastify@\^1/); + expect(doneFn).not.toHaveBeenCalled(); + }, + ); + + test.each(['5.0.0', '5.8.5', '6.0.0-alpha.1'])( + 'does not throw on supported fastify@%s', + version => { + const doneFn = vi.fn(); + const fastify = createFastifyInstanceMock({ version }); + + expect(() => clerkPlugin(fastify, {}, doneFn)).not.toThrow(); + expect(doneFn).toHaveBeenCalled(); + }, + ); }); diff --git a/packages/fastify/src/clerkPlugin.ts b/packages/fastify/src/clerkPlugin.ts index d6e95f6bb20..8c445c043bc 100644 --- a/packages/fastify/src/clerkPlugin.ts +++ b/packages/fastify/src/clerkPlugin.ts @@ -1,15 +1,32 @@ import type { FastifyInstance, FastifyPluginCallback } from 'fastify'; import fp from 'fastify-plugin'; +import { incompatibleFastifyVersion } from './errors'; import type { ClerkFastifyOptions } from './types'; import { ALLOWED_HOOKS } from './types'; import { withClerkMiddleware } from './withClerkMiddleware'; +const FASTIFY_MAJOR_MIN = 5; + +// Extracts the major version from a Fastify version string like "5.8.5" +// without introducing a `semver` dependency for a single integer compare. +const parseFastifyMajor = (version: string): number => { + const match = /^(\d+)/.exec(version); + return match ? Number.parseInt(match[1]!, 10) : 0; +}; + const plugin: FastifyPluginCallback = ( instance: FastifyInstance, opts: ClerkFastifyOptions, done, ) => { + // Throw a Clerk-branded, actionable error instead of fastify-plugin's + // stock FST_ERR_PLUGIN_VERSION_MISMATCH, which doesn't tell the user + // what to do (upgrade fastify or pin @clerk/fastify@^1). + if (parseFastifyMajor(instance.version) < FASTIFY_MAJOR_MIN) { + throw new Error(incompatibleFastifyVersion(instance.version)); + } + instance.decorateRequest('auth', null); instance.decorateRequest('clerk', null as any); @@ -26,5 +43,4 @@ const plugin: FastifyPluginCallback = ( export const clerkPlugin = fp(plugin, { name: '@clerk/fastify', - fastify: '5.x', }); diff --git a/packages/fastify/src/errors.ts b/packages/fastify/src/errors.ts index 4324554e363..333105e9a37 100644 --- a/packages/fastify/src/errors.ts +++ b/packages/fastify/src/errors.ts @@ -16,3 +16,14 @@ import { clerkPlugin } from '@clerk/fastify'; const server: FastifyInstance = Fastify({ logger: true }); server.register(clerkPlugin); `); + +export const incompatibleFastifyVersion = (foundVersion: string) => + createErrorMessage(`@clerk/fastify requires fastify@>=5 but is being registered on fastify@${foundVersion}. + +To resolve this, either: + - upgrade your fastify dependency to ^5, or + - pin @clerk/fastify@^1 to keep using fastify@4 (LTS): + + npm install @clerk/fastify@^1 + # or: pnpm add @clerk/fastify@^1 +`); diff --git a/packages/fastify/src/test/utils.ts b/packages/fastify/src/test/utils.ts index 651aa87c61a..76c45beb235 100644 --- a/packages/fastify/src/test/utils.ts +++ b/packages/fastify/src/test/utils.ts @@ -2,8 +2,9 @@ import type { FastifyInstance } from 'fastify'; import { vi } from 'vitest'; // expose decorated reply methods as methods in fastify instance -export function createFastifyInstanceMock() { +export function createFastifyInstanceMock(overrides: { version?: string } = {}) { const fastify = { + version: overrides.version ?? '5.8.5', decorateReply: vi.fn((name: string, fn) => { // @ts-expect-error - TS7053 fastify[name] = fn;