//! HTTP response helpers and query string parsing. //! //! pure utility module — no domain dependencies. used by all API handler modules //! to write raw HTTP responses to websocket connections and parse query parameters. const std = @import("std"); const http = std.http; const websocket = @import("websocket"); pub const Conn = websocket.Conn; pub fn httpRespond(conn: *Conn, status: http.Status, content_type: []const u8, body: []const u8) void { var buf: [512]u8 = undefined; const header = std.fmt.bufPrint(&buf, "HTTP/1.1 {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\nConnection: close\r\nServer: zlay\r\n\r\n", .{ httpStatusLine(status), content_type, body.len, }) catch return; conn.writeFramed(header) catch return; if (body.len > 0) conn.writeFramed(body) catch return; } pub fn respondJson(conn: *Conn, status: http.Status, body: []const u8) void { httpRespond(conn, status, "application/json", body); } pub fn respondText(conn: *Conn, status: http.Status, body: []const u8) void { httpRespond(conn, status, "text/plain", body); } pub fn httpStatusLine(status: http.Status) []const u8 { return switch (status) { .ok => "200 OK", .bad_request => "400 Bad Request", .unauthorized => "401 Unauthorized", .forbidden => "403 Forbidden", .not_found => "404 Not Found", .method_not_allowed => "405 Method Not Allowed", .conflict => "409 Conflict", .internal_server_error => "500 Internal Server Error", else => "500 Internal Server Error", }; } // --- query string helpers --- pub fn queryParam(query: []const u8, name: []const u8) ?[]const u8 { if (query.len == 0) return null; var iter = std.mem.splitScalar(u8, query, '&'); while (iter.next()) |pair| { const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue; if (std.mem.eql(u8, pair[0..eq], name)) { return pair[eq + 1 ..]; } } return null; } /// like queryParam but percent-decodes the value into buf. /// returns null if the param is missing, or a slice into buf with the decoded value. pub fn queryParamDecoded(query: []const u8, name: []const u8, buf: []u8) ?[]const u8 { const raw = queryParam(query, name) orelse return null; var i: usize = 0; var out: usize = 0; while (i < raw.len) { if (raw[i] == '%' and i + 2 < raw.len) { const hi = hexVal(raw[i + 1]) orelse { if (out >= buf.len) return null; buf[out] = raw[i]; out += 1; i += 1; continue; }; const lo = hexVal(raw[i + 2]) orelse { if (out >= buf.len) return null; buf[out] = raw[i]; out += 1; i += 1; continue; }; if (out >= buf.len) return null; buf[out] = (@as(u8, hi) << 4) | @as(u8, lo); out += 1; i += 3; } else if (raw[i] == '+') { if (out >= buf.len) return null; buf[out] = ' '; out += 1; i += 1; } else { if (out >= buf.len) return null; buf[out] = raw[i]; out += 1; i += 1; } } return buf[0..out]; } fn hexVal(c: u8) ?u4 { return switch (c) { '0'...'9' => @intCast(c - '0'), 'a'...'f' => @intCast(c - 'a' + 10), 'A'...'F' => @intCast(c - 'A' + 10), else => null, }; }