Skip to content

Commit 7000f03

Browse files
committed
Add res.setCookie
karlseguin#92
1 parent 3ed0e57 commit 7000f03

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

Diff for: readme.md

+26
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,32 @@ try res.headerOpts("Location", location, .{.dupe_value = true});
576576

577577
`HeaderOpts` currently supports `dupe_name: bool` and `dupe_value: bool`, both default to `false`.
578578

579+
## Cookies
580+
You can use the `res.setCookie(name, value, opts)` to set the "Set-Cookie" header.
581+
582+
```zig
583+
try res.setCookie("cookie_name3", "cookie value 3", .{
584+
.path = "/auth/",
585+
.domain = "www.openmymind.net",
586+
.max_age = 9001,
587+
.secure = true,
588+
.http_only = true,
589+
.partitioned = true,
590+
.same_site = .lax, // or .none, or .strict (or null to leave out)
591+
});
592+
```
593+
594+
`setCookie` does not validate the name, value, path or domain - it assumes you're setting proper values. It *will* double-quote values which contain spaces or commas (as required).
595+
596+
If, for whatever reason, `res.setCookie` doesn't work for you, you always have full control over the cookie value via `res.header("Set-Cookie", value)`.
597+
598+
```zig
599+
var cookies = req.cookies();
600+
if (cookies.get("auth")) |auth| {
601+
/// ...
602+
}
603+
```
604+
579605
## Writing
580606
By default, httpz will automatically flush your response. In more advance cases, you can use `res.write()` to explicitly flush it. This is useful in cases where you have resources that need to be freed/released only after the response is written. For example, my [LRU cache](https://github.com/karlseguin/cache.zig) uses atomic referencing counting to safely allow concurrent access to cached data. This requires callers to "release" the cached entry:
581607

Diff for: src/response.zig

+114
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ pub const Response = struct {
9494
self.headers.add(name, value);
9595
}
9696

97+
pub fn setCookie(self: *Response, name: []const u8, value: []const u8, opts: CookieOpts) !void {
98+
const serialized = try serializeCookie(self.arena, name, value, &opts);
99+
self.header("Set-Cookie", serialized);
100+
}
101+
97102
pub const HeaderOpts = struct {
98103
dupe_name: bool = false,
99104
dupe_value: bool = false,
@@ -407,6 +412,78 @@ pub const Response = struct {
407412
};
408413
};
409414

415+
pub const CookieOpts = struct {
416+
// by not using optional, we can get the len without the if check
417+
path: []const u8 = "",
418+
domain: []const u8 = "",
419+
max_age: ?i32 = null,
420+
secure: bool = false,
421+
http_only: bool = false,
422+
partitioned: bool = false,
423+
same_site: ?SameSite = null,
424+
425+
pub const SameSite = enum {
426+
lax,
427+
strict,
428+
none,
429+
};
430+
};
431+
432+
// we expect arena to be an ArenaAllocator
433+
pub fn serializeCookie(arena: Allocator, name: []const u8, value: []const u8, cookie: *const CookieOpts) ![]u8 {
434+
// Golang uses 110 as a "typical length of cookie attributes"
435+
const path = cookie.path;
436+
const domain = cookie.domain;
437+
438+
const estimated_len = name.len + value.len + path.len + domain.len + 110;
439+
var buf = std.ArrayListUnmanaged(u8){};
440+
441+
try buf.ensureTotalCapacity(arena, estimated_len);
442+
buf.appendSliceAssumeCapacity(name);
443+
buf.appendAssumeCapacity('=');
444+
445+
if (std.mem.indexOfAny(u8, value, ", ") != null) {
446+
buf.appendAssumeCapacity('"');
447+
buf.appendSliceAssumeCapacity(value);
448+
buf.appendAssumeCapacity('"');
449+
} else {
450+
buf.appendSliceAssumeCapacity(value);
451+
}
452+
453+
if (path.len != 0) {
454+
buf.appendSliceAssumeCapacity("; Path=");
455+
buf.appendSliceAssumeCapacity(path);
456+
}
457+
458+
if (domain.len != 0) {
459+
buf.appendSliceAssumeCapacity("; Domain=");
460+
buf.appendSliceAssumeCapacity(domain);
461+
}
462+
463+
if (cookie.max_age) |ma| {
464+
try buf.appendSlice(arena, "; Max-Age=");
465+
try std.fmt.formatInt(ma, 10, .lower, .{}, buf.writer(arena));
466+
}
467+
468+
if (cookie.http_only) {
469+
try buf.appendSlice(arena, "; HttpOnly");
470+
}
471+
if (cookie.secure) {
472+
try buf.appendSlice(arena, "; Secure");
473+
}
474+
if (cookie.partitioned) {
475+
try buf.appendSlice(arena, "; Partitioned");
476+
}
477+
478+
if (cookie.same_site) |ss| switch (ss) {
479+
.lax => try buf.appendSlice(arena, "; SameSite=Lax"),
480+
.strict => try buf.appendSlice(arena, "; SameSite=Strict"),
481+
.none => try buf.appendSlice(arena, "; SameSite=None"),
482+
};
483+
484+
return buf.items;
485+
}
486+
410487
// All the upfront memory allocation that we can do. Gets re-used from request
411488
// to request.
412489
pub const State = struct {
@@ -579,6 +656,43 @@ test "response: header" {
579656
}
580657
}
581658

659+
test "response: setCookie" {
660+
{
661+
var ctx = t.Context.init(.{});
662+
defer ctx.deinit();
663+
664+
var res = ctx.response();
665+
try res.setCookie("c-n", "c-v", .{});
666+
try t.expectString("c-n=c-v", res.headers.get("Set-Cookie").?);
667+
}
668+
669+
{
670+
var ctx = t.Context.init(.{});
671+
defer ctx.deinit();
672+
673+
var res = ctx.response();
674+
try res.setCookie("c-n2", "c,v", .{});
675+
try t.expectString("c-n2=\"c,v\"", res.headers.get("Set-Cookie").?);
676+
}
677+
678+
{
679+
var ctx = t.Context.init(.{});
680+
defer ctx.deinit();
681+
682+
var res = ctx.response();
683+
try res.setCookie("cookie_name3", "cookie value 3", .{
684+
.path = "/auth/",
685+
.domain = "www.openmymind.net",
686+
.max_age = 9001,
687+
.secure = true,
688+
.http_only = true,
689+
.partitioned = true,
690+
.same_site = .lax,
691+
});
692+
try t.expectString("cookie_name3=\"cookie value 3\"; Path=/auth/; Domain=www.openmymind.net; Max-Age=9001; HttpOnly; Secure; Partitioned; SameSite=Lax", res.headers.get("Set-Cookie").?);
693+
}
694+
}
695+
582696
test "response: direct writer" {
583697
defer t.reset();
584698
var ctx = t.Context.init(.{});

0 commit comments

Comments
 (0)