How does http::HeaderValue::from_str validate header values against HTTP specifications?

http::HeaderValue::from_str validates that the input string contains only valid HTTP header value characters according to RFC 7230, returning an error if the string contains invalid bytes. Valid header values are restricted to visible ASCII characters (0x21-0x7E), space (0x20), and horizontal tab (0x09)—control characters and non-ASCII bytes are forbidden. The method also allows obs-fold sequences (CRLF followed by space or tab) in specific contexts, though modern HTTP discourages their use. The validation ensures that the resulting HeaderValue can be safely transmitted over HTTP without encoding issues, as HTTP/1.1 headers are transmitted as ASCII text and intermediaries may reject malformed values.

What is a HeaderValue?

use http::HeaderValue;
 
// HeaderValue represents a valid HTTP header value
// It's a thin wrapper around bytes guaranteed to be valid
 
// Create from static string (compile-time validation)
let content_type = HeaderValue::from_static("application/json");
 
// Create from dynamic string (runtime validation)
let custom = HeaderValue::from_str("text/html; charset=utf-8").unwrap();
 
// Header values can also be created from bytes
let bytes = HeaderValue::from_bytes(b"valid value").unwrap();

HeaderValue is a type that guarantees its contents are valid for HTTP headers.

Basic Validation with from_str

use http::HeaderValue;
 
fn main() {
    // Valid: printable ASCII
    let valid = HeaderValue::from_str("application/json").unwrap();
    println!("Valid: {:?}", valid);
    
    // Valid: contains space
    let with_space = HeaderValue::from_str("text/html; charset=utf-8").unwrap();
    println!("With space: {:?}", with_space);
    
    // Valid: contains tab
    let with_tab = HeaderValue::from_str("value\twith\ttab").unwrap();
    println!("With tab: {:?}", with_tab);
    
    // Invalid: contains newline
    let result = HeaderValue::from_str("invalid\nvalue");
    match result {
        Ok(_) => println!("Unexpected success"),
        Err(e) => println!("Error: {}", e), // Invalid header value
    }
    
    // Invalid: contains control character
    let result2 = HeaderValue::from_str("invalid\x01value");
    match result2 {
        Ok(_) => println!("Unexpected success"),
        Err(e) => println!("Error: {}", e),
    }
}

from_str validates at runtime, returning Result<HeaderValue, InvalidHeaderValue>.

Valid Characters According to RFC 7230

use http::HeaderValue;
 
fn main() {
    // RFC 7230 defines valid header value characters:
    // - VCHAR: visible ASCII (0x21-0x7E)
    // - SP: space (0x20)
    // - HTAB: horizontal tab (0x09)
    // - obs-fold: CRLF followed by SP or HTAB (deprecated)
    
    // Visible ASCII characters (printable, no control chars)
    let visible = HeaderValue::from_str("!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~").unwrap();
    println!("All visible ASCII works");
    
    // Space (0x20) is allowed
    let spaces = HeaderValue::from_str("value with spaces").unwrap();
    
    // Horizontal tab (0x09) is allowed
    let tabs = HeaderValue::from_str("value\twith\ttabs").unwrap();
    
    // Control characters (0x00-0x1F except HTAB) are forbidden
    // NUL, CR, LF, etc.
    assert!(HeaderValue::from_str("bad\x00value").is_err()); // NUL
    assert!(HeaderValue::from_str("bad\rvalue").is_err());   // CR
    assert!(HeaderValue::from_str("bad\nvalue").is_err());   // LF
    assert!(HeaderValue::from_str("bad\x1Fvalue").is_err()); // Unit Separator
    
    // DEL (0x7F) is also forbidden
    assert!(HeaderValue::from_str("bad\x7Fvalue").is_err()); // DEL
    
    // Non-ASCII bytes (> 0x7F) are forbidden in from_str
    assert!(HeaderValue::from_str("café").is_err()); // é is non-ASCII
}

The validation follows HTTP/1.1 specifications for header field values.

Handling Validation Errors

use http::HeaderValue;
 
fn validate_header_value(input: &str) -> Result<HeaderValue, String> {
    HeaderValue::from_str(input).map_err(|e| {
        format!("Invalid header value '{}': {}", input, e)
    })
}
 
fn main() {
    // Safe validation with error context
    match validate_header_value("valid-value") {
        Ok(value) => println!("Valid: {}", value),
        Err(e) => println!("Error: {}", e),
    }
    
    match validate_header_value("invalid\nvalue") {
        Ok(value) => println!("Valid: {}", value),
        Err(e) => println!("Error: {}", e),
    }
}

The error type InvalidHeaderValue implements Display for error messages.

from_str vs from_bytes

use http::HeaderValue;
 
fn main() {
    // from_str: validates UTF-8 and HTTP validity
    // Rejects non-ASCII bytes
    let str_result = HeaderValue::from_str("value");
    println!("from_str valid: {:?}", str_result.is_ok());
    
    // from_bytes: only validates HTTP validity
    // Allows any bytes that are valid for HTTP (including non-UTF-8)
    let bytes_result = HeaderValue::from_bytes(b"value");
    println!("from_bytes valid: {:?}", bytes_result.is_ok());
    
    // from_bytes allows non-UTF-8 sequences that are HTTP-valid
    // (though such usage is unusual and typically indicates binary data)
    let binary_result = HeaderValue::from_bytes(&[0x20, 0x21, 0x22]);
    println!("from_bytes binary: {:?}", binary_result.is_ok());
    
    // Both reject control characters
    assert!(HeaderValue::from_bytes(b"bad\nvalue").is_err());
    assert!(HeaderValue::from_str("bad\nvalue").is_err());
}

from_str validates UTF-8 encoding plus HTTP validity; from_bytes only validates HTTP validity.

from_static for Compile-Time Validation

use http::HeaderValue;
 
fn main() {
    // from_static validates at compile time
    // Panics if invalid (shouldn't happen for known-good constants)
    const CONTENT_TYPE_JSON: HeaderValue = HeaderValue::from_static("application/json");
    
    // Useful for known constants
    let content_length = HeaderValue::from_static("0");
    let connection = HeaderValue::from_static("keep-alive");
    
    // Compile-time check ensures these are always valid
    // No runtime overhead for validation
}

from_static is const and validates at compile time for known-good values.

Obs-Fold Handling

use http::HeaderValue;
 
fn main() {
    // RFC 7230 allows "obs-fold" (obsolete line folding)
    // CRLF followed by at least one SP or HTAB
    // This is deprecated but still accepted
    
    // Modern usage: single line
    let modern = HeaderValue::from_str("value one two three").unwrap();
    
    // Obs-fold form (historically valid)
    // In practice, most implementations convert to spaces
    // Note: from_str may or may not accept this depending on version
    // Let's check:
    
    let with_fold = "value\r\n one";
    match HeaderValue::from_str(with_fold) {
        Ok(v) => println!("Obs-fold accepted: {:?}", v),
        Err(e) => println!("Obs-fold rejected: {}", e),
    }
}

Obs-fold is a deprecated legacy feature for multi-line header values.

Common Header Value Patterns

use http::HeaderValue;
 
fn main() {
    // Content-Type
    let content_type = HeaderValue::from_str("application/json; charset=utf-8").unwrap();
    
    // Content-Length (numeric)
    let content_length = HeaderValue::from_str("12345").unwrap();
    
    // Cache-Control
    let cache_control = HeaderValue::from_str("max-age=3600, public").unwrap();
    
    // Authorization (Bearer token)
    let auth = HeaderValue::from_str("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").unwrap();
    
    // User-Agent
    let user_agent = HeaderValue::from_str("Mozilla/5.0 (compatible; MyBot/1.0)").unwrap();
    
    // Custom headers must also follow the rules
    let custom = HeaderValue::from_str("X-Custom-Header-Value").unwrap();
}

All standard headers follow the same validation rules.

Invalid Characters in Practice

use http::HeaderValue;
 
fn main() {
    // Common invalid characters that cause validation to fail:
    
    // 1. Newlines (CRLF injection prevention)
    assert!(HeaderValue::from_str("value\nwith\nnewlines").is_err());
    assert!(HeaderValue::from_str("value\r\nwith\r\ncrlf").is_err());
    
    // 2. Null bytes
    assert!(HeaderValue::from_str("value\x00null").is_err());
    
    // 3. Non-ASCII characters (UTF-8 but outside ASCII)
    assert!(HeaderValue::from_str("café").is_err());        // é = 0xC3 0xA9
    assert!(HeaderValue::from_str("日本語").is_err());      // Japanese chars
    assert!(HeaderValue::from_str("emoji 😀").is_err());   // Emoji
    
    // 4. Other control characters
    assert!(HeaderValue::from_str("value\x01control").is_err()); // SOH
    assert!(HeaderValue::from_str("value\x02control").is_err()); // STX
    assert!(HeaderValue::from_str("value\x1bcontrol").is_err()); // ESC
    
    // 5. DEL character
    assert!(HeaderValue::from_str("value\x7f").is_err());
}

These restrictions prevent header injection attacks and ensure HTTP compatibility.

Converting Invalid Values

use http::HeaderValue;
 
fn safe_header_value(input: &str) -> HeaderValue {
    // If valid, use directly
    if let Ok(value) = HeaderValue::from_str(input) {
        return value;
    }
    
    // If invalid, we need to decide how to handle:
    
    // Option 1: percent-encode (depends on context)
    // Not directly supported by http crate
    
    // Option 2: Base64 encode for binary data
    let encoded = base64_encode(input);
    HeaderValue::from_str(&encoded).unwrap()
    
    // Option 3: Use percent encoding for URLs in headers
    // Use url::percent_encode for URL encoding
    
    // Option 4: Use from_bytes if you have controlled binary data
    // Only for valid HTTP bytes, not arbitrary binary
}
 
fn base64_encode(input: &str) -> String {
    use base64::{Engine as _, engine::general_purpose};
    general_purpose::STANDARD.encode(input.as_bytes())
}

When dealing with potentially invalid values, you must sanitize or encode.

Encoding Non-ASCII Values

use http::HeaderValue;
 
fn main() {
    // Non-ASCII values need encoding before use
    
    // For Content-Disposition filenames with Unicode:
    // RFC 6266 specifies the encoding
    
    // Option 1: Percent encode (URL encoding)
    fn percent_encode_filename(filename: &str) -> String {
        use url::percent_encoding::{percent_encode, NON_ALPHANUMERIC};
        percent_encode(filename.as_bytes(), NON_ALPHANUMERIC).to_string()
    }
    
    // Option 2: Base64 for binary data
    fn base64_encode_value(data: &[u8]) -> String {
        use base64::{Engine as _, engine::general_purpose};
        general_purpose::STANDARD.encode(data)
    }
    
    // Option 3: RFC 5987 encoding for Content-Disposition
    fn rfc5987_encode(value: &str) -> String {
        // Format: charset'lang'encoded-value
        // Example: UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E
        format!("UTF-8''{}", 
            url::percent_encoding::percent_encode(value.as_bytes(), 
                url::percent_encoding::NON_ALPHANUMERIC))
    }
    
    // After encoding, the result should be valid ASCII
    let encoded = base64_encode_value("binary\x00data".as_bytes());
    let header_value = HeaderValue::from_str(&encoded).unwrap();
}

Encode non-ASCII or binary values to ASCII before creating header values.

Using HeaderValue with HeaderMap

use http::{HeaderMap, HeaderName, HeaderValue};
 
fn main() {
    let mut headers = HeaderMap::new();
    
    // Create headers with validated values
    headers.insert(
        HeaderName::from_static("content-type"),
        HeaderValue::from_static("application/json"),
    );
    
    headers.insert(
        HeaderName::from_static("content-length"),
        HeaderValue::from_str("12345").unwrap(),
    );
    
    // Dynamic values need validation
    let user_agent = format!("MyApp/{}", "1.0.0");
    headers.insert(
        HeaderName::from_static("user-agent"),
        HeaderValue::from_str(&user_agent).unwrap(),
    );
    
    // Accessing header values
    if let Some(content_type) = headers.get("content-type") {
        println!("Content-Type: {}", content_type.to_str().unwrap());
    }
}
 
// Helper to safely add headers
fn add_header(
    headers: &mut HeaderMap,
    name: &str,
    value: &str
) -> Result<(), String> {
    let header_name = HeaderName::from_static(name);
    let header_value = HeaderValue::from_str(value)
        .map_err(|e| format!("Invalid header value: {}", e))?;
    headers.insert(header_name, header_value);
    Ok(())
}

HeaderMap stores HeaderValue instances, ensuring all values are pre-validated.

to_str for Safe Conversion Back

use http::HeaderValue;
 
fn main() {
    let value = HeaderValue::from_str("application/json").unwrap();
    
    // to_str converts back to &str
    // Returns Result because even valid HTTP bytes might not be valid UTF-8
    // (though from_str guarantees UTF-8, from_bytes might not)
    let s = value.to_str().unwrap();
    println!("Value as str: {}", s);
    
    // to_bytes for raw access
    let bytes = value.as_bytes();
    println!("Value as bytes: {:?}", bytes);
    
    // as_str for known-safe values (from_static)
    // Only use when you're certain the value came from from_static or from_str
}

to_str() safely converts back to a string reference when possible.

Security Implications

use http::HeaderValue;
 
fn main() {
    // Header injection attacks are prevented by validation
    
    // Attacker tries to inject new header via value
    let malicious = "application/json\r\nX-Injected: malicious";
    
    // This fails validation
    assert!(HeaderValue::from_str(malicious).is_err());
    
    // The CR and LF characters are rejected
    
    // Without validation, an attacker could:
    // HTTP/1.1 200 OK
    // Content-Type: application/json
    // X-Injected: malicious   <- injected!
    
    // Always validate untrusted input:
    fn safe_from_user_input(input: &str) -> Option<HeaderValue> {
        // Reject if contains control characters
        if input.chars().any(|c| c.is_control() && c != '\t') {
            return None;
        }
        HeaderValue::from_str(input).ok()
    }
}

Validation prevents header injection and request smuggling attacks.

Comparison with Other Validation Approaches

use http::HeaderValue;
 
fn main() {
    // Manual validation (error-prone)
    fn manual_validate(value: &str) -> bool {
        value.chars().all(|c| {
            c.is_ascii_graphic() || c == ' ' || c == '\t'
        })
    }
    
    // Problem: doesn't catch all edge cases
    // Problem: doesn't handle obs-fold
    // Problem: different interpretations of "valid"
    
    // Using HeaderValue::from_str
    // - Follows RFC 7230 precisely
    // - Handles all edge cases
    // - Consistent with other HTTP libraries
    // - Returns meaningful error
    
    // Example:
    let test_value = "application/json";
    
    // Manual might miss edge cases
    assert!(manual_validate(test_value));
    
    // HeaderValue is authoritative
    assert!(HeaderValue::from_str(test_value).is_ok());
}

Using HeaderValue::from_str ensures RFC-compliant validation.

Real-World Example: Building HTTP Response Headers

use http::{HeaderValue, HeaderMap, HeaderName};
 
fn build_response_headers(
    content_type: &str,
    content_length: u64,
    custom_headers: Vec<(&str, &str)>
) -> Result<HeaderMap, String> {
    let mut headers = HeaderMap::new();
    
    // Standard headers
    headers.insert(
        HeaderName::from_static("content-type"),
        HeaderValue::from_str(content_type)
            .map_err(|e| format!("Invalid content-type: {}", e))?
    );
    
    headers.insert(
        HeaderName::from_static("content-length"),
        HeaderValue::from_str(&content_length.to_string()).unwrap()
    );
    
    // Custom headers - validate each
    for (name, value) in custom_headers {
        let header_name = HeaderName::from_bytes(name.as_bytes())
            .map_err(|e| format!("Invalid header name '{}': {}", name, e))?;
        let header_value = HeaderValue::from_str(value)
            .map_err(|e| format!("Invalid header value for '{}': {}", name, e))?;
        headers.insert(header_name, header_value);
    }
    
    Ok(headers)
}
 
fn main() {
    match build_response_headers(
        "application/json",
        12345,
        vec![
            ("X-Request-Id", "abc-123"),
            ("X-Custom", "value"),
        ]
    ) {
        Ok(headers) => println!("Headers: {:?}", headers),
        Err(e) => println!("Error: {}", e),
    }
}

Production code should always validate header values from external sources.

Real-World Example: Parsing User Input for Headers

use http::HeaderValue;
 
// Safely handle user-provided header values
fn parse_user_header_value(user_input: String) -> Result<HeaderValue, String> {
    // Trim whitespace
    let trimmed = user_input.trim();
    
    // Validate
    HeaderValue::from_str(trimmed)
        .map_err(|e| {
            format!(
                "Header value contains invalid characters. \
                 Only printable ASCII, space, and tab are allowed: {}",
                e
            )
        })
}
 
// Handle Unicode content
fn parse_filename_header(filename: &str) -> Result<HeaderValue, String> {
    // ASCII filenames can go directly
    if filename.is_ascii() {
        return HeaderValue::from_str(filename)
            .map_err(|e| e.to_string());
    }
    
    // Non-ASCII needs encoding (e.g., Content-Disposition filename)
    // Use RFC 5987 encoding
    let encoded = format!("UTF-8''{}", 
        url::percent_encoding::percent_encode(
            filename.as_bytes(),
            url::percent_encoding::NON_ALPHANUMERIC
        )
    );
    
    HeaderValue::from_str(&encoded)
        .map_err(|e| e.to_string())
}
 
fn main() {
    // Test with user input
    match parse_user_header_value("valid-value".to_string()) {
        Ok(v) => println!("Valid: {}", v),
        Err(e) => println!("Error: {}", e),
    }
    
    match parse_user_header_value("invalid\nvalue".to_string()) {
        Ok(v) => println!("Valid: {}", v),
        Err(e) => println!("Error: {}", e),
    }
}

User input must always be validated before use in headers.

Synthesis

Valid characters for HTTP header values (RFC 7230):

Range Characters Allowed
0x00-0x08 Control chars ❌ No
0x09 Horizontal tab ✅ Yes
0x0A-0x1F Control chars ❌ No
0x20 Space ✅ Yes
0x21-0x7E Printable ASCII ✅ Yes
0x7F DEL ❌ No
0x80-0xFF Non-ASCII ❌ No (in from_str)

Methods comparison:

Method Input Validation Use Case
from_static &'static str Compile-time Known constants
from_str &str Runtime UTF-8 + HTTP Dynamic text
from_bytes &[u8] Runtime HTTP only Binary data
from_name HeaderName Typed headers

Common invalid characters:

Character Byte Why invalid
NUL 0x00 Control character
CR 0x0D Control, injection risk
LF 0x0A Control, injection risk
ESC 0x1B Control character
DEL 0x7F Control character
é 0xC3 0xA9 Non-ASCII (UTF-8)

Key insight: http::HeaderValue::from_str enforces HTTP/1.1 header value constraints by validating that input contains only visible ASCII characters (0x21-0x7E), space (0x20), and horizontal tab (0x09)—rejecting all control characters (including newlines) and non-ASCII bytes. This validation prevents header injection attacks where malicious CR/LF characters could inject fake headers into HTTP messages. The resulting HeaderValue type is a proof of validity that can be safely inserted into HeaderMap without additional checks. For non-ASCII content like filenames in Content-Disposition, applications must encode values before validation using RFC 5987 percent-encoding or base64. The distinction between from_str (validates UTF-8 plus HTTP constraints) and from_bytes (validates HTTP constraints only) matters for binary data that might be valid for HTTP but not valid UTF-8. Compile-time validation with from_static is preferred for constant header values to eliminate runtime overhead.