Skip to content

Commit eddbe5a

Browse files
committed
otlp: bug fix for extra headers
Signed-off-by: inge4pres <[email protected]>
1 parent a25127a commit eddbe5a

File tree

2 files changed

+132
-29
lines changed

2 files changed

+132
-29
lines changed

src/otlp.zig

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,21 @@ const HTTPClient = struct {
388388
self.allocator.destroy(self);
389389
}
390390

391-
fn requestOptions(config: *ConfigOptions) !http.Client.RequestOptions {
391+
fn extraHeaders(allocator: std.mem.Allocator, config: *ConfigOptions) ![]http.Header {
392+
var extra_headers = std.ArrayList(http.Header).init(allocator);
393+
if (config.headers) |h| {
394+
const parsed_headers = try parseHeaders(allocator, h);
395+
defer allocator.free(parsed_headers);
396+
try extra_headers.appendSlice(parsed_headers);
397+
}
398+
if (config.compression.encodingHeaderValue()) |comp| {
399+
const ce: http.Header = .{ .name = "content-encoding", .value = comp };
400+
try extra_headers.append(ce);
401+
}
402+
return extra_headers.toOwnedSlice();
403+
}
404+
405+
fn requestOptions(allocator: std.mem.Allocator, config: *ConfigOptions) !http.Client.RequestOptions {
392406
const headers: http.Client.Request.Headers = .{
393407
.accept_encoding = if (config.compression.encodingHeaderValue()) |v| .{ .override = v } else .default,
394408
.content_type = .{ .override = switch (config.protocol) {
@@ -398,13 +412,11 @@ const HTTPClient = struct {
398412
} },
399413
.user_agent = .{ .override = UserAgent },
400414
};
401-
var request_options: http.Client.RequestOptions = .{
415+
const request_options: http.Client.RequestOptions = .{
402416
.headers = headers,
403417
.server_header_buffer = undefined,
418+
.extra_headers = try extraHeaders(allocator, config),
404419
};
405-
if (config.headers) |h| {
406-
request_options.extra_headers = try parseHeaders(h);
407-
}
408420

409421
return request_options;
410422
}
@@ -434,7 +446,10 @@ const HTTPClient = struct {
434446
};
435447
defer self.allocator.free(req_body);
436448

437-
const req_opts = try requestOptions(self.config);
449+
const req_opts = try requestOptions(self.allocator, self.config);
450+
defer {
451+
if (req_opts.extra_headers.len > 0) self.allocator.free(req_opts.extra_headers);
452+
}
438453

439454
const fetch_request = http.Client.FetchOptions{
440455
.location = .{ .url = url },
@@ -537,15 +552,19 @@ fn calculateDelayMillisec(base_delay_ms: u64, max_delay_ms: u64, attempt: u32) u
537552
return delay + jitter;
538553
}
539554

540-
fn parseHeaders(key_values: []const u8) ConfigError![]std.http.Header {
555+
// Parses the key-value, comma separated list of headers from the config.
556+
// Caller owns the memory and must free it.
557+
fn parseHeaders(allocator: std.mem.Allocator, key_values: []const u8) ![]std.http.Header {
541558
// Maximum 64 items are allowd in the W3C baggage
542-
var headers = [_]std.http.Header{.{ .name = "", .value = "" }} ** 64;
543-
var split = std.mem.splitScalar(u8, key_values, ',');
559+
var headers = try allocator.alloc(std.http.Header, 64);
560+
defer allocator.free(headers);
561+
562+
var comma_split = std.mem.splitScalar(u8, key_values, ',');
544563

545564
var idx: usize = 0;
546565
// The sum of all characters in the key and value must be less than 8192 bytes (2^13).
547566
var cum_bytes: u13 = 0;
548-
while (split.next()) |item| {
567+
while (comma_split.next()) |item| {
549568
// Fail if there are more than 64 headers.
550569
if (idx == headers.len) {
551570
return ConfigError.InvalidHeadersTooManyItems;
@@ -562,40 +581,70 @@ fn parseHeaders(key_values: []const u8) ConfigError![]std.http.Header {
562581
if (kv.next()) |_| {
563582
return ConfigError.InvalidHeadersSyntax;
564583
}
565-
headers[idx] = std.http.Header{ .name = key, .value = value };
566-
idx += 1;
567584
// Fail when the sum of all bytes for the headers overflows.
568585
// Each header is accompanied by 3 more bytes: a colon, a space and a newline.
569586
cum_bytes = std.math.add(u13, cum_bytes, @intCast(key.len + value.len + 3)) catch return ConfigError.InvalidHeadersTooManyBytes;
587+
588+
headers[idx] = std.http.Header{ .name = key, .value = value };
589+
idx += 1;
570590
}
571-
return headers[0..idx];
591+
const ret = try allocator.alloc(std.http.Header, idx);
592+
std.mem.copyForwards(std.http.Header, ret, headers[0..idx]);
593+
return ret;
572594
}
573595

574596
test "otlp config parse headers" {
597+
const allocator = std.testing.allocator;
598+
599+
const single_header = "test-header=test-value";
600+
const single_parsed = try parseHeaders(allocator, single_header);
601+
defer allocator.free(single_parsed);
602+
603+
try std.testing.expectEqual(1, single_parsed.len);
604+
try std.testing.expectEqualSlices(u8, "test-header", single_parsed[0].name);
605+
try std.testing.expectEqualSlices(u8, "test-value", single_parsed[0].value);
606+
575607
const valid_headers = "a=b,123=456,key1=value1 , key2=value2";
576-
const parsed = try parseHeaders(valid_headers);
608+
const parsed = try parseHeaders(allocator, valid_headers);
609+
defer allocator.free(parsed);
577610

578611
try std.testing.expectEqual(parsed.len, 4);
579-
try std.testing.expectEqualSlices(u8, parsed[0].name, "a");
580-
try std.testing.expectEqualSlices(u8, parsed[0].value, "b");
581-
try std.testing.expectEqualSlices(u8, parsed[1].name, "123");
582-
try std.testing.expectEqualSlices(u8, parsed[1].value, "456");
583-
try std.testing.expectEqualSlices(u8, parsed[2].name, "key1");
584-
try std.testing.expectEqualSlices(u8, parsed[2].value, "value1");
585-
try std.testing.expectEqualSlices(u8, parsed[3].name, "key2");
586-
try std.testing.expectEqualSlices(u8, parsed[3].value, "value2");
612+
try std.testing.expectEqualSlices(u8, "a", parsed[0].name);
613+
try std.testing.expectEqualSlices(u8, "b", parsed[0].value);
614+
try std.testing.expectEqualSlices(u8, "123", parsed[1].name);
615+
try std.testing.expectEqualSlices(u8, "456", parsed[1].value);
616+
try std.testing.expectEqualSlices(u8, "key1", parsed[2].name);
617+
try std.testing.expectEqualSlices(u8, "value1", parsed[2].value);
618+
try std.testing.expectEqualSlices(u8, "key2", parsed[3].name);
619+
try std.testing.expectEqualSlices(u8, "value2", parsed[3].value);
587620

588621
const invalid_headers: [4][]const u8 = .{ "a=,", "=b", "a=b=c", "a=b,=c=d" };
589622
for (invalid_headers) |header| {
590-
try std.testing.expectError(ConfigError.InvalidHeadersSyntax, parseHeaders(header));
623+
try std.testing.expectError(ConfigError.InvalidHeadersSyntax, parseHeaders(allocator, header));
591624
}
592625

593626
// 150 bytes * 60 == 9000 bytes
594627
const invalid_too_many_bytes: []const u8 = "key=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA," ** 60;
595-
try std.testing.expectError(ConfigError.InvalidHeadersTooManyBytes, parseHeaders(invalid_too_many_bytes));
628+
try std.testing.expectError(ConfigError.InvalidHeadersTooManyBytes, parseHeaders(allocator, invalid_too_many_bytes));
596629

597630
const invalid_too_many_items: []const u8 = "key=val," ** 65;
598-
try std.testing.expectError(ConfigError.InvalidHeadersTooManyItems, parseHeaders(invalid_too_many_items));
631+
try std.testing.expectError(ConfigError.InvalidHeadersTooManyItems, parseHeaders(allocator, invalid_too_many_items));
632+
}
633+
634+
test "otlp HTTPClient extra headers" {
635+
const allocator = std.testing.allocator;
636+
var config = try ConfigOptions.init(allocator);
637+
defer config.deinit();
638+
639+
config.headers = "key1=value1,key2=value2";
640+
const headers = try HTTPClient.extraHeaders(allocator, config);
641+
defer allocator.free(headers);
642+
643+
try std.testing.expectEqual(2, headers.len);
644+
try std.testing.expectEqualSlices(u8, "key1", headers[0].name);
645+
try std.testing.expectEqualSlices(u8, "value1", headers[0].value);
646+
try std.testing.expectEqualSlices(u8, "key2", headers[1].name);
647+
try std.testing.expectEqualSlices(u8, "value2", headers[1].value);
599648
}
600649

601650
test "otlp exp backoff delay calculation" {

src/otlp_test.zig

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,27 @@ test "otlp HTTPClient compressed protobuf metrics payload" {
117117
try otlp.Export(allocator, config, otlp.Signal.Data{ .metrics = req });
118118
}
119119

120+
test "otlp HTTPClient send extra headers" {
121+
const allocator = std.testing.allocator;
122+
123+
var server = try HTTPTestServer.init(allocator, assertExtraHeaders);
124+
defer server.deinit();
125+
126+
const thread = try std.Thread.spawn(.{}, HTTPTestServer.processSingleRequest, .{server});
127+
defer thread.join();
128+
129+
const config = try ConfigOptions.init(allocator);
130+
defer config.deinit();
131+
const endpoint = try std.fmt.allocPrint(allocator, "127.0.0.1:{d}", .{server.port()});
132+
defer allocator.free(endpoint);
133+
config.endpoint = endpoint;
134+
config.headers = "test-header=test-value";
135+
136+
const dummy = try emptyMetricsExportRequest(allocator);
137+
defer dummy.deinit();
138+
try otlp.Export(allocator, config, otlp.Signal.Data{ .metrics = dummy });
139+
}
140+
120141
fn emptyMetricsExportRequest(allocator: std.mem.Allocator) !pbcollector_metrics.ExportMetricsServiceRequest {
121142
const rm = try allocator.alloc(pbmetrics.ResourceMetrics, 1);
122143
const rm0 = pbmetrics.ResourceMetrics{
@@ -175,6 +196,7 @@ const AssertionError = error{
175196
EmptyBody,
176197
ProtobufBodyMismatch,
177198
CompressionMismatch,
199+
ExtraHeaderMissing,
178200
};
179201

180202
fn badRequest(request: *http.Server.Request) anyerror!void {
@@ -209,18 +231,50 @@ fn assertUncompressedProtobufMetricsBodyCanBeParsed(request: *http.Server.Reques
209231

210232
fn assertCompressionHeaderGzip(request: *http.Server.Request) anyerror!void {
211233
var headers = request.iterateHeaders();
234+
var accept_found = false;
212235
while (headers.next()) |header| {
213236
if (std.mem.eql(u8, header.name, "accept-encoding")) {
214-
const content_encoding = header.value;
215-
if (!std.mem.eql(u8, content_encoding, "gzip")) {
216-
std.debug.print("otlp HTTP test - compression header mismatch, want 'gzip' got '{s}'\n", .{content_encoding});
237+
if (!std.mem.eql(u8, header.value, "gzip")) {
238+
std.debug.print("otlp HTTP test accept-encoding header mismatch, want 'gzip' got '{s}'\n", .{header.value});
239+
return AssertionError.CompressionMismatch;
240+
}
241+
accept_found = true;
242+
}
243+
}
244+
var content_found = false;
245+
var headers2 = request.iterateHeaders();
246+
while (headers2.next()) |header| {
247+
if (std.mem.eql(u8, header.name, "content-encoding")) {
248+
if (!std.mem.eql(u8, header.value, "gzip")) {
249+
std.debug.print("otlp HTTP test content-encoding header mismatch, want 'gzip' got '{s}'\n", .{header.value});
217250
return AssertionError.CompressionMismatch;
218251
}
252+
content_found = true;
253+
}
254+
}
255+
if (!content_found or !accept_found) {
256+
std.debug.print("otlp HTTP test compression headers not found: content-encoding {} | accept-encoding {}\n", .{
257+
content_found,
258+
accept_found,
259+
});
260+
return AssertionError.CompressionMismatch;
261+
}
262+
try request.respond("", .{ .status = .ok });
263+
}
264+
265+
fn assertExtraHeaders(request: *http.Server.Request) anyerror!void {
266+
var headers = request.iterateHeaders();
267+
while (headers.next()) |header| {
268+
if (std.mem.eql(u8, header.name, "test-header")) {
269+
if (!std.mem.eql(u8, header.value, "test-value")) {
270+
return AssertionError.ExtraHeaderMissing;
271+
}
272+
219273
try request.respond("", .{ .status = .ok });
220274
return;
221275
}
222276
}
223-
return AssertionError.CompressionMismatch;
277+
return AssertionError.ExtraHeaderMissing;
224278
}
225279

226280
const HTTPTestServer = struct {

0 commit comments

Comments
 (0)