╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │100 var conn_bw = stream.writer(io, &send_buffer); │ │101 var http_server = std.http.Server.init(&conn_br.interface, &conn_bw.interface); │ │102 │ │103 + // serve multiple requests on a single connection so HTTP/1.1 keep-alive │ │104 + // works — important for browser-native form POST: after the 303 the │ │105 + // browser issues GET / on the same socket, and forcibly closing here │ │106 + // would race the follow-up request into a "site can't be reached". │ │107 while (http_server.reader.state == .ready) { │ │108 var request = http_server.receiveHead() catch |receive_err| switch (receive_err) {│ │109 error.HttpConnectionClosing => break, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │111 else => |e| return e, │ │112 }; │ │113 │ │110 - handleRequest(io, &http_server, &request, allocator, admin_repo_path) catch |request_err| { │ │114 + handleRequest(io, &request, allocator, admin_repo_path) catch |request_err| { │ │115 try err.print("web ui request failed: {s}\n", .{@errorName(request_err)}); │ │116 try err.flush(); │ │113 - if (http_server.reader.state == .received_head) { │ │114 - http_server.reader.state = .ready; │ │115 - } │ │116 - try writeSimpleResponse(&http_server, 500, "Internal Server Error", "text/plain", @errorName(request_err));│ │117 + // best-effort 500. if handleRequest already started writing a │ │118 + // response before throwing, this respond may fail too, but at │ │119 + // that point the connection is unrecoverable anyway. │ │120 + request.respond(@errorName(request_err), .{ │ │121 + .status = .internal_server_error, │ │122 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │123 + }) catch {}; │ │124 }; │ │118 - try http_server.out.flush(); │ │119 - break; │ │125 } │ │126 } │ │127 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │128 fn handleRequest( │ │129 io: std.Io, │ │125 - http_server: *std.http.Server, │ │130 request: *std.http.Server.Request,│ │131 allocator: std.mem.Allocator, │ │132 admin_repo_path: []const u8, │ ╰───────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │136 const path = uri.path.percent_encoded; │ │137 │ │138 if (method == .POST and std.mem.eql(u8, path, "/login")) { │ │135 - return handleLogin(io, http_server, request, allocator, admin_repo_path);│ │139 + return handleLogin(io, request, allocator, admin_repo_path); │ │140 } │ │141 if (method == .POST and std.mem.eql(u8, path, "/logout")) { │ │138 - return handleLogout(http_server, request); │ │142 + return handleLogout(request); │ │143 } │ │144 │ │145 const get_or_head = method == .GET or method == .HEAD; │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │146 if (!get_or_head) { │ │143 - if (http_server.reader.state == .received_head) { │ │144 - http_server.reader.state = .ready; │ │145 - } │ │146 - try writeSimpleResponse(http_server, 405, "Method Not Allowed", "text/plain", "method not allowed");│ │147 + try request.respond("method not allowed", .{ │ │148 + .status = .method_not_allowed, │ │149 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │150 + }); │ │151 return; │ │152 } │ │153 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │154 if (std.mem.eql(u8, path, "/favicon.ico")) { │ │151 - if (http_server.reader.state == .received_head) { │ │152 - http_server.reader.state = .ready; │ │153 - } │ │154 - try writeStaticResponse(http_server, 204, "No Content", "image/x-icon", "", true);│ │155 + try request.respond("", .{ │ │156 + .status = .no_content, │ │157 + .extra_headers = &.{.{ .name = "content-type", .value = "image/x-icon" }}, │ │158 + }); │ │159 return; │ │160 } │ │161 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │158 - // for the index page we need to consult the session cookie before │ │159 - // consuming the request, so we don't dispatch through findEmbed here. │ │162 if (std.mem.eql(u8, path, "/") or std.mem.eql(u8, path, "/index.html")) {│ │163 const user_id_hex = getCookieValue(request, cookie_name); │ │164 const login_failure: ?ui.Home.Auth.Login.Failure = │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │171 null │ │172 else │ │173 null; │ │172 - if (http_server.reader.state == .received_head) { │ │173 - http_server.reader.state = .ready; │ │174 - } │ │174 const html = try renderIndexHtml(io, allocator, admin_repo_path, user_id_hex, login_failure); │ │175 defer allocator.free(html); │ │177 - // expire the flash cookie on the way out — it's been consumed by │ │178 - // this render and shouldn't survive a refresh. │ │179 - const clear_flash: []const u8 = if (login_failure != null) │ │180 - "Set-Cookie: " ++ login_failure_cookie ++ "=; Path=/; Max-Age=0\r\n" │ │181 - else │ │182 - ""; │ │183 - try http_server.out.print( │ │184 - "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n{s}Content-Length: {d}\r\n\r\n", │ │185 - .{ clear_flash, html.len }, │ │186 - ); │ │187 - if (method != .HEAD) try http_server.out.writeAll(html); │ │176 + // expire the flash cookie on the way out so a refresh doesn't keep │ │177 + // showing the failure label. │ │178 + var headers: [2]std.http.Header = undefined; │ │179 + headers[0] = .{ .name = "content-type", .value = "text/html; charset=utf-8" }; │ │180 + var headers_slice: []const std.http.Header = headers[0..1]; │ │181 + if (login_failure != null) { │ │182 + headers[1] = .{ .name = "set-cookie", .value = login_failure_cookie ++ "=; Path=/; Max-Age=0" };│ │183 + headers_slice = headers[0..2]; │ │184 + } │ │185 + try request.respond(html, .{ .extra_headers = headers_slice }); │ │186 return; │ │187 } │ │188 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │189 const embed = findEmbed(path) orelse { │ │192 - if (http_server.reader.state == .received_head) { │ │193 - http_server.reader.state = .ready; │ │194 - } │ │195 - try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found");│ │190 + try request.respond("not found", .{ │ │191 + .status = .not_found, │ │192 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │193 + }); │ │194 return; │ │195 }; │ │196 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │199 - if (http_server.reader.state == .received_head) { │ │200 - http_server.reader.state = .ready; │ │201 - } │ │202 - try writeStaticResponse(http_server, 200, "OK", embed.content_type, embed.body, method == .HEAD);│ │197 + try request.respond(embed.body, .{ │ │198 + .extra_headers = &.{.{ .name = "content-type", .value = embed.content_type }}, │ │199 + }); │ │200 } │ │201 │ │202 fn handleLogin( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯