Дело в том, что API, c которыми мне часто приходится работать, обычно довольно сложные и реализовать отправку множества разнообразных запросов голым curl крайне непродуктивно. Поэтому я с самого начала решил работать с API через библиотеку GuzzleHttp, которая взяла на себя все вопросы базовой подготовки запросов и их отправки различными способами, в зависимости от метода API. Такой подход позволяет сконцентрироваться на подготовке параметров запроса и главным образом всё, что оставалось для комфортной разработки — прописать соответствия методов HTTP методам API.
Грабли нашлись в тот момент, когда в спецификации API обнаружилось, что в POST-метод надо отправлять данные в формате application/x-www-form-urlencoded, т.е. сформированные так же, как обычно формируются GET-строки параметров. Но при этом все параметры должны иметь одно и то же имя. То есть POST запрос должен был выглядеть примерно так:
param=value1¶m=value2¶m=value3
Привыкнув к тому, что в подавляющем большинстве систем в POST всегда отдаются параметры с разными именами и достаточно отправить их в виде обычного ассоциативного массива PHP, меня этот формат передачи параметров поначалу удивил, но уже совсем скоро начал серьезно раздражать, т.к. очевидного решения для использования в связке с GuzzleHttp не было ни в голове, ни в гугле. GET-запрос с такой строкой передать проще простого - достаточно лишь собрать строку не через build_http_query, а самостоятельно, ручками. Но с POST такое не прокатывает. Одна из обычных практик отправки POST через GuzzleHttp - это создать клиента, настроенного на нужный адрес метода API, а затем передать ему параметр URI и сам запрос, примерно следующим образом:
$client = new GuzzleHttp\Client(['base_uri' => 'https://foo.com/api/']);
$client->request('POST', $uri, ['body' => $requestArray]);
//или, что то же самое:
$client->post($uri, ['body' => $requestArray']);
Однако все-таки гугл не был совсем бесполезен - было найдено решение, которое позволяет передавать POST в виде строки url-encoded параметров. Общая идея была в том, чтобы передавать данные не в формате miltipart/form-data, а в application/x-www-form-urlencoded. При таком раскладе можно отправить POST'ом предварительно сформированную строку параметров через
curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
Мысль о том, что придется переделывать всё на использование Curl, очень не понравилась - уже была написана большая часть кода, да и отказываться от использования удобной библиотеки совсем не хотелось. Однако надо помнить о том, что GuzzleHttp, как и многие другие компоненты - всего лишь обертка над более-менее низкоуровневыми интерфейсами. Хорошенько покопавшись в коде пакета, я нашел место, в котором устанавливался параметр CURLOPT_POSTFIELDS. В итоге работающее решение нашлось, хотя и является, по сути, хаком (подробнее о вытекающих из этого последствиях читайте в последнем абзаце) и выглядит несколько неуклюжим по сравнению с обычными вызовами, которые я приводил выше:
$client->post($uri, [
'body' => $requestString,
'curl' => [
'body_as_string' => true
],
'_conditional' => [
'Content-Type' => 'application/x-www-form-urlencoded'
]
]);
Когда решение нашлось, стало грустно от осознания того количества времени, которое было потрачено на его поиски. Как, впрочем, всегда бывает, когда оно умещается в пару строчек. Есть подозрение, что такая ситуация встречается редко, но тем не менее, вполне вероятна и еще у кого-то. Именно поэтому я и решил опубликовать работающее решение для тех, кто на эти грабли в будущем наткнется.
На всякий случай добавлю, что версия пакета GuzzleHttp, которую я использовал - 6.2.2. Параметры body_as_string и _conditional не относятся к задокументированным, поэтому в более поздних версиях этот хак может уже не работать. На это также намекает и то, что в прежних версиях работа с подобными запросами была организована немного иначе. Постараюсь обновить здесь информацию, если появится что-то новое.
Комментариев нет:
Отправить комментарий
Напишите что-нибудь по теме