Skip to content

Commit

Permalink
Add support for iterating headers (#16)
Browse files Browse the repository at this point in the history
Rationale: `Response.getHeader()` does not work with multiple headers
sharing the same name (which is not uncommon, e.g. the `Set-Cookie`
header).

Mainly adds `Response.iterateHeaders(options)` which returns a
`HeaderIterator`. See `examples/advanced.zig` for usage.

It also supports iterating over headers of redirected requests. Added
the following methods for easier testing:

- `Response.getRedirectCount()`
- `Easy.setFollowLocation(enable)`

---------

Co-authored-by: jiacai2050 <[email protected]>
  • Loading branch information
timothyqiu and jiacai2050 authored Jul 26, 2024
1 parent 5a296fb commit 5728019
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 12 deletions.
59 changes: 59 additions & 0 deletions examples/advanced.zig
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,53 @@ fn postMutliPart(easy: Easy) !void {
std.debug.print("resp:{s}\n", .{resp.body.?.items});
}

fn iterateHeaders(easy: Easy) !void {
// Reset old options, e.g. headers.
easy.reset();

const resp = try easy.get("https://httpbin.org/response-headers?X-Foo=1&X-Foo=2&X-Foo=3");
defer resp.deinit();

std.debug.print("Iterating all headers...\n", .{});
{
var iter = try resp.iterateHeaders(.{});
while (try iter.next()) |header| {
std.debug.print(" {s}: {s}\n", .{ header.name, header.get() });
}
}

// Iterating X-Foo only
{
var iter = try resp.iterateHeaders(.{ .name = "X-Foo" });
const expected_values = .{ "1", "2", "3" };
inline for (expected_values) |expected| {
const header = try iter.next() orelse unreachable;
try std.testing.expectEqualStrings(header.get(), expected);
}
try std.testing.expect((try iter.next()) == null);
}
}

fn iterateRedirectedHeaders(easy: Easy) !void {
// Reset old options, e.g. headers.
easy.reset();

try easy.setFollowLocation(true);
const resp = try easy.get("https://httpbin.org/redirect/1");
defer resp.deinit();

const redirects = try resp.getRedirectCount();
try std.testing.expectEqual(redirects, 1);

for (0..redirects + 1) |i| {
std.debug.print("Request #{} headers:\n", .{i});
var iter = try resp.iterateHeaders(.{ .request = i });
while (try iter.next()) |header| {
std.debug.print(" {s}: {s}\n", .{ header.name, header.get() });
}
}
}

pub fn main() !void {
const allocator = std.heap.page_allocator;

Expand All @@ -127,4 +174,16 @@ pub fn main() !void {
println("PUT with custom header demo");
try putWithCustomHeader(allocator, easy);
try postMutliPart(easy);

println("Iterate headers demo");
iterateHeaders(easy) catch |err| switch (err) {
error.NoCurlHeaderSupport => std.debug.print("No header support, skipping...\n", .{}),
else => return err,
};

println("Redirected headers demo");
iterateRedirectedHeaders(easy) catch |err| switch (err) {
error.NoCurlHeaderSupport => std.debug.print("No header support, skipping...\n", .{}),
else => return err,
};
}
110 changes: 98 additions & 12 deletions src/Easy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub const Response = struct {

fn polyfill_struct_curl_header() type {
if (has_parse_header_support()) {
return *c.struct_curl_header;
return c.struct_curl_header;
} else {
// return a dummy struct to make it compile on old version.
return struct {
Expand All @@ -82,7 +82,7 @@ pub const Response = struct {
}

pub const Header = struct {
c_header: polyfill_struct_curl_header(),
c_header: *polyfill_struct_curl_header(),
name: []const u8,

/// Get the first value associated with the given key.
Expand All @@ -99,17 +99,98 @@ pub const Response = struct {
}

var header: ?*c.struct_curl_header = null;
const code = c.curl_easy_header(self.handle, name.ptr, 0, c.CURLH_HEADER, -1, &header);
return if (errors.headerErrorFrom(code)) |err|
switch (err) {
error.Missing => null,
else => err,
return Response.getHeaderInner(self.handle, name, &header);
}

fn getHeaderInner(easy: ?*c.CURL, name: [:0]const u8, hout: *?*c.struct_curl_header) errors.HeaderError!?Header {
const code = c.curl_easy_header(
easy,
name.ptr,
0, // index, 0 means first header
c.CURLH_HEADER,
-1, // request, -1 means last request
hout,
);
return if (errors.headerErrorFrom(code)) |err| switch (err) {
error.Missing, error.NoHeaders => null,
else => err,
} else .{
.c_header = hout.*.?,
.name = name,
};
}

pub const HeaderIterator = struct {
handle: *c.CURL,
name: ?[:0]const u8,
request: ?usize,
c_header: ?*polyfill_struct_curl_header() = null,

pub fn next(self: *HeaderIterator) !?Header {
if (comptime !has_parse_header_support()) {
return error.NoCurlHeaderSupport;
}
else
.{
.c_header = header.?,
.name = name,
};

const request: c_int = if (self.request) |v| @intCast(v) else -1;

if (self.name) |filter_name| {
if (self.c_header) |c_header| {
// fast path
if (c_header.*.index + 1 == c_header.*.amount) {
return null;
}
} else {
return Response.getHeaderInner(self.handle, filter_name, &self.c_header);
}
}

while (true) {
const c_header = c.curl_easy_nextheader(
self.handle,
c.CURLH_HEADER,
request,
self.c_header,
) orelse return null;
self.c_header = c_header;

const name = std.mem.sliceTo(c_header.*.name, 0);
if (self.name) |filter_name| {
if (!std.ascii.eqlIgnoreCase(name, filter_name)) {
continue;
}
}

return Header{
.c_header = c_header,
.name = name,
};
}
}
};

pub const IterateHeadersOptions = struct {
/// Only iterate over headers matching a specific name.
name: ?[:0]const u8 = null,
/// Which request you want headers from. Useful when there are redirections.
/// Leaving `null` means the last request.
request: ?usize = null,
};

pub fn iterateHeaders(self: Response, options: IterateHeadersOptions) errors.HeaderError!HeaderIterator {
if (comptime !has_parse_header_support()) {
return error.NoCurlHeaderSupport;
}
return HeaderIterator{
.handle = self.handle,
.name = options.name,
.request = options.request,
};
}

pub fn getRedirectCount(self: Response) !usize {
var redirects: c_long = undefined;
try checkCode(c.curl_easy_getinfo(self.handle, c.CURLINFO_REDIRECT_COUNT, &redirects));
return @intCast(redirects);
}
};

Expand Down Expand Up @@ -247,6 +328,11 @@ pub fn setUpload(self: Self, up: *Upload) !void {
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_READDATA, up));
}

pub fn setFollowLocation(self: Self, enable: bool) !void {
const param: c_long = @intCast(@intFromBool(enable));
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_FOLLOWLOCATION, param));
}

pub fn reset(self: Self) void {
c.curl_easy_reset(self.handle);
}
Expand Down

0 comments on commit 5728019

Please sign in to comment.