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.
|
/// Perform a GET request with automatic retries on transient errors.
|
||||||
pub fn get(self: *Client, url: []const u8) HttpError!Response {
|
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;
|
var attempt: u8 = 0;
|
||||||
while (true) : (attempt += 1) {
|
while (true) : (attempt += 1) {
|
||||||
if (self.doGet(url)) |response| {
|
if (self.doRequest(method, url, body, extra_headers)) |response| {
|
||||||
return classifyResponse(response);
|
return classifyResponse(response);
|
||||||
} else |_| {
|
} else |_| {
|
||||||
if (attempt >= self.max_retries) return HttpError.RequestFailed;
|
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);
|
var aw: std.Io.Writer.Allocating = .init(self.allocator);
|
||||||
|
|
||||||
const result = self.http_client.fetch(.{
|
const result = self.http_client.fetch(.{
|
||||||
.location = .{ .url = url },
|
.location = .{ .url = url },
|
||||||
|
.method = method,
|
||||||
|
.payload = body,
|
||||||
|
.extra_headers = extra_headers,
|
||||||
.response_writer = &aw.writer,
|
.response_writer = &aw.writer,
|
||||||
}) catch |err| {
|
}) catch |err| {
|
||||||
aw.deinit();
|
aw.deinit();
|
||||||
// TLS 1.2-only hosts (e.g., finnhub.io) fail with Zig's TLS 1.3-only client.
|
// 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.
|
// Fall back to system curl for these cases.
|
||||||
if (err == error.TlsInitializationFailed) {
|
if (err == error.TlsInitializationFailed) {
|
||||||
return curlGet(self.allocator, url);
|
return curlRequest(self.allocator, method, url, body, extra_headers);
|
||||||
}
|
}
|
||||||
return HttpError.RequestFailed;
|
return HttpError.RequestFailed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const body = aw.toOwnedSlice() catch {
|
const resp_body = aw.toOwnedSlice() catch {
|
||||||
aw.deinit();
|
aw.deinit();
|
||||||
return HttpError.OutOfMemory;
|
return HttpError.OutOfMemory;
|
||||||
};
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.status = result.status,
|
.status = result.status,
|
||||||
.body = body,
|
.body = resp_body,
|
||||||
.allocator = self.allocator,
|
.allocator = self.allocator,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -92,11 +104,48 @@ pub const Client = struct {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Fallback HTTP GET using system curl for TLS 1.2 hosts.
|
/// Fallback HTTP request using system curl for TLS 1.2 hosts.
|
||||||
fn curlGet(allocator: std.mem.Allocator, url: []const u8) HttpError!Response {
|
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(.{
|
const result = std.process.Child.run(.{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.argv = &.{ "curl", "-sS", "-f", "-L", "--max-time", "30", url },
|
.argv = argv.items,
|
||||||
.max_output_bytes = 10 * 1024 * 1024,
|
.max_output_bytes = 10 * 1024 * 1024,
|
||||||
}) catch return HttpError.RequestFailed;
|
}) catch return HttpError.RequestFailed;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
//! classes). For those, use the `ticker::` alias field in the portfolio SRF file.
|
//! classes). For those, use the `ticker::` alias field in the portfolio SRF file.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const http = @import("../net/http.zig");
|
||||||
|
|
||||||
const api_url = "https://api.openfigi.com/v3/mapping";
|
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.
|
/// 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(
|
pub fn lookupCusip(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
cusip: []const u8,
|
cusip: []const u8,
|
||||||
|
|
@ -70,99 +71,26 @@ pub fn lookupCusips(
|
||||||
}
|
}
|
||||||
try body_buf.append(allocator, ']');
|
try body_buf.append(allocator, ']');
|
||||||
|
|
||||||
const body_str = body_buf.items;
|
// Build headers
|
||||||
|
var headers_buf: [2]std.http.Header = undefined;
|
||||||
// Build curl command
|
var n_headers: usize = 0;
|
||||||
var argv_buf: [12][]const u8 = undefined;
|
headers_buf[n_headers] = .{ .name = "Content-Type", .value = "application/json" };
|
||||||
var argc: usize = 0;
|
n_headers += 1;
|
||||||
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;
|
|
||||||
if (api_key) |key| {
|
if (api_key) |key| {
|
||||||
// Build header string: "X-OPENFIGI-APIKEY: <key>"
|
headers_buf[n_headers] = .{ .name = "X-OPENFIGI-APIKEY", .value = key };
|
||||||
const hdr = try std.fmt.allocPrint(allocator, "X-OPENFIGI-APIKEY: {s}", .{key});
|
n_headers += 1;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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(.{
|
var client = http.Client.init(allocator);
|
||||||
.allocator = allocator,
|
defer client.deinit();
|
||||||
.argv = argv_list.items,
|
|
||||||
.max_output_bytes = 1 * 1024 * 1024,
|
|
||||||
}) catch return error.RequestFailed;
|
|
||||||
defer allocator.free(result.stderr);
|
|
||||||
|
|
||||||
const success = switch (result.term) {
|
var response = try client.post(api_url, body_buf.items, headers_buf[0..n_headers]);
|
||||||
.Exited => |code| code == 0,
|
defer response.deinit();
|
||||||
else => false,
|
|
||||||
};
|
return parseResponse(allocator, response.body, cusips.len);
|
||||||
if (!success) {
|
|
||||||
allocator.free(result.stdout);
|
|
||||||
return error.RequestFailed;
|
|
||||||
}
|
|
||||||
return parseResponse(allocator, result.stdout, cusips.len);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseResponse(allocator: std.mem.Allocator, body: []const u8, expected_count: usize) ![]FigiResult {
|
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
|
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
|
||||||
return error.ParseError;
|
return error.ParseError;
|
||||||
defer parsed.deinit();
|
defer parsed.deinit();
|
||||||
|
|
@ -246,8 +174,7 @@ fn parseResponse(allocator: std.mem.Allocator, body: []const u8, expected_count:
|
||||||
test "parseResponse basic single CUSIP" {
|
test "parseResponse basic single CUSIP" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
// parseResponse takes ownership of body (frees it), so we must dupe
|
const body =
|
||||||
const body = try allocator.dupe(u8,
|
|
||||||
\\[
|
\\[
|
||||||
\\ {
|
\\ {
|
||||||
\\ "data": [
|
\\ "data": [
|
||||||
|
|
@ -255,7 +182,7 @@ test "parseResponse basic single CUSIP" {
|
||||||
\\ ]
|
\\ ]
|
||||||
\\ }
|
\\ }
|
||||||
\\]
|
\\]
|
||||||
);
|
;
|
||||||
|
|
||||||
const results = try parseResponse(allocator, body, 1);
|
const results = try parseResponse(allocator, body, 1);
|
||||||
defer {
|
defer {
|
||||||
|
|
@ -277,7 +204,7 @@ test "parseResponse basic single CUSIP" {
|
||||||
test "parseResponse prefers US exchange" {
|
test "parseResponse prefers US exchange" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
const body = try allocator.dupe(u8,
|
const body =
|
||||||
\\[
|
\\[
|
||||||
\\ {
|
\\ {
|
||||||
\\ "data": [
|
\\ "data": [
|
||||||
|
|
@ -286,7 +213,7 @@ test "parseResponse prefers US exchange" {
|
||||||
\\ ]
|
\\ ]
|
||||||
\\ }
|
\\ }
|
||||||
\\]
|
\\]
|
||||||
);
|
;
|
||||||
|
|
||||||
const results = try parseResponse(allocator, body, 1);
|
const results = try parseResponse(allocator, body, 1);
|
||||||
defer {
|
defer {
|
||||||
|
|
@ -304,13 +231,13 @@ test "parseResponse prefers US exchange" {
|
||||||
test "parseResponse warning (no match)" {
|
test "parseResponse warning (no match)" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
const body = try allocator.dupe(u8,
|
const body =
|
||||||
\\[
|
\\[
|
||||||
\\ {
|
\\ {
|
||||||
\\ "warning": "No identifier found."
|
\\ "warning": "No identifier found."
|
||||||
\\ }
|
\\ }
|
||||||
\\]
|
\\]
|
||||||
);
|
;
|
||||||
|
|
||||||
const results = try parseResponse(allocator, body, 1);
|
const results = try parseResponse(allocator, body, 1);
|
||||||
defer allocator.free(results);
|
defer allocator.free(results);
|
||||||
|
|
@ -323,7 +250,7 @@ test "parseResponse warning (no match)" {
|
||||||
test "parseResponse multiple CUSIPs" {
|
test "parseResponse multiple CUSIPs" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
const body = try allocator.dupe(u8,
|
const body =
|
||||||
\\[
|
\\[
|
||||||
\\ {
|
\\ {
|
||||||
\\ "data": [
|
\\ "data": [
|
||||||
|
|
@ -339,7 +266,7 @@ test "parseResponse multiple CUSIPs" {
|
||||||
\\ ]
|
\\ ]
|
||||||
\\ }
|
\\ }
|
||||||
\\]
|
\\]
|
||||||
);
|
;
|
||||||
|
|
||||||
const results = try parseResponse(allocator, body, 3);
|
const results = try parseResponse(allocator, body, 3);
|
||||||
defer {
|
defer {
|
||||||
|
|
@ -369,13 +296,13 @@ test "parseResponse multiple CUSIPs" {
|
||||||
test "parseResponse empty data array" {
|
test "parseResponse empty data array" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
const body = try allocator.dupe(u8,
|
const body =
|
||||||
\\[
|
\\[
|
||||||
\\ {
|
\\ {
|
||||||
\\ "data": []
|
\\ "data": []
|
||||||
\\ }
|
\\ }
|
||||||
\\]
|
\\]
|
||||||
);
|
;
|
||||||
|
|
||||||
const results = try parseResponse(allocator, body, 1);
|
const results = try parseResponse(allocator, body, 1);
|
||||||
defer allocator.free(results);
|
defer allocator.free(results);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue