Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.org
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#+TITLE: zig-curl
#+DATE: 2023-09-16T23:16:15+0800
#+LASTMOD: 2025-10-17T09:10:36+0800
#+LASTMOD: 2026-03-08T11:50:51+0800
#+OPTIONS: toc:nil num:nil
#+STARTUP: content

Expand Down Expand Up @@ -61,6 +61,23 @@ pub fn main() !void {

Check [[file:examples]] for more examples.

** Diagnostics
When a curl operation fails, it returns a generic =error.Curl=. To get more detailed information, you can use the =diagnostics= field in =Easy= or =Multi=.

#+begin_src zig
var easy = try curl.Easy.init(.{ ... });
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README snippet uses curl.Easy.init(.{ ... }), but ... is not valid Zig syntax inside a struct literal, so readers copying this example will get a compile error. Consider using a valid minimal init (e.g. .{} ) or a concrete option like . { .ca_bundle = ca_bundle } to keep the documentation runnable.

Suggested change
var easy = try curl.Easy.init(.{ ... });
var easy = try curl.Easy.init(.{});

Copilot uses AI. Check for mistakes.
defer easy.deinit();

easy.perform() catch |err| {
if (err == error.Curl) {
if (easy.diagnostics.getMessage()) |msg| {
std.log.err("curl failed: {s}", .{msg});
}
}
return err;
};
#+end_src

* Documentation
See https://jiacai2050.github.io/zig-curl/

Expand Down
2 changes: 1 addition & 1 deletion examples/header.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn iterateRedirectedHeaders(easy: *Easy) !void {
try easy.setFollowLocation(true);
const resp = try easy.fetch("https://edgebin.liujiacai.net/redirect/2", .{});

var diagnostics: Easy.Diagnostics = .{};
var diagnostics: curl.Diagnostics = .{};
const redirects = try resp.getRedirectCount(&diagnostics);
try std.testing.expectEqual(redirects, 2);

Expand Down
17 changes: 10 additions & 7 deletions examples/multi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ pub fn main() !void {
defer if (gpa.deinit() != .ok) @panic("leak");
const allocator = gpa.allocator();

var diagnostics: Multi.Diagnostics = .{};

const multi = try Multi.init(&diagnostics);
defer multi.deinit();
var multi = try Multi.init();
defer multi.deinit() catch |e| {
std.debug.print("multi handle deinit failed, err:{any}\n", .{e});
if (multi.diagnostics.getMessage()) |msg| {
std.debug.print("Diagnostics: {s}\n", .{msg});
}
};

var wtr1 = std.Io.Writer.Allocating.init(allocator);
defer wtr1.deinit();
Expand Down Expand Up @@ -67,16 +70,16 @@ pub fn main() !void {
}

// check that the request was successful
try checkCode(info.msg.data.result, &diagnostics);
try checkCode(info.msg.data.result, &multi.diagnostics);

// Read the HTTP status code
var status_code: c_long = 0;
try checkCode(c.curl_easy_getinfo(easy_handle, c.CURLINFO_RESPONSE_CODE, &status_code), &diagnostics);
try checkCode(c.curl_easy_getinfo(easy_handle, c.CURLINFO_RESPONSE_CODE, &status_code), &multi.diagnostics);
std.debug.print("Response Code: {any}\n", .{status_code});

// Get the private data (buffer) associated with this handle
var private_data: ?*anyopaque = null;
try checkCode(c.curl_easy_getinfo(easy_handle, c.CURLINFO_PRIVATE, &private_data), &diagnostics);
try checkCode(c.curl_easy_getinfo(easy_handle, c.CURLINFO_PRIVATE, &private_data), &multi.diagnostics);
const writer: *Writer = @ptrCast(@alignCast(private_data.?));

std.debug.print("Response body: {s}\n", .{writer.buffered()});
Expand Down
32 changes: 16 additions & 16 deletions src/Multi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -19,50 +19,50 @@ pub const Diagnostics = errors.Diagnostics;
const Self = @This();

multi: *c.CURLM,
diagnostics: ?*Diagnostics,
diagnostics: Diagnostics,

pub fn init(diagnostics: ?*Diagnostics) !Self {
pub fn init() !Self {
const core = c.curl_multi_init();
if (core == null) {
return error.InitMulti;
}
return .{
.multi = core.?,
.diagnostics = diagnostics,
.diagnostics = .{},
};
}

pub fn deinit(self: Self) void {
_ = self;
pub fn deinit(self: *Self) !void {
return checkMCode(c.curl_multi_cleanup(self.multi), &self.diagnostics);
Comment on lines +35 to +36
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deinit now returns !void, which makes common defer multi.deinit(); usage awkward and is inconsistent with Easy.deinit()/MultiPart.deinit() being infallible. Consider keeping deinit(self: *Self) void (always call curl_multi_cleanup) and either ignore the CURLMcode, assert in debug builds, or provide a separate fallible cleanup()/tryDeinit() if callers need to surface cleanup errors via diagnostics.

Suggested change
pub fn deinit(self: *Self) !void {
return checkMCode(c.curl_multi_cleanup(self.multi), &self.diagnostics);
pub fn deinit(self: *Self) void {
// Always attempt cleanup; record diagnostics but do not propagate an error.
checkMCode(c.curl_multi_cleanup(self.multi), &self.diagnostics) catch {};

Copilot uses AI. Check for mistakes.
}

/// Adds the easy handle to the multi_handle.
/// https://curl.se/libcurl/c/curl_multi_add_handle.html
pub fn addHandle(self: Self, easy: *Easy) !void {
pub fn addHandle(self: *Self, easy: *Easy) !void {
try easy.setCommonOpts();
return checkMCode(c.curl_multi_add_handle(self.multi, easy.handle), self.diagnostics);
return checkMCode(c.curl_multi_add_handle(self.multi, easy.handle), &self.diagnostics);
}

/// Removes a given easy_handle from the multi_handle.
/// https://curl.se/libcurl/c/curl_multi_remove_handle.html
pub fn removeHandle(self: Self, handle: *c.CURL) !void {
return checkMCode(c.curl_multi_remove_handle(self.multi, handle), self.diagnostics);
pub fn removeHandle(self: *Self, handle: *c.CURL) !void {
return checkMCode(c.curl_multi_remove_handle(self.multi, handle), &self.diagnostics);
}

/// Performs transfers on all the added handles that need attention in a non-blocking fashion.
/// Returns the number of handles that still transfer data. When that reaches zero, all transfers are done.
/// https://curl.se/libcurl/c/curl_multi_perform.html
pub fn perform(self: Self) !c_int {
pub fn perform(self: *Self) !c_int {
var still_running: c_int = undefined;
try checkMCode(c.curl_multi_perform(self.multi, &still_running), self.diagnostics);
try checkMCode(c.curl_multi_perform(self.multi, &still_running), &self.diagnostics);

return still_running;
}

/// Polls all file descriptors used by the curl easy handles contained in the given multi handle set.
/// Return the number of file descriptors on which there is activity.
/// https://curl.se/libcurl/c/curl_multi_poll.html
pub fn poll(self: Self, extra_fds: ?[]c.curl_waitfd, timeout_ms: c_int) !c_int {
pub fn poll(self: *Self, extra_fds: ?[]c.curl_waitfd, timeout_ms: c_int) !c_int {
var num_fds: c_int = undefined;
var fds: ?[*]c.curl_waitfd = null;
var fd_len: c_uint = 0;
Expand All @@ -71,15 +71,15 @@ pub fn poll(self: Self, extra_fds: ?[]c.curl_waitfd, timeout_ms: c_int) !c_int {
fd_len = @intCast(v.len);
}

try checkMCode(c.curl_multi_poll(self.multi, fds, fd_len, timeout_ms, &num_fds), self.diagnostics);
try checkMCode(c.curl_multi_poll(self.multi, fds, fd_len, timeout_ms, &num_fds), &self.diagnostics);
return num_fds;
}

/// Wakes up a sleeping curl_multi_poll call that is currently (or is about to be) waiting for activity or a timeout.
/// This function can be called from any thread.
/// https://curl.se/libcurl/c/curl_multi_wakeup.html
pub fn wakeup(self: Self) !void {
try checkMCode(c.curl_multi_wakeup(self.multi), self.diagnostics);
pub fn wakeup(self: *Self) !void {
try checkMCode(c.curl_multi_wakeup(self.multi), &self.diagnostics);
}

pub const Info = struct {
Expand All @@ -89,7 +89,7 @@ pub const Info = struct {

/// Ask the multi handle if there are any messages from the individual transfers.
/// https://curl.se/libcurl/c/curl_multi_info_read.html
pub fn readInfo(self: Self) !Info {
pub fn readInfo(self: *Self) !Info {
var msgs_in_queue: c_int = undefined;

const msg = c.curl_multi_info_read(self.multi, &msgs_in_queue);
Expand Down
18 changes: 9 additions & 9 deletions src/errors.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ pub const Diagnostics = struct {
/// https://curl.se/libcurl/c/libcurl-errors.html#CURLMcode
m_code: c.CURLMcode,
} = null,

/// Returns a human-readable error message based on the error code.
pub fn getMessage(self: Diagnostics) ?[]const u8 {
const error_code = self.error_code orelse return null;
return switch (error_code) {
.code => |code| std.mem.span(c.curl_easy_strerror(code)),
.m_code => |m_code| std.mem.span(c.curl_multi_strerror(m_code)),
};
}
Comment on lines +44 to +50

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The curl_easy_strerror function is not thread-safe as it uses a global static buffer. This can lead to data races and incorrect error messages when getMessage is called from multiple threads concurrently. curl_multi_strerror is thread-safe, so it's only the .code branch that is problematic.

To fix this without a breaking API change, you can copy the error message from curl_easy_strerror into a thread-local buffer. This makes the function thread-safe. The returned slice is valid until the next call to getMessage for a CURLcode error on the same thread, which should be documented.

You'll need to add a thread-local buffer at the file scope:

threadlocal var easy_error_buffer: [c.CURL_ERROR_SIZE]u8 = undefined;
    pub fn getMessage(self: Diagnostics) ?[]const u8 {
        const error_code = self.error_code orelse return null;
        return switch (error_code) {
            .code => |code| {
                // curl_easy_strerror is not thread-safe, so we copy to a thread-local buffer.
                const msg = std.mem.span(c.curl_easy_strerror(code));
                const len = @min(msg.len, easy_error_buffer.len);
                @memcpy(easy_error_buffer[0..len], msg[0..len]);
                return easy_error_buffer[0..len];
            },
            .m_code => |m_code| std.mem.span(c.curl_multi_strerror(m_code)),
        };
    }

Comment on lines +43 to +50
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diagnostics.getMessage() is a new public API but there’s no unit test covering its behavior (null when error_code is unset; correct mapping for both .code and .m_code). Adding a small test that compares getMessage() to curl_easy_strerror/curl_multi_strerror for a known non-OK code would help prevent regressions.

Copilot uses AI. Check for mistakes.
};

pub fn checkCode(code: c.CURLcode, diagnostics: ?*Diagnostics) !void {
Expand All @@ -48,9 +57,6 @@ pub fn checkCode(code: c.CURLcode, diagnostics: ?*Diagnostics) !void {

if (diagnostics) |diag| diag.error_code = .{ .code = code };

// https://curl.se/libcurl/c/libcurl-errors.html
std.log.debug("curl err code:{d}, msg:{s}\n", .{ code, c.curl_easy_strerror(code) });

return error.Curl;
}

Expand All @@ -61,11 +67,5 @@ pub fn checkMCode(code: c.CURLMcode, diagnostics: ?*Diagnostics) !void {

if (diagnostics) |diag| diag.error_code = .{ .m_code = code };

// https://curl.se/libcurl/c/libcurl-errors.html
std.log.debug("curlm err code:{d}, msg:{s}\n", .{
code,
c.curl_multi_strerror(code),
});

return error.Curl;
}