Skip to content

Commit 5ae58f3

Browse files
nilslicezshipko
andauthored
feat: support reading http response headers (#39)
* 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]>
1 parent 9e1b4ed commit 5ae58f3

File tree

5 files changed

+105
-1
lines changed

5 files changed

+105
-1
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,18 @@ jobs:
1414

1515
steps:
1616
- uses: actions/checkout@v3
17-
- uses: ./.github/actions/libextism
17+
# - uses: ./.github/actions/libextism
18+
- name: Install Extism CLI
19+
shell: sh
20+
run: sudo curl -s https://get.extism.org/cli | sh -s -- -q -y
21+
22+
- name: Check Extism version
23+
run: extism --version
1824

1925
- name: Install Zig
2026
uses: goto-bus-stop/setup-zig@v2
27+
with:
28+
version: 0.13.0
2129

2230
- name: Check Zig Version
2331
run: zig version
@@ -69,3 +77,5 @@ jobs:
6977
COUNT=$(echo $TEST | jq | grep "I'm the inner struct" | wc -l)
7078
test $COUNT -eq 3
7179
80+
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)
81+
echo $TEST | grep "text/html"

examples/basic.zig

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,35 @@ export fn http_get() i32 {
163163
return 0;
164164
}
165165

166+
export fn http_headers() i32 {
167+
const plugin = Plugin.init(allocator);
168+
169+
var req = http.HttpRequest.init("GET", "https://github.com");
170+
defer req.deinit(allocator);
171+
172+
const res = plugin.request(req, null) catch unreachable;
173+
defer res.deinit();
174+
175+
if (res.status != 200) {
176+
plugin.setError("request failed");
177+
return @as(i32, res.status);
178+
}
179+
var headers = res.headers(plugin.allocator) catch |err| {
180+
plugin.setErrorFmt("err: {any}, failed to get headers from response!", .{err}) catch unreachable;
181+
return -1;
182+
};
183+
defer headers.deinit();
184+
185+
const content_type = headers.get("content-type");
186+
if (content_type) |t| {
187+
plugin.logFmt(.Debug, "got content-type: {s}", .{t.value}) catch unreachable;
188+
} else {
189+
return 1;
190+
}
191+
192+
return 0;
193+
}
194+
166195
export fn greet() i32 {
167196
const plugin = Plugin.init(allocator);
168197
const user = plugin.getConfig("user") catch unreachable orelse {

src/ffi.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub extern "extism:host/env" fn store_u64(ExtismPointer, u64) void;
1717
pub extern "extism:host/env" fn load_u64(ExtismPointer) u64;
1818
pub extern "extism:host/env" fn http_request(ExtismPointer, ExtismPointer) ExtismPointer;
1919
pub extern "extism:host/env" fn http_status_code() i32;
20+
pub extern "extism:host/env" fn http_headers() ExtismPointer;
2021
pub extern "extism:host/env" fn get_log_level() i32;
2122
pub extern "extism:host/env" fn log_trace(ExtismPointer) void;
2223
pub extern "extism:host/env" fn log_debug(ExtismPointer) void;

src/http.zig

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
11
const std = @import("std");
22
const Memory = @import("Memory.zig");
3+
const extism = @import("ffi.zig");
4+
5+
pub const Headers = struct {
6+
allocator: std.mem.Allocator,
7+
raw: []const u8,
8+
internal: std.json.ArrayHashMap([]const u8),
9+
10+
/// Get a value (if it exists) from the Headers map at the provided name.
11+
/// NOTE: this may be a multi-value header, and will be a comma-separated list.
12+
pub fn get(self: Headers, name: []const u8) ?std.http.Header {
13+
const val = self.internal.map.get(name);
14+
if (val) |v| {
15+
return std.http.Header{
16+
.name = name,
17+
.value = v,
18+
};
19+
} else {
20+
return null;
21+
}
22+
}
23+
24+
/// Access the internal data to iterate over or mutate as needed.
25+
pub fn internal(self: Headers) std.json.ArrayHashMap([]const u8) {
26+
return self.internal;
27+
}
28+
29+
/// Check if the Headers is empty.
30+
pub fn isEmpty(self: Headers) bool {
31+
return self.internal.map.entries.len == 0;
32+
}
33+
34+
/// Check if a header exists in the Headers.
35+
pub fn contains(self: Headers, key: []const u8) bool {
36+
return self.internal.map.contains(key);
37+
}
38+
39+
pub fn deinit(self: *Headers) void {
40+
self.allocator.free(self.raw);
41+
self.internal.deinit(self.allocator);
42+
}
43+
};
344

445
pub const HttpResponse = struct {
546
memory: Memory,
647
status: u16,
48+
responseHeaders: Memory,
749

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

1658
pub fn deinit(self: HttpResponse) void {
1759
self.memory.free();
60+
self.responseHeaders.free();
1861
}
1962

2063
pub fn statusCode(self: HttpResponse) u16 {
2164
return self.status;
2265
}
66+
67+
/// IMPORTANT: it's the caller's responsibility to `deinit` the Headers if returned.
68+
pub fn headers(self: HttpResponse, allocator: std.mem.Allocator) !Headers {
69+
const data = try self.responseHeaders.loadAlloc(allocator);
70+
errdefer allocator.free(data);
71+
72+
const j = try std.json.parseFromSlice(std.json.ArrayHashMap([]const u8), allocator, data, .{ .allocate = .alloc_always, .ignore_unknown_fields = true });
73+
defer j.deinit();
74+
75+
return Headers{
76+
.allocator = allocator,
77+
.raw = data,
78+
.internal = j.value,
79+
};
80+
}
2381
};
2482

2583
pub const HttpRequest = struct {

src/main.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ pub const Plugin = struct {
143143

144144
pub fn logMemory(self: Plugin, level: LogLevel, memory: Memory) void {
145145
_ = self; // to make the interface consistent
146+
146147
switch (level) {
147148
.Trace => extism.log_trace(memory.offset),
148149
.Debug => extism.log_debug(memory.offset),
@@ -224,10 +225,15 @@ pub const Plugin = struct {
224225
const length = extism.length_unsafe(offset);
225226
const status: u16 = @intCast(extism.http_status_code());
226227

228+
const headersOffset = extism.http_headers();
229+
const headersLength = extism.length_unsafe(headersOffset);
230+
const headersMem = Memory.init(headersOffset, headersLength);
231+
227232
const mem = Memory.init(offset, length);
228233
return http.HttpResponse{
229234
.memory = mem,
230235
.status = status,
236+
.responseHeaders = headersMem,
231237
};
232238
}
233239
};

0 commit comments

Comments
 (0)