Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support reading http response headers #39

Merged
merged 10 commits into from
Nov 22, 2024
Merged
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@ jobs:

steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/libextism
# - uses: ./.github/actions/libextism
- name: Install Extism CLI
shell: sh
run: sudo curl -s https://get.extism.org/cli | sh -s -- -q -y

- name: Check Extism version
run: extism --version

- name: Install Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.13.0

- name: Check Zig Version
run: zig version
Expand Down Expand Up @@ -69,3 +77,5 @@ jobs:
COUNT=$(echo $TEST | jq | grep "I'm the inner struct" | wc -l)
test $COUNT -eq 3

TEST=$(extism call zig-out/bin/basic-example.wasm http_headers --input '' --allow-host github.com --enable-http-response-headers --log-level debug 2>&1)
echo $TEST | grep "text/html"
29 changes: 29 additions & 0 deletions examples/basic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,35 @@ export fn http_get() i32 {
return 0;
}

export fn http_headers() i32 {
const plugin = Plugin.init(allocator);

var req = http.HttpRequest.init("GET", "https://github.com");
defer req.deinit(allocator);

const res = plugin.request(req, null) catch unreachable;
defer res.deinit();

if (res.status != 200) {
plugin.setError("request failed");
return @as(i32, res.status);
}
var headers = res.headers(plugin.allocator) catch |err| {
plugin.setErrorFmt("err: {any}, failed to get headers from response!", .{err}) catch unreachable;
return -1;
};
defer headers.deinit();

const content_type = headers.get("content-type");
if (content_type) |t| {
plugin.logFmt(.Debug, "got content-type: {s}", .{t.value}) catch unreachable;
} else {
return 1;
}

return 0;
}

export fn greet() i32 {
const plugin = Plugin.init(allocator);
const user = plugin.getConfig("user") catch unreachable orelse {
Expand Down
1 change: 1 addition & 0 deletions src/ffi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub extern "extism:host/env" fn store_u64(ExtismPointer, u64) void;
pub extern "extism:host/env" fn load_u64(ExtismPointer) u64;
pub extern "extism:host/env" fn http_request(ExtismPointer, ExtismPointer) ExtismPointer;
pub extern "extism:host/env" fn http_status_code() i32;
pub extern "extism:host/env" fn http_headers() ExtismPointer;
pub extern "extism:host/env" fn get_log_level() i32;
pub extern "extism:host/env" fn log_trace(ExtismPointer) void;
pub extern "extism:host/env" fn log_debug(ExtismPointer) void;
Expand Down
58 changes: 58 additions & 0 deletions src/http.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
const std = @import("std");
const Memory = @import("Memory.zig");
const extism = @import("ffi.zig");

pub const Headers = struct {
allocator: std.mem.Allocator,
raw: []const u8,
internal: std.json.ArrayHashMap([]const u8),

/// Get a value (if it exists) from the Headers map at the provided name.
/// NOTE: this may be a multi-value header, and will be a comma-separated list.
pub fn get(self: Headers, name: []const u8) ?std.http.Header {
const val = self.internal.map.get(name);
if (val) |v| {
return std.http.Header{
.name = name,
.value = v,
};
} else {
return null;
}
}

/// Access the internal data to iterate over or mutate as needed.
pub fn internal(self: Headers) std.json.ArrayHashMap([]const u8) {
return self.internal;
}

/// Check if the Headers is empty.
pub fn isEmpty(self: Headers) bool {
return self.internal.map.entries.len == 0;
}

/// Check if a header exists in the Headers.
pub fn contains(self: Headers, key: []const u8) bool {
return self.internal.map.contains(key);
}

pub fn deinit(self: *Headers) void {
self.allocator.free(self.raw);
self.internal.deinit(self.allocator);
}
};

pub const HttpResponse = struct {
memory: Memory,
status: u16,
responseHeaders: Memory,

/// IMPORTANT: it's the caller's responsibility to free the returned string
pub fn body(self: HttpResponse, allocator: std.mem.Allocator) ![]u8 {
Expand All @@ -15,11 +57,27 @@ pub const HttpResponse = struct {

pub fn deinit(self: HttpResponse) void {
self.memory.free();
self.responseHeaders.free();
}

pub fn statusCode(self: HttpResponse) u16 {
return self.status;
}

/// IMPORTANT: it's the caller's responsibility to `deinit` the Headers if returned.
pub fn headers(self: HttpResponse, allocator: std.mem.Allocator) !Headers {
const data = try self.responseHeaders.loadAlloc(allocator);
errdefer allocator.free(data);

const j = try std.json.parseFromSlice(std.json.ArrayHashMap([]const u8), allocator, data, .{ .allocate = .alloc_always, .ignore_unknown_fields = true });
defer j.deinit();

return Headers{
.allocator = allocator,
.raw = data,
.internal = j.value,
};
}
};

pub const HttpRequest = struct {
Expand Down
6 changes: 6 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ pub const Plugin = struct {

pub fn logMemory(self: Plugin, level: LogLevel, memory: Memory) void {
_ = self; // to make the interface consistent

switch (level) {
.Trace => extism.log_trace(memory.offset),
.Debug => extism.log_debug(memory.offset),
Expand Down Expand Up @@ -224,10 +225,15 @@ pub const Plugin = struct {
const length = extism.length_unsafe(offset);
const status: u16 = @intCast(extism.http_status_code());

const headersOffset = extism.http_headers();
const headersLength = extism.length_unsafe(headersOffset);
const headersMem = Memory.init(headersOffset, headersLength);

const mem = Memory.init(offset, length);
return http.HttpResponse{
.memory = mem,
.status = status,
.responseHeaders = headersMem,
};
}
};
Expand Down
Loading