diff --git a/package-lock.json b/package-lock.json index 72668bdfd28..740d401fa17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "clean-deep": "^3.4.0", "commander": "^12.1.0", "comment-json": "^5.0.0", - "content-type": "^1.0.5", + "content-type": "^3.1.1", "cookie": "^2.0.0", "cron-parser": "^5.0.0", "debug": "^4.4.3", @@ -120,7 +120,6 @@ "@tsconfig/node22": "22.0.5", "@tsconfig/recommended": "^1.0.13", "@types/backoff": "^2.5.5", - "@types/content-type": "^1.1.9", "@types/debug": "^4.1.12", "@types/envinfo": "^7.8.4", "@types/eslint-config-prettier": "^6.11.3", @@ -6948,11 +6947,6 @@ "@types/node": "*" } }, - "node_modules/@types/content-type": { - "version": "1.1.9", - "dev": true, - "license": "MIT" - }, "node_modules/@types/debug": { "version": "4.1.12", "dev": true, @@ -8478,6 +8472,16 @@ "ms": "2.0.0" } }, + "node_modules/@verdaccio/middleware/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@verdaccio/middleware/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -10844,10 +10848,16 @@ } }, "node_modules/content-type": { - "version": "1.0.5", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-3.1.1.tgz", + "integrity": "sha512-GW4qUsfFo59d0HbUibDlWv5wPz+vAAcaTWbKIuKCf0JkC7wWkSyf8f13IpXn5JkeMlB2P8iTSSIjwXljorg2vA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/convert-source-map": { @@ -12681,6 +12691,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.7.2", "license": "MIT", @@ -20654,6 +20673,16 @@ "ms": "2.0.0" } }, + "node_modules/verdaccio-audit/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/verdaccio-audit/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -20952,6 +20981,16 @@ "ms": "2.0.0" } }, + "node_modules/verdaccio/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/verdaccio/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", diff --git a/package.json b/package.json index 36b01298c23..9a6b09c151b 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "clean-deep": "^3.4.0", "commander": "^12.1.0", "comment-json": "^5.0.0", - "content-type": "^1.0.5", + "content-type": "^3.1.1", "cookie": "^2.0.0", "cron-parser": "^5.0.0", "debug": "^4.4.3", @@ -158,7 +158,6 @@ "@tsconfig/node22": "22.0.5", "@tsconfig/recommended": "^1.0.13", "@types/backoff": "^2.5.5", - "@types/content-type": "^1.1.9", "@types/debug": "^4.1.12", "@types/envinfo": "^7.8.4", "@types/eslint-config-prettier": "^6.11.3", diff --git a/src/lib/functions/form-submissions-handler.ts b/src/lib/functions/form-submissions-handler.ts index c4c4e44880b..c438c55c7ce 100644 --- a/src/lib/functions/form-submissions-handler.ts +++ b/src/lib/functions/form-submissions-handler.ts @@ -78,7 +78,8 @@ export const createFormSubmissionHandler = function ({ const originalUrl = new URL(req.url, 'http://localhost') req.url = `/.netlify/functions/${handlerName}${originalUrl.search}` - const ct = parseContentType(req) + // A missing header parses to an empty type and takes the unsupported-type branch below. + const ct = parseContentType(req.headers['content-type'] ?? '') let fields = {} let files = {} if (ct.type.endsWith('/x-www-form-urlencoded')) { diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 107edd83b86..c88e8b42b19 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -20,7 +20,7 @@ import { ServerHandler } from '@netlify/server-dev' import { runBeforeProcessExit } from './shell.js' import type { AIGatewayContext } from '@netlify/ai/bootstrap' import type { MatchResult } from '@netlify/redirect-matcher' -import contentType from 'content-type' +import { parse as parseContentType } from 'content-type' import generateETag from 'etag' import getAvailablePort from 'get-port' import httpProxy from 'http-proxy' @@ -379,7 +379,7 @@ const serveRedirect = async function ({ return } - const ct = req.headers['content-type'] ? contentType.parse(req).type : '' + const ct = parseContentType(req.headers['content-type'] ?? '').type if ( req.method === 'POST' && !isInternal(req.url) && @@ -901,7 +901,7 @@ const onRequest = async ( const hasFormSubmissionHandler: boolean = functionsRegistry && getFormHandler({ functionsRegistry, logWarning: false }) - const ct = req.headers['content-type'] ? contentType.parse(req).type : '' + const ct = parseContentType(req.headers['content-type'] ?? '').type if ( hasFormSubmissionHandler && functionsServer && diff --git a/tests/integration/commands/dev/dev-forms-and-redirects.test.ts b/tests/integration/commands/dev/dev-forms-and-redirects.test.ts index 12e6d0726ac..5cc519ed677 100644 --- a/tests/integration/commands/dev/dev-forms-and-redirects.test.ts +++ b/tests/integration/commands/dev/dev-forms-and-redirects.test.ts @@ -213,6 +213,49 @@ describe.concurrent('commands/dev-forms-and-redirects', () => { }) }) + test('should keep serving when a form submission carries a malformed content type', async (t) => { + await withSiteBuilder(t, async (builder) => { + builder + .withContentFile({ + path: 'index.html', + content: '