Merge pull request #121 from jetzig-framework/debug-console

Debug console
This commit is contained in:
bobf 2024-11-23 20:12:34 +00:00 committed by GitHub
commit 835885a947
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 577 additions and 64 deletions

View File

@ -15,8 +15,6 @@ jobs:
build:
strategy:
matrix:
#Deactivated windows for I don't know why it fails
#os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:

21
LICENSE-Zig Normal file
View File

@ -0,0 +1,21 @@
The MIT License (Expat)
Copyright (c) Zig contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -110,6 +110,7 @@ pub fn build(b: *std.Build) !void {
const test_build_options = b.addOptions();
test_build_options.addOption(Environment, "environment", .testing);
test_build_options.addOption(bool, "build_static", true);
test_build_options.addOption(bool, "debug_console", false);
const run_main_tests = b.addRunArtifact(main_tests);
main_tests.root_module.addOptions("build_options", test_build_options);
@ -138,12 +139,24 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
"environment",
"Jetzig server environment.",
) orelse .development;
const build_static = b.option(
bool,
"build_static",
"Pre-render static routes. [default: false in development, true in testing/production]",
) orelse (environment != .development);
const debug_console = b.option(
bool,
"debug_console",
"Render a debug console on error. Default: false.",
) orelse false;
if (debug_console == true and environment != .development) {
std.debug.print("Environment must be `development` when `debug_console` is enabled.", .{});
return error.JetzigBuildError;
}
const jetzig_dep = b.dependency(
"jetzig",
.{
@ -171,6 +184,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
const build_options = b.addOptions();
build_options.addOption(Environment, "environment", environment);
build_options.addOption(bool, "build_static", build_static);
build_options.addOption(bool, "debug_console", debug_console);
jetzig_module.addOptions("build_options", build_options);
exe.root_module.addImport("jetzig", jetzig_module);

View File

@ -9,22 +9,20 @@ pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000;
/// Command line options for the `server` command.
pub const Options = struct {
reload: bool = true,
debug: bool = true,
pub const meta = .{
.full_text =
\\Launches a development server.
\\
\\The development server reloads when files in `src/` are updated.
\\
\\To disable this behaviour, pass `--reload=false`
\\
\\Example:
\\
\\ jetzig server
\\ jetzig server --reload=false
\\ jetzig server --reload=false --debug=false
,
.option_docs = .{
.reload = "Enable or disable automatic reload on update (default: true)",
.debug = "Enable or disable the development debug console (default: true)",
},
};
};
@ -62,18 +60,27 @@ pub fn run(
},
);
var argv = std.ArrayList([]const u8).init(allocator);
defer argv.deinit();
try argv.appendSlice(&.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"-Djetzig_runner=true",
});
if (options.debug) try argv.append("-Ddebug_console=true");
try argv.appendSlice(&.{
"install",
"--color",
"on",
});
while (true) {
util.runCommandInDir(
allocator,
&.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"-Djetzig_runner=true",
"install",
"--color",
"on",
},
argv.items,
.{ .path = realpath },
) catch {
std.debug.print("Build failed, waiting for file change...\n", .{});
@ -88,10 +95,9 @@ pub fn run(
std.process.exit(1);
}
const argv = &[_][]const u8{exe_path.?};
defer allocator.free(exe_path.?);
var process = std.process.Child.init(argv, allocator);
var process = std.process.Child.init(&.{exe_path.?}, allocator);
process.stdin_behavior = .Inherit;
process.stdout_behavior = .Inherit;
process.stderr_behavior = .Inherit;

View File

@ -1,7 +1,12 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub const layout = "application";
// Anti-CSRF middleware can be included in the view's `actions` declaration to apply CSRF
// protection just to this specific view, or it can be added to your application's global
// middleware stack defined in `jetzig_options` in `src/main.zig`.
//
// Use `{{context.authenticityToken()}}` or `{{context.authenticityFormField()}}` in a Zmpl
// template to generate a token, store it in the user's session, and inject it into the page.
pub const actions = .{
.before = .{jetzig.middleware.AntiCsrfMiddleware},

View File

@ -6,3 +6,5 @@
<input type="submit" value="Submit Spam" />
</form>
<div>Try clearing `_jetzig_session` cookie before clicking "Submit Spam"</div>

View File

@ -0,0 +1,15 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.renderTemplate("basic/index", .ok);
}
test "index" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/render_template", .{});
try response.expectStatus(.ok);
try response.expectBodyContains("Hello");
}

View File

@ -0,0 +1,3 @@
<div>
<span>Content goes here</span>
</div>

View File

@ -90,6 +90,7 @@ pub const jetzig_options = struct {
.same_site = true,
.secure = true,
.http_only = true,
.path = "/",
},
};

121
src/assets/debug.css Normal file
View File

@ -0,0 +1,121 @@
/* reset https://www.joshwcomeau.com/css/custom-css-reset/ */
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
}
h1, h2, h3, h4, h5, h6 {
text-wrap: balance;
}
#root, #__next {
isolation: isolate;
}
/* styles */
body {
background-color: #222;
}
h1 {
font-family: monospace;
color: #e55;
padding: 1rem;
font-size: 1.6rem;
}
h2 {
font-family: monospace;
color: #a0a0a0;
padding: 1rem;
font-size: 1.6rem;
}
.stack-trace {
/* background-color: #e555; */
padding: 1rem;
font-family: monospace;
}
.stack-trace .stack-source-line .file-name {
color: #90bfd7;
display: block;
padding: 0.4rem;
font-weight: bold;
}
.stack-trace .stack-source-line {
background-color: #333;
padding: 1rem;
margin: 0;
border-bottom: 1px solid #ffa3;
}
.stack-trace .stack-source-line:last-child {
border-bottom: none;
}
.stack-trace .stack-source-line .line-content {
margin: 0;
padding: 0;
}
.stack-trace .stack-source-line .line-content.surrounding {
color: #ffa;
}
.stack-trace .stack-source-line .line-content.target {
color: #faa;
background-color: #e552;
}
.stack-trace .stack-source-line .line-content .line-number {
display: inline;
}
pre {
display: inline;
margin-right: 1rem;
}
.response-data {
color: #fff;
background-color: #333;
padding: 1rem;
margin: 1rem;
}
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism-tomorrow&languages=json+zig */
code[class*=language-],pre[class*=language-]{color:#ccc;font-family:monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]:not(pre)>code[class*=language-]{white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
.stack-trace .stack-source-line .line-number {
color: #ffa !important;
}

5
src/assets/debug.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -23,6 +23,7 @@ pub const testing = @import("jetzig/testing.zig");
pub const config = @import("jetzig/config.zig");
pub const auth = @import("jetzig/auth.zig");
pub const callbacks = @import("jetzig/callbacks.zig");
pub const debug = @import("jetzig/debug.zig");
pub const TemplateContext = @import("jetzig/TemplateContext.zig");
pub const DateTime = jetcommon.types.DateTime;

218
src/jetzig/debug.zig Normal file
View File

@ -0,0 +1,218 @@
const std = @import("std");
const builtin = @import("builtin");
pub const ErrorInfo = struct {
stack_trace: ?*std.builtin.StackTrace = null,
err: ?anyerror = null,
};
pub fn sourceLocations(
allocator: std.mem.Allocator,
debug_info: *std.debug.SelfInfo,
stack_trace: *std.builtin.StackTrace,
) ![]const std.debug.SourceLocation {
var source_locations = std.ArrayList(std.debug.SourceLocation).init(allocator);
if (builtin.strip_debug_info) return error.MissingDebugInfo;
var frame_index: usize = 0;
var frames_left: usize = @min(stack_trace.index, stack_trace.instruction_addresses.len);
while (frames_left != 0) : ({
frames_left -= 1;
frame_index = (frame_index + 1) % stack_trace.instruction_addresses.len;
}) {
const return_address = stack_trace.instruction_addresses[frame_index];
const address = return_address - 1;
const module = try debug_info.getModuleForAddress(address);
const symbol_info = try module.getSymbolAtAddress(debug_info.allocator, address);
if (symbol_info.source_location) |source_location| {
try source_locations.append(source_location);
}
}
return try source_locations.toOwnedSlice();
}
pub const HtmlStackTrace = struct {
allocator: std.mem.Allocator,
stack_trace: *std.builtin.StackTrace,
pub fn format(self: HtmlStackTrace, _: anytype, _: anytype, writer: anytype) !void {
const debug_info = try std.debug.getSelfDebugInfo();
const source_locations = try sourceLocations(
self.allocator,
debug_info,
self.stack_trace,
);
for (source_locations) |source_location| {
defer debug_info.allocator.free(source_location.file_name);
try writer.print(
\\<div class='stack-source-line'>
\\ <span class='file-name'>{s}:{d}</span>
\\
,
.{
source_location.file_name,
source_location.line,
},
);
const surrounding_previous = try surroundingLinesFromFile(
self.allocator,
.previous,
3,
source_location.file_name,
source_location.line,
);
const surrounding_next = try surroundingLinesFromFile(
self.allocator,
.next,
3,
source_location.file_name,
source_location.line,
);
const target_source_line = try readLineFromFile(
self.allocator,
source_location.file_name,
source_location.line,
);
for (surrounding_previous) |source_line| {
try writer.print(surrounding_line_template, .{ source_line.line, source_line.content });
}
try writer.print(target_line_template, .{ target_source_line.line, target_source_line.content });
for (surrounding_next) |source_line| {
try writer.print(surrounding_line_template, .{ source_line.line, source_line.content });
}
try writer.print(
\\</div>
\\
, .{});
}
}
};
const SourceLine = struct { content: []const u8, line: usize };
pub fn readLineFromFile(allocator: std.mem.Allocator, path: []const u8, line: usize) !SourceLine {
const file = try std.fs.openFileAbsolute(path, .{});
var buf: [std.mem.page_size]u8 = undefined;
var count: usize = 1;
var cursor: usize = 0;
seek: {
while (true) {
const bytes_read = try file.readAll(buf[0..]);
for (buf[0..bytes_read]) |char| {
if (char == '\n') count += 1;
cursor += 1;
if (count == line) {
cursor += 1;
break :seek;
}
}
if (bytes_read < buf.len) return error.EndOfFile;
}
}
var size: usize = 0;
try file.seekTo(cursor);
read: {
const bytes_read = try file.readAll(buf[0..]);
if (std.mem.indexOf(u8, buf[0..bytes_read], "\n")) |index| {
size += index;
break :read;
} else if (bytes_read < buf.len) {
size += bytes_read;
break :read;
} else {
while (true) {
const more_bytes_read = try file.readAll(buf[0..]);
if (std.mem.indexOf(u8, buf[0..more_bytes_read], "\n")) |index| {
size += index;
break :read;
} else if (more_bytes_read < buf.len) {
size += more_bytes_read;
break :read;
} else {
size += more_bytes_read;
}
}
}
}
const line_content = try allocator.alloc(u8, size);
try file.seekTo(cursor);
const bytes_read = try file.readAll(line_content[0..]);
std.debug.assert(bytes_read == size);
return .{ .content = line_content[0..], .line = line };
}
fn surroundingLinesFromFile(
allocator: std.mem.Allocator,
context: enum { previous, next },
desired_count: usize,
path: []const u8,
target_line: usize,
) ![]SourceLine {
// This isn't very efficient but we only use it in debug mode so not a huge deal.
const start = switch (context) {
.previous => if (target_line > desired_count)
target_line - desired_count
else
target_line,
.next => target_line + 2,
};
var lines = std.ArrayList(SourceLine).init(allocator);
switch (context) {
.previous => {
for (start..target_line) |line| {
try lines.append(try readLineFromFile(allocator, path, line));
}
},
.next => {
for (0..desired_count, start..) |_, line| {
try lines.append(try readLineFromFile(allocator, path, line));
}
},
}
return try lines.toOwnedSlice();
}
pub const console_template =
\\<!DOCTYPE html>
\\<html>
\\ <head>
\\ <style>
\\{3s}
\\ </style>
\\ </head>
\\ <body>
\\ <h1>Encountered Error: {0s}</h1>
\\ <div class="stack-trace">
\\ {1}
\\ </div>
\\ <h2>Response Data</h2>
\\ <div class="response-data">
\\ <pre><code class="language-json">{2s}</code></pre>
\\ </div>
\\ <script src="/_jetzig_debug.js"></script>
\\ </body>
\\</html>
;
const surrounding_line_template =
\\<div class='line-content surrounding'><pre class="line-number">{d: >4}</pre><pre><code class="language-zig">{s}</code></pre></div>
\\
;
const target_line_template =
\\<div class='line-content target'><pre class="line-number">{d: >4}</pre><pre><code class="language-zig">{s}</code></pre></div>
\\
;

View File

@ -11,6 +11,16 @@ pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS,
pub const Modifier = enum { edit, new };
pub const Format = enum { HTML, JSON, UNKNOWN };
pub const Protocol = enum { http, https };
pub const RequestState = enum {
initial, // No processing has taken place
processed, // Request headers have been processed
after_request, // Initial middleware processing
rendered, // Rendered by middleware or view
redirected, // Redirected by middleware or view
failed, // Failed by middleware or view
before_response, // Post middleware processing
finalized, // Response generated
};
allocator: std.mem.Allocator,
path: jetzig.http.Path,
@ -30,19 +40,12 @@ parsed_multipart: ?*jetzig.data.Data = null,
_cookies: ?*jetzig.http.Cookies = null,
_session: ?*jetzig.http.Session = null,
body: []const u8 = undefined,
state: enum { initial, processed } = .initial,
response_started: bool = false,
state: RequestState = .initial,
dynamic_assigned_template: ?[]const u8 = null,
layout: ?[]const u8 = null,
layout_disabled: bool = false,
// TODO: Squash rendered/redirected/failed into
// `state: enum { initial, rendered, redirected, failed }`
rendered: bool = false,
redirected: bool = false,
failed: bool = false,
redirect_state: ?RedirectState = null,
middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null,
middleware_rendered_during_response: bool = false,
middleware_data: jetzig.http.middleware.MiddlewareData = undefined,
rendered_multiple: bool = false,
rendered_view: ?jetzig.views.View = null,
@ -177,6 +180,7 @@ pub fn respond(self: *Request) !void {
const status = jetzig.http.status_codes.get(self.response.status_code);
self.httpz_response.status = try status.getCodeInt();
self.httpz_response.body = self.response.content;
self.state = .finalized;
}
/// Set the root value for response data.
@ -191,10 +195,9 @@ pub fn data(self: Request, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data.
/// Render a response. This function can only be called once per request (repeat calls will
/// trigger an error).
pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
if (self.rendered or self.failed) self.rendered_multiple = true;
if (self.isRendered()) self.rendered_multiple = true;
self.rendered = true;
if (self.response_started) self.middleware_rendered_during_response = true;
self.state = .rendered;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
return self.rendered_view.?;
}
@ -202,15 +205,20 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
/// Render an error. This function can only be called once per request (repeat calls will
/// trigger an error).
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
if (self.rendered or self.redirected) self.rendered_multiple = true;
if (self.isRendered()) self.rendered_multiple = true;
self.rendered = true;
self.failed = true;
if (self.response_started) self.middleware_rendered_during_response = true;
self.state = .failed;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
return self.rendered_view.?;
}
pub inline fn isRendered(self: *const Request) bool {
return switch (self.state) {
.initial, .processed, .after_request, .before_response => false,
.rendered, .redirected, .failed, .finalized => true,
};
}
/// Issue a redirect to a new location.
/// ```zig
/// return request.redirect("https://www.example.com/", .moved_permanently);
@ -224,11 +232,9 @@ pub fn redirect(
location: []const u8,
redirect_status: enum { moved_permanently, found },
) jetzig.views.View {
if (self.rendered or self.failed) self.rendered_multiple = true;
if (self.isRendered()) self.rendered_multiple = true;
self.rendered = true;
self.redirected = true;
if (self.response_started) self.middleware_rendered_during_response = true;
self.state = .redirected;
const status_code = switch (redirect_status) {
.moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently,
@ -687,6 +693,19 @@ pub fn setTemplate(self: *Request, name: []const u8) void {
self.dynamic_assigned_template = name;
}
/// Set a custom template and render the response.
/// ```zig
/// return request.renderTemplate("blogs/comments/get", .ok);
/// ```
pub fn renderTemplate(
self: *Request,
name: []const u8,
status_code: jetzig.http.StatusCode,
) jetzig.views.View {
self.dynamic_assigned_template = name;
return self.render(status_code);
}
pub fn joinPath(self: *const Request, args: anytype) ![]const u8 {
const fields = std.meta.fields(@TypeOf(args));
var buf: [fields.len][]const u8 = undefined;

View File

@ -138,7 +138,8 @@ pub fn processNextRequest(
var middleware_data = try jetzig.http.middleware.afterRequest(&request);
if (request.middleware_rendered) |_| { // Request processing ends when a middleware renders or redirects.
if (request.middleware_rendered) |_| {
// Request processing ends when a middleware renders or redirects.
if (request.redirect_state) |state| {
try request.renderRedirect(state);
} else if (request.rendered_view) |rendered| {
@ -150,8 +151,8 @@ pub fn processNextRequest(
} else {
try self.renderResponse(&request);
try request.response.headers.append("Content-Type", response.content_type);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
try request.respond();
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
jetzig.http.middleware.deinit(&middleware_data, &request);
@ -197,9 +198,12 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
for (route.before_callbacks) |callback| {
try callback(request, route);
if (request.rendered_view) |view| {
if (request.failed) {
request.setResponse(try self.renderError(request, view.status_code), .{});
} else if (request.rendered) {
if (request.state == .failed) {
request.setResponse(
try self.renderError(request, view.status_code, .{}),
.{},
);
} else if (request.state == .rendered) {
// TODO: Allow callbacks to set content
}
return;
@ -259,7 +263,9 @@ fn renderHTML(
return request.setResponse(rendered_error, .{});
};
return if (request.redirected or request.failed or request.dynamic_assigned_template != null)
return if (request.state == .redirected or
request.state == .failed or
request.dynamic_assigned_template != null)
request.setResponse(rendered, .{})
else
request.setResponse(try self.renderNotFound(request), .{});
@ -327,12 +333,12 @@ fn renderView(
return try self.renderInternalServerError(request, @errorReturnTrace(), err);
};
if (request.failed) {
if (request.state == .failed) {
const view: jetzig.views.View = request.rendered_view orelse .{
.data = request.response_data,
.status_code = .internal_server_error,
};
return try self.renderError(request, view.status_code);
return try self.renderError(request, view.status_code, .{});
}
const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template|
@ -343,7 +349,7 @@ fn renderView(
if (request.rendered_multiple) return error.JetzigMultipleRenderError;
if (request.rendered_view) |rendered_view| {
if (request.redirected) return .{ .view = rendered_view, .content = "" };
if (request.state == .redirected) return .{ .view = rendered_view, .content = "" };
if (template) |capture| {
return .{
@ -352,12 +358,21 @@ fn renderView(
};
} else {
return switch (request.requestFormat()) {
.HTML, .UNKNOWN => try self.renderNotFound(request),
.HTML, .UNKNOWN => blk: {
try self.logger.DEBUG(
"Missing template for route `{s}.{s}`. Expected: `src/app/views/{s}.zmpl`.",
.{ route.view_name, @tagName(route.action), route.template },
);
if (comptime jetzig.build_options.debug_console) {
return error.ZmplTemplateNotFound;
}
break :blk try self.renderNotFound(request);
},
.JSON => .{ .view = rendered_view, .content = "" },
};
}
} else {
if (!request.redirected) {
if (request.state == .processed) {
try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
}
request.response_data.reset();
@ -461,32 +476,42 @@ fn renderInternalServerError(
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !RenderedView {
request.response_data.reset();
try self.logger.logError(stack_trace, err);
const status = .internal_server_error;
return try self.renderError(request, status);
const status = jetzig.http.StatusCode.internal_server_error;
return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err });
}
fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView {
request.response_data.reset();
const status: jetzig.http.StatusCode = .not_found;
return try self.renderError(request, status);
return try self.renderError(request, status, .{});
}
fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView {
request.response_data.reset();
const status: jetzig.http.StatusCode = .bad_request;
return try self.renderError(request, status);
return try self.renderError(request, status, .{});
}
fn renderError(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
error_info: jetzig.debug.ErrorInfo,
) !RenderedView {
if (comptime jetzig.build_options.debug_console) {
return try self.renderDebugConsole(request, status_code, error_info);
} else return try self.renderGeneralError(request, status_code);
}
fn renderGeneralError(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
) !RenderedView {
if (try self.renderErrorView(request, status_code)) |view| return view;
if (try renderStaticErrorPage(request, status_code)) |view| return view;
@ -494,6 +519,42 @@ fn renderError(
return try renderDefaultError(request, status_code);
}
fn renderDebugConsole(
self: Server,
request: *jetzig.http.Request,
status_code: jetzig.http.StatusCode,
error_info: jetzig.debug.ErrorInfo,
) !RenderedView {
if (comptime jetzig.build_options.debug_console) {
var buf = std.ArrayList(u8).init(request.allocator);
const writer = buf.writer();
if (error_info.stack_trace) |stack_trace| {
const debug_content = jetzig.debug.HtmlStackTrace{
.allocator = request.allocator,
.stack_trace = stack_trace,
};
const error_name = if (error_info.err) |err| @errorName(err) else "[UnknownError]";
try writer.print(
jetzig.debug.console_template,
.{
error_name,
debug_content,
try request.response_data.toJsonOptions(.{ .pretty = true }),
@embedFile("../../assets/debug.css"),
},
);
} else return try self.renderGeneralError(request, status_code);
const content = try buf.toOwnedSlice();
return .{
.view = .{ .data = request.response_data, .status_code = status_code },
.content = if (content.len == 0) "" else content,
};
} else unreachable;
}
fn renderErrorView(
self: Server,
request: *jetzig.http.Request,
@ -641,6 +702,13 @@ const StaticResource = struct {
};
fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
if (comptime jetzig.build_options.debug_console) {
if (std.mem.eql(u8, request.path.path, "/_jetzig_debug.js")) return .{
.content = @embedFile("../../assets/debug.js"),
.mime_type = "text/javascript",
};
}
// TODO: Map public and static routes at launch to avoid accessing the file system when
// matching any route - currently every request causes file system traversal.
const public_resource = try self.matchPublicContent(request);

View File

@ -60,8 +60,11 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
}
}
request.state = .after_request;
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "afterRequest")) continue;
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index).?;
try @call(
@ -72,7 +75,8 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
} else {
try @call(.always_inline, middleware.afterRequest, .{request});
}
if (request.rendered or request.redirected) {
if (request.state != .after_request) {
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "afterRequest" };
break;
}
@ -86,11 +90,11 @@ pub fn beforeResponse(
middleware_data: *MiddlewareData,
request: *jetzig.http.Request,
) !void {
request.response_started = true;
request.state = .before_response;
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "beforeResponse")) continue;
if (!request.middleware_rendered_during_response) {
if (request.state == .before_response) {
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index).?;
try @call(
@ -99,11 +103,19 @@ pub fn beforeResponse(
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response },
);
} else {
try @call(.always_inline, middleware.beforeResponse, .{ request, request.response });
try @call(
.always_inline,
middleware.beforeResponse,
.{ request, request.response },
);
}
}
if (request.middleware_rendered_during_response) {
request.middleware_rendered = .{ .name = @typeName(middleware), .action = "beforeResponse" };
if (request.state != .before_response) {
request.middleware_rendered = .{
.name = @typeName(middleware),
.action = "beforeResponse",
};
break;
}
}

View File

@ -107,7 +107,7 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request)
if (request.middleware_rendered) |middleware| middleware.action else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.white ++ ":" else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.bright_cyan else "",
if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "",
if (request.middleware_rendered) |_| @tagName(request.state) else "",
if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.reset else "",
if (request.middleware_rendered) |_| "]" else "",
request.path.path,

View File

@ -86,7 +86,7 @@ pub fn logRequest(self: ProductionLogger, request: *const jetzig.http.Request) !
if (request.middleware_rendered) |_| ":" else "",
if (request.middleware_rendered) |middleware| middleware.action else "",
if (request.middleware_rendered) |_| ":" else "",
if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "",
if (request.middleware_rendered) |_| @tagName(request.state) else "",
if (request.middleware_rendered) |_| "]" else "",
request.path.path,
}, .stdout);

View File

@ -20,7 +20,11 @@ pub fn afterRequest(request: *jetzig.http.Request) !void {
/// If a redirect was issued during request processing, reset any response data, set response
/// status to `200 OK` and replace the `Location` header with a `HX-Redirect` header.
pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
if (response.status_code != .moved_permanently and response.status_code != .found) return;
switch (response.status_code) {
.moved_permanently, .found => {},
else => return,
}
if (request.headers.get("HX-Request") == null) return;
if (response.headers.get("Location")) |location| {