Всем привет!
Сегодня, работая с soap-сервисами столкнулся с неожиданной для меня проблемой: по неопределенной причине soap-сервис прекрасно работал из php-клиента (SoapClient) но напрочь отказывался работать из под python (suds).
Описание сервиса
Вначале стоит рассказать об программном окружении. Это:
- Nginx как web-сервер;
- Gunicorn 18.0 как wsgi-сервер;
- Django 1.4 как web-фреймворк;
- Spyne 2.11 как инструментарий для создания SOAP-сервисов.
В общем – ничего особенного за исключением того, что сервис работал только по протоколу HTTPS, а по HTTP отдавался редирект на него.
Конфигурация Nginx самая обычная:
location /soap/ { if ( $scheme = "http" ) { rewrite ^/(.*)$ https://$host/$1 permanent; } proxy_pass http://unix:/var/tmp/backend.sock:$uri$is_args$args; }
Проблема в WSDL-схеме
Несмотря на то, что сервис изначально отдавал свою WSDL-схему по HTTPS-протоколу, схема, описывающая все его методы и типы данных содержала в качестве адресов сервисов (элемент wsdl:address из wsdl:port из wsdl:service) адреса с протоколом HTTP вместо исходного HTTPS.
<wsdl:service name="MySoapService"> <wsdl:port name="MySoapService" binding="tns:MySoapService"> <soap:address location="http://my-domain.ru/soap/"/> </wsdl:port> </wsdl:service>
Так происходило потому, что Gunicorn не знал, по какому протоколу был запрошен ресурс и не заполнял переменную окружения wsgi.url_scheme, а nginx ему об этом не сообщал нужными заголовками X-FORWARDED-PROTOCOL, X-FORWARDED-PROTO и X-FORWARDED-SSL.
Работа из php через SoapClient
Cамое интересное, что даже с неправильными адресами soap-сервисов из схемы (HTTP вместо HTTPS) сервис прекрасно работает через класс SoapClient из стандартной библиотеки php.
$client = new SoapClient('https://my-domain.ru/soap/?wsdl'); // ---> GET-запрос на https://my-domain.ru/soap/?wsdl $client->someMethod(); // ---> POST-запрос <SOAP:Envelope>...</...> на http://my-domain.ru/soap/ // <--- 301 REDIRECT на https://my-domain.ru/soap/ // // ---> повторный POST-запроса <SOAP:Envelope>...</...> на https://my-domain.ru/soap/ // <--- 200 OK // <SOAP:Envelope>...</...>
Почему SoapClient повторно отправляет POST-запрос после получения перенаправления я не знаю, но эту особенность стоит помнить.
Работа из python через Suds
В отличии от SoapClient, suds написанный на python и использующий urllib2 в качестве транспорта такой особенностью не обладает, и не отправляет повторно все тело первого запроса после направления:
from suds.client import Client client = Client('https://my-domain.ru/soap/?wsdl') // ---> GET-запрос на https://my-domain.ru/soap/?wsdl client.service.someMethod() // ---> POST-запрос <SOAP:Envelope>...</...> на http://my-domain.ru/soap/ // <--- 301 REDIRECT на https://my-domain.ru/soap/ // // ---> GET-запрос на https://my-domain.ru/soap/ // <--- 500 Internal server error // Ошибка вызова сервиса без тела запроса. Ошибка парсинга XML
Исправление проблемы между nginx и gunicorn
Для исправления моей проблемы достаточно было просто добавить в секцию location отправку заголовка X-Forwarded-Proto:
location /soap/ { if ( $scheme = "http" ) { rewrite ^/(.*)$ https://$host/$1 permanent; } proxy_pass http://unix:/var/tmp/backend.sock:$uri$is_args$args; proxy_set_header X-Forwarded-Proto $scheme; }
И оба клиента заработали одинаково.