How does http::uri::Builder::scheme enable safe URI construction with validation?

http::uri::Builder::scheme provides a type-safe, validated approach to constructing URIs by enforcing scheme correctness at compile time where possible and validating at runtime when necessary. The builder pattern ensures that invalid schemes cannot be set, preventing malformed URIs from being constructed. Each builder method validates its input and returns a Result or the builder itself, allowing for chained construction that maintains invariants throughout the building process. This approach catches errors early—during construction rather than when the URI is used—providing clear error messages and preventing invalid URIs from propagating through the application.

Basic URI Builder Usage

use http::uri::{Builder, Scheme, Uri};
 
fn main() {
    // Build a URI step by step
    let uri: Uri = Builder::new()
        .scheme("https")
        .authority("example.com")
        .path_and_query("/path?query=value")
        .build()
        .expect("Invalid URI");
    
    println!("URI: {}", uri);
    println!("Scheme: {}", uri.scheme().unwrap());
    println!("Authority: {}", uri.authority().unwrap());
}

The builder pattern allows constructing URIs incrementally with validation at each step.

Scheme Validation

use http::uri::Builder;
 
fn main() {
    // Valid schemes
    let valid = Builder::new()
        .scheme("https")
        .authority("example.com")
        .path_and_query("/")
        .build();
    println!("Valid scheme: {:?}", valid.is_ok());
    
    // Invalid scheme (contains invalid characters)
    let invalid = Builder::new()
        .scheme("http$")
        .authority("example.com")
        .path_and_query("/")
        .build();
    println!("Invalid scheme: {:?}", invalid);
    
    // Scheme must start with letter
    let invalid_start = Builder::new()
        .scheme("123http")
        .authority("example.com")
        .path_and_query("/")
        .build();
    println!("Invalid start: {:?}", invalid_start);
}

Schemes are validated according to RFC 3986: they must start with a letter and contain only letters, digits, +, -, or ..

Using Scheme Type Directly

use http::uri::{Builder, Scheme};
 
fn main() {
    // Create a typed Scheme
    let scheme: Scheme = "https".parse().unwrap();
    
    // Use typed Scheme with builder
    let uri = Builder::new()
        .scheme(scheme.clone())
        .authority("example.com")
        .path_and_query("/api")
        .build()
        .unwrap();
    
    println!("URI: {}", uri);
    
    // Common schemes are predefined
    let http_scheme = Scheme::HTTP;
    let https_scheme = Scheme::HTTPS;
    
    println!("HTTP scheme: {}", http_scheme);
    println!("HTTPS scheme: {}", https_scheme);
}

Scheme type provides compile-time safety for known schemes and validation for dynamic values.

Common Scheme Constants

use http::uri::Scheme;
 
fn main() {
    // Predefined schemes
    let http: Scheme = Scheme::HTTP;
    let https: Scheme = Scheme::HTTPS;
    
    println!("HTTP: {}", http);
    println!("HTTPS: {}", https);
    
    // These are constants, not strings
    // No validation needed
    let uri = http::uri::Builder::new()
        .scheme(Scheme::HTTPS)
        .authority("rust-lang.org")
        .path_and_query("/")
        .build()
        .unwrap();
    
    println!("URI: {}", uri);
}

Scheme::HTTP and Scheme::HTTPS are predefined constants that bypass validation overhead.

Scheme String Requirements

use http::uri::Builder;
 
fn main() {
    // Valid scheme characters
    let examples = [
        "http",           // Lowercase letters
        "HTTP",           // Uppercase letters
        "https",          // Mixed use
        "my-scheme",      // With hyphen
        "my.scheme",      // With dot
        "my+scheme",      // With plus
        "scheme123",      // With digits (not at start)
        "a",              // Single letter
    ];
    
    for scheme in examples {
        let result = Builder::new()
            .scheme(scheme)
            .authority("example.com")
            .path_and_query("/")
            .build();
        println!("{}: valid={}", scheme, result.is_ok());
    }
    
    // Invalid scheme examples
    let invalid_examples = [
        "123scheme",      // Starts with digit
        "scheme name",    // Contains space
        "scheme:name",    // Contains colon
        "scheme/",        // Contains slash
        "",               // Empty
    ];
    
    for scheme in invalid_examples {
        let result = Builder::new()
            .scheme(scheme)
            .authority("example.com")
            .path_and_query("/")
            .build();
        println!("{}: valid={}", scheme, result.is_ok());
    }
}

Schemes must follow RFC 3986: letter followed by letters, digits, +, -, or ..

Error Handling for Invalid Schemes

use http::uri::Builder;
 
fn build_uri(scheme: &str, host: &str) -> Result<http::Uri, String> {
    Builder::new()
        .scheme(scheme)
        .authority(host)
        .path_and_query("/")
        .build()
        .map_err(|e| format!("URI build failed: {}", e))
}
 
fn main() {
    match build_uri("https", "example.com") {
        Ok(uri) => println!("Built: {}", uri),
        Err(e) => println!("Error: {}", e),
    }
    
    match build_uri("invalid scheme", "example.com") {
        Ok(uri) => println!("Built: {}", uri),
        Err(e) => println!("Error: {}", e),
    }
}

Builder returns Result allowing proper error handling for invalid inputs.

Building Complete URIs

use http::uri::{Builder, Scheme, Authority, PathAndQuery};
 
fn main() {
    // Method 1: String components
    let uri1 = Builder::new()
        .scheme("https")
        .authority("api.example.com:8080")
        .path_and_query("/users/123?active=true")
        .build()
        .unwrap();
    println!("URI 1: {}", uri1);
    
    // Method 2: Typed components
    let scheme: Scheme = "https".parse().unwrap();
    let authority: Authority = "api.example.com:8080".parse().unwrap();
    let path: PathAndQuery = "/users/123?active=true".parse().unwrap();
    
    let uri2 = Builder::new()
        .scheme(scheme)
        .authority(authority)
        .path_and_query(path)
        .build()
        .unwrap();
    println!("URI 2: {}", uri2);
    
    // Both produce identical URIs
    assert_eq!(uri1, uri2);
}

Components can be provided as strings (parsed and validated) or as typed values.

Incremental Building

use http::uri::Builder;
 
fn main() {
    // Start with empty builder
    let mut builder = Builder::new();
    
    // Add components conditionally
    let use_https = true;
    let has_port = true;
    
    builder = if use_https {
        builder.scheme("https")
    } else {
        builder.scheme("http")
    };
    
    builder = builder.authority(if has_port {
        "example.com:8080"
    } else {
        "example.com"
    });
    
    builder = builder.path_and_query("/api/v1");
    
    // Build at the end
    let uri = builder.build().unwrap();
    println!("URI: {}", uri);
}

Builder supports incremental construction with conditional components.

Partial URI Construction

use http::uri::Builder;
 
fn main() {
    // Scheme-only (for relative references)
    let scheme_only = Builder::new()
        .scheme("https")
        .build()
        .unwrap();
    println!("Scheme only: {}", scheme_only);
    
    // Scheme and authority only
    let no_path = Builder::new()
        .scheme("https")
        .authority("example.com")
        .build()
        .unwrap();
    println!("No path: {}", no_path);
    
    // All components
    let complete = Builder::new()
        .scheme("https")
        .authority("example.com")
        .path_and_query("/path")
        .build()
        .unwrap();
    println!("Complete: {}", complete);
}

The builder can create partial URIs with only some components.

Modifying Existing URIs

use http::Uri;
 
fn main() {
    // Start with existing URI
    let original: Uri = "http://example.com/path".parse().unwrap();
    
    // Create parts for modification
    let mut builder = http::uri::Builder::new();
    
    // Copy components, changing scheme
    builder = builder
        .scheme("https")
        .authority(original.authority().unwrap().clone())
        .path_and_query(original.path_and_query().unwrap().clone());
    
    let modified = builder.build().unwrap();
    println!("Original: {}", original);
    println!("Modified: {}", modified);
}

Existing URIs can be deconstructed and rebuilt with modifications.

Scheme Case Normalization

use http::uri::{Builder, Scheme};
 
fn main() {
    // Schemes are case-insensitive per RFC 3986
    let uri1 = Builder::new()
        .scheme("HTTP")
        .authority("example.com")
        .path_and_query("/")
        .build()
        .unwrap();
    
    let uri2 = Builder::new()
        .scheme("http")
        .authority("example.com")
        .path_and_query("/")
        .build()
        .unwrap();
    
    // Both URIs are equivalent
    println!("URI1: {}", uri1);
    println!("URI2: {}", uri2);
    
    // Scheme comparison is case-insensitive
    let scheme1: Scheme = "HTTP".parse().unwrap();
    let scheme2: Scheme = "http".parse().unwrap();
    
    println!("Schemes equal: {}", scheme1 == scheme2);
}

Schemes are normalized and compared case-insensitively.

Custom Schemes

use http::uri::Builder;
 
fn main() {
    // Custom schemes are allowed if valid
    let custom = Builder::new()
        .scheme("myapp")
        .authority("localhost")
        .path_and_query("/resource")
        .build()
        .unwrap();
    println!("Custom scheme: {}", custom);
    
    // URI schemes for custom protocols
    let schemes = [
        "ftp", "ws", "wss", "file", "mailto", "tel",
        "my-custom-app", "app+extension"
    ];
    
    for scheme in schemes {
        let uri = Builder::new()
            .scheme(scheme)
            .authority("host")
            .path_and_query("/")
            .build();
        println!("{}: {:?}", scheme, uri.is_ok());
    }
}

Custom schemes work if they follow RFC 3986 character rules.

Integration with HTTP Requests

use http::{uri::Builder, Request, Method};
 
fn build_request(scheme: &str, host: &str, path: &str) -> Result<Request<String>, http::Error> {
    let uri = Builder::new()
        .scheme(scheme)
        .authority(host)
        .path_and_query(path)
        .build()?;
    
    Request::builder()
        .method(Method::GET)
        .uri(uri)
        .body(String::new())
}
 
fn main() {
    match build_request("https", "api.github.com", "/users/rust-lang") {
        Ok(request) => {
            println!("Method: {}", request.method());
            println!("URI: {}", request.uri());
        }
        Err(e) => println!("Error: {}", e),
    }
}

Built URIs integrate directly with HTTP request construction.

Builder Consumption

use http::uri::Builder;
 
fn main() {
    // Builder is consumed by build()
    let builder = Builder::new()
        .scheme("https")
        .authority("example.com")
        .path_and_query("/");
    
    let uri1 = builder.build();
    println!("First build: {:?}", uri1.is_ok());
    
    // Cannot use builder after build()
    // let uri2 = builder.build(); // Error: builder moved
    
    // Create new builder for another URI
    let uri2 = Builder::new()
        .scheme("http")
        .authority("example.com")
        .path_and_query("/")
        .build()
        .unwrap();
    println!("Second URI: {}", uri2);
}

build() consumes the builder, enforcing single-use semantics.

URI Parts Access

use http::Uri;
 
fn main() {
    let uri: Uri = http::uri::Builder::new()
        .scheme("https")
        .authority("user:pass@host.com:8080")
        .path_and_query("/path?query=value")
        .build()
        .unwrap();
    
    println!("Full URI: {}", uri);
    
    // Access individual parts
    if let Some(scheme) = uri.scheme() {
        println!("Scheme: {}", scheme);
    }
    
    if let Some(authority) = uri.authority() {
        println!("Authority: {}", authority);
        println!("  Host: {}", authority.host());
        if let Some(port) = authority.port() {
            println!("  Port: {}", port);
        }
    }
    
    if let Some(path) = uri.path_and_query() {
        println!("Path: {}", path.path());
        if let Some(query) = path.query() {
            println!("Query: {}", query);
        }
    }
}

The resulting URI provides typed access to all components.

Validation Guarantees

use http::uri::Builder;
 
fn main() {
    // The builder ensures invariants:
    
    // 1. Valid scheme characters
    let bad_chars = Builder::new()
        .scheme("scheme with spaces")
        .build();
    assert!(bad_chars.is_err());
    
    // 2. Valid authority format
    let bad_auth = Builder::new()
        .scheme("https")
        .authority("not valid authority")
        .build();
    assert!(bad_auth.is_err());
    
    // 3. Valid path format
    let bad_path = Builder::new()
        .scheme("https")
        .authority("example.com")
        .path_and_query("invalid path")
        .build();
    assert!(bad_path.is_err());
    
    // Successful builds produce valid URIs
    let valid = Builder::new()
        .scheme("https")
        .authority("example.com")
        .path_and_query("/valid/path")
        .build();
    assert!(valid.is_ok());
    
    println!("All validations passed");
}

Each component is validated independently before building.

Comparison with String Parsing

use http::{Uri, uri::Builder};
 
fn main() {
    // Method 1: Parse from string
    let parsed: Uri = "https://example.com/path?query=value"
        .parse()
        .unwrap();
    println!("Parsed: {}", parsed);
    
    // Method 2: Build from components
    let built = Builder::new()
        .scheme("https")
        .authority("example.com")
        .path_and_query("/path?query=value")
        .build()
        .unwrap();
    println!("Built: {}", built);
    
    // Both produce equivalent URIs
    assert_eq!(parsed.to_string(), built.to_string());
    
    // Builder advantages:
    // 1. Type-safe components
    // 2. Clear validation errors
    // 3. Incremental construction
    // 4. Conditional component setting
}

Builder provides more control than parsing complete URI strings.

Synthesis

Scheme validation rules (RFC 3986):

Rule Description
First character Must be a letter (A-Z, a-z)
Subsequent characters Letters, digits, +, -, .
Case insensitive HTTP = http = HtTp
No reserved characters No :, /, ?, #, etc.

Builder method types:

Method Input Type Returns
scheme(&str) String slice Builder
scheme(Scheme) Typed scheme Builder
build() - Result<Uri, Error>

Predefined schemes:

Constant Value
Scheme::HTTP "http"
Scheme::HTTPS "https"

Builder vs string parsing:

Aspect Builder String Parsing
Type safety Typed components String only
Error location Per-component Whole string
Conditional building Supported Difficult
Incremental Supported Not applicable

Key insight: http::uri::Builder::scheme enables safe URI construction by validating each component independently before combining them into a complete URI. The builder pattern provides three key safety guarantees: (1) scheme validation ensures only valid RFC 3986 characters are accepted, preventing malformed URIs at construction time; (2) typed components (Scheme, Authority, PathAndQuery) can be pre-parsed and reused, avoiding redundant validation; (3) the build() method returns a Result, forcing explicit error handling rather than panicking on invalid input. This approach catches errors early—during URI construction—rather than when the URI is used in network operations, providing clear error messages about which component failed validation. The builder also supports incremental and conditional construction, making it suitable for configuration-driven URI generation where components may vary based on runtime conditions.