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