Skip to content

Commit

Permalink
feat: support reading http response headers (#39)
Browse files Browse the repository at this point in the history
* wip: http response headers

* feat: add response header support

* chore: use headers in an example to test

* ci: test http response headers

* fix: merge conflict

* ci: pin zig version to 0.13.0

* chore: uncomment some example code

* ci: install extism cli from release

* chore: update error to include the value

* chore: try different host for testing

---------

Co-authored-by: zach <[email protected]>
  • Loading branch information
nilslice and zshipko authored Nov 22, 2024
1 parent 9e1b4ed commit 5ae58f3
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 1 deletion.
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

0 comments on commit 5ae58f3

Please sign in to comment.