调试6 分钟阅读

如何修复 Node.js 中的 ERR_INVALID_URL 错误

Node.js 中的 ERR_INVALID_URL 错误发生在 URL 构造函数或 url.parse() 接收到一个不合法的 URL 字符串时。常见原因包括缺少协议、未编码的特殊字符以及格式错误的 percent-encoding。解决方法是在解析前校验输入并对特殊字符进行编码。

ERR_INVALID_URL 是什么原因导致的?

ERR_INVALID_URL 错误(完整形式为 TypeError [ERR_INVALID_URL]: Invalid URL)是当 URL 构造函数或旧版的 url.parse() 函数接收到一个无法被解析为合法 URL 的字符串时,由 Node.js 抛出的。这个错误在处理用户输入、解析配置文件或处理来自外部来源的 URL 时很常见。

URL 构造函数遵循 WHATWG URL 标准,它比旧的基于 RFC 的解析方式更严格。一个字符串必须包含合法的 scheme(例如 https:)、合法的 authority(主机名),以及格式正确的路径和查询组件,才能被接受为合法的 URL。

// 这会抛出 ERR_INVALID_URL
try {
  const url = new URL('not-a-url');
} catch (err) {
  console.log(err.code);    // 'ERR_INVALID_URL'
  console.log(err.message); // 'Invalid URL: not-a-url'
  console.log(err.input);   // 'not-a-url'
}

常见原因及修复方法

原因 1:缺少协议/scheme。 URL 构造函数要求提供 https://http:// 这样的 scheme。像 example.com/pathwww.example.com 这样的字符串会失败。

// 失败:没有协议
new URL('example.com/path');  // ERR_INVALID_URL

// 修复:加上协议
new URL('https://example.com/path');  // 有效!

// 修复:如果缺少协议则补上
function ensureProtocol(urlString) {
  if (!/^https?:\/\//i.test(urlString)) {
    return 'https://' + urlString;
  }
  return urlString;
}
new URL(ensureProtocol('example.com/path'));  // 有效!

原因 2:未编码的特殊字符。 空格、花括号、竖线以及某些 Unicode 字符等,如果不经过编码,是不允许出现在 URL 中的。

// 失败:未编码的空格和特殊字符
new URL('https://example.com/my file.pdf');    // ERR_INVALID_URL
new URL('https://example.com/path?q=a b');     // 在某些版本中可能失败

// 修复:对有问题的部分进行编码
const filename = encodeURIComponent('my file.pdf');
new URL('https://example.com/' + filename);    // 有效!

// 修复:对包含空格的完整 URL 使用 encodeURI
const rawUrl = 'https://example.com/my file.pdf';
new URL(encodeURI(rawUrl));  // 有效!

原因 3:格式错误的 percent-encoding。 如果 URL 中包含一个百分号,而其后没有紧跟恰好两位十六进制数字,解析器就会拒绝它。

// 失败:格式错误的 percent encoding
new URL('https://example.com/100%done');       // ERR_INVALID_URL
new URL('https://example.com/path?q=50%');     // ERR_INVALID_URL

// 修复:把落单的百分号编码为 %25
function fixPercentSigns(urlString) {
  return urlString.replace(/%(?![0-9A-Fa-f]{2})/g, '%25');
}
new URL(fixPercentSigns('https://example.com/100%done'));
// 有效!URL 变为 https://example.com/100%25done

原因 4:空输入或 null 输入。 向 URL 构造函数传入空字符串、null 或 undefined 同样会触发这个错误。

// 失败:空输入或 null 输入
new URL('');           // ERR_INVALID_URL
new URL(null);         // ERR_INVALID_URL
new URL(undefined);    // ERR_INVALID_URL

// 修复:在解析前校验输入
function parseUrl(input) {
  if (!input || typeof input !== 'string') {
    return null;
  }
  try {
    return new URL(input);
  } catch {
    return null;
  }
}

原因 5:没有 base 的相对 URL。 URL 构造函数默认将第一个参数当作绝对 URL 处理。像 /path/to/page 这样的相对 URL 需要将 base URL 作为第二个参数传入。

// 失败:没有 base 的相对 URL
new URL('/api/users');  // ERR_INVALID_URL

// 修复:提供一个 base URL
new URL('/api/users', 'https://example.com');
// 有效!→ https://example.com/api/users

// 适用于 API 客户端的实用模式
const BASE_URL = 'https://api.example.com';
const endpoint = new URL('/v2/users?page=1', BASE_URL);
console.log(endpoint.href);
// "https://api.example.com/v2/users?page=1"

如何在解析前校验 URL

在 Node.js 中校验 URL 最可靠的方法,就是在 try-catch 块中使用 URL 构造函数。(截至 Node.js 22)并不存在内置的 URL.isValid() 方法,因此捕获错误是标准的做法。

// 简单的 URL 校验器
function isValidUrl(string) {
  try {
    new URL(string);
    return true;
  } catch {
    return false;
  }
}

console.log(isValidUrl('https://example.com'));  // true
console.log(isValidUrl('not-a-url'));            // false
console.log(isValidUrl(''));                     // false

// 带允许协议限制的校验器
function isValidHttpUrl(string) {
  try {
    const url = new URL(string);
    return url.protocol === 'http:' || url.protocol === 'https:';
  } catch {
    return false;
  }
}

console.log(isValidHttpUrl('https://example.com'));     // true
console.log(isValidHttpUrl('ftp://example.com'));       // false
console.log(isValidHttpUrl('javascript:alert(1)'));     // false

安全的 URL 解析模式

下面是一个健壮的 URL 解析模式,它能处理所有常见的错误情况,并返回一个解析后的 URL 对象或一条有意义的错误信息。

class UrlParser {
  static parse(input, base) {
    // 校验输入
    if (!input || typeof input !== 'string') {
      return { ok: false, error: 'Input must be a non-empty string' };
    }

    // 去除首尾空白
    const trimmed = input.trim();

    // 修复常见问题
    let urlString = trimmed;

    // 如果缺少协议则补上
    if (/^[a-zA-Z0-9]/.test(urlString) && !urlString.includes('://')) {
      urlString = 'https://' + urlString;
    }

    // 修复落单的百分号
    urlString = urlString.replace(/%(?![0-9A-Fa-f]{2})/g, '%25');

    try {
      const url = base ? new URL(urlString, base) : new URL(urlString);

      // 可选:限制为安全的协议
      if (!['http:', 'https:'].includes(url.protocol)) {
        return { ok: false, error: 'Unsupported protocol: ' + url.protocol };
      }

      return { ok: true, url };
    } catch (err) {
      return { ok: false, error: err.message };
    }
  }
}

// 用法
const result = UrlParser.parse('example.com/path?q=hello world');
if (result.ok) {
  console.log(result.url.href);
} else {
  console.error(result.error);
}

预防最佳实践

遵循以下实践将帮助你在 Node.js 应用中避免 ERR_INVALID_URL 错误,并构建更具韧性的 URL 处理逻辑。

  • 始终将 URL 解析包裹在 try-catch 中。 永远不要假设一个 URL 字符串是合法的,尤其是当它来自用户输入、环境变量、数据库或外部 API 时。
  • 使用 URL API 而非字符串拼接。 使用 URL 构造函数和 URLSearchParams 来构建 URL,而不是拼接字符串。API 会自动处理编码。
  • 在启动时校验环境变量。 如果你的应用从环境变量或配置文件中读取 URL,请在应用启动时就校验它们,而不是等到首次使用时。
  • 在将用户输入嵌入 URL 之前先进行编码。 对查询参数值始终使用 encodeURIComponent(),对用户提供的完整 URL 使用 encodeURI()
  • 对复杂场景使用 URL 构建器。 对于需要拼装大量动态部分的 URL,请创建一个能一致处理编码的辅助函数或类。
  • 出错时记录原始输入。 当你捕获到 ERR_INVALID_URL 错误时,记录下导致该错误的输入(如果其中可能包含敏感数据则先做脱敏处理),以帮助排查问题。
// 良好实践:在启动时校验配置中的 URL
const requiredUrls = ['API_BASE_URL', 'AUTH_SERVER_URL', 'WEBHOOK_URL'];

for (const envVar of requiredUrls) {
  const value = process.env[envVar];
  if (!value) {
    throw new Error(envVar + ' environment variable is required');
  }
  try {
    new URL(value);
  } catch {
    throw new Error(envVar + ' is not a valid URL: ' + value);
  }
}

// 良好实践:使用 URL API 来构建 URL
function buildApiUrl(endpoint, params) {
  const url = new URL(endpoint, process.env.API_BASE_URL);
  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, String(value));
  }
  return url.toString();
}

const searchUrl = buildApiUrl('/api/search', {
  q: 'Node.js & Express',
  page: 1
});
// "https://api.example.com/api/search?q=Node.js+%26+Express&page=1"

相关文章

试用我们的免费工具