fix(http): prevent duplicate transport-owned headers#3567
Draft
djwhitt wants to merge 4 commits into
Draft
Conversation
When a user supplies a Content-Type header on a webhook/log-drain backend, Tesla's JSON middleware appended a second one and Tesla.put_headers/2 does not dedupe, so requests went out with two Content-Type values. Receivers concatenated them into an unparseable value and delivered an empty body. Give transport-owned headers a single source instead of relying on header ordering: - Add Logflare.Backends.Adaptor.HttpBased.Headers with drop_reserved/2 and normalize_keys/1. - WebhookAdaptor.Client.send/1 strips the reserved set per request: content-type only when the JSON middleware will encode the body (binary bodies keep a custom content-type), content-encoding when gzip is enabled. - HttpBased.Client.new/1 strips content-encoding (gzip) and authorization (token/basic auth). - WebhookAdaptor.cast_config/2 lowercases header keys so stored config cannot hold case-variant duplicates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HttpBased.Client deliberately excluded content-type from its reserved
header set because the body is not known at client-build time, so it
could not tell whether Tesla.Middleware.JSON would encode and own it.
But OtlpAdaptor passes user-configured headers alongside ProtobufFormatter,
which always sets content-type to application/x-protobuf. A user-supplied
content-type therefore survived next to the formatter's, reintroducing the
duplicate-header failure mode for OTLP backends.
Let the formatter that owns the header declare it instead of having the
client guess what middleware will do:
- HttpBased.Client.reserved_header_names/1 folds in an optional
reserved_headers/0 exported by the formatter module (handles both the
module and {module, opts} forms).
- OtlpAdaptor.ProtobufFormatter and SentryAdaptor.EnvelopeBuilder declare
reserved_headers/0 => ["content-type"].
- OtlpAdaptor.cast_config/2 lowercases header keys (parity with
WebhookAdaptor) so stored config cannot hold case-variant duplicates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unredact_headers/2 looked up the stored secret by an exact key match. When stored config predates normalize_keys/1 (e.g. "Authorization") and the submitted redacted key uses different casing (e.g. "authorization"), the lookup missed and the secret was silently dropped before normalization ran. Normalize the existing headers and look up by downcased key so the restore path is case-insensitive, matching the rest of the header handling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When an HTTP backend is configured with a user-supplied
Content-Typeheader, requests went out with twoContent-Typeheaders (e.g.application/jsonapplication/json). Tesla middleware set the header by appending, andTesla.put_headers/2appends rather than de-duplicates, so the user's header survived alongside the transport's. Receivers concatenated the two values, failed to parse the body, and delivered an empty body.Two paths exhibited this:
Tesla.Middleware.JSONappendscontent-typewhen it encodes the body.OtlpAdaptorpasses user-configured headers alongsideProtobufFormatter, which always setscontent-typetoapplication/x-protobuf. A user-suppliedcontent-typesurvived next to the formatter's.Fix
Give transport-owned headers a single source rather than de-duplicating by header order (which is non-deterministic).
Shared plumbing:
Logflare.Backends.Adaptor.HttpBased.Headerswithdrop_reserved/2(case-insensitive strip) andnormalize_keys/1.Webhook path:
WebhookAdaptor.Client.send/1strips the reserved set per request:content-typeonly when the JSON middleware will actually encode the body — binary payloads (e.g. NDJSON) keep a custom content-type — andcontent-encodingwhen gzip is enabled.authorizationis intentionally not reserved here, since several adaptors set it directly.WebhookAdaptor.cast_config/2lowercases header keys so stored config cannot hold case-variant duplicates of the same header.HTTP-based / OTLP path:
HttpBased.Client.new/1stripscontent-encoding(gzip) andauthorization(token/basic auth).content-type. Instead, a formatter that setscontent-typeitself declares ownership via an optionalreserved_headers/0, and the client drops those names from user headers regardless of body.OtlpAdaptor.ProtobufFormatterandSentryAdaptor.EnvelopeBuilderdeclare["content-type"].OtlpAdaptor.cast_config/2lowercases header keys, matching the webhook path.