cshttp Tutorial - Day 10: Remembering Visitors (Cookies)

Back to cshttp Tutorial Main Menu

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:

  1. Start with a clean, minimal Program.cs (only / and /cookies) so we can focus 100% on Cookies.
  2. Issue (create) and change a cookie using Response.Cookies.Add(new HttpCookie(...)).
  3. Read a cookie on the C# server using Request.Cookies["name"].
  4. Delete a cookie by setting its expiration date in the past (DateTime.Now.AddDays(-1)).
  5. Protect sensitive cookies from JavaScript using cookie.HttpOnly = true, and see the difference between Request.Cookies (C#) and document.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.Expires do?
    • 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!

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, so Request.Cookies["favorite_color"] returns null.

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:

  1. By the C# Server (via Request.Cookies on every request).
  2. 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:

  1. Box 1: What the C# Server sees (Request.Cookies).
  2. 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>? Because content is 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's if (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:

  1. Set / Change Favourite Colour (action='set-color')
  2. Delete Favourite Colour (action='delete-color')
  3. Set Secret Cookie (action='set-secret')
  4. 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

  1. Run the application (dotnet run or press Start in Visual Studio) and visit http://localhost:8080/cookies.
  2. At first, both Box 1 (Server) and Box 2 (JavaScript) say (no cookie).
  3. 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.
  4. Change a Cookie: Choose Light Green and click Set / Change Cookie again. The old cookie value is replaced with lightgreen!
  5. Test HttpOnly Security: 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_token is completely invisible to JavaScript because we set cookie.HttpOnly = true!
  6. Inspect Cookies in Browser DevTools:
    • Press F12 in 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_color and secret_token, their expiration dates (30 days from today), and a checkmark under the HttpOnly column for secret_token!
  7. 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

GoalC# Code
Issue (create) or change a cookieHttpCookie c = new HttpCookie("name", "value");
Response.Cookies.Add(c);
Keep cookie after browser closesc.Expires = DateTime.Now.AddDays(30);
Read a cookie on the serverHttpCookie c = Request.Cookies["name"]; (check if (c != null)!)
Delete a cookieIssue 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!