Commit 6812e92dea0 for nodejs

commit 6812e92dea0c5fb502d9e382e8cfc059f77e02b4
Author: James M Snell <jasnell@gmail.com>
Date:   Mon Oct 5 00:47:29 2026 -0500

    util: make mime parsing faster

    Signed-off-by: James M Snell <jasnell@gmail.com>
    Assisted-by: Opencode
    PR-URL: https://github.com/nodejs/node/pull/66369
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>

diff --git a/benchmark/mime/mimetype-parse.js b/benchmark/mime/mimetype-parse.js
new file mode 100644
index 00000000000..2aec20ceb06
--- /dev/null
+++ b/benchmark/mime/mimetype-parse.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const { MIMEType } = require('util');
+
+const bench = common.createBenchmark(main, {
+  n: [1e5],
+  value: [
+    'application/json',
+    'text/html; charset=utf-8',
+    'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
+    'text/plain; charset="utf-8"; foo="b\\"ar"; x=y',
+    'not a mime type',
+  ],
+  operation: ['parse', 'essence', 'params.get'],
+});
+
+function main({ n, value, operation }) {
+  const length = 1024;
+  const array = [];
+  let fn;
+  switch (operation) {
+    case 'parse':
+      fn = () => MIMEType.parse(value);
+      break;
+    case 'essence':
+      fn = () => MIMEType.parse(value)?.essence ?? null;
+      break;
+    case 'params.get':
+      fn = () => MIMEType.parse(value)?.params.get('charset') ?? null;
+      break;
+    default:
+      throw new Error(`Unsupported operation ${operation}`);
+  }
+
+  // Warm up.
+  for (let i = 0; i < length; ++i) {
+    array.push(fn());
+  }
+
+  bench.start();
+  for (let i = 0; i < n; ++i) {
+    array[i % length] = fn();
+  }
+  bench.end(n);
+
+  // Verify the entries to prevent dead code elimination from making
+  // the benchmark invalid.
+  for (let i = 0; i < length; ++i) {
+    assert.notStrictEqual(array[i], undefined);
+  }
+}
diff --git a/lib/internal/mime.js b/lib/internal/mime.js
index bb368a36aa5..7c7b18f088b 100644
--- a/lib/internal/mime.js
+++ b/lib/internal/mime.js
@@ -3,115 +3,297 @@
 const {
   FunctionPrototypeCall,
   ObjectDefineProperty,
-  RegExpPrototypeExec,
   SafeMap,
-  SafeStringPrototypeSearch,
-  StringPrototypeCharAt,
-  StringPrototypeIndexOf,
+  StringPrototypeCharCodeAt,
   StringPrototypeSlice,
   StringPrototypeToLowerCase,
   Symbol,
   SymbolIterator,
+  Uint8Array,
 } = primordials;
 const {
   ERR_ILLEGAL_CONSTRUCTOR,
   ERR_INVALID_MIME_SYNTAX,
 } = require('internal/errors').codes;

-const NOT_HTTP_TOKEN_CODE_POINT = /[^!#$%&'*+\-.^_`|~A-Za-z0-9]/g;
-const NOT_HTTP_QUOTED_STRING_CODE_POINT = /[^\t\u0020-~\u0080-\u00FF]/g;
-
-const END_BEGINNING_WHITESPACE = /[^\r\n\t ]|$/;
-const START_ENDING_WHITESPACE = /[\r\n\t ]*$/;
+// Lookup table for the code point classes used by the MIME Sniffing
+// standard. Only code points <= 0xFF can be in either class.
+// https://mimesniff.spec.whatwg.org/#http-token-code-point
+// https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
+const kHTTPToken = 1;
+const kHTTPQuotedStringToken = 2;
+const codePointClass = new Uint8Array(256);
+{
+  codePointClass[0x09] = kHTTPQuotedStringToken;
+  for (let c = 0x20; c <= 0x7E; c++) codePointClass[c] = kHTTPQuotedStringToken;
+  for (let c = 0x80; c <= 0xFF; c++) codePointClass[c] = kHTTPQuotedStringToken;
+  const tokens = "!#$%&'*+-.^_`|~0123456789" +
+    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+  for (let i = 0; i < tokens.length; i++) {
+    codePointClass[StringPrototypeCharCodeAt(tokens, i)] |= kHTTPToken;
+  }
+}

 const kNoThrow = Symbol('kNoThrow');

+// https://fetch.spec.whatwg.org/#http-whitespace
+function isHTTPWhitespace(c) {
+  return c === 0x20 || c === 0x09 || c === 0x0A || c === 0x0D;
+}
+
+/**
+ * Returns the offset from `start` of the first code point in
+ * `str[start, end)` that is not in `cls`, or -1 if they all are.
+ * @param {string} str
+ * @param {number} start
+ * @param {number} end
+ * @param {number} cls
+ * @returns {number}
+ */
+function findInvalid(str, start, end, cls) {
+  for (let i = start; i < end; i++) {
+    const c = StringPrototypeCharCodeAt(str, i);
+    if (c > 0xFF || (codePointClass[c] & cls) === 0) return i - start;
+  }
+  return -1;
+}
+
 function toASCIILower(str) {
-  // eslint-disable-next-line no-control-regex
-  if (!/[^\x00-\x7f]/.test(str)) return StringPrototypeToLowerCase(str);
-  let result = '';
+  let hasUpper = false;
   for (let i = 0; i < str.length; i++) {
-    const char = str[i];
-
-    result += char >= 'A' && char <= 'Z' ?
-      StringPrototypeToLowerCase(char) :
-      char;
+    const c = StringPrototypeCharCodeAt(str, i);
+    if (c > 0x7F) {
+      let result = '';
+      for (let j = 0; j < str.length; j++) {
+        const char = str[j];
+        result += char >= 'A' && char <= 'Z' ?
+          StringPrototypeToLowerCase(char) :
+          char;
+      }
+      return result;
+    }
+    if (c <= 0x5A && c >= 0x41) hasUpper = true;
   }
-  return result;
+  // Returning the input unchanged when possible avoids an allocation and
+  // keeps its cached hash for Map lookups.
+  return hasUpper ? StringPrototypeToLowerCase(str) : str;
 }

-const SOLIDUS = '/';
-const SEMICOLON = ';';
-
-function parseTypeAndSubtype(str, noThrow = null) {
-  // Skip only HTTP whitespace from start
-  let position = SafeStringPrototypeSearch(str, END_BEGINNING_WHITESPACE);
-  // read until '/'
-  const typeEnd = StringPrototypeIndexOf(str, SOLIDUS, position);
-  const trimmedType = typeEnd === -1 ?
-    StringPrototypeSlice(str, position) :
-    StringPrototypeSlice(str, position, typeEnd);
-  const invalidTypeIndex = SafeStringPrototypeSearch(trimmedType,
-                                                     NOT_HTTP_TOKEN_CODE_POINT);
-  if (trimmedType === '' || invalidTypeIndex !== -1 || typeEnd === -1) {
-    if (noThrow === kNoThrow) return null;
-    throw new ERR_INVALID_MIME_SYNTAX('type', str, invalidTypeIndex);
+// Results of parseTypeAndSubtype(). Kept in module state to avoid allocating
+// a result object per parse; they are consumed synchronously by the caller.
+let parsedType = '';
+let parsedSubtype = '';
+let parsedParamsStart = 0;
+let parsedParamsEnd = 0;
+let failedProduction = '';
+let failedIndex = -1;
+
+/**
+ * Parses the type and subtype of a MIME type string and locates its
+ * parameters. Returns false on failure, with failedProduction and
+ * failedIndex describing the error.
+ * @see https://mimesniff.spec.whatwg.org/#parse-a-mime-type
+ * @param {string} str
+ * @returns {boolean}
+ */
+function parseTypeAndSubtype(str) {
+  const length = str.length;
+
+  // Skip leading HTTP whitespace.
+  let position = 0;
+  while (position < length &&
+         isHTTPWhitespace(StringPrototypeCharCodeAt(str, position))) {
+    position++;
   }
-  // skip type and '/'
-  position = typeEnd + 1;
-  const type = toASCIILower(trimmedType);
-  // read until ';'
-  const subtypeEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
-  const rawSubtype = subtypeEnd === -1 ?
-    StringPrototypeSlice(str, position) :
-    StringPrototypeSlice(str, position, subtypeEnd);
-  position += rawSubtype.length;
-  if (subtypeEnd !== -1) {
-    // skip ';'
-    position += 1;
+
+  // Collect the type, up to '/'.
+  const typeStart = position;
+  let invalidTypeIndex = -1;
+  let typeHasUpper = false;
+  for (; position < length; position++) {
+    const c = StringPrototypeCharCodeAt(str, position);
+    if (c === 0x2F /* / */) break;
+    if (c > 0xFF || (codePointClass[c] & kHTTPToken) === 0) {
+      if (invalidTypeIndex === -1) invalidTypeIndex = position - typeStart;
+    } else if (c <= 0x5A && c >= 0x41) {
+      typeHasUpper = true;
+    }
   }
-  const trimmedSubtype = StringPrototypeSlice(
-    rawSubtype,
-    0,
-    SafeStringPrototypeSearch(rawSubtype, START_ENDING_WHITESPACE));
-  const invalidSubtypeIndex = SafeStringPrototypeSearch(trimmedSubtype,
-                                                        NOT_HTTP_TOKEN_CODE_POINT);
-  if (trimmedSubtype === '' || invalidSubtypeIndex !== -1) {
-    if (noThrow === kNoThrow) return null;
-    throw new ERR_INVALID_MIME_SYNTAX('subtype', str, invalidSubtypeIndex);
+  if (position === typeStart || invalidTypeIndex !== -1 || position >= length) {
+    failedProduction = 'type';
+    failedIndex = invalidTypeIndex;
+    return false;
   }
-  const subtype = toASCIILower(trimmedSubtype);
-  return [
-    type,
-    subtype,
-    position,
-  ];
+  const typeEnd = position;
+
+  // Skip '/' and collect the subtype, up to ';'. In the same pass, find the
+  // first non-token code point and where the trailing HTTP whitespace starts.
+  position++;
+  const subtypeStart = position;
+  let subtypeEnd = position;
+  let firstNonToken = -1;
+  let subtypeHasUpper = false;
+  for (; position < length; position++) {
+    const c = StringPrototypeCharCodeAt(str, position);
+    if (c === 0x3B /* ; */) break;
+    if (c <= 0xFF && (codePointClass[c] & kHTTPToken) !== 0) {
+      if (c <= 0x5A && c >= 0x41) subtypeHasUpper = true;
+    } else if (firstNonToken === -1) {
+      firstNonToken = position;
+    }
+    if (!isHTTPWhitespace(c)) subtypeEnd = position + 1;
+  }
+  const subtypeRawEnd = position;
+  // Whitespace is not a token code point, so a non-token code point at or
+  // after subtypeEnd is part of the trailing whitespace, which is removed.
+  const invalidSubtypeIndex = firstNonToken !== -1 && firstNonToken < subtypeEnd ?
+    firstNonToken - subtypeStart : -1;
+  if (subtypeEnd === subtypeStart || invalidSubtypeIndex !== -1) {
+    failedProduction = 'subtype';
+    failedIndex = invalidSubtypeIndex;
+    return false;
+  }
+
+  // Parameters are everything after the ';' up to the trailing whitespace.
+  let paramsEnd = length;
+  while (paramsEnd > subtypeRawEnd &&
+         isHTTPWhitespace(StringPrototypeCharCodeAt(str, paramsEnd - 1))) {
+    paramsEnd--;
+  }
+
+  const type = StringPrototypeSlice(str, typeStart, typeEnd);
+  const subtype = StringPrototypeSlice(str, subtypeStart, subtypeEnd);
+  // Both are ASCII-only at this point, so toLowerCase() is ASCII lowercase.
+  parsedType = typeHasUpper ? StringPrototypeToLowerCase(type) : type;
+  parsedSubtype = subtypeHasUpper ? StringPrototypeToLowerCase(subtype) : subtype;
+  parsedParamsStart = subtypeRawEnd + 1;
+  parsedParamsEnd = paramsEnd;
+  return true;
 }

-const EQUALS_SEMICOLON_OR_END = /[;=]|$/;
-const QUOTED_VALUE_PATTERN = /^(?:([\\]$)|[\\][\s\S]|[^"])*(?:(")|$)/u;
-
-function removeBackslashes(str) {
-  let ret = '';
-  // We stop at str.length - 1 because we want to look ahead one character.
-  let i;
-  for (i = 0; i < str.length - 1; i++) {
-    const c = str[i];
-    if (c === '\\') {
-      i++;
-      ret += str[i];
+/**
+ * Parses MIME type parameters from `str[position, end)` into `params`.
+ * `position` is just past the ';' that follows the subtype, and `end`
+ * excludes trailing HTTP whitespace. This is step 11 of
+ * https://mimesniff.spec.whatwg.org/#parse-a-mime-type
+ * @param {string} str
+ * @param {number} position
+ * @param {number} end
+ * @param {SafeMap<string, string>} params
+ */
+function parseParameters(str, position, end, params) {
+  while (position < end) {
+    // Skip HTTP whitespace.
+    while (position < end &&
+           isHTTPWhitespace(StringPrototypeCharCodeAt(str, position))) {
+      position++;
+    }
+
+    // Collect the parameter name, up to ';' or '='.
+    const nameStart = position;
+    let nameIsToken = true;
+    let nameHasUpper = false;
+    for (; position < end; position++) {
+      const c = StringPrototypeCharCodeAt(str, position);
+      if (c === 0x3B /* ; */ || c === 0x3D /* = */) break;
+      if (c > 0xFF || (codePointClass[c] & kHTTPToken) === 0) {
+        nameIsToken = false;
+      } else if (c <= 0x5A && c >= 0x41) {
+        nameHasUpper = true;
+      }
+    }
+    const nameEnd = position;
+
+    if (position < end) {
+      // Parameters without a value are ignored.
+      if (StringPrototypeCharCodeAt(str, position) === 0x3B /* ; */) {
+        position++;
+        continue;
+      }
+      // Skip '='.
+      position++;
+    }
+    if (position >= end) break;
+
+    let value;
+    if (StringPrototypeCharCodeAt(str, position) === 0x22 /* " */) {
+      // Collect an HTTP quoted string with the extract-value flag.
+      // https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
+      position++;
+      value = '';
+      let chunkStart = position;
+      while (true) {
+        while (position < end) {
+          const c = StringPrototypeCharCodeAt(str, position);
+          if (c === 0x22 /* " */ || c === 0x5C /* \ */) break;
+          position++;
+        }
+        if (position >= end) {
+          value += StringPrototypeSlice(str, chunkStart, position);
+          break;
+        }
+        const quoteOrBackslash = StringPrototypeCharCodeAt(str, position);
+        value += StringPrototypeSlice(str, chunkStart, position);
+        position++;
+        if (quoteOrBackslash === 0x5C /* \ */) {
+          if (position >= end) {
+            value += '\\';
+            break;
+          }
+          // The escaped code point starts the next chunk.
+          chunkStart = position;
+          position++;
+        } else {
+          break;
+        }
+      }
+      // Skip anything else up to the next ';'.
+      while (position < end &&
+             StringPrototypeCharCodeAt(str, position) !== 0x3B /* ; */) {
+        position++;
+      }
+      if (findInvalid(value, 0, value.length, kHTTPQuotedStringToken) !== -1) {
+        position++;
+        continue;
+      }
     } else {
-      ret += c;
+      // Collect the value up to ';', validating it and finding where its
+      // trailing HTTP whitespace starts in the same pass. CR and LF are not
+      // quoted-string token code points, but trailing ones are removed, so
+      // the value is only invalid if a bad code point comes before valueEnd.
+      const valueStart = position;
+      let valueEnd = position;
+      let firstInvalid = -1;
+      for (; position < end; position++) {
+        const c = StringPrototypeCharCodeAt(str, position);
+        if (c === 0x3B /* ; */) break;
+        if (firstInvalid === -1 &&
+            (c > 0xFF || (codePointClass[c] & kHTTPQuotedStringToken) === 0)) {
+          firstInvalid = position;
+        }
+        if (!isHTTPWhitespace(c)) valueEnd = position + 1;
+      }
+      // Parameters with an empty or invalid value are ignored.
+      if (valueEnd === valueStart ||
+          (firstInvalid !== -1 && firstInvalid < valueEnd)) {
+        position++;
+        continue;
+      }
+      value = StringPrototypeSlice(str, valueStart, valueEnd);
     }
+
+    if (nameEnd !== nameStart && nameIsToken) {
+      // The name is ASCII-only, so toLowerCase() is ASCII lowercase.
+      const name = nameHasUpper ?
+        StringPrototypeToLowerCase(StringPrototypeSlice(str, nameStart, nameEnd)) :
+        StringPrototypeSlice(str, nameStart, nameEnd);
+      if (!params.has(name)) params.set(name, value);
+    }
+    // Skip ';'.
+    position++;
   }
-  // We add the last character if we didn't skip to it.
-  if (i === str.length - 1) {
-    ret += str[i];
-  }
-  return ret;
 }

-
 function escapeQuoteOrSolidus(str) {
   let result = '';
   for (let i = 0; i < str.length; i++) {
@@ -123,28 +305,29 @@ function escapeQuoteOrSolidus(str) {

 const encode = (value) => {
   if (value.length === 0) return '""';
-  const encode = SafeStringPrototypeSearch(value, NOT_HTTP_TOKEN_CODE_POINT) !== -1;
-  if (!encode) return value;
-  const escaped = escapeQuoteOrSolidus(value);
-  return `"${escaped}"`;
+  if (findInvalid(value, 0, value.length, kHTTPToken) === -1) return value;
+  return `"${escapeQuoteOrSolidus(value)}"`;
 };

 class MIMEParams {
-  #data = new SafeMap();
-  // We set the flag the MIMEParams instance as processed on initialization
-  // to defer the parsing of a potentially large string.
-  #processed = true;
+  // Parsing is deferred until the parameters are first accessed, as most
+  // users only need the type and subtype. Until then #data is null and
+  // #string[#start, #end) holds the unparsed parameters.
+  #data = null;
   #string = null;
+  #start = 0;
+  #end = 0;

   /**
    * Used to instantiate a MIMEParams object within the MIMEType class and
    * to allow it to be parsed lazily.
    * @returns {MIMEParams}
    */
-  static instantiateMimeParams(str) {
+  static instantiateMimeParams(str, start, end) {
     const instance = new MIMEParams();
     instance.#string = str;
-    instance.#processed = false;
+    instance.#start = start;
+    instance.#end = end;
     return instance;
   }

@@ -153,31 +336,23 @@ class MIMEParams {
    * @returns {void}
    */
   delete(name) {
-    this.#parse();
-    this.#data.delete(toASCIILower(`${name}`));
+    this.#parse().delete(toASCIILower(`${name}`));
   }

   get(name) {
-    this.#parse();
-    const data = this.#data;
-    name = toASCIILower(`${name}`);
-    if (data.has(name)) {
-      return data.get(name);
-    }
-    return null;
+    const value = this.#parse().get(toASCIILower(`${name}`));
+    return value === undefined ? null : value;
   }

   has(name) {
-    this.#parse();
-    return this.#data.has(toASCIILower(`${name}`));
+    return this.#parse().has(toASCIILower(`${name}`));
   }

   set(name, value) {
-    this.#parse();
-    const data = this.#data;
+    const data = this.#parse();
     name = toASCIILower(`${name}`);
     value = `${value}`;
-    const invalidNameIndex = SafeStringPrototypeSearch(name, NOT_HTTP_TOKEN_CODE_POINT);
+    const invalidNameIndex = findInvalid(name, 0, name.length, kHTTPToken);
     if (name.length === 0 || invalidNameIndex !== -1) {
       throw new ERR_INVALID_MIME_SYNTAX(
         'parameter name',
@@ -185,9 +360,8 @@ class MIMEParams {
         invalidNameIndex,
       );
     }
-    const invalidValueIndex = SafeStringPrototypeSearch(
-      value,
-      NOT_HTTP_QUOTED_STRING_CODE_POINT);
+    const invalidValueIndex = findInvalid(value, 0, value.length,
+                                          kHTTPQuotedStringToken);
     if (invalidValueIndex !== -1) {
       throw new ERR_INVALID_MIME_SYNTAX(
         'parameter value',
@@ -199,24 +373,20 @@ class MIMEParams {
   }

   *entries() {
-    this.#parse();
-    yield* this.#data.entries();
+    yield* this.#parse().entries();
   }

   *keys() {
-    this.#parse();
-    yield* this.#data.keys();
+    yield* this.#parse().keys();
   }

   *values() {
-    this.#parse();
-    yield* this.#data.values();
+    yield* this.#parse().values();
   }

   toString() {
-    this.#parse();
     let ret = '';
-    for (const { 0: key, 1: value } of this.#data) {
+    for (const { 0: key, 1: value } of this.#parse()) {
       const encoded = encode(value);
       // Ensure they are separated
       if (ret.length) ret += ';';
@@ -225,99 +395,20 @@ class MIMEParams {
     return ret;
   }

-  // Used to act as a friendly class to stringifying stuff
-  // not meant to be exposed to users, could inject invalid values
+  /**
+   * Parses the deferred parameter string on first use.
+   * @returns {SafeMap<string, string>}
+   */
   #parse() {
-    if (this.#processed) return;  // already parsed
-    const paramsMap = this.#data;
-    let position = 0;
+    let data = this.#data;
+    if (data !== null) return data;
+    data = this.#data = new SafeMap();
     const str = this.#string;
-    const endOfSource = SafeStringPrototypeSearch(
-      StringPrototypeSlice(str, position),
-      START_ENDING_WHITESPACE,
-    ) + position;
-    while (position < endOfSource) {
-      // Skip any whitespace before parameter
-      position += SafeStringPrototypeSearch(
-        StringPrototypeSlice(str, position),
-        END_BEGINNING_WHITESPACE,
-      );
-      // Read until ';' or '='
-      const afterParameterName = SafeStringPrototypeSearch(
-        StringPrototypeSlice(str, position),
-        EQUALS_SEMICOLON_OR_END,
-      ) + position;
-      const parameterString = toASCIILower(
-        StringPrototypeSlice(str, position, afterParameterName),
-      );
-      position = afterParameterName;
-      // If we found a terminating character
-      if (position < endOfSource) {
-        // Safe to use because we never do special actions for surrogate pairs
-        const char = StringPrototypeCharAt(str, position);
-        // Skip the terminating character
-        position += 1;
-        // Ignore parameters without values
-        if (char === ';') {
-          continue;
-        }
-      }
-      // If we are at end of the string, it cannot have a value
-      if (position >= endOfSource) break;
-      // Safe to use because we never do special actions for surrogate pairs
-      const char = StringPrototypeCharAt(str, position);
-      let parameterValue = null;
-      if (char === '"') {
-        // Handle quoted-string form of values
-        // skip '"'
-        position += 1;
-        // Find matching closing '"' or end of string
-        //   use $1 to see if we terminated on unmatched '\'
-        //   use $2 to see if we terminated on a matching '"'
-        //   so we can skip the last char in either case
-        const insideMatch = RegExpPrototypeExec(
-          QUOTED_VALUE_PATTERN,
-          StringPrototypeSlice(str, position));
-        position += insideMatch[0].length;
-        // Skip including last character if an unmatched '\' or '"' during
-        // unescape
-        const inside = insideMatch[1] || insideMatch[2] ?
-          StringPrototypeSlice(insideMatch[0], 0, -1) :
-          insideMatch[0];
-        // Unescape '\' quoted characters
-        parameterValue = removeBackslashes(inside);
-        // If we did have an unmatched '\' add it back to the end
-        if (insideMatch[1]) parameterValue += '\\';
-      } else {
-        // Handle the normal parameter value form
-        const valueEnd = StringPrototypeIndexOf(str, SEMICOLON, position);
-        const rawValue = valueEnd === -1 ?
-          StringPrototypeSlice(str, position) :
-          StringPrototypeSlice(str, position, valueEnd);
-        position += rawValue.length;
-        const trimmedValue = StringPrototypeSlice(
-          rawValue,
-          0,
-          SafeStringPrototypeSearch(rawValue, START_ENDING_WHITESPACE),
-        );
-        // Ignore parameters without values
-        if (trimmedValue === '') continue;
-        parameterValue = trimmedValue;
-      }
-      if (
-        parameterString !== '' &&
-        SafeStringPrototypeSearch(parameterString,
-                                  NOT_HTTP_TOKEN_CODE_POINT) === -1 &&
-        SafeStringPrototypeSearch(parameterValue,
-                                  NOT_HTTP_QUOTED_STRING_CODE_POINT) === -1 &&
-        paramsMap.has(parameterString) === false
-      ) {
-        paramsMap.set(parameterString, parameterValue);
-      }
-      position++;
+    if (str !== null) {
+      this.#string = null;
+      parseParameters(str, this.#start, this.#end, data);
     }
-    this.#data = paramsMap;
-    this.#processed = true;
+    return data;
   }
 }
 const MIMEParamsStringify = MIMEParams.prototype.toString;
@@ -342,23 +433,34 @@ class MIMEType {
   #subtype;
   #parameters;
   constructor(string, noThrowSymbol = null) {
+    // MIMEType.parse() has already parsed the string successfully.
+    if (noThrowSymbol === kNoThrow) {
+      this.#init(string);
+      return;
+    }
     string = `${string}`;
-    // noThrowSymbol can be null or kNoThrow, but not any other value
-    if (noThrowSymbol != null && noThrowSymbol !== kNoThrow) {
+    if (noThrowSymbol != null) {
       throw new ERR_ILLEGAL_CONSTRUCTOR();
     }
-    const data = parseTypeAndSubtype(string, noThrowSymbol);
-    if (data != null) {
-      this.#type = data[0];
-      this.#subtype = data[1];
-      this.#parameters = instantiateMimeParams(StringPrototypeSlice(string, data[2]));
+    if (!parseTypeAndSubtype(string)) {
+      throw new ERR_INVALID_MIME_SYNTAX(failedProduction, string, failedIndex);
     }
+    this.#init(string);
+  }
+
+  // Consumes the results of a successful parseTypeAndSubtype(string).
+  #init(string) {
+    this.#type = parsedType;
+    this.#subtype = parsedSubtype;
+    this.#parameters = instantiateMimeParams(string, parsedParamsStart,
+                                             parsedParamsEnd);
   }

   // Like the constructor, but returns null instead of throwing on invalid input.
   static parse(string) {
-    const mt = new MIMEType(string, kNoThrow);
-    return mt.type ? mt : null;
+    string = `${string}`;
+    if (!parseTypeAndSubtype(string)) return null;
+    return new MIMEType(string, kNoThrow);
   }

   get type() {
@@ -367,7 +469,7 @@ class MIMEType {

   set type(v) {
     v = `${v}`;
-    const invalidTypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
+    const invalidTypeIndex = findInvalid(v, 0, v.length, kHTTPToken);
     if (v.length === 0 || invalidTypeIndex !== -1) {
       throw new ERR_INVALID_MIME_SYNTAX('type', v, invalidTypeIndex);
     }
@@ -380,7 +482,7 @@ class MIMEType {

   set subtype(v) {
     v = `${v}`;
-    const invalidSubtypeIndex = SafeStringPrototypeSearch(v, NOT_HTTP_TOKEN_CODE_POINT);
+    const invalidSubtypeIndex = findInvalid(v, 0, v.length, kHTTPToken);
     if (v.length === 0 || invalidSubtypeIndex !== -1) {
       throw new ERR_INVALID_MIME_SYNTAX('subtype', v, invalidSubtypeIndex);
     }
diff --git a/test/parallel/test-mime-whatwg.js b/test/parallel/test-mime-whatwg.js
index b61e6d620ab..399e1a33d73 100644
--- a/test/parallel/test-mime-whatwg.js
+++ b/test/parallel/test-mime-whatwg.js
@@ -11,9 +11,11 @@ function test(mimes) {
     const { input, output } = entry;
     if (output === null) {
       assert.throws(() => new MIMEType(input), /ERR_INVALID_MIME_SYNTAX/i);
+      assert.strictEqual(MIMEType.parse(input), null);
     } else {
       const str = `${new MIMEType(input)}`;
       assert.strictEqual(str, output);
+      assert.strictEqual(`${MIMEType.parse(input)}`, output);
     }
   }
 }