Skip to content

Commit 55dac6b

Browse files
authored
Propagate server errors as HTTP errors
1 parent 2eebcfe commit 55dac6b

File tree

17 files changed

+538
-151
lines changed

17 files changed

+538
-151
lines changed

.cursor/rules/typescript-coding-guidelines.mdc

Lines changed: 0 additions & 79 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
### Added
3232
- Added basic unit tests
3333
- Added publisher config
34+
- Add AI assist rules. Based on https://github.com/hashintel/hash
3435

3536
### Changed
3637
- Upgrade to rust 1.87.0
3738
- Upgrade to fastly-cli 11.3.0
3839
- Changed to use constants for headers
3940
- Changed to use log statements
4041
- Updated fastly.toml for local development
42+
- Changed to propagate server errors as HTTP errors
43+
44+
### Fixed
45+
- Rebuild when `TRUSTED_SERVER__*` env variables change
4146

4247
### Fixed
4348
- Rebuild when `TRUSTED_SERVER__*` env variables change

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ cargo check
6464
### Configuration Files
6565
- `fastly.toml`: Fastly service configuration and build settings
6666
- `trusted-server.toml`: Application settings (ad servers, KV stores, ID templates)
67-
- `rust-toolchain.toml`: Pins Rust version to 1.83.0
67+
- `rust-toolchain.toml`: Pins Rust version to 1.87.0
6868

6969
### Key Functionality Areas
7070
1. **Synthetic ID Generation**: Privacy-preserving user identification using HMAC

Cargo.lock

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/common/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ license = "Apache-2.0"
1212
chrono = "0.4"
1313
config = "0.15.11"
1414
cookie = "0.18.1"
15+
derive_more = { version = "1.0", features = ["display", "error"] }
16+
error-stack = "0.5"
1517
fastly = "0.11.5"
1618
futures = "0.3"
1719
handlebars = "6.3.2"
@@ -30,6 +32,9 @@ url = "2.4.1"
3032
serde = { version = "1.0", features = ["derive"] }
3133
serde_json = "1.0.91"
3234
config = "0.15.11"
35+
derive_more = { version = "1.0", features = ["display", "error"] }
36+
error-stack = "0.5"
37+
http = "1.3.1"
3338

3439
[dev-dependencies]
3540
regex = "1.1.1"

crates/common/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#[path = "src/error.rs"]
2+
mod error;
3+
14
#[path = "src/settings.rs"]
25
mod settings;
36

crates/common/src/cookies.rs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
//! Cookie handling utilities.
2+
//!
3+
//! This module provides functionality for parsing and creating cookies
4+
//! used in the trusted server system.
5+
16
use cookie::{Cookie, CookieJar};
7+
use error_stack::{Report, ResultExt};
28
use fastly::http::header;
39
use fastly::Request;
410

11+
use crate::error::TrustedServerError;
512
use crate::settings::Settings;
613

714
const COOKIE_MAX_AGE: i32 = 365 * 24 * 60 * 60; // 1 year
815

9-
// return empty cookie jar for unparsable cookies
16+
/// Parses a cookie string into a [`CookieJar`].
17+
///
18+
/// Returns an empty jar if the cookie string is unparseable.
19+
/// Individual invalid cookies are skipped rather than failing the entire parse.
1020
pub fn parse_cookies_to_jar(s: &str) -> CookieJar {
1121
let cookie_str = s.trim().to_owned();
1222
let mut jar = CookieJar::new();
@@ -19,20 +29,39 @@ pub fn parse_cookies_to_jar(s: &str) -> CookieJar {
1929
jar
2030
}
2131

22-
pub fn handle_request_cookies(req: &Request) -> Option<CookieJar> {
32+
/// Extracts and parses cookies from an HTTP request.
33+
///
34+
/// Attempts to parse the Cookie header into a [`CookieJar`] for easy access
35+
/// to individual cookies.
36+
///
37+
/// # Errors
38+
///
39+
/// - [`TrustedServerError::InvalidHeaderValue`] if the Cookie header contains invalid UTF-8
40+
pub fn handle_request_cookies(
41+
req: &Request,
42+
) -> Result<Option<CookieJar>, Report<TrustedServerError>> {
2343
match req.get_header(header::COOKIE) {
2444
Some(header_value) => {
25-
let header_value_str: &str = header_value.to_str().unwrap_or("");
26-
let jar: CookieJar = parse_cookies_to_jar(header_value_str);
27-
Some(jar)
45+
let header_value_str =
46+
header_value
47+
.to_str()
48+
.change_context(TrustedServerError::InvalidHeaderValue {
49+
message: "Cookie header contains invalid UTF-8".to_string(),
50+
})?;
51+
let jar = parse_cookies_to_jar(header_value_str);
52+
Ok(Some(jar))
2853
}
2954
None => {
30-
log::warn!("No cookie header found in request");
31-
None
55+
log::debug!("No cookie header found in request");
56+
Ok(None)
3257
}
3358
}
3459
}
3560

61+
/// Creates a synthetic ID cookie string.
62+
///
63+
/// Generates a properly formatted cookie with security attributes
64+
/// for storing the synthetic ID.
3665
pub fn create_synthetic_cookie(settings: &Settings, synthetic_id: &str) -> String {
3766
format!(
3867
"synthetic_id={}; Domain={}; Path=/; Secure; SameSite=Lax; Max-Age={}",
@@ -84,7 +113,9 @@ mod tests {
84113
#[test]
85114
fn test_handle_request_cookies() {
86115
let req = Request::get("http://example.com").with_header(header::COOKIE, "c1=v1;c2=v2");
87-
let jar = handle_request_cookies(&req).unwrap();
116+
let jar = handle_request_cookies(&req)
117+
.expect("should parse cookies")
118+
.expect("should have cookie jar");
88119

89120
assert!(jar.iter().count() == 2);
90121
assert_eq!(jar.get("c1").unwrap().value(), "v1");
@@ -94,23 +125,27 @@ mod tests {
94125
#[test]
95126
fn test_handle_request_cookies_with_empty_cookie() {
96127
let req = Request::get("http://example.com").with_header(header::COOKIE, "");
97-
let jar = handle_request_cookies(&req).unwrap();
128+
let jar = handle_request_cookies(&req)
129+
.expect("should parse cookies")
130+
.expect("should have cookie jar");
98131

99132
assert!(jar.iter().count() == 0);
100133
}
101134

102135
#[test]
103136
fn test_handle_request_cookies_no_cookie_header() {
104137
let req: Request = Request::get("https://example.com");
105-
let jar = handle_request_cookies(&req);
138+
let jar = handle_request_cookies(&req).expect("should handle missing cookie header");
106139

107140
assert!(jar.is_none());
108141
}
109142

110143
#[test]
111144
fn test_handle_request_cookies_invalid_cookie_header() {
112145
let req = Request::get("http://example.com").with_header(header::COOKIE, "invalid");
113-
let jar = handle_request_cookies(&req).unwrap();
146+
let jar = handle_request_cookies(&req)
147+
.expect("should parse cookies")
148+
.expect("should have cookie jar");
114149

115150
assert!(jar.iter().count() == 0);
116151
}

0 commit comments

Comments
 (0)