http-cookie_jar v0.2.0
HTTP::CookieJar
Standalone HTTP cookie jar for Crystal stdlib HTTP types.
HTTP::CookieJar builds on top of:
HTTP::CookieHTTP::CookiesHTTP::HeadersURI
Status
This shard is usable today, but still early-stage.
It already covers the main workflow needed by real HTTP clients:
- extract cookies from responses
- store them in memory
- return matching cookies for later requests
- generate request
Cookieheaders
It does not yet aim for full feature parity with Python's http.cookiejar or complete RFC 6265 edge-case coverage.
Current functionality:
- in-memory cookie storage
- add single cookies or
HTTP::Cookies - extract cookies from
Set-Cookieresponse headers - return cookies for a request URI
- add the
Cookieheader to request headers - RFC-oriented domain matching
- RFC-oriented path matching
- host-only and domain cookie distinction
- replacement by cookie name/domain/path
- ordered cookie return by path specificity
- clear all cookies
- clear cookies by domain/path/name
- clear expired cookies
Max-Ageaware expiration handling- immediate removal of expired replacement cookies
- JSON persistence
- file-backed cookie jars
- Mozilla / Netscape
cookies.txtpersistence - domain normalization
- RFC-style cookie path normalization and defaulting
- host validation during acceptance and return
- configurable cookie policy
Installation
Add this to your application's shard.yml:
dependencies:
cookie_jar:
github: mamantoha/http-cookie_jar
Then require it:
require "cookie_jar"
Usage
Create a jar
require "cookie_jar"
jar = HTTP::CookieJar.new
Add cookies
jar.add(
"https://example.com/login",
HTTP::Cookie.new("session_id", "abc123")
)
jar.add(
"https://example.com/login",
HTTP::Cookie.new("theme", "dark", path: "/", secure: true)
)
Add multiple cookies
cookies = HTTP::Cookies.new
cookies << HTTP::Cookie.new("session_id", "abc123")
cookies << HTTP::Cookie.new("theme", "dark")
jar.add("https://example.com/login", cookies)
Extract cookies from response headers
headers = HTTP::Headers{
"Set-Cookie" => [
"session_id=abc123; Path=/; HttpOnly",
"theme=dark; Path=/",
],
}
jar.extract(headers, "https://example.com/login")
Return cookies for a request URI
cookies = jar.cookies_for("https://example.com/account")
cookies["session_id"].value
# => "abc123"
Domain and path matching
jar.add(
"https://example.com/login",
HTTP::Cookie.new("session_id", "abc123", domain: "example.com", path: "/account")
)
jar.cookies_for("https://api.example.com/account/settings")["session_id"].value
# => "abc123"
jar.cookies_for("https://api.example.com/admin")["session_id"]?
# => nil
Cookies are returned in a stable order, with more specific paths first.
Add the Cookie header to request headers
headers = HTTP::Headers.new
jar.add_cookie_header(headers, "https://example.com/account")
headers["Cookie"]?
# => "session_id=abc123; theme=dark"
Use with HTTP::Client
client = HTTP::Client.new(URI.parse("https://example.com"))
jar = HTTP::CookieJar.new
login_headers = HTTP::Headers.new
jar.add_cookie_header(login_headers, "https://example.com/login")
login_response = client.post("/login", headers: login_headers, body: "user=john&password=secret")
jar.extract(login_response.headers, "https://example.com/login")
account_headers = HTTP::Headers.new
jar.add_cookie_header(account_headers, "https://example.com/account")
account_response = client.get("/account", headers: account_headers)
Use with HTTP::Client request hooks
client = HTTP::Client.new(URI.parse("https://example.com"))
jar = HTTP::CookieJar.new
client.before_request do |request|
uri = URI.parse("https://example.com#{request.resource}")
jar.add_cookie_header(request.headers, uri)
end
response = client.get("/login")
jar.extract(response.headers, "https://example.com/login")
This pattern lets the jar stay independent while still fitting naturally into a stateful client workflow.
Clear cookies
jar.clear
jar.clear("example.com")
jar.clear("example.com", "/")
jar.clear("example.com", "/", "session_id")
jar.clear_expired
Replacement and deletion semantics
jar.add("https://example.com", HTTP::Cookie.new("session_id", "old", path: "/"))
jar.add("https://example.com", HTTP::Cookie.new("session_id", "new", path: "/"))
jar.cookies_for("https://example.com/account")["session_id"].value
# => "new"
jar.add(
"https://example.com",
HTTP::Cookie.new("session_id", "", path: "/", max_age: Time::Span.zero)
)
jar.cookies_for("https://example.com/account")["session_id"]?
# => nil
Persist the jar to disk
jar = HTTP::MozillaCookieJar.new("cookies.txt")
jar.extract(login_response.headers, "https://example.com/login")
jar.save(include_session: true)
restored = HTTP::MozillaCookieJar.new("cookies.txt")
restored.load
restored.cookies_for("https://example.com/account")
Use include_session: true when you want to persist session cookies for later reuse.
JSON persistence
File.write("cookiejar.json", jar.to_json)
restored = HTTP::CookieJar.from_json(File.read("cookiejar.json"))
restored.cookies_for("https://example.com/account")
Use a Mozilla / Netscape cookie jar
jar = HTTP::MozillaCookieJar.new("cookies.txt")
jar.add(
"https://example.com/login",
HTTP::Cookie.new("session_id", "abc123", path: "/", expires: Time.utc + 1.day)
)
jar.save
jar.load
This format is compatible with the classic Netscape cookies.txt layout used by tools such as curl.
Policy
HTTP::CookieJar accepts a policy object.
The default policy is HTTP::DefaultCookiePolicy.
policy = HTTP::DefaultCookiePolicy.new(
allowed_domains: ["example.com"],
blocked_domains: ["tracker.example"],
)
jar = HTTP::CookieJar.new(policy: policy)
Current policy behavior:
- optional domain allow-list
- optional domain block-list
- host-only cookies only return to the original host
- domain cookies may return to matching subdomains
- domain cookies are rejected for IP hosts
- low-quality domain attributes such as
comare rejected - optional country-code second-level domain rejection (
strict_domain) - path matching is enforced for returned cookies
- secure cookies are only returned for secure schemes
- empty request hosts are rejected
Strict domain policy
Like Python's http.cookiejar.DefaultCookiePolicy, the default policy offers an optional strict_domain heuristic, disabled by default:
policy = HTTP::DefaultCookiePolicy.new(strict_domain: true)
jar = HTTP::CookieJar.new(policy: policy)
When enabled, it rejects two-label Domain attributes with a two-character final label and a common second-level label (for example, co.uk, gov.uk, or co.nz). Case and leading dots are normalized. These Domain attributes are also rejected when they equal the response host; host-only cookies remain supported. Valid parent-domain cookies such as example.co.uk remain supported.
This is a limited heuristic, not Public Suffix List validation. It does not cover private suffixes such as github.io, wildcard rules, or exception rules, and may reject registrable domains that fit the pattern. The default remains permissive, so the attacker.co.uk to victim.co.uk injection remains possible unless strict mode is enabled. Like Python's option, this affects acceptance only; it does not revalidate cookies loaded from storage. No dependency, bundled list, Python installation, or network access is required.
API
Main types:
HTTP::CookieJarHTTP::CookieJar::EntryHTTP::CookiePolicyHTTP::DefaultCookiePolicyHTTP::FileCookieJarHTTP::MozillaCookieJar
Main methods:
HTTP::CookieJar#addHTTP::CookieJar#extractHTTP::CookieJar#cookies_forHTTP::CookieJar#add_cookie_headerHTTP::CookieJar#clearHTTP::CookieJar#clear_expiredHTTP::CookieJar#to_jsonHTTP::CookieJar.from_jsonHTTP::FileCookieJar#saveHTTP::FileCookieJar#load
Samples
- samples/httpbin.cr simple cookie round-trip with
HTTP::Client - samples/login_and_dashboard.cr login flow with later authenticated request
- samples/persisted_dashboard_session.cr reuse a persisted cookie jar without sending login credentials again
Development
Run the specs from the shard root:
crystal spec
Contributors
- mamantoha Anton Maminov - creator, maintainer
License
Copyright: 2026 Anton Maminov (anton.maminov@gmail.com)
This library is distributed under the MIT license. Please see the LICENSE file.
http-cookie_jar
- 0
- 0
- 0
- 1
- 0
- 5 days ago
- March 26, 2026
MIT License
Wed, 21 Oct 2026 15:58:45 GMT