Module 'node:url' is disallowed

Describe the problem/error/question

Hi guys, I have a workflow which I am moving from hosting it on render to my own server (run with Coolify) and suddenly my code nodes are failing me.
Seems like node.js is networking as it should despite me adding all variables already:
NODE_FUNCTION_ALLOW_BUILTIN = url,fs,path,crypto,os,querystring,stream

I am more a tech noob and definitely not a programmer but I have tried it all by now. I can only assume the reason is somewhere with my server setup as it runs fine on render…

What is the error message (if any)?

{
“errorMessage”: “Module ‘node:url’ is disallowed [line 2]”,
“errorDetails”: {},
“n8nDetails”: {
“n8nVersion”: “1.111.0 (Self Hosted)”,
“binaryDataMode”: “default”,
“stackTrace”: [
“Error: Module ‘node:url’ is disallowed”,
" at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/@n8n+task-runner@file+packages+@n8n+task-runner_@[email protected]_@opentelemetry_5147392a3f2dab4b87aa45b14b25796aopentelemetryopentelemetry_5147392a3f2dab4b87aa45b14b25796aopentelemetry_5147392a3f2dab4b87aa45b14b25796a/node_modules/@n8n/task-runner/dist/js-task-runner/require-resolver.js:16:27",
" at VmCodeWrapper (evalmachine.:2:14)“,
" at evalmachine.:120:2”,
" at Script.runInContext (node:vm:149:12)“,
" at runInContext (node:vm:301:6)”,
" at result (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/@n8n+task-rPlease share your workflow

{“nodes”: [{“parameters”: {“mode”: “runOnceForEachItem”,“jsCode”: “// By requiring ‘url’, we can use the modern URL constructor\nconst url  = require(‘node:url’);\n\n/**\n * Safely resolves any URL using the standard URL constructor.\n * @param {string} href The link’s href.\n * @param {string} base The base URL.\n * @returns {string} The full, valid URL or an empty string.\n /\nfunction resolveUrl(href, base) {\n  try {\n    const fullUrl = new url.URL(href, base).toString();\n    return /^https?:/.test(fullUrl) ? fullUrl : “”;\n  } catch {\n    return “”;\n  }\n}\n\n// — Main Logic —\nconst item = $json;\nconst baseUrl = (item.cleaned_url || item.website_url || “”).toString().trim();\nconst html = (item.footer_html || item.data || item.body || “”).toString();\n\nlet impressumUrl = “”;\nlet foundKeyword = “”;\n\nif (html && baseUrl) {\n  // Keywords are grouped by priority to be used in our new scoring system\n  const highPriorityKeywords = [‘impressum’, ‘imprint’, ‘legal-notice’, ‘legal notice’, ‘mentions-legales’, ‘mentions légales’, ‘anbieterkennzeichnung’, ‘note-legali’, ‘aviso-legal’];\n  const mediumPriorityKeywords = [‘kontakt’, ‘contact’, ‘contatti’];\n  const lowPriorityKeywords = [‘about’, ‘ueber-uns’, ‘über uns’, ‘team’, ‘datenschutz’, ‘privacy’];\n\n  // This helper function finds the first matching keyword from a list in a piece of text\n  function findKeyword(text, keywords) {\n    for (const keyword of keywords) {\n      if (text.includes(keyword)) {\n        return keyword;\n      }\n    }\n    return null;\n  }\n\n  // This function finds the best link within a given piece of HTML using a weighted score\n  function findBestLink(searchHtml) {\n    let bestMatch = { url: ‘’, keyword: ‘’, score: 0 };\n    const linkRegex = /<a\s[^>]href\s=\s"'["'][^>]>(?[\s\S]?)<\/a>/gi;\n    const allLinks = […searchHtml.matchAll(linkRegex)];\n\n    for (const linkMatch of allLinks) {\n      const { href, text } = linkMatch.groups;\n      const hrefLower = href.toLowerCase();\n      const textLower = text.toLowerCase();\n      let currentScore = 0;\n      let currentKeyword = ‘’;\n\n      // NEW: Weighted scoring logic based on keyword priority\n      let keywordInHref, keywordInText;\n      \n      // Check for high-priority keywords\n      keywordInHref = findKeyword(hrefLower, highPriorityKeywords);\n      keywordInText = findKeyword(textLower, highPriorityKeywords);\n      if (keywordInHref) { currentScore = 100; currentKeyword = keywordInHref; } \n      else if (keywordInText) { currentScore = 50; currentKeyword = keywordInText; }\n      \n      // Check for medium-priority keywords only if no high-priority match was found\n      if (currentScore === 0) {\n        keywordInHref = findKeyword(hrefLower, mediumPriorityKeywords);\n        keywordInText = findKeyword(textLower, mediumPriorityKeywords);\n        if (keywordInHref) { currentScore = 10; currentKeyword = keywordInHref; }\n        else if (keywordInText) { currentScore = 5; currentKeyword = keywordInText; }\n      }\n      \n      // Check for low-priority keywords only if no better match was found\n      if (currentScore === 0) {\n        keywordInHref = findKeyword(hrefLower, lowPriorityKeywords);\n        keywordInText = findKeyword(textLower, lowPriorityKeywords);\n        if (keywordInHref) { currentScore = 2; currentKeyword = keywordInHref; }\n        else if (keywordInText) { currentScore = 1; currentKeyword = keywordInText; }\n      }\n\n      // If this link has a higher score, it’s our new best match\n      if (currentScore > bestMatch.score) {\n        const resolvedUrl = resolveUrl(href, baseUrl);\n        if (resolvedUrl) {\n          bestMatch = { url: resolvedUrl, keyword: currentKeyword, score: currentScore };\n        }\n      }\n    }\n    return bestMatch;\n  }\n\n  // — Search Execution —\n  // 1. First, try to search only within the  tag\n  const footerMatch = html.match(/<footer[\s\S]?<\/footer>/i);\n  let finalMatch = { url: ‘’, keyword: ‘’ };\n\n  if (footerMatch) {\n    const footerHtml = footerMatch[0];\n    finalMatch = findBestLink(footerHtml);\n  }\n\n  // 2. If no link was found in the footer, search the entire document\n  if (!finalMatch.url) {\n    finalMatch = findBestLink(html);\n  }\n  \n  impressumUrl = finalMatch.url;\n  foundKeyword = finalMatch.keyword;\n}\n\n// — Return Output —\nreturn {\n  row_number: item.row_number ?? null,\n  company_name: item.company_name ?? “”,\n  website_url: baseUrl,\n  impressum_url: impressumUrl,\n  found_keyword: foundKeyword,\n  status: impressumUrl ? “ok” : “no_impressum_found”\n};"},“type”: “n8n-nodes-base.code”,“typeVersion”: 2,“position”: [-752,736],“id”: “1d51ef95-51f1-4d3e-a658-cbbf8eef56b2”,“name”: “Look for Impressum Links”},{“parameters”: {“jsCode”: "// Clean and normalize website URLs for HTTP requests\nconst items = $input.all();\nconst cleanedResults = [];\n\nfor (const item of items) {\n  const data = item.json;\n  let websiteUrl = data[‘Website URL’] || data.website_url || ‘’;\n  const companyName = data[‘Company name’] || data[‘Company Name’] || data.company_name || ‘’;\n  const rowNumber = data.row_number || ‘’;\n  \n  // Skip if no URL provided\n  if (!websiteUrl || websiteUrl.trim() === ‘’) {\n    cleanedResults.push({\n      json: {\n        row_number: rowNumber,\n        company_name: companyName,\n        original_url: websiteUrl,\n        cleaned_url: ‘’,\n        error: ‘No URL provided’,\n        status: ‘error’\n      }\n    });\n    continue;\n  }\n  \n  // Clean the URL\n  let cleanedUrl = websiteUrl.trim();\n  \n  // ADDED: Handle special cases first\n  if (cleanedUrl.includes(‘treatwell.de’)) {\n    cleanedUrl = ‘https://www.treatwell.de’;\n  } else {\n    // CHANGED: A much safer way to handle protocols and ‘www’\n    // 1. Add https:// only if no protocol exists\n    if (!cleanedUrl.startsWith(‘http’)) {\n      cleanedUrl = ‘https://’ + cleanedUrl;\n    }\n    \n    // 2. Remove ‘www.’ without affecting the protocol\n    cleanedUrl = cleanedUrl.replace(‘://www.’, ‘://’);\n    \n    // 3. Remove trailing slash\n    cleanedUrl = cleanedUrl.replace(/\/$/, ‘’);\n  }\n  \n  // REPLACED: Use a reliable regex for validation instead of new URL()\n  const urlRegex = /^https?:\/\/[^\s$.?#].[^\s]$/i;\n  if (urlRegex.test(cleanedUrl)) {\n    cleanedResults.push({\n      json: {\n        row_number: rowNumber,\n        company_name: companyName,\n        original_url: websiteUrl,\n        cleaned_url: cleanedUrl,\n        status: ‘ready’\n      }\n    });\n  } else {\n    cleanedResults.push({\n      json: {\n        row_number: rowNumber,\n        company_name: companyName,\n        original_url: websiteUrl,\n        cleaned_url: cleanedUrl,\n        error: ‘Invalid URL format after cleaning’,\n        status: ‘error’\n      }\n    });\n  }\n}\n\nreturn cleanedResults;”},“type”: “n8n-nodes-base.code”,“typeVersion”: 2,“position”: [-1648,736],“id”: “866c336e-c0ce-4715-b3de-f48c2af6efe3”,“name”: “Clean URLs to work”},{“parameters”: {“mode”: “combine”,“combineBy”: “combineByPosition”,“options”: {}},“type”: “n8n-nodes-base.merge”,“typeVersion”: 3.2,“position”: [-960,736],“id”: “107d7ea1-2951-47a6-9cde-0933929ed67a”,“name”: “Merge”},{“parameters”: {“mode”: “runOnceForEachItem”,“jsCode”: “// Works with Response Format = Text or Autodetect (string in $json.data)\n\nfunction getHtml(json, binary){\n  if (typeof json.data === ‘string’ && json.data.length) return json.data;\n  if (typeof json.body === ‘string’ && json.body.length) return json.body; // older nodes\n  if (json.data && typeof json.data === ‘object’) try { return JSON.stringify(json.data); } catch {}\n  if (binary?.data?.data) try { return Buffer.from(binary.data.data, ‘base64’).toString(‘utf8’); } catch {}\n  return ‘’;\n}\n\nconst htmlRaw = getHtml($json, $binary);\n\n// strip heavy blocks\nconst cleaned = htmlRaw\n  .replace(/<script[\s\S]?<\/script>/gi, ’ ')\n  .replace(/<style[\s\S]?<\/style>/gi, ’ ‘)\n  .replace(/<noscript[\s\S]?<\/noscript>/gi, ’ ');\n\n// prefer real , else smart tail near keywords\nconst footers = cleaned.match(/<footer\b[\s\S]?<\/footer>/gi);\nconst tail = cleaned.slice(-15000);\nconst kw = /(impressum|imprint|offenlegung|kontakt|datenschutz|privacy)/i;\nlet smartTail = ‘’;\nconst pos = tail.search(kw);\nif (pos !== -1) {\n  const start = Math.max(0, pos - 4000);\n  smartTail = tail.slice(start, Math.min(tail.length, start + 9000));\n}\n\nconst footer_html = (footers && footers.length) ? footers.join(’\n’) : (smartTail || tail);\nconst footer_text = footer_html.replace(/<[^>]+>/g, ’ ').replace(/\s+/g, ’ ').trim();\n\nreturn {\n  http_status: $json.statusCode ?? ‘’,\n  source_url: $json.request?.url ?? $json.url ?? ‘’,\n  html_size: htmlRaw.length,\n  footer_len: footer_html.length,\n  footer_html,\n};”},“type”: “n8n-nodes-base.code”,“typeVersion”: 2,“position”: [-1184,736],“id”: “feb1eec3-e902-442e-bf1c-70b259d24b46”,“name”: “Reduce to footer content only1”},{“parameters”: {“assignments”: {“assignments”: [{“id”: “ab73d894-b74c-4916-840d-2dd6f76bdb4b”,“name”: “Website URL”,“value”: “={{ $(‘Research loop’).item.json[“Website URL”] }}”,“type”: “string”}]},“options”: {}},“type”: “n8n-nodes-base.set”,“typeVersion”: 3.4,“position”: [-1872,736],“id”: “5e9bc278-2d39-45db-a601-5ed4b019ae28”,“name”: “Edit Fields”},{“parameters”: {“url”: “={{ $json.cleaned_url }}”,“sendHeaders”: true,“headerParameters”: {“parameters”: [{“name”: “User-Agent”,“value”: “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36”},{“name”: “Accept”,“value”: “text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8”},{“name”: “Accept-Encoding”,“value”: “gzip, deflate, br”},{“name”: “Accept-Language”,“value”: “en-US,en;q=0.9,de;q=0.8”},{“name”: “Referrer”,“value”: “={{ $json.website_url }}”}]},“options”: {“response”: {“response”: {“fullResponse”: true}}}},“type”: “n8n-nodes-base.httpRequest”,“typeVersion”: 4.2,“position”: [-1408,736],“id”: “db93bcaa-3419-434d-9f93-75c96b2f767b”,“name”: “Read all homepages”,“notesInFlow”: true,“retryOnFail”: true,“onError”: “continueRegularOutput”,“notes”: “Reads all landing pages and returns the html for a later node to find the impressum link in there.”}],“connections”: {“Look for Impressum Links”: {“main”: []},“Clean URLs to work”: {“main”: [[{“node”: “Read all homepages”,“type”: “main”,“index”: 0},{“node”: “Merge”,“type”: “main”,“index”: 1}]]},“Merge”: {“main”: [[{“node”: “Look for Impressum Links”,“type”: “main”,“index”: 0}]]},“Reduce to footer content only1”: {“main”: [[{“node”: “Merge”,“type”: “main”,“index”: 0}]]},“Edit Fields”: {“main”: [[{“node”: “Clean URLs to work”,“type”: “main”,“index”: 0}]]},“Read all homepages”: {“main”: [[{“node”: “Reduce to footer content only1”,“type”: “main”,“index”: 0}]]}},“pinData”: {},“meta”: {“templateCredsSetupCompleted”: true,“instanceId”: “221b02b6cc99293a6c6505fedb49c9f4ba2d0578b820c17b79f632c2ee437149”}}
(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)

Share the output returned by the last node

none, erroring out

Information on your n8n setup

  • n8n version: 1.111.0 (self hosted)
  • Database (default: SQLite): no clue
  • n8n EXECUTIONS_PROCESS setting (default: own, main): default
  • Running n8n via (Docker, npm, n8n cloud, desktop app): Docker
  • Operating system: iOS

Could you try to re-share your workflow? Currently the quotes are all broken.
Also, have you tried:

NODE_FUNCTION_ALLOW_BUILTIN=*

Feel free to show us how/where you pass that variable too

The error Module 'node:url' is disallowed means n8n is blocking access to that Node.js module inside your Code node, likely due to missing or misconfigured environment variables in your Docker setup.

Double-check that this variable is actually being passed into your running container. If you’re using Coolify, make sure it’s added to the Docker Compose environment or app settings, then fully restart the container.

Also, try setting:

NODE_FUNCTION_ALLOW_EXTERNAL=*

to allow external packages if you’re using any.
Quick fix (alternative): Instead of require('node:url'), just use the global URL constructor (built-in to JS), like this:

const fullUrl = new URL(href, base).toString();

No need to import anything
works the same!

1 Like

Double-check that this variable is actually being passed into your running container. If you’re using Coolify, make sure it’s added to the Docker Compose environment or app settings, then fully restart the container.

That was the issue, I had to manually add the variables in the docker compose file because it was not added automatically,
Very mean…

Thanks a ton for your help!!

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.