move the curl fallback mess into http.zig
This commit is contained in:
parent
f87021b57e
commit
0f09ef5cff
2 changed files with 84 additions and 108 deletions
|
|
@ -40,9 +40,18 @@ pub const Client = struct {
|
|||
|
||||
/// Perform a GET request with automatic retries on transient errors.
|
||||
pub fn get(self: *Client, url: []const u8) HttpError!Response {
|
||||
return self.request(.GET, url, null, &.{});
|
||||
}
|
||||
|
||||
/// Perform a POST request with automatic retries on transient errors.
|
||||
pub fn post(self: *Client, url: []const u8, body: []const u8, extra_headers: []const std.http.Header) HttpError!Response {
|
||||
return self.request(.POST, url, body, extra_headers);
|
||||
}
|
||||
|
||||
fn request(self: *Client, method: std.http.Method, url: []const u8, body: ?[]const u8, extra_headers: []const std.http.Header) HttpError!Response {
|
||||
var attempt: u8 = 0;
|
||||
while (true) : (attempt += 1) {
|
||||
if (self.doGet(url)) |response| {
|
||||
if (self.doRequest(method, url, body, extra_headers)) |response| {
|
||||
return classifyResponse(response);
|
||||
} else |_| {
|
||||
if (attempt >= self.max_retries) return HttpError.RequestFailed;
|
||||
|
|
@ -52,30 +61,33 @@ pub const Client = struct {
|
|||
}
|
||||
}
|
||||
|
||||
fn doGet(self: *Client, url: []const u8) HttpError!Response {
|
||||
fn doRequest(self: *Client, method: std.http.Method, url: []const u8, body: ?[]const u8, extra_headers: []const std.http.Header) HttpError!Response {
|
||||
var aw: std.Io.Writer.Allocating = .init(self.allocator);
|
||||
|
||||
const result = self.http_client.fetch(.{
|
||||
.location = .{ .url = url },
|
||||
.method = method,
|
||||
.payload = body,
|
||||
.extra_headers = extra_headers,
|
||||
.response_writer = &aw.writer,
|
||||
}) catch |err| {
|
||||
aw.deinit();
|
||||
// TLS 1.2-only hosts (e.g., finnhub.io) fail with Zig's TLS 1.3-only client.
|
||||
// Fall back to system curl for these cases.
|
||||
if (err == error.TlsInitializationFailed) {
|
||||
return curlGet(self.allocator, url);
|
||||
return curlRequest(self.allocator, method, url, body, extra_headers);
|
||||
}
|
||||
return HttpError.RequestFailed;
|
||||
};
|
||||
|
||||
const body = aw.toOwnedSlice() catch {
|
||||
const resp_body = aw.toOwnedSlice() catch {
|
||||
aw.deinit();
|
||||
return HttpError.OutOfMemory;
|
||||
};
|
||||
|
||||
return .{
|
||||
.status = result.status,
|
||||
.body = body,
|
||||
.body = resp_body,
|
||||
.allocator = self.allocator,
|
||||
};
|
||||
}
|
||||
|
|
@ -92,11 +104,48 @@ pub const Client = struct {
|
|||
}
|
||||
};
|
||||
|
||||
/// Fallback HTTP GET using system curl for TLS 1.2 hosts.
|
||||
fn curlGet(allocator: std.mem.Allocator, url: []const u8) HttpError!Response {
|
||||
/// Fallback HTTP request using system curl for TLS 1.2 hosts.
|
||||
fn curlRequest(
|
||||
allocator: std.mem.Allocator,
|
||||
method: std.http.Method,
|
||||
url: []const u8,
|
||||
body: ?[]const u8,
|
||||
extra_headers: []const std.http.Header,
|
||||
) HttpError!Response {
|
||||
var argv: std.ArrayList([]const u8) = .empty;
|
||||
defer argv.deinit(allocator);
|
||||
|
||||
// Heap-allocated strings that need freeing after Child.run
|
||||
var to_free: std.ArrayList([]const u8) = .empty;
|
||||
defer {
|
||||
for (to_free.items) |s| allocator.free(s);
|
||||
to_free.deinit(allocator);
|
||||
}
|
||||
|
||||
argv.appendSlice(allocator, &.{ "curl", "-sS", "-f", "-L", "--max-time", "30" }) catch
|
||||
return HttpError.OutOfMemory;
|
||||
|
||||
if (method != .GET) {
|
||||
argv.appendSlice(allocator, &.{ "-X", @tagName(method) }) catch
|
||||
return HttpError.OutOfMemory;
|
||||
}
|
||||
|
||||
for (extra_headers) |hdr| {
|
||||
const val = std.fmt.allocPrint(allocator, "{s}: {s}", .{ hdr.name, hdr.value }) catch
|
||||
return HttpError.OutOfMemory;
|
||||
to_free.append(allocator, val) catch return HttpError.OutOfMemory;
|
||||
argv.appendSlice(allocator, &.{ "-H", val }) catch return HttpError.OutOfMemory;
|
||||
}
|
||||
|
||||
if (body) |b| {
|
||||
argv.appendSlice(allocator, &.{ "-d", b }) catch return HttpError.OutOfMemory;
|
||||
}
|
||||
|
||||
argv.append(allocator, url) catch return HttpError.OutOfMemory;
|
||||
|
||||
const result = std.process.Child.run(.{
|
||||
.allocator = allocator,
|
||||
.argv = &.{ "curl", "-sS", "-f", "-L", "--max-time", "30", url },
|
||||
.argv = argv.items,
|
||||
.max_output_bytes = 10 * 1024 * 1024,
|
||||
}) catch return HttpError.RequestFailed;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
//! classes). For those, use the `ticker::` alias field in the portfolio SRF file.
|
||||
|
||||
const std = @import("std");
|
||||
const http = @import("../net/http.zig");
|
||||
|
||||
const api_url = "https://api.openfigi.com/v3/mapping";
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ pub const FigiResult = struct {
|
|||
};
|
||||
|
||||
/// Look up a single CUSIP via OpenFIGI. Caller must free returned strings.
|
||||
/// Returns null ticker if not found. Uses system curl for the POST request.
|
||||
/// Returns null ticker if not found.
|
||||
pub fn lookupCusip(
|
||||
allocator: std.mem.Allocator,
|
||||
cusip: []const u8,
|
||||
|
|
@ -70,99 +71,26 @@ pub fn lookupCusips(
|
|||
}
|
||||
try body_buf.append(allocator, ']');
|
||||
|
||||
const body_str = body_buf.items;
|
||||
|
||||
// Build curl command
|
||||
var argv_buf: [12][]const u8 = undefined;
|
||||
var argc: usize = 0;
|
||||
argv_buf[argc] = "curl";
|
||||
argc += 1;
|
||||
argv_buf[argc] = "-sS";
|
||||
argc += 1;
|
||||
argv_buf[argc] = "-f";
|
||||
argc += 1;
|
||||
argv_buf[argc] = "--max-time";
|
||||
argc += 1;
|
||||
argv_buf[argc] = "30";
|
||||
argc += 1;
|
||||
argv_buf[argc] = "-X";
|
||||
argc += 1;
|
||||
argv_buf[argc] = "POST";
|
||||
argc += 1;
|
||||
argv_buf[argc] = "-H";
|
||||
argc += 1;
|
||||
argv_buf[argc] = "Content-Type: application/json";
|
||||
argc += 1;
|
||||
// Build headers
|
||||
var headers_buf: [2]std.http.Header = undefined;
|
||||
var n_headers: usize = 0;
|
||||
headers_buf[n_headers] = .{ .name = "Content-Type", .value = "application/json" };
|
||||
n_headers += 1;
|
||||
if (api_key) |key| {
|
||||
// Build header string: "X-OPENFIGI-APIKEY: <key>"
|
||||
const hdr = try std.fmt.allocPrint(allocator, "X-OPENFIGI-APIKEY: {s}", .{key});
|
||||
defer allocator.free(hdr);
|
||||
argv_buf[argc] = "-H";
|
||||
argc += 1;
|
||||
argv_buf[argc] = hdr;
|
||||
argc += 1;
|
||||
argv_buf[argc] = "-d";
|
||||
argc += 1;
|
||||
|
||||
// Need to add body and URL — but we've used all slots for the -H key header.
|
||||
// Restructure: use a dynamic list.
|
||||
var argv_list: std.ArrayList([]const u8) = .empty;
|
||||
defer argv_list.deinit(allocator);
|
||||
try argv_list.appendSlice(allocator, &.{
|
||||
"curl", "-sS", "--max-time", "30",
|
||||
"-X", "POST", "-H", "Content-Type: application/json",
|
||||
"-H", hdr, "-d", body_str,
|
||||
api_url,
|
||||
});
|
||||
|
||||
const result = std.process.Child.run(.{
|
||||
.allocator = allocator,
|
||||
.argv = argv_list.items,
|
||||
.max_output_bytes = 1 * 1024 * 1024,
|
||||
}) catch return error.RequestFailed;
|
||||
defer allocator.free(result.stderr);
|
||||
|
||||
const success = switch (result.term) {
|
||||
.Exited => |code| code == 0,
|
||||
else => false,
|
||||
};
|
||||
if (!success) {
|
||||
allocator.free(result.stdout);
|
||||
return error.RequestFailed;
|
||||
headers_buf[n_headers] = .{ .name = "X-OPENFIGI-APIKEY", .value = key };
|
||||
n_headers += 1;
|
||||
}
|
||||
return parseResponse(allocator, result.stdout, cusips.len);
|
||||
} else {
|
||||
// No API key
|
||||
var argv_list: std.ArrayList([]const u8) = .empty;
|
||||
defer argv_list.deinit(allocator);
|
||||
try argv_list.appendSlice(allocator, &.{
|
||||
"curl", "-sS", "--max-time", "30",
|
||||
"-X", "POST", "-H", "Content-Type: application/json",
|
||||
"-d", body_str, api_url,
|
||||
});
|
||||
|
||||
const result = std.process.Child.run(.{
|
||||
.allocator = allocator,
|
||||
.argv = argv_list.items,
|
||||
.max_output_bytes = 1 * 1024 * 1024,
|
||||
}) catch return error.RequestFailed;
|
||||
defer allocator.free(result.stderr);
|
||||
var client = http.Client.init(allocator);
|
||||
defer client.deinit();
|
||||
|
||||
const success = switch (result.term) {
|
||||
.Exited => |code| code == 0,
|
||||
else => false,
|
||||
};
|
||||
if (!success) {
|
||||
allocator.free(result.stdout);
|
||||
return error.RequestFailed;
|
||||
}
|
||||
return parseResponse(allocator, result.stdout, cusips.len);
|
||||
}
|
||||
var response = try client.post(api_url, body_buf.items, headers_buf[0..n_headers]);
|
||||
defer response.deinit();
|
||||
|
||||
return parseResponse(allocator, response.body, cusips.len);
|
||||
}
|
||||
|
||||
fn parseResponse(allocator: std.mem.Allocator, body: []const u8, expected_count: usize) ![]FigiResult {
|
||||
defer allocator.free(body);
|
||||
|
||||
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||
return error.ParseError;
|
||||
defer parsed.deinit();
|
||||
|
|
@ -246,8 +174,7 @@ fn parseResponse(allocator: std.mem.Allocator, body: []const u8, expected_count:
|
|||
test "parseResponse basic single CUSIP" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// parseResponse takes ownership of body (frees it), so we must dupe
|
||||
const body = try allocator.dupe(u8,
|
||||
const body =
|
||||
\\[
|
||||
\\ {
|
||||
\\ "data": [
|
||||
|
|
@ -255,7 +182,7 @@ test "parseResponse basic single CUSIP" {
|
|||
\\ ]
|
||||
\\ }
|
||||
\\]
|
||||
);
|
||||
;
|
||||
|
||||
const results = try parseResponse(allocator, body, 1);
|
||||
defer {
|
||||
|
|
@ -277,7 +204,7 @@ test "parseResponse basic single CUSIP" {
|
|||
test "parseResponse prefers US exchange" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const body = try allocator.dupe(u8,
|
||||
const body =
|
||||
\\[
|
||||
\\ {
|
||||
\\ "data": [
|
||||
|
|
@ -286,7 +213,7 @@ test "parseResponse prefers US exchange" {
|
|||
\\ ]
|
||||
\\ }
|
||||
\\]
|
||||
);
|
||||
;
|
||||
|
||||
const results = try parseResponse(allocator, body, 1);
|
||||
defer {
|
||||
|
|
@ -304,13 +231,13 @@ test "parseResponse prefers US exchange" {
|
|||
test "parseResponse warning (no match)" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const body = try allocator.dupe(u8,
|
||||
const body =
|
||||
\\[
|
||||
\\ {
|
||||
\\ "warning": "No identifier found."
|
||||
\\ }
|
||||
\\]
|
||||
);
|
||||
;
|
||||
|
||||
const results = try parseResponse(allocator, body, 1);
|
||||
defer allocator.free(results);
|
||||
|
|
@ -323,7 +250,7 @@ test "parseResponse warning (no match)" {
|
|||
test "parseResponse multiple CUSIPs" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const body = try allocator.dupe(u8,
|
||||
const body =
|
||||
\\[
|
||||
\\ {
|
||||
\\ "data": [
|
||||
|
|
@ -339,7 +266,7 @@ test "parseResponse multiple CUSIPs" {
|
|||
\\ ]
|
||||
\\ }
|
||||
\\]
|
||||
);
|
||||
;
|
||||
|
||||
const results = try parseResponse(allocator, body, 3);
|
||||
defer {
|
||||
|
|
@ -369,13 +296,13 @@ test "parseResponse multiple CUSIPs" {
|
|||
test "parseResponse empty data array" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const body = try allocator.dupe(u8,
|
||||
const body =
|
||||
\\[
|
||||
\\ {
|
||||
\\ "data": []
|
||||
\\ }
|
||||
\\]
|
||||
);
|
||||
;
|
||||
|
||||
const results = try parseResponse(allocator, body, 1);
|
||||
defer allocator.free(results);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue