{"record":{"id":"fea9186a142293ed","repo":"nodejs/node","slug":"expected-opts-methods-to-only-contain-safe-http-me","errorCode":null,"errorMessage":"expected opts.methods to only contain safe HTTP methods, got ${method}","messagePattern":"expected opts\\.methods to only contain safe HTTP methods, got (.+?)","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"deps/undici/src/lib/interceptor/deduplicate.js","lineNumber":32,"sourceCode":"module.exports = (opts = {}) => {\n  const {\n    methods = ['GET'],\n    skipHeaderNames = [],\n    excludeHeaderNames = [],\n    maxBufferSize = 5 * 1024 * 1024\n  } = opts\n\n  if (typeof opts !== 'object' || opts === null) {\n    throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)\n  }\n\n  if (!Array.isArray(methods)) {\n    throw new TypeError(`expected opts.methods to be an array, got ${typeof methods}`)\n  }\n\n  for (const method of methods) {\n    if (!util.safeHTTPMethods.includes(method)) {\n      throw new TypeError(`expected opts.methods to only contain safe HTTP methods, got ${method}`)\n    }\n  }\n\n  if (!Array.isArray(skipHeaderNames)) {\n    throw new TypeError(`expected opts.skipHeaderNames to be an array, got ${typeof skipHeaderNames}`)\n  }\n\n  if (!Array.isArray(excludeHeaderNames)) {\n    throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`)\n  }\n\n  if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) {\n    throw new TypeError(`expected opts.maxBufferSize to be a positive finite number, got ${maxBufferSize}`)\n  }\n\n  // Convert to lowercase Set for case-insensitive header matching\n  const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase()))\n","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/nodejs/node/blob/1b2de5e052fc0fb95fd7fb6846dcec4ade598e9e/deps/undici/src/lib/interceptor/deduplicate.js#L14-L50","documentation":"Thrown by the deduplicate interceptor when every element of the `methods` array is not a member of `util.safeHTTPMethods`, which is frozen to `['GET', 'HEAD', 'OPTIONS', 'TRACE']`. Deduplication shares a single response across N callers, which is only safe for methods defined as cacheable and side-effect-free. Including any non-safe method (POST, PUT, DELETE, PATCH, etc.) would let one caller trigger another's write, so the interceptor rejects them up front. It is a synchronous TypeError from the factory.","triggerScenarios":"Passing `methods: ['GET', 'POST']`, `methods: ['PUT']`, or any array containing a method not in `['GET','HEAD','OPTIONS','TRACE']`. Case matters: `methods: ['get']` also triggers it because the list contains uppercase entries and there is no normalization. Method names with trailing spaces or alternate casing fail the strict `.includes()`.","commonSituations":"Assuming all HTTP methods are deduplicable; trying to coalesce write/load-test traffic; copy-pasting a methods list from a caching layer that permits more methods; passing lowercase method names from a framework that lowercases verbs. The uppercase-only comparison is the most frequent surprise.","solutions":["Restrict `methods` to uppercase entries from ['GET', 'HEAD', 'OPTIONS', 'TRACE'].","If you passed lowercase verbs, uppercase them: `methods: opts.methods.map(m => m.toUpperCase())`.","For write methods, drop deduplication and use a different strategy (request coalescing is unsafe for non-idempotent writes).","If you genuinely need to coalesce a PUT/POST, reconsider — deduplication would corrupt the semantics of shared responses."],"exampleFix":"// before\ndeduplicate({ methods: ['GET', 'POST'] })\ndeduplicate({ methods: ['get'] })\n\n// after\ndeduplicate({ methods: ['GET'] })\ndeduplicate({ methods: ['GET', 'HEAD'] })","handlingStrategy":"type-guard","validationCode":"const SAFE = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE'])\nfunction validateMethods(methods) {\n  const up = methods.map(m => String(m).toUpperCase())\n  if (!up.every(m => SAFE.has(m))) {\n    throw new TypeError('methods must be subset of GET/HEAD/OPTIONS/TRACE')\n  }\n  return up\n}","typeGuard":"const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS', 'TRACE']\nfunction isSafeMethodArray(v) {\n  return Array.isArray(v) && v.every(m => typeof m === 'string' && SAFE_METHODS.includes(m.toUpperCase()))\n}","tryCatchPattern":null,"preventionTips":["Uppercase method names before passing them.","Remember deduplication is only valid for safe/idempotent verbs.","Document the allowed set next to where you build the option."],"tags":["undici","interceptor","deduplicate","http-method","idempotency","validation"],"backgroundTag":null,"analyzedSha":"1b2de5e052fc0fb95fd7fb6846dcec4ade598e9e","analyzedAt":"2026-08-13T00:53:24.642Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}