elastic / elastic/elastic-otel-php

PSR-18 and cURL instrumentations both inject `traceparent`, breaking requests to Elasticsearch

Open
#396 1 comment 1 reaction 1 assignee Claimed by @intuibase View on GitHub
Dominant language
Shell
Stars
15
Forks
9
Avg merge
1d 23h
Merged PRs (30d)
1

Description

## Summary

When both the PSR-18 and cURL auto-instrumentations are active, HTTP requests made through a PSR-18 client backed by cURL receive **two** `traceparent` headers — one injected at each layer.

Elasticsearch rejects requests with duplicate `traceparent` headers with a `400 Bad Request`, so any application using the official `elasticsearch/elasticsearch` PHP client is broken by default after installing EDOT PHP.

## Environment

| | |
|---|---|
| EDOT PHP | 1.8.0 |
| PHP | 8.4.22 (FPM, NTS) |
| Base image | `php:8.4.22-fpm-trixie` (amd64) |
| Elasticsearch | 8.19.7 |
| Elasticsearch client | `elasticsearch/elasticsearch` 8.19.0 (via `elastic-transport-php`) |
| HTTP client | `GuzzleHttp\Client` (resolved via `php-http/discovery`, not configured explicitly; cURL handler) |
| Exporter | OTLP `http/protobuf` → OTel Collector (`otel/opentelemetry-collector-contrib`) → APM Server |

## Steps to reproduce

1. Install EDOT PHP 1.8.0 with default instrumentation settings.
2. In a PHP application, issue any request through `elasticsearch/elasticsearch`.

## Expected behaviour

The request succeeds and exactly one `traceparent` header is sent.

## Actual behaviour

The request fails:

```
Elastic\Elasticsearch\Exception\ClientResponseException: 400 Bad Request:
{"error":{"root_cause":[{"type":"illegal_argument_exception",
"reason":"multiple values for single-valued header [traceparent]."}],
"type":"illegal_argument_exception",
"reason":"multiple values for single-valued header [traceparent]."},"status":400}
```

## Cause

Two instrumentations inject the header independently for the same logical request:

- The **PSR-18** instrumentation hooks `Psr\Http\Client\ClientInterface::sendRequest`. Its documentation states it will "add a traceparent header to the request to facilitate distributed tracing".
- The **cURL** instrumentation injects the header again when the PSR-18 client dispatches the request through `curl_exec`.

Neither checks whether the header is already present. Since Guzzle (and `Symfony\HttpClient`) default to the cURL transport whenever `ext-curl` is available, this affects the common configuration rather than an edge case.

`traceparent` is single-valued per the W3C Trace Context specification, and Elasticsearch enforces this. This was addressed deliberately in [elastic/elasticsearch#107338](https://github.com/elastic/elasticsearch/issues/107338) (closed via #107355), where the same duplicate-header situation — caused there by two APM agents running on a single Node.js application — was changed from a dropped connection to a clean `400`. Elasticsearch returning `400` is therefore the intended behaviour, which means the duplicate must be prevented on the client side.

## Impact

Any PHP application that uses Elastic's own Elasticsearch client is broken immediately after installing EDOT PHP, with no configuration change on the user's part. The failure mode does not obviously point at the APM agent, so the cause is not easy to identify.

This may be related to the addition of PSR-18 auto-instrumentation in 1.7.0, but I have not tested earlier versions and cannot confirm when the conflict was introduced.

## Workaround

```
OTEL_PHP_DISABLED_INSTRUMENTATIONS=psr18
```

This restores working requests, at the cost of losing the logical-request span that would otherwise group retries and redirects performed by the client.

## Suggested fix

Some options, in rough order of preference:

1. Have each instrumentation skip injection when `traceparent` is already present on the outgoing request.
2. Suppress cURL-level context injection when the call originates from an already-instrumented PSR-18 client, so the outer layer owns propagation.
3. If neither is feasible in the short term, document the conflict and ship a safe default combination.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.