пятница, 26 ноября 2010 г.

[prog] Любопытная ошибка при использовании libcurl, CURLOPT_READFUNCTION и std::stringstream

Преамбула. Необходимо было через libcurl делать HTTP POST запросы и получать ответы от HTTP-сервера. Тело POST-запроса подготавливалось в std::stringstream. Для того, чтобы отдать его содержимое библиотеке libcurl я сделал специальную вспомогательную функцию, приблизительно такого вида:

size_t
std_istream_read_function(
  void * to,
  size_t size,
  size_t nmemb,
  void * raw_stream_pointer )
  {
    std::istream * from =
        reinterpret_cast< std::istream * >( raw_stream_pointer );
    if( !from )
      return CURL_READFUNC_ABORT;

    if( !(*from) )
      if( from->eof() )
          return 0;
      else
        return CURL_READFUNC_ABORT;

    const size_t capacity = size * nmemb;
    from->read(
        reinterpret_cast< char * >( to ), capacity );

    return from->gcount();
  }

А затем регистрировал данную функцию в качестве параметра CURLOPT_READFUNCTION (а в качестве CURLOPT_READDATA передавал указатель на stringstream). Соответственно, когда libcurl устанавливал соединение, проходил HTTP-аутентификацию и отсылал на удаленную сторону HTTP-заголовки, он вызывал мою функцию и получал от меня тело POST-а.

Теперь амбула :)

Все это дело красиво работало. Но не всегда. Часть запросов почему-то завершалась неудачно. После чего спецы, которые обслуживали HTTP-сервер, стали жаловаться, что в некоторых случаях от нас приходят только HTTP-заголовки, но нет тела POST-а.

Разбирательство показало, что в этих случаях происходит следующее:

  • для взаимодействия с HTTP-сервером используются Keep-Alive соединения;
  • 1, 2, 3,…, (i-1)-ый обмен с HTTP-сервером проходят без сучка и задоринки;
  • между (i-1)-м и i-м запросами проходит несколько десятков секунд;
  • при выполнении i-го запроса libcurl передает HTTP-заголовки и тело POST, после чего libcurl:
    • пишет, что текущее соединение с сервером оказалось разорвано;
    • предупреждает о том, что попытается восстановить соединение;
    • сообщает, что соединение восстановлено;
    • отсылает HTTP-заголовки;
    • некоторое время ничего не делает;
    • получает ответ от удаленной стороны.

Т.е., действительно, в таких случаях от нас тело POST-а не уходило повторно. По сообщениям libcurl ситуация выглядит приблизительно так:

# Re-using existing connection! (#0) with host YYY
# Connected to YYY (zzz.zzz.zzz.zzz) port 8080 (#0)
< POST /bla-bla-bla HTTP/1.1
< Content-Type: …
< Content-Length:…
<
< <?xml>…</xml>
# Connection died, retryinga fresh connect
# Closing connection #0
# Issue another request to this URL: 'http://YYY:8080/bla-bla-bla'
# About to connect() to YYY port 8080 (#0)
#    Trying 193.41.60.77...
# connected
# Connected to YYY (zzz.zzz.zzz.zzz) port 8080 (#0)
< POST /bla-bla-bla HTTP/1.1
< Content-Type: …
< Content-Length:…
# HTTP 1.0, assume close after body
> HTTP/1.0 200 OK
> Server: Apache-Coyote/1.1
>  Content-Type: text/xml;charset=ISO-8859-1
> Content-Length: 152

Т.е. соединение висело без дела достаточно времени, удаленная сторона его прибила (или оно просто само умерло из-за каких-то сетевых неприятностей). При попытке переиспользования соединения libcurl это диагностировал и попытался восстановить соединение и передать данные заново. Только почему-то не передал.

А не передал по простой причине – моя функция при новом обращении к ней от libcurl-а ничего больше не отдала.

Ларчик, как водится, открывался просто – во время первого обращения к std_istream_read_function был достигнут конец входного потока (у объекта stringstream был установлен eofbit). А посему последующие обращения сразу же обламывались.

Вот такая вот неожиданная для меня особенность работы libcurl. И, что обидно, нигде не замечал в документации описания того, что libcurl может обращаться к ней повторно для того, чтобы перечитать поток заново с самого начала.

В процессе разбирательства с libcurl выяснилось, что в нее в версии 7.18.0 была добавлена специальная опция CURLOPT_SEEKFUNCTION, которая специально для таких вещей предназначена – для перехода на конкретную позицию передаваемых на HTTP-сервер данных. Но, как показали эксперименты, для POST-запросов она не используется. Поскольку предназначена для операций upload (т.е. HTTP PUT-запросов) и для случаев с разрывами keep-alive соединений она не задействуется.

Посему пришлось вносить в свою функцию несколько строк:

if( !(*from) )
  if( from->eof() )
    {
      // Для того, чтобы можно было начать читать поток заново,
      // если это потребуется из-за разрыва keep-alive соединения.
      from->clear();
      from->seekg( 0 );

      return 0;
    }
  else
    return CURL_READFUNC_ABORT;

После чего все заработало.

PS. Логгирования мало не бывает! :)

6 комментариев:

  1. тяжкое наследие си

    спагетти из кучи разных функций, вызываемых (ли?) в неизвестно каком порядке неизвестно сколько раз

    (хотя емнип в effective STL есть пример, где кажется, что функтор вызывется по 1 разу на 1 элемент последовательности, но фактически это не так)

    > Но, как показали эксперименты, для POST-запросов она не используется.

    видимо баг-репорт надо написать, т.к. нет гарантии, что в следущих версиях CURLOPT_READFUNCTION будет вызываться после окончания потока, а не посредине

    вообще крайне неприятны такие тихие фейлы; как должна бы быть спроектирована библиотека, чтобы любой фейл отлавливался?

    на ум приходит

    1. честно отдавать общее количество отравленных байт в последнем успешном хттп-запросе (чтобы потом можно было это проверить)

    2. использовать че-то типа метапрограммирования для генерации кода на случай "Connection died, retryinga fresh connect" -- ведь код любого retry-я это достаточно просто преобразованный код соответствующего try-я

    ОтветитьУдалить
  2. а почему жаловаться стали HTTP-серверные-спецы, а не прога в своих логах?

    по идее, хттп-сервер должен отдавать ответ с неким дайджестом (вовсе не обязательно мд5), и прога должна его проверять; в случае несовпадения писать в лог или даже завершаться

    а то, что я написал выше (1, 2) в отдельности явно недостаточно, хотя и хотелось бы обсудить

    ОтветитьУдалить
  3. >а почему жаловаться стали HTTP-серверные-спецы, а не прога в своих логах?

    Это не они стали жаловаться. Поставщик сервиса в этих случаях возвращал вполне корректный HTTP-POST-response в котором был XML с ответом, а в этом ответе стоял некий код ошибки.

    Т.е. для программы все выглядело так, как будто мы корректно выполнили запрос и получили корректный ответ. А самая проблема была диагностирована когда занялись выяснением причин наличия этого кода ошибки (он доминировал среди других причин ошибок и это стало вызывать вопросы).

    ОтветитьУдалить
  4. А по поводу bug-репорта... Нужно подумать. Имхо, это вполне логичное поведение libcurl. Только его нужно задокументировать.

    ОтветитьУдалить
  5. я не спорю, что что-то типа кольцевого буфера -- это логично

    однако надежда на такое поведение либкурл ниразу не логичнa -- так как в си нет исключений, то логично считать, что возможен такой вот код:

    Something f2(.....)
    {
    .....
    if( fread (buffer,1,lSize,pFile)==0 ) return so_far_accumulated_result;
    .....
    }

    void f1(.....)
    {
    .....
    something = f2(.....);
    if( fread (buffer,1,lSize,pFile)==0 ) return;
    }

    с первого взгляда кажется, что CURLOPT_READFUNCTION должна вести себя аналогично fread

    ОтветитьУдалить
  6. Вот мне и не нравится то, что приходится гадать -- должно так быть или же нет. Было бы поведение описано в документации, гадать не пришлось.

    Надо, наверное, у разработчика libcurl попросить разъяснить этот момент.

    ОтветитьУдалить