пятница, 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. Логгирования мало не бывает! :)

Отправить комментарий