cshttp Tutorial - Day 10: Remembering Visitors (Cookies)
Day 10: Remembering Visitors (Cookies)
Here is a surprising fact about how the web works: by default, a web server has zero memory of who you are!
Every HTTP request is like a phone call from a complete stranger. Even if you visited a website 5 seconds ago, when you click to the next page, the server has no idea that the second request came from the same person as the first request!
So how do websites remember that you are logged in, what items are in your shopping cart, or whether you prefer Light Mode or Dark Mode? They use Cookies.
A Cookie is a small note (a name and a value) that the server hands to the browser. The browser saves that note on the visitor's computer, and automatically sends it back to the server with every single request after that!
1. Server --> Browser: "Please remember this note: favorite_color = pink"
2. Browser --> Server: (On every future request) "Here is my note: favorite_color = pink"
Today, we will build an interactive Cookie Playground (/cookies) step by step! We will:
- Start with a clean, minimal
Program.cs(only/and/cookies) so we can focus 100% on Cookies. - Issue (create) and change a cookie using
Response.Cookies.Add(new HttpCookie(...)). - Read a cookie on the C# server using
Request.Cookies["name"]. - Delete a cookie by setting its expiration date in the past (
DateTime.Now.AddDays(-1)). - Protect sensitive cookies from JavaScript using
cookie.HttpOnly = true, and see the difference betweenRequest.Cookies(C#) anddocument.cookie(JavaScript)!
1. Starting with a Clean Foundation
Just like Day 9, let's start with a clean, lightweight Program.cs that has only our Home page (/), our Cookies page (/cookies), our 404 Not Found page, and RenderPage():
using System;
using cshttp;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
WebApp.Run(8080, OnRequest);
}
static void OnRequest()
{
string path = Request.Path.ToLower();
switch (path)
{
case "/":
case "/home":
HandleHome();
break;
case "/cookies":
HandleCookies();
break;
default:
HandleNotFound();
break;
}
}
static void RenderPage(string title, string bodyContent)
{
string html = $@"<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
</head>
<body>
<nav>
<a href='/'>Home</a> |
<a href='/cookies'>Cookies</a>
</nav>
<hr />
{bodyContent}
<hr />
<footer>
<small>Server Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}</small>
</footer>
</body>
</html>";
Response.Write(html);
}
static void HandleHome()
{
string content = @"
<h1>Welcome Home</h1>
<p>Click on <a href='/cookies'>Cookies</a> in the menu to open the Cookie Playground!</p>";
RenderPage("Home", content);
}
static void HandleNotFound()
{
Response.StatusCode = 404;
string content = @"
<h1>404 Not Found</h1>
<p>The page that you are looking for does not exist.</p>";
RenderPage("404 Not Found", content);
}
}
}
2. How to Create, Read, Change, and Delete Cookies in C#
Before we write HandleCookies(), let's look at the four basic cookie operations in cshttp:
Operation 1: Issuing (Creating) a Cookie
To send a cookie from the server to the visitor's browser, create an HttpCookie object and add it to Response.Cookies:
HttpCookie cookie = new HttpCookie("favorite_color", "pink");
cookie.Expires = DateTime.Now.AddDays(30); // Keep it for 30 days
Response.Cookies.Add(cookie);
- What does
cookie.Expiresdo?- If you do not set
Expires, the cookie is a Session Cookie: the browser keeps it only while the browser window is open, and erases it as soon as the visitor closes their browser. - If you do set
Expires(for example,DateTime.Now.AddDays(30)), the browser saves it to disk and remembers it for 30 days—even if the visitor closes their browser or restarts their computer!
- If you do not set
Operation 2: Reading a Cookie on the Server
When the browser makes its next request, it sends its cookies back to the server inside Request.Cookies:
HttpCookie cookie = Request.Cookies["favorite_color"];
if (cookie != null)
{
string color = cookie.Value; // "pink"
}
Always check for
null! When a brand-new visitor opens your website for the first time, their browser does not have the"favorite_color"cookie yet, soRequest.Cookies["favorite_color"]returnsnull.
To make reading cookies super convenient, let's add a tiny helper method called GetCookieValue(string name) to class Program:
// Returns the cookie's value, or null if the browser did not send this cookie
static string GetCookieValue(string name)
{
HttpCookie cookie = Request.Cookies[name];
if (cookie == null)
{
return null;
}
return WebApp.UrlDecode(cookie.Value);
}
(Why WebApp.UrlDecode? Because cookie values in HTTP headers cannot contain special characters like ; or spaces. Using WebApp.UrlEncode when saving a cookie and WebApp.UrlDecode when reading it ensures any text is stored safely!)
Operation 3: Changing a Cookie
There is no special "edit" command for cookies. To change a cookie's value, you simply issue a new HttpCookie with the exact same name ("favorite_color"), and the browser automatically overwrites the old value!
Operation 4: Deleting a Cookie
How do you tell the browser to delete a cookie? You send a cookie with the same name, but set its Expires date to the past (such as yesterday: DateTime.Now.AddDays(-1)):
HttpCookie cookie = new HttpCookie("favorite_color", "");
cookie.Expires = DateTime.Now.AddDays(-1); // Yesterday = already expired!
Response.Cookies.Add(cookie);
As soon as the browser receives a cookie whose expiration date has already passed, it immediately deletes the cookie!
3. Normal Cookies vs. HttpOnly Secret Cookies
By default, cookies stored in the browser can be read in two places:
- By the C# Server (via
Request.Cookieson every request). - By JavaScript running inside the browser (via
document.cookie).
That is fine for harmless preferences like favorite_color = pink.
However, what if a cookie holds a secret login token (secret_token = ABC-123-XYZ) that proves the user is logged into their bank account? We never want JavaScript in the browser to be able to read a login token (remember the <script> XSS attacks from Day 6!).
To hide a cookie from JavaScript while still letting the C# server read it, we set one property:
HttpCookie cookie = new HttpCookie("secret_token", "ABC-123-XYZ");
cookie.Expires = DateTime.Now.AddDays(30);
cookie.HttpOnly = true; // <-- JavaScript is NOT allowed to see this cookie!
Response.Cookies.Add(cookie);
When cookie.HttpOnly = true, the browser still sends secret_token to our C# server on every HTTP request, but completely hides it from JavaScript's document.cookie!
4. Building HandleCookies() — Part 1: Reading & Displaying Cookies (GET)
Let's build HandleCookies() in two steps. First, let's write the GET part that reads favorite_color and secret_token on the server, and displays:
- Box 1: What the C# Server sees (
Request.Cookies). - Box 2: What JavaScript in the Browser sees (
document.cookie).
static void HandleCookies()
{
// 1. Read both cookies on the C# server
string favoriteColor = GetCookieValue("favorite_color");
string secretToken = GetCookieValue("secret_token");
string colorText = favoriteColor == null ? "(no cookie)" : WebApp.HtmlEncode(favoriteColor);
string secretText = secretToken == null ? "(no cookie)" : WebApp.HtmlEncode(secretToken);
// 2. If favorite_color is one of our valid colors, use it as the background color of Box 1!
string boxColor = "white";
if (favoriteColor == "pink" || favoriteColor == "lightblue" || favoriteColor == "lightgreen")
{
boxColor = favoriteColor;
}
string content = $@"
<h1>Cookie Playground</h1>
<div style='background: {boxColor}; border: 1px solid #ccc; padding: 15px; margin-bottom: 15px;'>
<h2>1. What the SERVER sees (C#)</h2>
<p>favorite_color = <b>{colorText}</b></p>
<p>secret_token = <b>{secretText}</b></p>
</div>
<div style='background: #f9f9f9; border: 1px solid #ccc; padding: 15px; margin-bottom: 15px;'>
<h2>2. What JAVASCRIPT sees (in the browser)</h2>
<pre id='jsCookies'></pre>
<p><small>Notice: <b>secret_token</b> never appears here, because it is <b>HttpOnly</b>!</small></p>
</div>
<script>
var text = document.cookie;
if (text === '') {{
text = '(no cookies visible to JavaScript)';
}}
document.getElementById('jsCookies').textContent = text;
</script>";
RenderPage("Cookies", content);
}
C# Tip — Why did we write
{{and}}inside<script>? Becausecontentis an interpolated C# string ($@"..."), single curly braces{...}are used by C# to insert variables (like{boxColor}). When we want to write a literal{or}character for JavaScript'sif (text === '') { ... }, we type it twice ({{and}}) so C# knows to output a real{and}into the HTML!
5. Building HandleCookies() — Part 2: Adding Forms to Set & Delete Cookies (POST)
Now let's add the buttons and forms so the visitor can test setting, changing, and deleting both favorite_color and secret_token!
Since we have four different buttons on the same /cookies page:
- Set / Change Favourite Colour (
action='set-color') - Delete Favourite Colour (
action='delete-color') - Set Secret Cookie (
action='set-secret') - Delete Secret Cookie (
action='delete-secret')
How does our C# POST handler know which of the four buttons the visitor clicked?
Each <form> includes a hidden input field (<input type='hidden' name='action' value='...' />):
<!-- Form 1: Set / Change Color -->
<form action='/cookies' method='post'>
<input type='hidden' name='action' value='set-color' />
<select name='color'>
<option value='pink'>Pink</option>
<option value='lightblue'>Light Blue</option>
<option value='lightgreen'>Light Green</option>
</select>
<button type='submit'>Set / Change Cookie</button>
</form>
<!-- Form 2: Delete Color -->
<form action='/cookies' method='post'>
<input type='hidden' name='action' value='delete-color' />
<button type='submit'>Delete Cookie</button>
</form>
When the visitor clicks a button, our C# code reads string action = Request.Form["action"]; and runs the matching if / else if block:
if (Request.HttpMethod == "POST")
{
string action = Request.Form["action"];
if (action == "set-color")
{
// ISSUE or CHANGE: same name = the browser replaces the old value
string color = Request.Form["color"];
HttpCookie cookie = new HttpCookie("favorite_color", WebApp.UrlEncode(color));
cookie.Expires = DateTime.Now.AddDays(30);
Response.Cookies.Add(cookie);
}
else if (action == "delete-color")
{
// DELETE: send it again with an expiry date in the past
HttpCookie cookie = new HttpCookie("favorite_color", "");
cookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(cookie);
}
else if (action == "set-secret")
{
// HttpOnly: JavaScript in the browser is NOT allowed to read this cookie
HttpCookie cookie = new HttpCookie("secret_token", "ABC-123-XYZ");
cookie.Expires = DateTime.Now.AddDays(30);
cookie.HttpOnly = true;
Response.Cookies.Add(cookie);
}
else if (action == "delete-secret")
{
HttpCookie cookie = new HttpCookie("secret_token", "");
cookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(cookie);
}
Response.Redirect("/cookies");
return;
}
6. Complete Code for Day 10
Here is the complete, self-contained Program.cs for Day 10:
using System;
using cshttp;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
WebApp.Run(8080, OnRequest);
}
static void OnRequest()
{
string path = Request.Path.ToLower();
switch (path)
{
case "/":
case "/home":
HandleHome();
break;
case "/cookies":
HandleCookies();
break;
default:
HandleNotFound();
break;
}
}
static void RenderPage(string title, string bodyContent)
{
string html = $@"<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
</head>
<body>
<nav>
<a href='/'>Home</a> |
<a href='/cookies'>Cookies</a>
</nav>
<hr />
{bodyContent}
<hr />
<footer>
<small>Server Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}</small>
</footer>
</body>
</html>";
Response.Write(html);
}
// NEW IN DAY 10: A small helper that reads a cookie and decodes it.
// Returns null if the browser did not send this cookie.
static string GetCookieValue(string name)
{
HttpCookie cookie = Request.Cookies[name];
if (cookie == null)
{
return null;
}
return WebApp.UrlDecode(cookie.Value);
}
// NEW IN DAY 10: The Cookie Playground
static void HandleCookies()
{
if (Request.HttpMethod == "POST")
{
string action = Request.Form["action"];
if (action == "set-color")
{
// ISSUE or CHANGE: same name = the browser replaces the old value
string color = Request.Form["color"];
HttpCookie cookie = new HttpCookie("favorite_color", WebApp.UrlEncode(color));
cookie.Expires = DateTime.Now.AddDays(30);
Response.Cookies.Add(cookie);
}
else if (action == "delete-color")
{
// DELETE: send it again with an expiry date in the past
HttpCookie cookie = new HttpCookie("favorite_color", "");
cookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(cookie);
}
else if (action == "set-secret")
{
// HttpOnly: JavaScript in the browser is NOT allowed to read this cookie
HttpCookie cookie = new HttpCookie("secret_token", "ABC-123-XYZ");
cookie.Expires = DateTime.Now.AddDays(30);
cookie.HttpOnly = true;
Response.Cookies.Add(cookie);
}
else if (action == "delete-secret")
{
HttpCookie cookie = new HttpCookie("secret_token", "");
cookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(cookie);
}
Response.Redirect("/cookies");
return;
}
// READ on the server
string favoriteColor = GetCookieValue("favorite_color");
string secretToken = GetCookieValue("secret_token");
string colorText = favoriteColor == null ? "(no cookie)" : WebApp.HtmlEncode(favoriteColor);
string secretText = secretToken == null ? "(no cookie)" : WebApp.HtmlEncode(secretToken);
// Only allow colours from our own list
string boxColor = "white";
if (favoriteColor == "pink" || favoriteColor == "lightblue" || favoriteColor == "lightgreen")
{
boxColor = favoriteColor;
}
string content = $@"
<h1>Cookie Playground</h1>
<div style='background: {boxColor}; border: 1px solid #ccc; padding: 15px; margin-bottom: 15px;'>
<h2>1. What the SERVER sees (C#)</h2>
<p>favorite_color = <b>{colorText}</b></p>
<p>secret_token = <b>{secretText}</b></p>
</div>
<div style='background: #f9f9f9; border: 1px solid #ccc; padding: 15px; margin-bottom: 15px;'>
<h2>2. What JAVASCRIPT sees (in the browser)</h2>
<pre id='jsCookies'></pre>
<p><small>Notice: <b>secret_token</b> is missing here, because it is <b>HttpOnly</b>!</small></p>
</div>
<div style='background: #f9f9f9; border: 1px solid #ccc; padding: 15px; margin-bottom: 15px;'>
<h2>3. Favourite Colour (a normal cookie)</h2>
<form action='/cookies' method='post'>
<input type='hidden' name='action' value='set-color' />
<select name='color'>
<option value='pink'>Pink</option>
<option value='lightblue'>Light Blue</option>
<option value='lightgreen'>Light Green</option>
</select>
<button type='submit'>Set / Change Cookie</button>
</form>
<br />
<form action='/cookies' method='post'>
<input type='hidden' name='action' value='delete-color' />
<button type='submit'>Delete Cookie</button>
</form>
</div>
<div style='background: #f9f9f9; border: 1px solid #ccc; padding: 15px; margin-bottom: 15px;'>
<h2>4. Secret Token (an HttpOnly cookie)</h2>
<form action='/cookies' method='post'>
<input type='hidden' name='action' value='set-secret' />
<button type='submit'>Set Secret Cookie</button>
</form>
<br />
<form action='/cookies' method='post'>
<input type='hidden' name='action' value='delete-secret' />
<button type='submit'>Delete Secret Cookie</button>
</form>
</div>
<script>
// READ in the browser: document.cookie holds every cookie JavaScript is allowed to see
var text = document.cookie;
if (text === '') {{
text = '(no cookies visible to JavaScript)';
}}
document.getElementById('jsCookies').textContent = text;
</script>";
RenderPage("Cookies", content);
}
static void HandleHome()
{
string content = @"
<h1>Welcome Home</h1>
<p>Click on <a href='/cookies'>Cookies</a> in the menu to open the Cookie Playground!</p>";
RenderPage("Home", content);
}
static void HandleNotFound()
{
Response.StatusCode = 404;
string content = @"
<h1>404 Not Found</h1>
<p>The page that you are looking for does not exist.</p>";
RenderPage("404 Not Found", content);
}
}
}
7. Test It in Your Browser
- Run the application (
dotnet runor press Start in Visual Studio) and visithttp://localhost:8080/cookies. - At first, both Box 1 (Server) and Box 2 (JavaScript) say
(no cookie). - Issue a Cookie: In Box 3, choose Pink and click Set / Change Cookie.
- Box 1 turns pink and shows
favorite_color = pink. - Box 2 shows
favorite_color=pink.
- Box 1 turns pink and shows
- Change a Cookie: Choose Light Green and click Set / Change Cookie again. The old cookie value is replaced with
lightgreen! - Test
HttpOnlySecurity: In Box 4, click Set Secret Cookie.- Look at Box 1 (Server): C# sees
secret_token = ABC-123-XYZ! - Look at Box 2 (JavaScript):
secret_tokenis completely invisible to JavaScript because we setcookie.HttpOnly = true!
- Look at Box 1 (Server): C# sees
- Inspect Cookies in Browser DevTools:
- Press
F12in your browser to open Developer Tools. - In Chrome / Edge, click the Application tab → Cookies →
http://localhost:8080(in Firefox, click Storage → Cookies). - You will see a table listing
favorite_colorandsecret_token, their expiration dates (30 days from today), and a checkmark under the HttpOnly column forsecret_token!
- Press
- Delete the Cookies: Click Delete Cookie and Delete Secret Cookie—both cookies immediately disappear from the page and from the F12 Cookies table!
8. Quick Reference Summary
| Goal | C# Code |
|---|---|
| Issue (create) or change a cookie | HttpCookie c = new HttpCookie("name", "value");Response.Cookies.Add(c); |
| Keep cookie after browser closes | c.Expires = DateTime.Now.AddDays(30); |
| Read a cookie on the server | HttpCookie c = Request.Cookies["name"]; (check if (c != null)!) |
| Delete a cookie | Issue it again with c.Expires = DateTime.Now.AddDays(-1); |
Hide cookie from JavaScript (XSS protection) | c.HttpOnly = true; |
In Day 11, we will learn how JavaScript's fetch() function lets our web page talk to our C# server in the background without reloading the page!