diff --git a/demo/src/app/views/render_text.zig b/demo/src/app/views/render_text.zig
new file mode 100644
index 0000000..dba87f9
--- /dev/null
+++ b/demo/src/app/views/render_text.zig
@@ -0,0 +1,17 @@
+const std = @import("std");
+const jetzig = @import("jetzig");
+
+pub fn index(request: *jetzig.Request) !jetzig.View {
+ request.response.content_type = "text/xml";
+ return request.renderText("baz", .ok);
+}
+
+test "index" {
+ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
+ defer app.deinit();
+
+ const response = try app.request(.GET, "/render_text", .{});
+ try response.expectStatus(.ok);
+ try response.expectBodyContains("baz");
+ try response.expectHeader("content-type", "text/xml");
+}
diff --git a/demo/src/app/views/render_text/index.zmpl b/demo/src/app/views/render_text/index.zmpl
new file mode 100644
index 0000000..76457d0
--- /dev/null
+++ b/demo/src/app/views/render_text/index.zmpl
@@ -0,0 +1,3 @@
+
+ Content goes here
+
diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig
index 5137903..f3caf66 100644
--- a/src/jetzig/http.zig
+++ b/src/jetzig/http.zig
@@ -26,3 +26,5 @@ pub const params = @import("http/params.zig");
pub const SimplifiedRequest = struct {
location: ?[]const u8,
};
+
+pub const default_content_type = "application/octet-stream";
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig
index 4e31d3e..582706a 100644
--- a/src/jetzig/http/Request.zig
+++ b/src/jetzig/http/Request.zig
@@ -16,6 +16,7 @@ pub const RequestState = enum {
processed, // Request headers have been processed
after_request, // Initial middleware processing
rendered, // Rendered by middleware or view
+ rendered_text, // Rendered text by middleware or view
redirected, // Redirected by middleware or view
failed, // Failed by middleware or view
before_response, // Post middleware processing
@@ -230,7 +231,7 @@ pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) je
pub inline fn isRendered(self: *const Request) bool {
return switch (self.state) {
.initial, .processed, .after_request, .before_response => false,
- .rendered, .redirected, .failed, .finalized => true,
+ .rendered, .rendered_text, .redirected, .failed, .finalized => true,
};
}
@@ -736,6 +737,18 @@ pub fn joinPath(self: *const Request, args: anytype) ![]const u8 {
return try std.mem.join(self.allocator, "/", buf[0..]);
}
+pub fn renderText(
+ self: *Request,
+ text: []const u8,
+ status_code: jetzig.http.StatusCode,
+) jetzig.views.View {
+ self.state = .rendered_text;
+ self.rendered_view = .{ .data = self.response_data, .status_code = status_code };
+ self.setResponse(.{ .view = self.rendered_view.?, .content = text }, .{});
+
+ return self.rendered_view.?;
+}
+
pub fn joinPaths(self: *const Request, paths: []const []const []const u8) ![]const u8 {
var buf = std.ArrayList([]const u8).init(self.allocator);
defer buf.deinit();
@@ -753,10 +766,12 @@ pub fn setResponse(
) void {
self.response.content = rendered_view.content;
self.response.status_code = rendered_view.view.status_code;
- self.response.content_type = options.content_type orelse switch (self.requestFormat()) {
- .HTML, .UNKNOWN => "text/html",
- .JSON => "application/json",
- };
+ if (self.response.content_type == null) {
+ self.response.content_type = options.content_type orelse switch (self.requestFormat()) {
+ .HTML, .UNKNOWN => "text/html",
+ .JSON => "application/json",
+ };
+ }
}
fn setCookieHeaders(self: *Request) !void {
diff --git a/src/jetzig/http/Response.zig b/src/jetzig/http/Response.zig
index f2c26b5..db1e843 100644
--- a/src/jetzig/http/Response.zig
+++ b/src/jetzig/http/Response.zig
@@ -11,7 +11,7 @@ allocator: std.mem.Allocator,
headers: jetzig.http.Headers,
content: []const u8,
status_code: http.status_codes.StatusCode,
-content_type: []const u8,
+content_type: ?[]const u8 = null,
httpz_response: *httpz.Response,
pub fn init(
@@ -22,8 +22,11 @@ pub fn init(
.allocator = allocator,
.httpz_response = httpz_response,
.status_code = .no_content,
- .content_type = "application/octet-stream",
.content = "",
.headers = jetzig.http.Headers.init(allocator, &httpz_response.headers),
};
}
+
+pub inline fn contentType(self: *const jetzig.http.Response) []const u8 {
+ return self.content_type orelse jetzig.http.default_content_type;
+}
diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig
index 9fd9ce1..7c1e073 100644
--- a/src/jetzig/http/Server.zig
+++ b/src/jetzig/http/Server.zig
@@ -159,7 +159,7 @@ pub fn processNextRequest(
}
try self.renderResponse(&request);
- try request.response.headers.append("Content-Type", response.content_type);
+ try request.response.headers.append("Content-Type", response.contentType());
try jetzig.http.middleware.beforeResponse(&middleware_data, &request);
try request.respond();
@@ -178,7 +178,7 @@ fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.
// TODO: Allow middleware to set content
request.setResponse(.{ .view = rendered, .content = "" }, .{});
}
- try request.response.headers.append("Content-Type", response.content_type);
+ try request.response.headers.append("Content-Type", response.contentType());
try request.respond();
return true;
} else return false;
@@ -356,12 +356,16 @@ fn renderView(
return try self.renderInternalServerError(request, @errorReturnTrace(), err);
};
- 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, .{});
+ switch (request.state) {
+ .failed => {
+ const status_code = request.rendered_view.?.status_code;
+ return try self.renderError(request, status_code, .{});
+ },
+ .rendered_text => {
+ const view = request.rendered_view.?; // a panic here is a bug.
+ return .{ .view = view, .content = request.response.content };
+ },
+ else => {},
}
const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template|
@@ -721,7 +725,7 @@ fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetz
const StaticResource = struct {
content: []const u8,
- mime_type: []const u8 = "application/octet-stream",
+ mime_type: []const u8 = jetzig.http.default_content_type,
};
fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
@@ -780,7 +784,7 @@ fn matchPublicContent(self: *Server, request: *jetzig.http.Request) !?StaticReso
jetzig.config.get(usize, "max_bytes_public_content"),
);
const extension = std.fs.path.extension(file_path);
- const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream";
+ const mime_type = if (self.mime_map.get(extension)) |mime| mime else jetzig.http.default_content_type;
return .{
.content = content,
.mime_type = mime_type,
diff --git a/src/jetzig/testing.zig b/src/jetzig/testing.zig
index 6b9cbf4..10db7d6 100644
--- a/src/jetzig/testing.zig
+++ b/src/jetzig/testing.zig
@@ -120,14 +120,31 @@ pub fn expectBodyContains(expected: []const u8, response: TestResponse) !void {
}
pub fn expectHeader(expected_name: []const u8, expected_value: ?[]const u8, response: TestResponse) !void {
+ var mismatches = std.ArrayList([]const u8).init(response.allocator);
+ defer mismatches.deinit();
+
for (response.headers) |header| {
if (!std.ascii.eqlIgnoreCase(header.name, expected_name)) continue;
if (expected_value) |value| {
- if (std.mem.eql(u8, header.value, value)) return;
+ if (std.mem.eql(u8, header.value, value)) {
+ return;
+ } else {
+ try mismatches.append(header.value);
+ }
} else {
return;
}
}
+
+ logFailure(
+ "Expected header " ++
+ jetzig.colors.cyan("{s}") ++
+ ": " ++
+ jetzig.colors.green("{?s}") ++
+ ", found: " ++
+ jetzig.colors.red("{s}"),
+ .{ expected_name, expected_value, mismatches.items },
+ );
return error.JetzigExpectHeaderError;
}