Three places in Session::SendRequest where the shared curl client tells an exporter
something other than what it then does. This is ext/src/http/client/curl/, the production
client every HTTP exporter runs on, not the embedded test server whose scope was settled on
#4287.
All three are on main today.
1. A gzip failure reports the request as failed and then sends it anyway
http_client_curl.cc, inside the kGzip branch, which is compiled under
ENABLE_OTLP_COMPRESSION_PREVIEW, so this one reaches only builds with compression enabled:
if (stream != Z_OK)
{
if (callback)
{
callback->OnEvent(SessionState::CreateFailed, zs.msg ? zs.msg : "");
}
is_session_active_.store(false, std::memory_order_release);
}
deflateEnd(&zs);
There is no return. Control falls through to curl_operation_.reset(new HttpOperation(...))
and SendAsync, so the exporter has already been told the request failed while the request
goes out.
The body is the worrying part. deflateInPlace compresses into the caller's buffer, and
only the success path adds Content-Encoding: gzip and resizes to max_size. A failure
partway through therefore leaves a buffer that may have been written in place, at its
original length, with no encoding header, and that is what gets sent.
What an exporter does with this depends on the exporter, but the shapes are all bad: report
failure and let a retry send the batch a second time, or report failure while the server
accepts something it cannot parse.
2. Cancelled and OnResponse can both fire for one operation
In the SendAsync completion lambda:
if (operation.WasAborted())
{
callback->OnEvent(SessionState::Cancelled, "");
}
if (operation.GetSessionState() == SessionState::Response)
{
...
callback->OnResponse(*response);
}
Two independent ifs, so an operation aborted after a response arrived delivers both. A
handler that treats either as terminal sees its export settle twice. That is the same shape
as #4338 on the Elasticsearch side, where the fix under review adds a first-writer-wins
guard in the exporter. The guard is needed in every consumer as long as the client can do
this.
3. One setup failure produces two terminal events
HttpOperation::SendAsync dispatches ConnectFailed and returns non-CURLE_OK
(http_operation_curl.cc). Back in Session::SendRequest, success is false, so the else
branch dispatches CreateFailed as well. One failure, two events, and a handler counting
terminal states counts two.
What would settle it
A contract on EventHandler: exactly one terminal event per SendRequest, and no bytes on
the wire after one has been delivered. Concretely that means returning after the gzip
failure, making the abort and response branches mutually exclusive, and letting the setup
path own its single event rather than both layers reporting.
Until then every consumer needs its own idempotence guard to be correct, which is a rule
nothing in the interface states.
I have not sent a patch. Three separate behaviour changes to a shared client is more than
belongs in one, and I would rather hear which of the three you want changed and whether the
EventHandler contract should be written down first.
Three places in
Session::SendRequestwhere the shared curl client tells an exportersomething other than what it then does. This is
ext/src/http/client/curl/, the productionclient every HTTP exporter runs on, not the embedded test server whose scope was settled on
#4287.
All three are on
maintoday.1. A gzip failure reports the request as failed and then sends it anyway
http_client_curl.cc, inside thekGzipbranch, which is compiled underENABLE_OTLP_COMPRESSION_PREVIEW, so this one reaches only builds with compression enabled:There is no
return. Control falls through tocurl_operation_.reset(new HttpOperation(...))and
SendAsync, so the exporter has already been told the request failed while the requestgoes out.
The body is the worrying part.
deflateInPlacecompresses into the caller's buffer, andonly the success path adds
Content-Encoding: gzipand resizes tomax_size. A failurepartway through therefore leaves a buffer that may have been written in place, at its
original length, with no encoding header, and that is what gets sent.
What an exporter does with this depends on the exporter, but the shapes are all bad: report
failure and let a retry send the batch a second time, or report failure while the server
accepts something it cannot parse.
2. Cancelled and OnResponse can both fire for one operation
In the
SendAsynccompletion lambda:Two independent
ifs, so an operation aborted after a response arrived delivers both. Ahandler that treats either as terminal sees its export settle twice. That is the same shape
as #4338 on the Elasticsearch side, where the fix under review adds a first-writer-wins
guard in the exporter. The guard is needed in every consumer as long as the client can do
this.
3. One setup failure produces two terminal events
HttpOperation::SendAsyncdispatchesConnectFailedand returns non-CURLE_OK(
http_operation_curl.cc). Back inSession::SendRequest,successis false, so the elsebranch dispatches
CreateFailedas well. One failure, two events, and a handler countingterminal states counts two.
What would settle it
A contract on
EventHandler: exactly one terminal event perSendRequest, and no bytes onthe wire after one has been delivered. Concretely that means returning after the gzip
failure, making the abort and response branches mutually exclusive, and letting the setup
path own its single event rather than both layers reporting.
Until then every consumer needs its own idempotence guard to be correct, which is a rule
nothing in the interface states.
I have not sent a patch. Three separate behaviour changes to a shared client is more than
belongs in one, and I would rather hear which of the three you want changed and whether the
EventHandlercontract should be written down first.