Как проверить кодирование URL-параметров iOS на облачном Mac
В поле поиска вводят C++ 入门, а сервер получает C 入门; в фильтре передают два параметра с одним именем, но в итоге остаётся только один. Причина таких сбоев обычно не в потере сетевых пакетов, а в несовпадении правил кодирования на клиенте и декодирования на сервере. Если на облачном Mac выделить построение URL в чистую функцию и закрепить граничные случаи тестами XCTest, найти причину будет проще, чем раз за разом перехватывать запросы в рабочей среде. При этом запускать симулятор не нужно.
Сначала определите контракт параметров
Строка запроса — не словарь. В контракте API нужно заранее определить, допустимы ли повторяющиеся имена параметров, важен ли их порядок, равнозначны ли отсутствующий параметр, параметр без значения и пустая строка, а также применяет ли сервер правила декодирования форм. Даже если и клиент, и сервер «используют стандартную библиотеку», ответы на эти вопросы могут различаться.
Например, flag, flag= и полное отсутствие flag — три разных варианта входных данных. На клиенте их можно выразить через URLQueryItem: передать nil, пустую строку или вовсе не добавлять элемент. Сохраняет ли серверный фреймворк эти различия, нужно проверять на реальном парсере.
В обычной строке запроса URL буквальный плюс сам по себе не означает пробел. Замена плюса на пробел — правило кодирования форм; его нельзя автоматически распространять на все API с параметрами запроса.
Отразите эти договорённости в названиях тестов. Не ограничивайтесь одним сценарием «обычный поиск проходит»: китайский текст может обрабатываться правильно, а плюсы и повторяющиеся фильтры — по-прежнему теряться.
Стройте URL с помощью чистой функции
Создайте цель модульного тестирования для macOS и добавьте в неё следующий Swift-файл. Адрес в примере используется только для локального разбора URL: запросы по сети не отправляются. При подключении к рабочему коду замените фиксированный адрес конфигурацией API из проекта. Значения параметров нужно передавать в исходном виде, без предварительного ручного процентного кодирования.
import Foundation
import XCTest
func makeQueryURL(
items: [URLQueryItem],
formDecoder: Bool = false
) throws -> URL {
var parts = URLComponents()
parts.scheme = "https"
parts.host = "xcodenode.com"
parts.path = "/query-fixture"
parts.queryItems = items
if formDecoder, let encoded = parts.percentEncodedQuery {
parts.percentEncodedQuery = encoded.replacingOccurrences(
of: "+",
with: "%2B"
)
}
return try XCTUnwrap(parts.url)
}
final class QueryEncodingTests: XCTestCase {
func testReservedCharactersRoundTrip() throws {
let value = "C++ 入门 & x=1 # 50% / ?"
let url = try makeQueryURL(items: [
URLQueryItem(name: "q", value: value)
])
let parts = try XCTUnwrap(
URLComponents(url: url, resolvingAgainstBaseURL: false)
)
let items = try XCTUnwrap(parts.queryItems)
XCTAssertEqual(items.count, 1)
XCTAssertEqual(items.first?.value, value)
XCTAssertNil(parts.fragment)
}
func testDuplicatesAndEmptyValues() throws {
let url = try makeQueryURL(items: [
URLQueryItem(name: "tag", value: "swift"),
URLQueryItem(name: "tag", value: "ios"),
URLQueryItem(name: "flag", value: nil),
URLQueryItem(name: "empty", value: "")
])
let parts = try XCTUnwrap(
URLComponents(url: url, resolvingAgainstBaseURL: false)
)
let items = try XCTUnwrap(parts.queryItems)
XCTAssertEqual(items.map(\.name), [
"tag", "tag", "flag", "empty"
])
XCTAssertEqual(items.map(\.value), [
"swift", "ios", nil, ""
])
}
func testFormDecoderReceivesLiteralPlus() throws {
let value = "C++ 50%"
let url = try makeQueryURL(
items: [URLQueryItem(name: "q", value: value)],
formDecoder: true
)
let parts = try XCTUnwrap(
URLComponents(url: url, resolvingAgainstBaseURL: false)
)
let query = try XCTUnwrap(parts.percentEncodedQuery)
XCTAssertFalse(query.contains("+"))
let pair = try XCTUnwrap(
query.split(separator: "=", maxSplits: 1).last
)
let decoded = String(pair)
.replacingOccurrences(of: "+", with: " ")
.removingPercentEncoding
XCTAssertEqual(decoded, value)
}
}
Первый тест проверяет, что зарезервированные символы не создали лишних параметров или фрагмента URL. Второй подтверждает, что повторяющиеся имена и порядок элементов массива сохранились. Третий моделирует один проход декодирования по правилам форм. Это не полноценный серверный парсер: тест проверяет только обработку плюса в одном значении и не заменяет интеграционные тесты API.
Добавляйте проблемные примеры, а не сетевые запросы
Дополните тот же набор тестовых данных китайским текстом, комбинируемыми символами, эмодзи, пустыми значениями, пробелами в начале и конце, а также буквальной строкой %2B. Последний случай особенно важен: знак процента в пользовательском вводе должен кодироваться, а не восприниматься как часть уже закодированного плюса.
Если в результате появляется %252B, сначала выясните, что было на входе: обычный плюс или буквальная строка %2B. В первом случае это обычно указывает на двойное кодирование, а во втором результат вполне может быть правильным. Если считать любую последовательность %25 ошибкой, исправление способно породить новый сбой.
Не кодируйте весь уже собранный URL одним общим набором допустимых символов. Имена параметров, их значения и путь относятся к разным контекстам; простая конкатенация &, = и # может изменить структуру URL. Поручите формирование структуры конструктору, а затем отдельно проверьте количество, имена и значения параметров после разбора.
Для слишком длинных поисковых запросов задайте отдельное ограничение на длину входных данных. Число байтов после кодирования не равно числу символов. Проверяйте ограничения согласно контракту API, а не исходя из предположения, что один китайский символ занимает один байт.
Для подписанных запросов проверяйте точное представление в байтах
Семантически одинаковые URL не обязательно совпадают побайтово. На подпись могут влиять порядок параметров, способ представления пробелов и регистр букв в процентных последовательностях. Для обычного запроса достаточно проверить, что после кодирования и декодирования значение сохранилось; для подписанного запроса нужно также проверить каноническую строку, предусмотренную протоколом.
Если протокол требует сортировки, сначала определите, как сортировать параметры с одинаковыми именами, и только затем подписывайте итоговую строку. Если порядок должен сохраняться, оставьте параметры в массиве — не восстанавливайте их из словаря. После вычисления подписи не переключайте режим обработки плюсов, иначе передаваемые данные перестанут совпадать с входными данными для подписи.
Фиксированные примеры должны содержать повторяющиеся имена параметров, пустые значения и текст вне ASCII; в тестах нужно проверять итоговую закодированную строку. «Фиксированные» здесь означает соответствие формату, принятому в проекте, а не безусловное признание однажды полученного системного вывода вечным стандартом. На сервере следует сохранить те же примеры и убедиться, что обе стороны используют одинаковые входные данные для подписи.
Запускайте тесты на облачном узле и сохраняйте результаты
Для тестовой цели достаточно Foundation; настраивать подпись приложения не требуется. Запускайте тесты через общую схему (Shared Scheme) тестовой цели. Имена в примере ниже замените реальными именами из проекта. Каталог для результатов на момент запуска ещё не должен существовать.
set -euo pipefail
mkdir -p artifacts
result="artifacts/query-$(uuidgen).xcresult"
xcodebuild test \
-project QueryContract.xcodeproj \
-scheme QueryContract \
-destination 'platform=macOS' \
-resultBundlePath "$result"
Перед запуском запишите версию Xcode, идентификатор коммита и режим конструктора URL. В пределах одной серии тестов используйте один и тот же набор инструментов. После его обновления заново проверьте байты, участвующие в подписи, а не только количество успешно пройденных тестов.
При приёмке убедитесь в четырёх вещах: исходные значения не кодируются заранее; повторяющиеся имена параметров не теряются при преобразовании в словарь; режим декодирования форм выбирается по контракту API; реальный серверный парсер проходит тот же набор примеров. В отчёте о сбое достаточно сохранить категорию входных данных и результат кодирования. Если данные содержат реальные поисковые запросы или токены, используйте вместо них синтетические примеры.
В итоге тесты чистой функции должны защищать структуру запроса на клиенте, а серверные контрактные тесты — проверять границу декодирования. Только прохождение обоих уровней подтверждает, что параметры передаются правильно, а не просто случайно совпадают результаты двух разборов на клиенте.
Часто задаваемые вопросы
Всегда ли URLComponents кодирует плюс как %2B?
Нет. В строке запроса URL плюс может остаться обычным символом, но декодер формы способен заменить его пробелом. Если сервер использует такое декодирование, замените оставшиеся плюсы в percentEncodedQuery на %2B после кодирования и отдельно проверьте серверный декодер.
Можно ли преобразовать параметры в словарь перед подписью?
Только если протокол запрещает повторные ключи, не учитывает порядок и задаёт правила нормализации. Иначе сохраняйте массив URLQueryItem и отдельно проверяйте итоговую последовательность закодированных байтов, участвующую в подписи.
Выберите выделенный облачный Mac для разработки
Физические узлы Apple Silicon, не виртуальные машины. Аренда на день, неделю, месяц или квартал. Конфигурации и цены опубликованы; актуальная доступность отображается в консоли.