8000 refactor(nitro,rspack,vite,webpack): move cors handling into nitro builder by danielroe · Pull Request #34048 · nuxt/nuxt · GitHub
[go: up one dir, main page]

Skip to content

refactor(nitro,rspack,vite,webpack): move cors handling into nitro builder#34048

Merged
danielroe merged 4 commits intomainfrom
refactor/nitro-cors
Jan 9, 2026
Merged

refactor(nitro,rspack,vite,webpack): move cors handling into nitro builder#34048
danielroe merged 4 commits intomainfrom
refactor/nitro-cors

Conversation

@danielroe
Copy link
Member

🔗 Linked issue

📚 Description

this aims to make the nuxt builders less dependent on the specific version of nitro/h3 and leaves that to the nitro builder integration.

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@socket-security
Copy link
socket-security bot commented Jan 9, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedtinyglobby@​0.2.151001007783100
Addedstd-env@​3.10.01001009982100
Addedpostcss@​8.5.61001008284100
Addedjiti@​2.6.19710010082100
Addedpathe@​2.0.31001009283100
Addedignore@​7.0.510010010083100
Addedmagic-string@​0.30.2110010010086100
Addedsemver@​7.7.310010010087100
Addedtypescript@​5.9.31001009010090
Addedprettier@​3.7.4901009795100

View full report

@socket-security
Copy link
socket-security bot commented Jan 9, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm safer-buffer is 94.0% likely obfuscated

Confidence: 0.94

Location: Package overview

From: pnpm-lock.yamlnpm/safer-buffer@2.1.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/safer-buffer@2.1.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai
Copy link
Contributor
coderabbitai bot commented Jan 9, 2026

Walkthrough

This PR updates dev-server middleware across Nitro, Vite and Webpack. It changes the server:devHandler hook signature to accept an options object with a cors function, adds CORS handling wiring in Nitro and dev-server middleware, implements cross-version H3 event compatibility in the Vite dev plugin with a public defineEventHandler helper, introduces an ErrorPartial type and replaces createError usage with plain error-shaped objects, and adjusts Webpack's dev middleware adapter to the new handler invocation pattern.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main refactoring objective: moving CORS handling into the Nitro builder across multiple packages.
Description check ✅ Passed The description relates to the changeset by explaining the intent to reduce builder dependency on Nitro/h3 version specifics through centralised CORS handling.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/nitro-cors

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.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
packages/webpack/src/webpack.ts (1)

138-173: Improve the error object at line 143.

The thrown error object { status: 403 } is missing a message property, which is inconsistent with the ErrorPartial pattern used elsewhere in this PR (see packages/vite/src/plugins/vite-node.ts lines 277, 288, 314, 321 where errors include both status and message).

💬 Proposed fix to add message property
-    throw { status: 403 }
+    throw { status: 403, message: 'Cross-site request blocked in no-cors mode' }
packages/vite/src/plugins/dev-server.ts (1)

113-188: resolveHandler() isn’t caching (outer handler is never assigned) + query strings can trigger false 404s.

This currently rebuilds proxy patterns per request, and $-anchored devHandlerRegexes can fail when url includes ?….

Proposed fix
       function resolveHandler () {
         if (handler) {
           return handler
         }

         // Pre-process proxy configuration once
         const proxyConfig = viteServer.config.server.proxy
         const proxyPatterns: Array<{ type: 'string' | 'regex', value: string | RegExp }> = []

         if (proxyConfig) {
           for (const key in proxyConfig) {
             if (key.startsWith('^')) {
               try {
                 proxyPatterns.push({ type: 'regex', value: new RegExp(key) })
               } catch {
                 // Invalid regex, skip this key
               }
             } else {
               proxyPatterns.push({ type: 'string', value: key })
             }
           }
         }

         function isProxyPath (path: string) {
           for (const pattern of proxyPatterns) {
             if (pattern.type === 'regex' && (pattern.value as RegExp).test(path)) {
               return true
             } else if (pattern.type === 'string' && path.startsWith(pattern.value as string)) {
               return true
             }
           }
           return false
         }

-        return defineEventHandler(async (event: H3V1Event | H3V2Event) => {
-          const url = 'url' in event ? event.url.pathname + event.url.search + event.url.hash : event.path
-          const isBasePath = url.startsWith(viteServer.config.base!)
+        handler = defineEventHandler(async (event: H3V1Event | H3V2Event) => {
+          const fullUrl = 'url' in event ? (event.url.pathname + event.url.search + event.url.hash) : event.path
+          const pathname = fullUrl.split('?')[0]!.split('#')[0]!
+          const isBasePath = pathname.startsWith(viteServer.config.base!)

           // Check if this is a vite-handled route or proxy path
           let isViteRoute = isBasePath
           if (!isViteRoute) {
             // Check vite middleware routes (must be done per-request as middleware stack can change)
             for (const viteRoute of viteServer.middlewares.stack) {
-              if (viteRoute.route.length > 1 && url.startsWith(viteRoute.route)) {
+              if (viteRoute.route.length > 1 && pathname.startsWith(viteRoute.route)) {
                 isViteRoute = true
                 break
               }
             }
             // Check proxy paths
-            isViteRoute ||= isProxyPath(url)
+            isViteRoute ||= isProxyPath(pathname)
           }

           const { req, res } = 'runtime' in event ? event.runtime!.node! : event.node
           if (!isViteRoute) {
             // @ts-expect-error _skip_transform is a private property
             req._skip_transform = true
           }

           // Workaround: vite devmiddleware modifies req.url
           const _originalPath = req.url
           await new Promise((resolve, reject) => {
             viteServer.middlewares.handle(req as IncomingMessage, res as ServerResponse, (err: Error) => {
               req.url = _originalPath
               return err ? reject(err) : resolve(null)
             })
           })

           // if vite has not handled the request, we want to send a 404 for paths which are not in any static base or dev server handlers
-          if (url.startsWith(nuxt.options.app.buildAssetsDir) && !staticBases.some(baseURL => url.startsWith(baseURL)) && !devHandlerRegexes.some(regex => regex.test(url))) {
+          if (pathname.startsWith(nuxt.options.app.buildAssetsDir) && !staticBases.some(baseURL => pathname.startsWith(baseURL)) && !devHandlerRegexes.some(regex => regex.test(pathname))) {
             throw { status: 404 } satisfies ErrorPartial
           }
         })
+        return handler
       }
🤖 Fix all issues with AI agents
In @packages/nitro-server/src/index.ts:
- Around line 864-875: Hoist the one-time check of options?.cors?.() out of the
per-request handler by evaluating it when registering the hook (store a boolean
like corsEnabled) so the defineEventHandler does not call options.cors() on
every request; inside the handler call handleCors(event,
nuxt.options.devServer.cors) only when corsEnabled is true. When setting the
Vary header (currently via setHeader(event, 'Vary', 'Origin')), preserve any
existing Vary value instead of overwriting: read the current header, append
"Origin" if not already present (comma-separated), and write the combined value
back via setHeader. Ensure you update the nuxt.hook('server:devHandler') block
and the devMiddlewareHandler.set(...) closure to use the hoisted corsEnabled and
the merged Vary header logic.
🧹 Nitpick comments (4)
packages/webpack/src/webpack.ts (1)

228-232: Consider the maintainability of the local defineEventHandler implementation.

This local implementation of defineEventHandler avoids depending on a specific h3 version, but it duplicates framework internals by relying on the __is_handler__ convention. If h3's internal marking mechanism changes, this code will silently fail to work correctly.

Consider adding a comment documenting:

  • Why this local implementation is necessary
  • The expected lifecycle for removing it
  • Any risks if h3's handler marking changes

Alternatively, explore whether this can be imported from a common location to reduce duplication.

packages/vite/src/plugins/vite-node.ts (1)

277-321: Verify error consistency across all throw sites.

The error objects follow a mostly consistent pattern with satisfies ErrorPartial annotations:

  • Lines 277, 288, 321: Include status and message
  • Line 314: Includes data and message but omits status

Line 314's module fetch error differs by including error data but no status code. Verify whether this is intentional or if it should include status: 500 to indicate a server error.

Based on learnings, error handling patterns should be used consistently.

🔍 Suggested verification and optional fix

Consider whether line 314 should include a status code:

-                throw { data: errorData, message: err.message || 'Error fetching module' } satisfies ErrorPartial
+                throw { status: 500, data: errorData, message: err.message || 'Error fetching module' } satisfies ErrorPartial

This would align all error objects to include a status code for consistent error handling downstream.

packages/vite/src/vite-node-entry.ts (1)

51-70: Type _err as ErrorPartial (currently implicitly any) and consider adding statusCode/statusMessage for compatibility.

Right now let _err discards the type-safety from satisfies ErrorPartial, and consumers commonly key off statusCode/statusMessage rather than statusText.

Proposed tweak
-        let _err
+        let _err: ErrorPartial
         try {
           const { message, stack } = formatViteError(errorData, id)
           _err = {
+            statusCode: 500,
+            statusMessage: 'Vite Error',
             statusText: 'Vite Error',
             message,
             stack,
           } satisfies ErrorPartial
         } catch (formatError) {
           consola.warn('Internal nuxt error while formatting vite-node error. Please report this!', formatError)
           const message = `[vite-node] [TransformError] ${errorData?.message || '-'}`
           consola.error(message, errorData)
           throw {
+            statusCode: 500,
+            statusMessage: 'Vite Error',
             statusText: 'Vite Error',
             message,
             stack: `${message}\nat ${id}\n` + (errorData?.stack || ''),
           } satisfies ErrorPartial
         }
packages/vite/src/plugins/dev-server.ts (1)

200-204: Avoid potential confusion with the local defineEventHandler name (it’s not H3’s helper).

Consider renaming to something like markAsH3Handler to make intent explicit.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4f46c6 and 57ba285.

⛔ Files ignored due to path filters (4)
  • packages/rspack/package.json is excluded by !**/package.json
  • packages/vite/package.json is excluded by !**/package.json
  • packages/webpack/package.json is excluded by !**/package.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (8)
  • packages/nitro-server/src/augments.ts
  • packages/nitro-server/src/index.ts
  • packages/vite/src/plugins/dev-server.ts
  • packages/vite/src/plugins/vite-node.ts
  • packages/vite/src/types.ts
  • packages/vite/src/vite-node-entry.ts
  • packages/vite/src/vite-node.ts
  • packages/webpack/src/webpack.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/vite/src/types.ts
  • packages/vite/src/plugins/vite-node.ts
  • packages/vite/src/vite-node.ts
  • packages/vite/src/plugins/dev-server.ts
  • packages/nitro-server/src/augments.ts
  • packages/nitro-server/src/index.ts
  • packages/vite/src/vite-node-entry.ts
  • packages/webpack/src/webpack.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx,vue}: Use clear, descriptive variable and function names
Add comments only to explain complex logic or non-obvious implementations
Keep functions focused and manageable (generally under 50 lines), and extract complex logic into separate domain-specific files
Remove code that is not used or needed
Use error handling patterns consistently

Files:

  • packages/vite/src/types.ts
  • packages/vite/src/plugins/vite-node.ts
  • packages/vite/src/vite-node.ts
  • packages/vite/src/plugins/dev-server.ts
  • packages/nitro-server/src/augments.ts
  • packages/nitro-server/src/index.ts
  • packages/vite/src/vite-node-entry.ts
  • packages/webpack/src/webpack.ts
🧠 Learnings (2)
📚 Learning: 2025-11-25T11:42:16.132Z
Learnt from: CR
Repo: nuxt/nuxt PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-25T11:42:16.132Z
Learning: Applies to **/*.{ts,tsx,js,jsx,vue} : Use error handling patterns consistently

Applied to files:

  • packages/vite/src/plugins/vite-node.ts
  • packages/vite/src/vite-node.ts
  • packages/vite/src/vite-node-entry.ts
📚 Learning: 2024-11-05T15:22:54.759Z
Learnt from: GalacticHypernova
Repo: nuxt/nuxt PR: 26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.

Applied to files:

  • packages/vite/src/plugins/vite-node.ts
  • packages/vite/src/plugins/dev-server.ts
  • packages/nitro-server/src/index.ts
🧬 Code graph analysis (3)
packages/vite/src/plugins/vite-node.ts (2)
packages/vite/src/types.ts (1)
  • ErrorPartial (4-4)
packages/nuxt/src/app/index.ts (1)
  • FetchResult (8-8)
packages/nitro-server/src/augments.ts (2)
packages/schema/src/index.ts (1)
  • HookResult (8-8)
packages/schema/src/types/hooks.ts (1)
  • HookResult (16-16)
packages/vite/src/vite-node-entry.ts (1)
packages/vite/src/types.ts (1)
  • ErrorPartial (4-4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: codeql (javascript-typescript)
🔇 Additional comments (7)
packages/vite/src/types.ts (1)

1-4: LGTM! Clean type definition for error compatibility.

The ErrorPartial type provides a flexible union for handling errors across h3 v1 and v2, using Partial to allow subset representations. This is used consistently throughout the Vite integration (packages/vite/src/plugins/vite-node.ts and packages/vite/src/vite-node-entry.ts).

packages/vite/src/plugins/vite-node.ts (1)

23-23: LGTM! Proper import of ErrorPartial type.

The import supports the transition from h3's createError to plain error objects, aligning with the broader refactoring pattern.

packages/vite/src/vite-node.ts (1)

147-151: LGTM! Improved type safety for error handling.

The error construction now uses explicit typing Error & { stack?: string, data?: unknown, status?: number, statusCode?: number } instead of relying on @ts-expect-error comments. This provides better type safety whilst maintaining compatibility with both status and statusCode properties.

The approach correctly:

  • Creates a proper Error instance with message
  • Augments it with additional properties from the IPC response
  • Supports both status and statusCode for compatibility
packages/nitro-server/src/augments.ts (1)

76-76: LGTM! Backwards-compatible hook signature update.

The addition of the optional options parameter with a cors function properly supports the CORS refactoring whilst maintaining backwards compatibility. The signature change aligns with the implementations in packages/webpack/src/webpack.ts (line 132) where { cors: () => true } is passed.

packages/webpack/src/webpack.ts (2)

124-132: The non-null assertion pattern and CORS configuration are established practices for 8000 H3 compatibility in dev middleware.

This code implements a deliberate compatibility layer between H3 event objects and legacy webpack middleware. The pattern 'runtime' in event ? event.runtime!.node! : event.node is consistently used throughout the codebase (also at line 141 in the same file and in vite dev-server) to handle both H3 v1 and v2 event structures, as evidenced by the type definition GenericHandler = (event: H3V1Event | H3V2Event).

The CORS configuration { cors: () => true } is intentionally permissive for development purposes; this is a documented H3 pattern for local development servers where cross-origin requests should be allowed.

The type casts (as IncomingMessage, as ServerResponse) are necessary because webpack-dev-middleware and webpack-hot-middleware expect Node.js-style request and response objects, requiring TypeScript casts when providing H3 event properties. This is standard middleware integration practice.

No changes needed — this is consistent with established patterns in the codebase.


2-3: Dual H3 version compatibility is intentionally implemented and properly handled.

The imports of H3Event from both h3-next and h3 enable the webpack builder to work with both versions. The code correctly uses structural feature detection ('runtime' in event) to handle the different H3Event structures across versions, making this pattern appropriate for the transition period. No consolidation needed at this time.

packages/vite/src/plugins/dev-server.ts (1)

189-196: CORS hook wiring looks sensible; please sanity-check the H3 v1/v2 event-shape guards against the actual h3-next/h3 versions in use.

Specifically 'url' in event and 'runtime' in event assumptions.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @packages/nitro-server/src/index.ts:
- Around line 864-875: The code uses event.path which breaks on H3 v2 where the
path is at event.url.pathname; update the dev middleware handler (inside
nuxt.hook and devMiddlewareHandler.set / defineEventHandler) to compute a
cross-version request path (e.g. const requestPath = event.path ??
event.url?.pathname) and use that requestPath when calling
options.cors(requestPath) and when determining CORS preflight via
handleCors(event, ...), leaving the rest of the handler (setHeader and return
h(event)) unchanged.

In @packages/vite/src/plugins/dev-server.ts:
- Around line 215-219: Remove the legacy sentinel assignment in
defineEventHandler: the function should simply return the handler unchanged
instead of using Object.assign to add a __is_handler__ property; update the
defineEventHandler implementation (and any local references) to return handler
directly and delete the unused "__is_handler__" marker so no internal sentinel
is written by defineEventHandler.
🧹 Nitpick comments (1)
packages/vite/src/plugins/dev-server.ts (1)

113-150: Consider renaming for clarity.

The outer function isProxyPath and the cached inner function _isProxyPath share the same name (one with underscore), which may reduce readability. Consider a more descriptive name like _cachedMatcher or _proxyPathMatcher.

♻️ Suggested refactor for clarity
-      let _isProxyPath: ((url: string) => boolean) | undefined
+      let _proxyPathMatcher: ((url: string) => boolean) | undefined
 
       function isProxyPath (url: string) {
-        if (_isProxyPath) {
-          return _isProxyPath(url)
+        if (_proxyPathMatcher) {
+          return _proxyPathMatcher(url)
         }
 
         // Pre-process proxy configuration once
         const proxyConfig = viteServer.config.server.proxy
         const proxyPatterns: Array<{ type: 'string' | 'regex', value: string | RegExp }> = []
 
         if (proxyConfig) {
           for (const key in proxyConfig) {
             if (key.startsWith('^')) {
               try {
                 proxyPatterns.push({ type: 'regex', value: new RegExp(key) })
               } catch {
                 // Invalid regex, skip this key
               }
             } else {
               proxyPatterns.push({ type: 'string', value: key })
             }
           }
         }
 
-        _isProxyPath = function isProxyPath (path: string) {
+        _proxyPathMatcher = function (path: string) {
           for (const pattern of proxyPatterns) {
             if (pattern.type === 'regex' && (pattern.value as RegExp).test(path)) {
               return true
             } else if (pattern.type === 'string' && path.startsWith(pattern.value as string)) {
               return true
             }
           }
           return false
         }
 
-        return _isProxyPath(url)
+        return _proxyPathMatcher(url)
       }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57ba285 and 83ef7be.

⛔ Files ignored due to path filters (4)
  • packages/rspack/package.json is excluded by !**/package.json
  • packages/vite/package.json is excluded by !**/package.json
  • packages/webpack/package.json is excluded by !**/package.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (4)
  • packages/nitro-server/src/augments.ts
  • packages/nitro-server/src/index.ts
  • packages/vite/src/plugins/dev-server.ts
  • packages/vite/src/vite-node-entry.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/nitro-server/src/augments.ts
  • packages/vite/src/vite-node-entry.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nitro-server/src/index.ts
  • packages/vite/src/plugins/dev-server.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx,vue}: Use clear, descriptive variable and function names
Add comments only to explain complex logic or non-obvious implementations
Keep functions focused and manageable (generally under 50 lines), and extract complex logic into separate domain-specific files
Remove code that is not used or needed
Use error handling patterns consistently

Files:

  • packages/nitro-server/src/index.ts
  • packages/vite/src/plugins/dev-server.ts
🧠 Learnings (1)
📚 Learning: 2024-11-05T15:22:54.759Z
Learnt from: GalacticHypernova
Repo: nuxt/nuxt PR: 26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.

Applied to files:

  • packages/nitro-server/src/index.ts
  • packages/vite/src/plugins/dev-server.ts
🧬 Code graph analysis (1)
packages/vite/src/plugins/dev-server.ts (1)
packages/vite/src/types.ts (1)
  • ErrorPartial (4-4)
🔇 Additional comments (5)
packages/nitro-server/src/index.ts (1)

19-19: LGTM: H3 CORS utilities imported.

The additional imports handleCors and setHeader are appropriate for implementing CORS handling within the Nitro builder.

packages/vite/src/plugins/dev-server.ts (4)

5-6: LGTM: Cross-version H3 event type imports.

The dual imports with aliasing enable compatibility with both H3 v1 and v2, aligning with the PR's goal of reducing version-specific dependencies.


11-11: LGTM: ErrorPartial type import.

Type-only import for cross-version error handling.


190-210: LGTM: CORS decision logic integrated.

The hook correctly delegates CORS handling decisions to the Nitro builder via the cors option, ensuring CORS is applied only when Vite isn't handling it. The logic appropriately covers Vite base paths, middleware routes, and proxy paths.


152-189: The URL reconstruction on line 153 is correct and safe. The event.url object in h3-next is a standard WHATWG URL API implementation where pathname, search, and hash are always defined properties (defaulting to empty strings if absent). Concatenating these properties will not produce undefined values; the operation is valid regardless of whether the URL contains a query string or fragment.

No action required.

Comment on lines +864 to +875
nuxt.hook('server:devHandler', (h, options) => {
devMiddlewareHandler.set(defineEventHandler((event) => {
if (options.cors(event.path)) {
const isPreflight = handleCors(event, nuxt.options.devServer.cors)
if (isPreflight) {
return null
}
setHeader(event, 'Vary', 'Origin')
}
return h(event)
}))
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Cross-version H3 event compatibility issue with event.path.

Line 866 accesses event.path, but H3 v2 events use event.url.pathname instead. This will break CORS handling when using H3 v2.

🔧 Proposed fix for cross-version compatibility
-    nuxt.hook('server:devHandler', (h, options) => {
-      devMiddlewareHandler.set(defineEventHandler((event) => {
-        if (options.cors(event.path)) {
+    nuxt.hook('server:devHandler', (h, options) => {
+      devMiddlewareHandler.set(defineEventHandler((event) => {
+        const path = 'url' in event ? event.url.pathname : event.path
+        if (options.cors(path)) {
           const isPreflight = handleCors(event, nuxt.options.devServer.cors)
           if (isPreflight) {
             return null
           }
           setHeader(event, 'Vary', 'Origin')
         }
         return h(event)
       }))
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nuxt.hook('server:devHandler', (h, options) => {
devMiddlewareHandler.set(defineEventHandler((event) => {
if (options.cors(event.path)) {
const isPreflight = handleCors(event, nuxt.options.devServer.cors)
if (isPreflight) {
return null
}
setHeader(event, 'Vary', 'Origin')
}
return h(event)
}))
})
nuxt.hook('server:devHandler', (h, options) => {
devMiddlewareHandler.set(defineEventHandler((event) => {
const path = 'url' in event ? event.url.pathname : event.path
if (options.cors(path)) {
const isPreflight = handleCors(event, nuxt.options.devServer.cors)
if (isPreflight) {
return null
}
setHeader(event, 'Vary', 'Origin')
}
return h(event)
}))
})
🤖 Prompt for AI Agents
In @packages/nitro-server/src/index.ts around lines 864 - 875, The code uses
event.path which breaks on H3 v2 where the path is at event.url.pathname; update
the dev middleware handler (inside nuxt.hook and devMiddlewareHandler.set /
defineEventHandler) to compute a cross-version request path (e.g. const
requestPath = event.path ?? event.url?.pathname) and use that requestPath when
calling options.cors(requestPath) and when determining CORS preflight via
handleCors(event, ...), leaving the rest of the handler (setHeader and return
h(event)) unchanged.

Comment on lines +215 to +219
3E9C
type GenericHandler = (event: H3V1Event | H3V2Event) => unknown | Promise<unknown>

function defineEventHandler (handler: GenericHandler): GenericHandler {
return Object.assign(handler, { __is_handler__: true })
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, examine the dev-server.ts file to understand context
cat -n packages/vite/src/plugins/dev-server.ts | head -250 | tail -50

Repository: nuxt/nuxt

Length of output: 2244


🏁 Script executed:

# Check package.json for h3 version specifications
cat package.json | grep -A 5 -B 5 "h3"

Repository: nuxt/nuxt

Length of output: 97


🏁 Script executed:

# Search for __is_handler__ usage in the codebase
rg "__is_handler__" --type ts --type tsx

Repository: nuxt/nuxt

Length of output: 80


🏁 Script executed:

# Check if there are any imports of defineEventHandler from h3
rg "defineEventHandler" --type ts --type tsx -A 3 -B 3

Repository: nuxt/nuxt

Length of output: 80


🏁 Script executed:

# Find package.json locations
fd "package.json" | head -5

Repository: nuxt/nuxt

Length of output: 35


🏁 Script executed:

# Search for __is_handler__ without type filter
rg "__is_handler__"

Repository: nuxt/nuxt

Length of output: 240


🏁 Script executed:

# Search for H3V1Event and H3V2Event definitions
rg "H3V1Event|H3V2Event" -A 2 -B 2

Repository: nuxt/nuxt

Length of output: 2123


🏁 Script executed:

# Check imports at the top of dev-server.ts
head -50 packages/vite/src/plugins/dev-server.ts

Repository: nuxt/nuxt

Length of output: 2114


🏁 Script executed:

# Search for how defineEventHandler is used/called in the codebase
rg "defineEventHandler" -A 2 -B 2

Repository: nuxt/nuxt

Length of output: 33734


🏁 Script executed:

# Search for how this handler is actually called/used
rg "viteMiddleware" packages/vite/src/plugins/dev-server.ts -B 5 -A 5

Repository: nuxt/nuxt

Length of output: 1011


🏁 Script executed:

# Look at the callHook usage to see what h3 expects
rg "callHook.*devHandler" packages/vite/src/plugins/dev-server.ts -B 3 -A 10

Repository: nuxt/nuxt

Length of output: 477


🏁 Script executed:

# Search for any comments or documentation about the handler flag
rg "__is_handler__" -B 5 -A 5

Repository: nuxt/nuxt

Length of output: 997


🏁 Script executed:

# Check if there are any issues or discussions about h3 version compatibility
fd "README|CHANGELOG" packages/vite | xargs cat 2>/dev/null | head -100

Repository: nuxt/nuxt

Length of output: 35


🏁 Script executed:

# Check Nitro integration to see how handlers are processed
rg "server:devHandler" --type ts -B 3 -A 8

Repository: nuxt/nuxt

Length of output: 3615


🏁 Script executed:

# Look for h3 package details
fd "package.json" packages/ | xargs grep -l "h3" 2>/dev/null | head -3 | xargs cat

Repository: nuxt/nuxt

Length of output: 35


🏁 Script executed:

# Check how nitro processes these dev handlers
rg "devHandlers" packages/nitro-server -A 5

Repository: nuxt/nuxt

Length of output: 1666


🌐 Web query:

h3 __is_handler__ flag h3-next compatibility

💡 Result:

Short answer: The "is_handler" marker is an internal TypeScript/JS sentinel used by the older h3 (v0.x / "h3" used by Nuxt/h3) typings to identify event/route handlers. h3 v2 / "h3-next" uses a different handler model and does not rely on that private flag — rely on the public APIs (defineHandler / defineEventHandler, isEventHandler, isEvent, or the H3 app/handler abstractions) for compatibility instead of checking is_handler directly.

References:

  • h3 (v0.x) type definitions showing the "is_handler" sentinel. [1]
  • h3 v2 / "h3-next" docs describing the new handler model (handlers as first-class/event handlers, defineHandler/defineEventHandler, etc.). [2][3]

🏁 Script executed:

# Search for any code that checks __is_handler__ flag
rg "__is_handler__" --type ts

Repository: nuxt/nuxt

Length of output: 240


🏁 Script executed:

# Check if isEventHandler or other h3 APIs are used to identify handlers
rg "isEventHandler|isHandler" --type ts -B 2 -A 2

Repository: nuxt/nuxt

Length of output: 35


🏁 Script executed:

# Look at the complete flow in nitro-server where the hook is processed
sed -n '80,120p' packages/nitro-server/src/index.ts

Repository: nuxt/nuxt

Length of output: 2225


🏁 Script executed:

# Verify h3 imports used in nitro-server
grep -n "from 'h3'" packages/nitro-server/src/index.ts

Repository: nuxt/nuxt

Length of output: 244


Remove the unused __is_handler__ flag; h3-next v2 does not recognise or use this internal sentinel.

The __is_handler__: true marker is an internal mechanism from h3 v0.x and is not part of h3-next v2's handler model. The flag is never checked anywhere in the codebase and provides no functional value. Handlers work correctly because they are properly wrapped by h3's defineEventHandler in the Nitro integration. Remove this dead code to maintain clarity and comply with the guideline of removing unnecessary code.

🤖 Prompt for AI Agents
In @packages/vite/src/plugins/dev-server.ts around lines 215 - 219, Remove the
legacy sentinel assignment in defineEventHandler: the function should simply
return the handler unchanged instead of using Object.assign to add a
__is_handler__ property; update the defineEventHandler implementation (and any
local references) to return handler directly and delete the unused
"__is_handler__" marker so no internal sentinel is written by
defineEventHandler.

@codspeed-hq
Copy link
codspeed-hq bot commented Jan 9, 2026

CodSpeed Performance Report

Merging this PR will not alter performance

Comparing refactor/nitro-cors (83ef7be) with main (fa21bb1)1

Summary

✅ 10 untouched benchmarks

Footnotes

  1. No successful run was found on main (7f50342) during the generation of this report, so fa21bb1 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@pkg-pr-new
Copy link
pkg-pr-new bot commented Jan 9, 2026

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@34048

@nuxt/nitro-server

npm i https://pkg.pr.new/@nuxt/nitro-server@34048

nuxt

npm i https://pkg.pr.new/nuxt@34048

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@34048

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@34048

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@34048

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@34048

commit: 83ef7be

@danielroe danielroe merged commit 139ef2b into main Jan 9, 2026
101 of 103 checks passed
@danielroe danielroe deleted the refactor/nitro-cors branch January 9, 2026 19:42
@github-actions github-actions bot mentioned this pull request Jan 9, 2026
@github-actions github-actions bot mentioned this pull request Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0