URL Decode JavaScript — decodeURIComponent()

·Front-end & Node.js Developer·ПровереноMarcus Webb·Опубликовано

Используйте бесплатный URL Decode Online прямо в браузере — установка не требуется.

Попробовать URL Decode Online онлайн →

Процентно-закодированные строки постоянно встречаются в JavaScript-коде — поисковый запрос приходит как q=standing+desk%26price%3A200, OAuth redirect как next=https%3A%2F%2Fdashboard.internal%2F, путь к файлу как reports%2F2025%2Fq1.pdf. URL-декодирование в JavaScript сводится к выбору правильной из трёх встроенных функций: decodeURIComponent(), decodeURI() и URLSearchParams — и выбор между ними является главной причиной большинства случаев скрытого повреждения данных, которые я наблюдал в продакшен-кодовых базах, особенно крайний случай с + как пробелом и двойным декодированием. Для быстрого разового декодирования без написания кода URL Decoder от ToolDeck обрабатывает это мгновенно в браузере. Данное руководство подробно рассматривает все три функции (ES2015+ / Node.js 10+): когда использовать каждую, чем они отличаются для пробелов и зарезервированных символов, декодирование из файлов и HTTP-запросов, безопасная обработка ошибок и четыре ошибки, вызывающие наиболее коварные баги в продакшене.

  • decodeURIComponent() декодирует все процентно-закодированные последовательности — это правильный выбор для отдельных значений параметров запроса и сегментов пути
  • decodeURI() сохраняет структурные символы URI, такие как / ? & = # : — используйте его только при декодировании полной строки URL, никогда для отдельных значений
  • URLSearchParams.get() возвращает уже декодированные значения — вызов decodeURIComponent() поверх него приводит к двойному декодированию
  • decodeURIComponent() НЕ декодирует + как пробел — для данных в формате application/x-www-form-urlencoded используйте URLSearchParams или замените + перед декодированием
  • Всегда оборачивайте decodeURIComponent() в try/catch, когда входные данные поступают от пользователя — одиночный % или неполная последовательность выбрасывает URIError

Что такое URL-декодирование?

Процентное кодирование (формально определено в RFC 3986) заменяет символы, небезопасные или имеющие структурное значение в URL, знаком % и двумя шестнадцатеричными цифрами — значением байта UTF-8 этого символа. URL-декодирование обращает это преобразование: каждая последовательность %XX конвертируется обратно в исходный байт, а результирующая последовательность байт интерпретируется как текст UTF-8. Пробел декодируется из %20, косая черта из %2F, символ не-ASCII «й» из %D0%B9 (его двухбайтовое представление UTF-8).

Символы, которые никогда не кодируются, называются незарезервированными символами — буквы A–Z и a–z, цифры 0–9 и - _ . ~. Всё остальное либо имеет структурную роль в URL (например, / разделяет сегменты пути, а & разделяет параметры запроса), либо должно быть закодировано при использовании как данные. Практическое следствие: фильтр поиска вида status=active&tier=premium, закодированный как единое значение параметра, приходит совершенно непохожим на оригинал.

Before · text
After · text
// Процентно-закодировано — как получено в HTTP-запросе или полезных данных вебхука
"q=price%3A%5B200+TO+800%5D%20AND%20brand%3ANorthwood%20%26%20status%3Ain-stock"
// После URL-декодирования — исходный фильтр Elasticsearch
"q=price:[200 TO 800] AND brand:Northwood & status:in-stock"

decodeURIComponent() — стандартная функция для декодирования значений

decodeURIComponent() — основная рабочая лошадка URL-декодирования в JavaScript. Она декодирует каждую последовательность %XX во входной строке — включая символы, имеющие структурное значение в URL, такие как %2F (косая черта), %3F (вопросительный знак), %26 (амперсанд) и %3D (знак равенства). Это делает её правильным выбором для декодирования отдельных значений параметров запроса и сегментов пути, но неправильным для декодирования полного URL — где эти структурные символы должны оставаться закодированными. Это глобальная функция: импорт не требуется ни в одной JavaScript-среде.

Минимальный рабочий пример

JavaScript (browser / Node.js)
// Декодирование отдельных значений параметров запроса — типичный сценарий

const city     = decodeURIComponent('S%C3%A3o%20Paulo')          // 'São Paulo'
const district = decodeURIComponent('It%C3%A1im%20Bibi')         // 'Itáim Bibi'
const category = decodeURIComponent('office%20furniture')         // 'office furniture'
const filter   = decodeURIComponent('price%3A%5B200+TO+800%5D')  // 'price:[200+TO+800]'
// Примечание: + НЕ декодируется как пробел — см. раздел о + в сравнительной таблице

console.log(city)      // São Paulo
console.log(district)  // Itáim Bibi
console.log(category)  // office furniture
console.log(filter)    // price:[200+TO+800]

Декодирование URL перенаправления, извлечённого из параметра запроса

JavaScript
// URL перенаправления был процентно-закодирован при встраивании как значение параметра
// на принимающей стороне: извлечь его, затем использовать — ручное декодирование не нужно с URLSearchParams

const incomingUrl = 'https://auth.company.com/callback' +
  '?next=https%3A%2F%2Fdashboard.internal%2Freports%3Fview%3Dweekly%26team%3Dplatform' +
  '&session_id=sid_7x9p2k'

const url       = new URL(incomingUrl)
const rawNext   = url.searchParams.get('next')       // Автоматически декодируется URLSearchParams
const sessionId = url.searchParams.get('session_id') // 'sid_7x9p2k'

// rawNext уже декодирован: 'https://dashboard.internal/reports?view=weekly&team=platform'
// НЕ вызывайте decodeURIComponent(rawNext) снова — это будет двойное декодирование

const nextUrl = new URL(rawNext!)
console.log(nextUrl.hostname)                    // dashboard.internal
console.log(nextUrl.searchParams.get('view'))    // weekly
console.log(nextUrl.searchParams.get('team'))    // platform

Декодирование не-ASCII и Unicode сегментов пути

JavaScript
// REST API с интернационализированными сегментами пути
// Каждый байт UTF-8 исходного символа был процентно-закодирован отдельно

const encodedSegments = [
  '%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0',   // Москва — 2 байта на символ
  '%D0%A1%D0%B0%D0%BD%D0%BA%D1%82-%D0%9F%D0%B5%D1%82%D0%B5%D1%80%D0%B1%D1%83%D1%80%D0%B3',  // Санкт-Петербург
  'caf%C3%A9',            // café             — é как прекомпозированный NFC
  'S%C3%A3o%20Paulo',     // São Paulo
]

encodedSegments.forEach(seg => {
  console.log(decodeURIComponent(seg))
})
// Москва
// Санкт-Петербург
// café
// São Paulo

// Извлечение ключа файла из URL API хранилища
// Ключ объекта содержал / поэтому был закодирован как %2F внутри сегмента пути
const storageUrl = 'https://storage.api.example.com/v1/objects/reports%2F2025%2Fq1-financials.pdf'
const rawKey     = new URL(storageUrl).pathname.replace('/v1/objects/', '')
// .pathname декодирует кодирование уровня URL, но %2F (как %252F на уровне URL) остаётся
// Используйте decodeURIComponent для финального шага:
const fileKey = decodeURIComponent(rawKey)  // 'reports/2025/q1-financials.pdf'
console.log(fileKey)
Примечание:decodeURIComponent() выбрасывает URIError, когда входные данные содержат %, за которым не следуют две допустимые шестнадцатеричные цифры — например одиночный % в конце строки или последовательность вроде %GH. Всегда оборачивайте его в try/catch при декодировании пользовательского ввода. Безопасные паттерны декодирования рассмотрены в разделе об обработке ошибок ниже.

Функции URL-декодирования JavaScript — справочник по символам

Три встроенные функции декодирования отличаются именно тем, какие закодированные последовательности они декодируют. Таблица показывает поведение для символов, которые наиболее важны на практике:

ЗакодированоСимволdecodeURIComponent()decodeURI()URLSearchParams
%20пробелпробел ✅пробел ✅пробел ✅
++ (форма)+ (не изм.)+ (не изм.)пробел ✅
%2B+ литерал+ ✅+ ✅+ ✅
%26&& ✅& (не изм.) ❌& ✅
%3D== ✅= (не изм.) ❌= ✅
%3F?? ✅? (не изм.) ❌? ✅
%23## ✅# (не изм.) ❌# ✅
%2F// ✅/ (не изм.) ❌/ ✅
%3A:: ✅: (не изм.) ❌: ✅
%40@@ ✅@ (не изм.) ❌@ ✅
%25%% ✅% ✅% ✅
%C3%BCüü ✅ü ✅ü ✅

Две критически важные строки — это + и структурные символы (%26, %3D, %3F). URLSearchParams декодирует + как пробел, поскольку следует спецификации application/x-www-form-urlencoded — правильно для HTML-форм, но отличается от того, что decodeURIComponent() делает с тем же символом. А decodeURI() молча пропускает %26, %3D и %3F — что выглядит правильным, пока значение на самом деле не содержит закодированный амперсанд или знак равенства.

decodeURI() — декодирование полного URL без нарушения его структуры

decodeURI() является аналогом encodeURI(). Он декодирует полную строку URL, сохраняя символы, имеющие структурное значение в URI: ; , / ? : @ & = + $ #. Они остаются в процентно-закодированной форме (или как буквальные символы, если появились незакодированными во входных данных). Это делает decodeURI() безопасным для санитизации полных URL, которые могут содержать не-ASCII символы в пути или имени хоста, без случайного разрушения структуры строки запроса.

Санитизация URL с не-ASCII сегментами пути

JavaScript
// URL CDN с интернационализированными сегментами пути и структурированной строкой запроса
// decodeURI() декодирует не-ASCII путь, но сохраняет ? & = нетронутыми

const encodedUrl =
  'https://cdn.example.com/assets/%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0%2F2025%2Fq1-report.pdf' +
  '?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600'

const readable = decodeURI(encodedUrl)
console.log(readable)
// https://cdn.example.com/assets/Москва/2025/q1-report.pdf?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600
// ↑ Не-ASCII декодировано; ? & = сохранены — URL остаётся структурно корректным

// decodeURIComponent уничтожил бы URL — : / ? & = декодированы все сразу
const broken = decodeURIComponent(encodedUrl)
// Выглядит похоже здесь, но URL вроде 'https%3A%2F%2F...' был бы уничтожен
Примечание:Предпочитайте конструктор URL вместо decodeURI(), когда вам также нужен доступ к отдельным компонентам URL. new URL(str) нормализует входные данные, проверяет структуру и предоставляет уже декодированные свойства .pathname, .searchParams и .hostname. Оставьте decodeURI() для случаев, когда нужен только строковый результат и нельзя использовать конструктор URL (например, в очень старых средах Node.js 6 без глобального класса URL).

URLSearchParams — автоматическое декодирование строк запроса

URLSearchParams — идиоматический способ разбора строк запроса в современном JavaScript. Каждое значение, возвращаемое .get(), .getAll() или при итерации, автоматически декодируется — включая + как пробел для данных в form-encoded формате. Он доступен глобально во всех современных браузерах и Node.js 10+, не требует импорта и обрабатывает крайние случаи, с которыми ручное разбиение строк справляется неверно.

Разбор входящей строки запроса

JavaScript (browser / Node.js 10+)
// Разбор строки запроса callback вебхука или OAuth redirect
const rawSearch =
  '?event_id=evt_9c2f4a1b' +
  '&product_name=Standing+Desk+Pro' +
  '&filter=price%3A%5B200+TO+800%5D' +
  '&tag=ergonomic&tag=adjustable' +
  '&redirect=https%3A%2F%2Fdashboard.internal%2Forders%3Fview%3Dpending'

const params = new URLSearchParams(rawSearch)

console.log(params.get('event_id'))      // 'evt_9c2f4a1b'
console.log(params.get('product_name'))  // 'Standing Desk Pro'       ← + декодируется как пробел
console.log(params.get('filter'))        // 'price:[200+TO+800]'      ← %3A и %5B декодированы
console.log(params.getAll('tag'))        // ['ergonomic', 'adjustable']
console.log(params.get('redirect'))      // 'https://dashboard.internal/orders?view=pending'

// Итерация по всем параметрам
for (const [key, value] of params) {
  console.log(`${key}: ${value}`)
}
// event_id: evt_9c2f4a1b
// product_name: Standing Desk Pro
// filter: price:[200+TO+800]
// tag: ergonomic
// tag: adjustable
// redirect: https://dashboard.internal/orders?view=pending

Разбор параметров запроса из текущего URL браузера

JavaScript (browser)
// Текущий URL: https://app.example.com/search
//   ?q=standing+desk
//   &category=office+furniture
//   &sort=price_asc
//   &page=2

interface SearchFilters {
  query:    string | null
  category: string | null
  sort:     string
  page:     number
}

function getSearchFilters(): SearchFilters {
  const params = new URLSearchParams(window.location.search)
  return {
    query:    params.get('q'),                       // 'standing desk'
    category: params.get('category'),                // 'office furniture'
    sort:     params.get('sort') ?? 'relevance',
    page:     Number(params.get('page') ?? '1'),
  }
}

const filters = getSearchFilters()
console.log(filters.query)     // standing desk   (+ декодируется автоматически)
console.log(filters.category)  // office furniture

Декодирование URL-закодированных данных из файлов и API-ответов

В реальных проектах постоянно встречаются два сценария: обработка файла на диске, содержащего процентно-закодированные данные (журналы доступа, экспорты данных, файлы захвата вебхуков), и разбор URL входящего HTTP-запроса в Node.js-сервере. Оба следуют одному принципу — используйте конструктор URL или URLSearchParams, а не ручное разбиение строк — но детали отличаются.

Чтение и декодирование URL-закодированных записей из файла

JavaScript (Node.js 10+)
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

// Файл: orders-export.txt — одна URL-закодированная запись на строку
// customer_name=%D0%90%D0%BB%D0%B5%D0%BA%D1%81%D0%B5%D0%B9+%D0%98%D0%B2%D0%B0%D0%BD%D0%BE%D0%B2&order_id=ord_9c2f4a&product=Standing+Desk+Pro&total=149.99%20RUB
// customer_name=%D0%9C%D0%B0%D1%80%D0%B8%D1%8F+%D0%A1%D0%BE%D0%BA%D0%BE%D0%BB%D0%BE%D0%B2%D0%B0&order_id=ord_7b3a1c&product=Ergonomic+Chair&total=89.00%20RUB

interface Order {
  customerName: string | null
  orderId:      string | null
  product:      string | null
  total:        string | null
}

async function parseOrdersFile(filePath: string): Promise<Order[]> {
  const fileStream = createReadStream(filePath, { encoding: 'utf-8' })
  const rl         = createInterface({ input: fileStream, crlfDelay: Infinity })
  const orders: Order[] = []

  for await (const line of rl) {
    if (!line.trim()) continue

    // URLSearchParams декодирует + как пробел и последовательности %XX автоматически
    const params = new URLSearchParams(line)

    orders.push({
      customerName: params.get('customer_name'),  // 'Алексей Иванов'
      orderId:      params.get('order_id'),        // 'ord_9c2f4a'
      product:      params.get('product'),         // 'Standing Desk Pro'
      total:        params.get('total'),           // '149.99 RUB'
    })
  }

  return orders
}

const orders = await parseOrdersFile('./orders-export.txt')
console.log(orders[0])
// { customerName: 'Алексей Иванов', orderId: 'ord_9c2f4a', product: 'Standing Desk Pro', total: '149.99 RUB' }

Разбор журнала доступа Nginx для декодирования поисковых запросов

JavaScript (Node.js)
import { createReadStream } from 'fs'
import { createInterface } from 'readline'

// Строки журнала доступа Nginx выглядят так:
// 192.168.1.42 - - [11/Mar/2026:10:23:01 +0000] "GET /api/search?q=standing%20desk%26brand%3ANorthwood&sort=price_asc HTTP/1.1" 200 1842

async function extractSearchQueries(logFile: string): Promise<string[]> {
  const rl     = createInterface({ input: createReadStream(logFile), crlfDelay: Infinity })
  const queries: string[] = []

  for await (const line of rl) {
    // Извлечение пути запроса из строки журнала
    const match = line.match(/"GET ([^ ]+) HTTP/)
    if (!match) continue

    try {
      const requestUrl = new URL(match[1], 'http://localhost')
      const query      = requestUrl.searchParams.get('q')
      if (query) queries.push(query)  // URLSearchParams декодирует автоматически
    } catch {
      // Пропускаем некорректные строки — журналы доступа могут содержать усечённые записи
    }
  }

  return queries
}

const queries = await extractSearchQueries('/var/log/nginx/access.log')
console.log(queries)
// ['standing desk&brand:Northwood', 'ergonomic chair', 'monitor arm 27 inch']

Разбор параметров запроса в HTTP-сервере Node.js

JavaScript (Node.js 10+)
import http from 'http'

// Входящий URL: /api/products?q=standing+desk&warehouse=eu%2Dwest%2D1&minStock=10&cursor=eyJpZCI6MTIzfQ%3D%3D

const server = http.createServer((req, res) => {
  // Создаём полный URL — второй аргумент является базовым, требуемым конструктором URL
  const requestUrl = new URL(req.url!, 'http://localhost')

  const searchQuery = requestUrl.searchParams.get('q')            // 'standing desk'
  const warehouseId = requestUrl.searchParams.get('warehouse')    // 'eu-west-1'
  const minStock    = Number(requestUrl.searchParams.get('minStock') ?? '0')
  const cursor      = requestUrl.searchParams.get('cursor')       // 'eyJpZCI6MTIzfQ=='

  if (!warehouseId) {
    res.writeHead(400, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ error: 'warehouse parameter is required' }))
    return
  }

  const cursorData = cursor ? JSON.parse(Buffer.from(cursor, 'base64').toString()) : null

  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ searchQuery, warehouseId, minStock, cursorData }))
})

server.listen(3000)

Когда нужно проверить закодированный URL во время разработки — чтобы понять, что отправляет вебхук, до написания кода разбора — вставьте его прямо в URL Decoder от ToolDeck и мгновенно увидите декодированную форму без запуска скрипта.

URL-декодирование в командной строке

Для shell-скриптов, CI-пайплайнов или быстрой разовой проверки закодированных строк существует несколько подходов, не требующих написания полного скрипта. Однострочники Node.js работают кроссплатформенно; на macOS и Linux также всегда доступен python3.

bash
# ── Однострочники Node.js ──────────────────────────────────────────────────

# Декодирование одного процентно-закодированного значения
node -e "console.log(decodeURIComponent(process.argv[1]))" "S%C3%A3o%20Paulo%20%26%20Rio"
# São Paulo & Rio

# Разбор строки запроса и вывод каждой пары ключ=значение (декодированной)
node -e "
  const params = new URLSearchParams(process.argv[1])
  for (const [k, v] of params) console.log(`${k} = ${v}`)
" "q=standing+desk&warehouse=eu%2Dwest%2D1&minStock=10"
# q = standing desk
# warehouse = eu-west-1
# minStock = 10

# Декодирование и красивый вывод URL-закодированного JSON тела (часто в отладке вебхуков)
node -e "
  const raw = decodeURIComponent(process.argv[1])
  console.log(JSON.stringify(JSON.parse(raw), null, 2))
" '%7B%22event%22%3A%22purchase%22%2C%22amount%22%3A149.99%2C%22currency%22%3A%22RUB%22%7D'
# {
#   "event": "purchase",
#   "amount": 149.99,
#   "currency": "RUB"
# }

# ── Однострочник Python (доступен на большинстве macOS/Linux систем) ─────────
# unquote_plus также декодирует + как пробел — правильно для form-encoded данных
python3 -c "
from urllib.parse import unquote_plus
import sys
print(unquote_plus(sys.argv[1]))
" "Standing+Desk+%26+Ergonomic+Chair"
# Standing Desk & Ergonomic Chair

# ── curl — логирование и декодирование URL перенаправления из заголовка ответа ──
curl -sI "https://api.example.com/short/abc123" | grep -i location |   node -e "
    const line = require('fs').readFileSync('/dev/stdin', 'utf8')
    const url  = line.replace(/^location:s*/i, '').trim()
    console.log(decodeURIComponent(url))
  "

Устойчивое декодирование с decode-uri-component

Встроенная decodeURIComponent() выбрасывает URIError для любой некорректной последовательности — включая завершающий % без двух следующих шестнадцатеричных цифр. В продакшен-коде, обрабатывающем URL от пользователей или сторонних сервисов — журналы поисковых запросов, URL переходов из email-кампаний, данные веб-скрапинга — некорректные последовательности встречаются достаточно часто, чтобы вызывать сбои. Пакет decode-uri-component (~30M еженедельных загрузок из npm) обрабатывает некорректные последовательности без ошибок, возвращая исходную последовательность без изменений вместо выброса исключения.

bash
npm install decode-uri-component
# or
pnpm add decode-uri-component
JavaScript (Node.js)
import decodeUriComponent from 'decode-uri-component'

// Нативная функция выбрасывает при некорректных входных данных — может сломать обработчик запроса
try {
  decodeURIComponent('product%name%')   // ❌ URIError: URI malformed
} catch (e) {
  console.error('Нативная выбросила:', (e as Error).message)
}

// decode-uri-component возвращает исходную последовательность вместо выброса
console.log(decodeUriComponent('product%name%'))
// product%name%   ← некорректные последовательности оставлены как есть, без исключения

// Корректные последовательности декодируются правильно
console.log(decodeUriComponent('S%C3%A3o%20Paulo%20%26%20Rio'))
// São Paulo & Rio

// Смешанные: корректные декодируются, некорректные сохраняются
console.log(decodeUriComponent('Berlin%20Office%20%ZZ%20HQ'))
// Berlin Office %ZZ HQ   ← %ZZ не является допустимой hex-последовательностью — сохранено как есть

// Практическое применение в пайплайне обработки журналов
const rawSearchQueries = [
  'standing%20desk%20ergonomic',
  'price%3A%5B200+TO+800%5D',
  '50%25+off',       // ← выбросило бы исключение с decodeURIComponent (одиночный %)
  'wireless%20keyboard%20%26%20mouse',
]

const decoded = rawSearchQueries.map(q => decodeUriComponent(q))
console.log(decoded)
// ['standing desk ergonomic', 'price:[200+TO+800]', '50%25+off', 'wireless keyboard & mouse']

Используйте decodeURIComponent() с try/catch для кода приложения, где вы хотите сразу узнать о неожиданных входных данных. Обращайтесь к decode-uri-component в пайплайнах данных, обработчиках журналов и обработчиках вебхуков, где нужно продолжать обработку даже при некорректном кодировании в отдельных входных данных.

Обработка некорректных процентно-закодированных строк

Одиночный %, неполная последовательность вроде %A, или недопустимая пара вроде %GH — всё это заставляет decodeURIComponent() выбросить URIError: URI malformed. Любые пользовательские данные — поисковые запросы, фрагменты URL, поля форм, содержащие URL, параметры из ссылок email-кампаний — могут содержать эти последовательности. Безопасная обёртка необходима для любого кода, работающего с внешними данными.

Безопасные обёртки для декодирования в типичных сценариях

JavaScript
// ── 1. Базовое безопасное декодирование — возвращает исходную строку при ошибке ──────
function safeDecode(encoded: string): string {
  try {
    return decodeURIComponent(encoded)
  } catch {
    return encoded  // возвращаем исходные данные при ошибке декодирования — никогда не падаем
  }
}

// ── 2. Безопасное декодирование + обработка + как пробела (для form-encoded значений) ──
function safeFormDecode(formEncoded: string): string {
  try {
    return decodeURIComponent(formEncoded.replace(/+/g, ' '))
  } catch {
    return formEncoded.replace(/+/g, ' ')  // хотя бы заменяем + если остальное не удалось
  }
}

// ── 3. Безопасный разбор строки запроса → простой объект ────────────────────────────
function parseQueryString(queryString: string): Record<string, string> {
  const result: Record<string, string> = {}
  try {
    const params = new URLSearchParams(queryString)
    for (const [key, value] of params) {
      result[key] = value  // URLSearchParams обрабатывает всё декодирование внутри
    }
  } catch {
    // Молча возвращаем пустой объект при полностью некорректных входных данных
  }
  return result
}

// Использование
console.log(safeDecode('S%C3%A3o%20Paulo'))         // São Paulo
console.log(safeDecode('search%20for%2050%25+off'))  // search for 50% off (одиночный %)
                                                     // → на самом деле ок; % здесь %25
console.log(safeDecode('malformed%string%'))         // malformed%string%  (без исключения)

console.log(safeFormDecode('standing+desk+pro'))     // standing desk pro

console.log(parseQueryString('q=hello+world&tag=node%20js&page=2'))
// { q: 'hello world', tag: 'node js', page: '2' }

Распространённые ошибки

Я видел, как эти четыре паттерна вызывают скрытое повреждение данных или неожиданные сбои в продакшене — обычно только тогда, когда значение случайно содержит специальный символ, поэтому они проскальзывают через юнит-тесты и проявляются только с реальными пользовательскими данными.

Ошибка 1 — Двойное декодирование значения URLSearchParams

Проблема: URLSearchParams.get() возвращает уже декодированную строку. Вызов decodeURIComponent() поверх него двойным образом декодирует значение — превращая любой оставшийся % в декодированном выводе в %25, что повреждает данные. Исправление: используйте значение из URLSearchParams.get() напрямую — дополнительное декодирование не нужно.

Before · JavaScript
After · JavaScript
// ❌ URLSearchParams уже декодировал — повторное декодирование портит значения с %
const qs        = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rawRate   = qs.get('rate')       // '50%'   ← уже декодировано
const wrongRate = decodeURIComponent(rawRate)
// '50%25'  ← % был декодирован в %25 при втором проходе — теперь снова неверно
// ✅ Используйте URLSearchParams.get() напрямую — декодирование автоматическое
const qs       = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rate     = qs.get('rate')        // '50%'   ← верно, дополнительный шаг не нужен
const redirect = qs.get('redirect')    // 'https://dashboard.internal/'
const parsed   = new URL(redirect!)    // Безопасно конструировать — уже декодировано
console.log(parsed.hostname)           // dashboard.internal

Ошибка 2 — Отсутствие декодирования + как пробела для form-encoded данных

Проблема: Отправки форм и некоторые OAuth-библиотеки кодируют пробелы как + (application/x-www-form-urlencoded). Вызов decodeURIComponent() для этих данных оставляет + как буквальный знак плюс. Исправление: используйте URLSearchParams для разбора form-encoded данных, или замените + на пробел перед вызовом decodeURIComponent().

Before · JavaScript
After · JavaScript
// ❌ decodeURIComponent воспринимает + как буквальный плюс, а не пробел
// Тело формы: customer_name=Алексей+Иванов&product=Standing+Desk+Pro&qty=2
const formBody   = 'customer_name=%D0%90%D0%BB%D0%B5%D0%BA%D1%81%D0%B5%D0%B9+%D0%98%D0%B2%D0%B0%D0%BD%D0%BE%D0%B2&product=Standing+Desk+Pro&qty=2'
const [, rawVal] = formBody.split('&')[0].split('=')
const name       = decodeURIComponent(rawVal)
console.log(name)  // 'Алексей+Иванов'  ← + не преобразован в пробел
// ✅ Используйте URLSearchParams — следует спецификации application/x-www-form-urlencoded
const formBody   = 'customer_name=%D0%90%D0%BB%D0%B5%D0%BA%D1%81%D0%B5%D0%B9+%D0%98%D0%B2%D0%B0%D0%BD%D0%BE%D0%B2&product=Standing+Desk+Pro&qty=2'
const params     = new URLSearchParams(formBody)
console.log(params.get('customer_name'))  // 'Алексей Иванов'   ← + правильно декодирован как пробел
console.log(params.get('product'))        // 'Standing Desk Pro'

Ошибка 3 — Использование decodeURI() для отдельных значений параметров запроса

Проблема: decodeURI() не декодирует %26 (&), %3D (=) и %3F (?) — символы, часто закодированные внутри значений параметров. Использование его для декодирования одного значения оставляет эти последовательности нетронутыми, молча производя неверный вывод. Исправление: используйте decodeURIComponent() для отдельных значений; оставьте decodeURI() для полных строк URL.

Before · JavaScript
After · JavaScript
// ❌ decodeURI не декодирует & и = — значение остаётся повреждённым
const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'
const filter        = decodeURI(encodedFilter)
console.log(filter)
// 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'  ← %3D и %26 не декодированы
// ✅ decodeURIComponent декодирует все последовательности, включая & и =
const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west'
const filter        = decodeURIComponent(encodedFilter)
console.log(filter)
// 'status=active&tier=premium&region=eu-west'  ← правильно декодировано

Ошибка 4 — Отсутствие обёртки decodeURIComponent в try/catch для пользовательского ввода

Проблема: Одиночный % без двух следующих шестнадцатеричных цифр — часто встречается в поисковых запросах от пользователей («50% скидка», «100% хлопок») — заставляет decodeURIComponent() выбросить URIError: URI malformed, аварийно завершая обработчик запроса, если не перехватить. Исправление: всегда оборачивайте в try/catch, когда входные данные поступают от пользователей, фрагментов URL или внешних систем.

Before · JavaScript
After · JavaScript
// ❌ Пользователь ввёл '100% хлопок' в строке поиска — одиночный % ломает сервер
// GET /api/search?q=100%25+хлопок  ← Этот конкретный случай нормален (%25 = %)
// GET /api/search?q=100%+хлопок    ← Это падает (% не за которым 2 hex-цифры)
app.get('/api/search', (req, res) => {
  const query = decodeURIComponent(req.query.q as string)
  // ↑ выбрасывает URIError: URI malformed для '100% хлопок' → необработанная ошибка 500
})
// ✅ Оборачиваем в try/catch — возвращаем исходные данные при ошибке декодирования
app.get('/api/search', (req, res) => {
  let query: string
  try {
    query = decodeURIComponent(req.query.q as string)
  } catch {
    query = req.query.q as string  // используем исходное значение вместо аварийного завершения
  }
  // продолжаем безопасную обработку — query либо декодирован, либо исходный
})

decodeURIComponent vs decodeURI vs URLSearchParams — краткое сравнение

МетодДекодирует %XXДекодирует + как пробелВыбрасывает при ошибкеДекодирует & = ? #Сценарий использованияТребует установки
decodeURIComponent()✅ все❌ нет✅ URIError✅ даОтдельные значения и сегменты путиNo
decodeURI()✅ большинство❌ нет✅ URIError❌ нетПолные строки URLNo
URLSearchParams✅ все✅ да❌ молча✅ даРазбор строк запроса с автодекодированиемNo
URL constructor✅ все✅ да✅ TypeError✅ даПолный разбор и нормализация URLNo
decode-uri-component✅ все❌ нет❌ молча✅ даПакетное декодирование с устойчивостью к ошибкамnpm install
querystring.unescape()✅ все❌ нет❌ молча✅ даУстаревший Node.js (deprecated в v16)No (built-in)

В подавляющем большинстве случаев выбор сводится к трём сценариям. Используйте URLSearchParams для разбора строк запроса — он обрабатывает декодирование, правило + как пробел и повторяющиеся ключи автоматически. Используйте decodeURIComponent() (обёрнутый в try/catch) для одного значения или сегмента пути, особенно когда ожидаются косые черты, закодированные как %2F внутри сегмента. Используйте decodeURI() только когда у вас полная строка URL с не-ASCII символами в пути и нужно, чтобы структурные символы (/ ? & =) остались закодированными в выводе.

Часто задаваемые вопросы

В чём разница между decodeURIComponent() и decodeURI() в JavaScript?
decodeURIComponent() декодирует каждую процентно-закодированную последовательность в строке, включая символы, имеющие структурное значение в URL — & (%26), = (%3D), ? (%3F), # (%23), / (%2F), : (%3A). Предназначена для декодирования отдельных значений параметров запроса или сегментов пути. decodeURI() сохраняет эти структурные символы — не декодирует %26, %3D, %3F, %23, %2F, %3A — поскольку предназначена для санитизации полного URL без нарушения структуры его строки запроса. Использование decodeURI() для одного значения параметра, содержащего закодированный & или =, молча оставит эти последовательности недекодированными, производя неверный вывод.
Почему decodeURIComponent() выбрасывает URIError для некоторых строк?
decodeURIComponent() выбрасывает URIError: URI malformed, когда входные данные содержат % без двух следующих допустимых шестнадцатеричных цифр. Частые триггеры: одиночный % в конце строки («50% скидка», набранная пользователем), неполная последовательность ("%A") или недопустимая пара ("%GH"). Это чаще всего происходит с поисковыми запросами, набранными пользователем, или значениями, скопированными из текста, который никогда не предназначался для URL-кодирования. Исправление — обернуть decodeURIComponent() в блок try/catch и вернуть исходную строку при ошибке. Npm-пакет decode-uri-component предлагает то же решение без необходимости обёртки try/catch.
Автоматически ли URLSearchParams декодирует процентно-закодированные значения?
Да. Каждое значение, возвращаемое URLSearchParams.get() и URLSearchParams.getAll(), полностью декодировано — никогда не нужно вызывать decodeURIComponent() для их вывода. URLSearchParams также следует спецификации application/x-www-form-urlencoded, которая декодирует + как пробел, что правильно для HTML-форм и OAuth token-ответов. Единственный случай, когда URLSearchParams не поможет — декодирование отдельного закодированного значения, не являющегося частью строки запроса — для этого используйте decodeURIComponent() с try/catch.
Как декодировать знак + как пробел в JavaScript?
decodeURIComponent() воспринимает + как буквальный знак плюс — он не конвертирует + в пробел. Это намеренно: + как пробел является соглашением application/x-www-form-urlencoded, отдельным от стандарта процентного кодирования. Для декодирования + как пробела используйте URLSearchParams (который следует спецификации form-encoded), или замените + перед вызовом decodeURIComponent(): decodeURIComponent(str.replace(/\+/g, ' ')). Замена + на %20 перед декодированием также работает, но чуть более многословна. Всегда предпочитайте URLSearchParams для разбора полных строк запроса — он правильно обрабатывает и %20, и +.
Как URL-декодировать строку в Node.js?
Используйте те же глобальные функции, что и в браузере — импорт не нужен в Node.js 10+. decodeURIComponent() для отдельных значений, decodeURI() для полных строк URL, URLSearchParams для строк запроса. Для URL входящих HTTP-запросов используйте new URL(req.url, 'http://localhost') — конструктор URL нормализует URL и предоставляет .searchParams (автодекодирование) и .pathname (декодированный на уровне URL). Устаревшая функция querystring.unescape() ещё существует, но deprecated с Node.js 16 — предпочитайте decodeURIComponent().
Как разобрать строку запроса URL в объект в JavaScript?
Используйте URLSearchParams с Object.fromEntries(): const params = Object.fromEntries(new URLSearchParams(queryString)). Это даёт простой объект со всеми ключами параметров и декодированными значениями. Обратите внимание, что Object.fromEntries() сохраняет только последнее значение для дублирующихся ключей — для повторяющихся ключей вроде tag=one&tag=two используйте URLSearchParams.getAll('tag') вместо этого. Для браузера new URLSearchParams(window.location.search) разбирает строку запроса текущей страницы автоматически. Для Node.js разбирайте из new URL(req.url, base).searchParams.

Связанные инструменты

Для разового декодирования без написания кода вставьте вашу процентно-закодированную строку прямо в URL Decoder от ToolDeck — он мгновенно декодирует в браузере, с результатом готовым для копирования в ваш код или терминал.

AC
Alex ChenFront-end & Node.js Developer

Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.

MW
Marcus WebbТехнический рецензент

Marcus specialises in JavaScript performance, build tooling, and the inner workings of the V8 engine. He has spent years profiling and optimising React applications, working on bundler configurations, and squeezing every millisecond out of critical rendering paths. He writes about Core Web Vitals, JavaScript memory management, and the tools developers reach for when performance really matters.