Skip to content

Commit 3d75311

Browse files
committed
server: restore HTTP API — migrate to std.Io.net + std.http.Server v2
From fleet code review 2026-06-01 (`incomplete-undefined`, CRITICAL). `codescan serve` had been returning `error.HttpServerNotMigrated` ever since the Zig 0.15→0.16 port; the placeholder commit added a graceful stderr message but the server was still dead. This commit replaces the placeholder with a real listen-accept-dispatch loop: - `address.listen(io, .{ .reuse_address = true })` per the 0.16 `std.Io.net.IpAddress` API; deferred close of the listening socket - accept loop with logged-and-continue on per-connection errors (so a malformed request doesn't kill the server) - per-connection 16 KB header + 16 KB write buffer (enough for typical requests; clients sending oversized heads get `error.HttpHeadersOversize`) - `std.http.Server.init(&reader.interface, &writer.interface)` for the v2 io-aware HTTP frontend - `receiveHead` → existing `handleRequest(allocator, &req, db, embedder, settings)` (which was already written against the v2 Request API) - handler errors logged with the request target; best-effort 500 response if nothing was sent yet Smoke-tested end-to-end against the codescan repo's own index: GET /health → 200 `{"status":"ok"}` GET /status → 200 (full index JSON: 101 files, 6402 symbols) GET /help → 200 (API docs) POST /search → 200 (lexical hits, correct ranking) PLAN.md: both HTTP server items checked off (Phase 4 + Phase 5b). All 51 test binaries still pass.
1 parent 23f8d44 commit 3d75311

2 files changed

Lines changed: 70 additions & 19 deletions

File tree

PLAN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
- [x] Define plugin interface + registry
3333
- [x] Implement Zig extractor (function spans + comments)
3434
- [x] Implement Elixir extractor (function spans + comments)
35-
- [ ] Restore HTTP server + endpoints (index/update/search/health) — code exists in `src/server.zig` but `serve()` returns `error.HttpServerNotMigrated` after the Zig 0.16 port; needs migration to `std.Io.net.IpAddress.listen` + `std.http.Server` v2. CLI surface advertises the command and now prints a clear redirect to `codescan search` / `codescan mcp-serve` until this lands. (Was checked in error — code shipped but never re-implemented after 0.16 port.)
35+
- [x] Restore HTTP server + endpoints (index/update/search/health) — DONE 2026-06-01. `server.serve()` now uses `std.Io.net.IpAddress.listen` + `std.http.Server` v2 (io-aware) per Zig 0.16. Accept-loop dispatches each connection through `handleRequest`, with per-connection 16 KB header/write buffers. Smoke-tested: `GET /health`, `GET /status`, `GET /help`, `POST /search` all return 200 with correct JSON/text from the codescan repo's own index.
3636
- [x] Add JSON output + human output formatting
3737
- [x] Wire main CLI (config merge, commands, .codescan setup)
3838
- [x] Add hybrid weight knobs (CLI/config/HTTP) + tests
@@ -140,7 +140,7 @@ Captured from the 9 dimension review notes in `inbox/`. Items that landed in thi
140140
- [ ] Decompose `fn search` (`src/search.zig` lines 94-406, 312 lines) — extract `runLexicalOnly`, `runVectorOnly`, `runHybrid` private fns; public `search` becomes ~30-line dispatcher. Each phase becomes independently testable. Reviewer: `disorganized` (WARN).
141141
- [ ] Extract stderr-writer boilerplate helper — pattern `var stderr_buf: [4096]u8 = undefined; var stderr_writer = std.Io.File.stderr().writer(io_singleton.getOrInit(), &stderr_buf); const stderr = &stderr_writer.interface;` appears in main.zig at lines 137, 159, 189, 212 and dozens elsewhere. Define `pub const STDERR_BUF_SIZE = 4096;` once and a `withStderr(comptime cb)` or `stderrWriter()` helper. Reviewer: `disorganized` (WARN).
142142
- [ ] Windows watcher-mgmt: surface "not supported on Windows" instead of empty-list/false silent return in `discoverWatchers`, `getActiveCwds`, `stopWatcher` (`src/watcher_mgmt.zig:121,146,164`). Either log a one-line warning before short-circuiting OR gate the commands at the CLI level with a clearer message. Reviewer: `incomplete-undefined` (WARN).
143-
- [ ] Restore HTTP server functionality — placeholder commit landed graceful error; the migration to `std.Io.net.IpAddress.listen` + `std.http.Server` v2 (io-aware API) still needs to happen. (Same item as PLAN.md:35 above.) Reviewer: `incomplete-undefined` (CRITICAL).
143+
- [x] Restore HTTP server functionality — DONE 2026-06-01. Migrated `serve()` to `std.Io.net.IpAddress.listen` + `std.http.Server` v2; smoke-tested end-to-end with curl. Reviewer: `incomplete-undefined` (CRITICAL).
144144
- [ ] Test coverage gaps for language extractors — `extract_lua.zig`, `extract_idris.zig`, `extract_nix.zig`, `extract_nim.zig`, `extract_haskell.zig`, `extract_lean.zig`, `extract_bash.zig`, `extract_text.zig`, `extract_log.zig` each have a single happy-path test. Establish a 6-test smoke matrix per extractor (function/method/no-doc/multi-doc/empty-file/UTF-8 identifier). Reviewer: `inadequate-tests` (WARN).
145145
- [ ] Enum-value stability test for `src/kind.zig` `Kind` enum — values persist to SQLite so reordering would silently break old indices. Add a snapshot assertion. Reviewer: `inadequate-tests` (WARN).
146146
- [ ] Strengthen 4 weak-assertion tests:

src/server.zig

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,24 +87,75 @@ pub fn serve(allocator: std.mem.Allocator, settings: Settings) !void {
8787
.dialect = settings.embedding_dialect,
8888
.auth_header = settings.embedding_auth_header,
8989
};
90+
// `embedder_adapter` must be `var` because `.embedder()` takes a mutable
91+
// pointer — and the value must outlive the listen loop.
92+
var embedder_adapter_mut = embedder_adapter;
93+
const embedder = embedder_adapter_mut.embedder();
94+
9095
const address = try parseAddress(settings.http_host, settings.http_port);
91-
_ = address;
92-
_ = embedder_adapter;
93-
// TODO(zig-0.16): migrate std.http.Server + std.net.Address.listen to the new
94-
// std.Io.net.IpAddress.listen + std.http.Server v2 (io-aware) API. The
95-
// refactor was deferred during the 0.15→0.16 port and has not landed.
96-
// Surface a clear user-facing message instead of an opaque error code.
97-
var stderr_buf: [4096]u8 = undefined;
98-
var stderr_writer = io_singleton.stderrWriter(&stderr_buf);
99-
const stderr = &stderr_writer.interface;
100-
_ = stderr.writeAll(
101-
"error: `codescan serve` is temporarily unavailable.\n" ++
102-
" The HTTP server is mid-migration to std.Io.net + std.http.Server v2 (Zig 0.16).\n" ++
103-
" Use `codescan search` for queries or `codescan mcp-serve` for the MCP/JSON-RPC stdio server.\n" ++
104-
" Track progress in PLAN.md (\"HTTP server\" — currently unchecked).\n",
105-
) catch {};
106-
_ = stderr.flush() catch {};
107-
return error.HttpServerNotMigrated;
96+
const io = io_singleton.getOrInit();
97+
98+
var net_server = address.listen(io, .{ .reuse_address = true }) catch |err| {
99+
var sb: [4096]u8 = undefined;
100+
var sw = io_singleton.stderrWriter(&sb);
101+
const stderr = &sw.interface;
102+
_ = stderr.print("error: could not listen on {s}:{d}: {s}\n", .{ settings.http_host, settings.http_port, @errorName(err) }) catch {};
103+
_ = stderr.flush() catch {};
104+
return err;
105+
};
106+
defer net_server.socket.close(io);
107+
108+
{
109+
var sb: [4096]u8 = undefined;
110+
var sw = io_singleton.stderrWriter(&sb);
111+
const stderr = &sw.interface;
112+
_ = stderr.print("codescan serve: listening on http://{s}:{d}/\n", .{ settings.http_host, settings.http_port }) catch {};
113+
_ = stderr.flush() catch {};
114+
}
115+
116+
// Per-connection buffers. The header buffer must be large enough to hold
117+
// any single client's entire request head — `std.http.Server.receiveHead`
118+
// returns `error.HttpHeadersOversize` if a client sends a bigger one.
119+
var read_buf: [16 * 1024]u8 = undefined;
120+
var write_buf: [16 * 1024]u8 = undefined;
121+
122+
while (true) {
123+
var stream = net_server.accept(io) catch |err| {
124+
var sb: [4096]u8 = undefined;
125+
var sw = io_singleton.stderrWriter(&sb);
126+
const stderr = &sw.interface;
127+
_ = stderr.print("codescan serve: accept error: {s}\n", .{@errorName(err)}) catch {};
128+
_ = stderr.flush() catch {};
129+
continue;
130+
};
131+
defer stream.close(io);
132+
133+
var stream_reader = stream.reader(io, &read_buf);
134+
var stream_writer = stream.writer(io, &write_buf);
135+
var http_server = std.http.Server.init(&stream_reader.interface, &stream_writer.interface);
136+
137+
var request = http_server.receiveHead() catch |err| switch (err) {
138+
error.HttpConnectionClosing => continue,
139+
else => {
140+
var sb: [4096]u8 = undefined;
141+
var sw = io_singleton.stderrWriter(&sb);
142+
const stderr = &sw.interface;
143+
_ = stderr.print("codescan serve: receiveHead error: {s}\n", .{@errorName(err)}) catch {};
144+
_ = stderr.flush() catch {};
145+
continue;
146+
},
147+
};
148+
149+
handleRequest(allocator, &request, db, embedder, settings) catch |err| {
150+
var sb: [4096]u8 = undefined;
151+
var sw = io_singleton.stderrWriter(&sb);
152+
const stderr = &sw.interface;
153+
_ = stderr.print("codescan serve: handler error for {s}: {s}\n", .{ request.head.target, @errorName(err) }) catch {};
154+
_ = stderr.flush() catch {};
155+
// Best-effort 500 if nothing was sent yet.
156+
request.respond("internal server error\n", .{ .status = .internal_server_error }) catch {};
157+
};
158+
}
108159
}
109160

110161
fn ensureModelAvailableOrExit(

0 commit comments

Comments
 (0)