bunner_cors_rs

Crates.iobunner_cors_rs
lib.rsbunner_cors_rs
version0.1.2
created_at2025-10-07 17:34:45.324198+00
updated_at2025-10-12 13:29:06.437739+00
descriptionA lightweight CORS (Cross-Origin Resource Sharing) core library for Rust
homepagehttps://github.com/parkrevil/bunner-cors-rs
repositoryhttps://github.com/parkrevil/bunner-cors-rs
max_upload_size
id1872078
size438,289
박준형 (parkrevil)

documentation

https://github.com/parkrevil/bunner-cors-rs

README

bunner-cors-rs

Crates.io version tests coverage License

English | 한국어


✨ Introduction

bunner-cors-rs is a library that provides CORS decision-making and header generation.

  • Standards Compliant: Adheres to WHATWG Fetch standard and CORS specification

  • Configuration Validation: Blocks invalid option combinations at creation time

  • Origin Matching: Supports exact strings, lists, regular expressions, and custom logic

  • Private Network Access: PNA header support for preflight requests

  • Thread-safe: Cors instances can be shared

  • Framework Neutral: Does not depend on HTTP request/response types

[!IMPORTANT] This library does not provide HTTP server or middleware functionality, so you must write integration code tailored to your framework.


📚 Table of Contents


🚀 Getting Started

Installation

Add the library using cargo add:

cargo add bunner_cors_rs

Or add it directly to Cargo.toml:

[dependencies]
bunner_cors_rs = "0.1.0"

Quick Start

The example below uses the http crate to construct responses and demonstrates how to turn the result returned by Cors::check() into an actual HTTP response.

use bunner_cors_rs::{
    Cors, CorsDecision, CorsError, CorsOptions, Headers, Origin, RequestContext,
};
use http::{header::HeaderName, HeaderValue, Response, StatusCode};

fn apply_headers(target: &mut http::HeaderMap, headers: Headers) {
    for (name, value) in headers {
        let name = HeaderName::from_bytes(name.as_bytes()).expect("valid header name");
        let value = HeaderValue::from_str(&value).expect("valid header value");
        target.insert(name, value);
    }
}

fn handle_request(cors: &Cors, ctx: RequestContext<'_>) -> Result<Response<String>, CorsError> {
    match cors.check(&ctx)? {
        CorsDecision::PreflightAccepted { headers } => {
            let mut response = Response::builder()
                .status(StatusCode::NO_CONTENT)
                .body(String::new())
                .unwrap();
            apply_headers(response.headers_mut(), headers);
            Ok(response)
        }
        CorsDecision::PreflightRejected(rejection) => {
            let mut response = Response::builder()
                .status(StatusCode::FORBIDDEN)
                .body(String::new())
                .unwrap();
            apply_headers(response.headers_mut(), rejection.headers);
            Ok(response)
        }
        CorsDecision::SimpleAccepted { headers } => {
            let mut response = Response::builder()
                .status(StatusCode::OK)
                .body("application response".into())
                .unwrap();
            apply_headers(response.headers_mut(), headers);
            Ok(response)
        }
        CorsDecision::SimpleRejected(rejection) => {
            let mut response = Response::builder()
                .status(StatusCode::FORBIDDEN)
                .body(String::new())
                .unwrap();
            apply_headers(response.headers_mut(), rejection.headers);
            Ok(response)
        }
        CorsDecision::NotApplicable => Ok(Response::builder()
            .status(StatusCode::OK)
            .body("non-CORS response".into())
            .unwrap()),
    }
}

let cors = Cors::new(CorsOptions::new()).expect("valid configuration");

let request = RequestContext {
    method: "GET",
    origin: Some("https://example.com"),
    access_control_request_method: None,
    access_control_request_headers: None,
    access_control_request_private_network: false,
};

match handle_request(&cors, request) {
    Ok(response) => {
        println!("status: {}", response.status());
    }
    Err(error) => {
        eprintln!("CORS error: {error}");
    }
}

[!TIP] Create the Cors instance once at application startup and reuse it.


⚙️ CorsOptions

Configure CorsOptions to match your application requirements. The table below lists the default values of CorsOptions.

Option Default Description
origin Origin::Any Allow all origins
methods ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"] Common HTTP methods
allowed_headers AllowedHeaders::default() No explicitly allowed headers
exposed_headers ExposedHeaders::default() No exposed headers
credentials false Credentials not allowed
max_age None Preflight cache not configured
allow_null_origin false Does not allow the null origin
allow_private_network false Private network access not allowed
timing_allow_origin None Timing information not exposed

origin

Specifies which origins to allow.

Origin::Any

Allows all origins.

use bunner_cors_rs::{CorsOptions, Origin};

let options = CorsOptions::new();
Access-Control-Allow-Origin: *
Vary: Origin

[!IMPORTANT] Origin::Any cannot be used when credentials: true.

Origin::exact

Use when allowing only a single origin.

let options = CorsOptions::new()
    .origin(Origin::exact("https://app.example.com"))
    .credentials(true);
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Origin::list

Explicitly allows multiple origins.

use bunner_cors_rs::{CorsOptions, OriginMatcher};

let options = CorsOptions::new()
    .origin(Origin::list(vec![
        OriginMatcher::exact("https://app.example.com"),
        OriginMatcher::exact("https://admin.example.com"),
    ]));
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

OriginMatcher::pattern_str

Flexible matching using regular expressions.

let options = CorsOptions::new()
    .origin(Origin::list(vec![
        OriginMatcher::pattern_str(r"https://.*\\.example\\.com")
            .expect("valid pattern"),
    ]));
Access-Control-Allow-Origin: https://api.example.com
Vary: Origin

[!CAUTION] Pattern length is limited to 50,000 characters and compile time to 100ms. Exceeding these limits will raise a PatternError.

Origin::predicate

Allows you to set custom validation logic. Returns the request Origin as-is when returning true, rejects when returning false.

let options = CorsOptions::new()
    .origin(Origin::predicate(|origin, _ctx| {
        origin.ends_with(".trusted.com") || origin == "https://partner.io"
    }));
Access-Control-Allow-Origin: https://api.trusted.com
Vary: Origin

Origin::disabled

Disables CORS evaluation. Returns OriginDecision::Skip, so CorsDecision::NotApplicable is returned and no CORS headers are generated.

let options = CorsOptions::new().origin(Origin::disabled());

let decision = cors.check(&request_context)?;
assert!(matches!(decision, CorsDecision::NotApplicable));

Origin::custom

Directly controls OriginDecision to implement complex logic:

use bunner_cors_rs::OriginDecision;

let options = CorsOptions::new()
    .origin(Origin::custom(|maybe_origin, ctx| {
        match maybe_origin {
            Some(origin) if origin.starts_with("https://") => {
                if origin.ends_with(".trusted.com") {
                    OriginDecision::Mirror
                } else if origin == "https://special.partner.io" {
                    OriginDecision::Exact("https://partner.io".into())
                } else {
                    OriginDecision::Disallow
                }
            }
            Some(_) => OriginDecision::Disallow,
            None => OriginDecision::Skip,
        }
    }));

[!WARNING] If a user callback returns OriginDecision::Any when credentials: true, a runtime error occurs. According to the CORS standard, credentials and wildcard origins cannot be used together.


methods

Specifies HTTP methods to allow in preflight and simple requests.

use bunner_cors_rs::{AllowedMethods, CorsOptions, Origin};

let options = CorsOptions::new()
    .methods(AllowedMethods::list(["GET", "POST", "DELETE"]));
Access-Control-Allow-Methods: GET,POST,DELETE

allowed_headers

Specifies headers the client can send in preflight requests.

use bunner_cors_rs::{AllowedHeaders, CorsOptions, Origin};

let options = CorsOptions::new()
    .allowed_headers(AllowedHeaders::list([
        "Content-Type",
        "Authorization",
        "X-Api-Key",
    ]));
Access-Control-Allow-Headers: Content-Type,Authorization,X-Api-Key

[!IMPORTANT]

  • AllowedHeaders::Any cannot be used when credentials: true.
  • "*" cannot be included in the allowed headers list. Use AllowedHeaders::Any if you need a wildcard.

exposed_headers

Specifies response headers to expose to the client in simple requests.

use bunner_cors_rs::{CorsOptions, ExposedHeaders, Origin};

let options = CorsOptions::new()
    .exposed_headers(ExposedHeaders::list(["X-Total-Count", "X-Page-Number"]));
Access-Control-Expose-Headers: X-Total-Count,X-Page-Number

[!IMPORTANT]

  • ExposedHeaders::Any cannot be used when credentials: true.
  • "*" cannot be mixed with other header names.

credentials

Specifies whether to allow requests with credentials.

let options = CorsOptions::new()
    .origin(Origin::exact("https://app.example.com"))
    .credentials(true);
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

[!IMPORTANT] When credentials: true, the following configurations cannot be used: Origin::Any, AllowedHeaders::Any, ExposedHeaders::Any, TimingAllowOrigin::Any.


max_age

Specifies the preflight response cache time in seconds.

let options = CorsOptions::new()
    .max_age(3600);
Access-Control-Max-Age: 3600

[!NOTE] Some(0) sends the Access-Control-Max-Age: 0 header. None does not send the header.


allow_null_origin

Specifies whether to allow requests with Origin header value "null".

let options = CorsOptions::new()
    .allow_null_origin(true);
Access-Control-Allow-Origin: null
Vary: Origin

allow_private_network

Allows Private Network Access requests.

let options = CorsOptions::new()
    .origin(Origin::exact("https://app.example.com"))
    .credentials(true)
    .allow_private_network(true);
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Private-Network: true
Vary: Origin

[!IMPORTANT] To use this option, credentials: true and a specific origin configuration are required.


timing_allow_origin

Specifies the Timing-Allow-Origin header.

use bunner_cors_rs::{CorsOptions, Origin, TimingAllowOrigin};

let options = CorsOptions::new()
    .timing_allow_origin(TimingAllowOrigin::list([
        "https://analytics.example.com",
    ]));
Timing-Allow-Origin: https://analytics.example.com

[!IMPORTANT] TimingAllowOrigin::Any cannot be used when credentials: true.


🚨 Errors

Validation Errors

Cors::new() returns a ValidationError if there are invalid configuration combinations. The main validation errors are:

Error Description
CredentialsRequireSpecificOrigin Cannot use Origin::Any when credentials: true
AllowedHeadersAnyNotAllowedWithCredentials Cannot use AllowedHeaders::Any when credentials: true
AllowedHeadersListCannotContainWildcard Cannot include "*" in allowed headers list (use AllowedHeaders::Any)
AllowedHeadersCannotContainEmptyToken Cannot include empty or whitespace-only values in allowed headers list
AllowedHeadersListContainsInvalidToken Allowed header is not a valid HTTP header name
ExposeHeadersWildcardRequiresCredentialsDisabled Need credentials: false to use "*" in exposed headers
ExposeHeadersWildcardCannotBeCombined Cannot specify "*" with other headers in exposed headers
ExposeHeadersCannotContainEmptyValue Cannot include empty or whitespace-only values in exposed headers list
ExposeHeadersListContainsInvalidToken Exposed header is not a valid HTTP header name
PrivateNetworkRequiresCredentials credentials: true required when allow_private_network: true
PrivateNetworkRequiresSpecificOrigin Cannot use Origin::Any when allow_private_network: true
AllowedMethodsCannotContainEmptyToken Cannot include empty or whitespace-only values in allowed methods list
AllowedMethodsCannotContainWildcard Cannot include "*" in allowed methods list
AllowedMethodsListContainsInvalidToken Allowed method is not a valid HTTP method token
TimingAllowOriginWildcardNotAllowedWithCredentials Cannot use TimingAllowOrigin::Any when credentials: true
TimingAllowOriginCannotContainEmptyValue Cannot include empty or whitespace-only values in Timing-Allow-Origin list

Runtime Errors

Cors::check() can return a CorsError.

Error Description
InvalidOriginAnyWithCredentials When Origin::custom callback returns OriginDecision::Any in a credentials: true situation (violates CORS standard)

📋 Request Evaluation and Result Handling

Preparing Request Context

HTTP request information must be converted to RequestContext for CORS evaluation.

Field Type HTTP Header Description
method &'a str Request method Actual HTTP method string ("GET", "POST", "OPTIONS", etc.)
origin Option<&'a str> Origin Request origin. Use None when the header is absent.
access_control_request_method Option<&'a str> Access-Control-Request-Method Method to execute in preflight request. None if absent
access_control_request_headers Option<&'a str> Access-Control-Request-Headers Comma-separated list of headers to use in preflight request. None if absent
access_control_request_private_network bool Access-Control-Request-Private-Network Header presence (true/false).
use bunner_cors_rs::RequestContext;

let context = RequestContext {
    method: "POST",
    origin: Some("https://app.example.com"),
    access_control_request_method: Some("POST"),
    access_control_request_headers: Some("content-type"),
    access_control_request_private_network: false,
};

let decision = cors.check(&context)?;

Processing Decision Results

cors.check() returns one of the following four results depending on request type and option combination.

Variant Return Condition Additional Description
PreflightAccepted OPTIONS request with allowed Origin, method, and headers Includes all CORS headers needed for preflight response
PreflightRejected OPTIONS request but Origin or requested method/headers not allowed Can check rejection reason via PreflightRejectionReason
SimpleAccepted Non-OPTIONS request with allowed Origin check and request method in allowed list Includes Access-Control-Allow-Origin and other necessary headers when origin is allowed
SimpleRejected Non-OPTIONS request with Disallow Origin check Returns rejection headers including Vary header
NotApplicable CORS processing not needed or should be skipped Cases like no Origin header, method not in allowed list, or using Origin::disabled()

PreflightAccepted

OPTIONS request succeeded. Add the returned headers to the response.

use bunner_cors_rs::CorsDecision;

match cors.check(&context)? {
    CorsDecision::PreflightAccepted { headers } => {
        let mut response = Response::builder().status(204).body(().into()).unwrap();

        for (name, value) in headers {
            response.headers_mut().insert(
                name.parse().unwrap(),
                value.parse().unwrap(),
            );
        }

        return response;
    }
    _ => {}
}

PreflightRejected

Returns this variant when origin is not allowed or requested method/headers violate policy. PreflightRejection.reason contains one of: OriginNotAllowed, MethodNotAllowed, HeadersNotAllowed.

CorsDecision::PreflightRejected(rejection) => {
    eprintln!("CORS Preflight Rejected: {:?}", rejection.reason);

    return Response::builder().status(403).body(().into()).unwrap();
}

SimpleAccepted

Simple request. Add the returned headers directly to the response.

CorsDecision::SimpleAccepted { headers } => {
    let mut response = HttpResponse::Ok();

    for (name, value) in headers {
        response.append_header((name, value));
    }

    return response.body(your_content);
}

SimpleRejected

Simple request with disallowed origin. Use the returned headers (e.g., Vary: Origin) with a rejection response.

CorsDecision::SimpleRejected(rejection) => {
    let mut response = HttpResponse::Forbidden();

    for (name, value) in rejection.headers {
        response.append_header((name, value));
    }

    return response.finish();
}

NotApplicable

CORS processing not needed. Do not add CORS headers.

📝 Examples

Framework-specific examples are in the /examples directory.

axum

cargo run --example axum
curl -X GET -H "Origin: http://api.example.com" -I http://127.0.0.1:5001/greet

Actix Web

cargo run --example actix
curl -X GET -H "Origin: http://api.example.com" -I http://127.0.0.1:5002/greet

hyper

cargo run --example hyper
curl -X GET -H "Origin: http://api.example.com" -I http://127.0.0.1:5003/greet

Testing

This library includes unit tests, integration tests, property-based tests, and snapshot tests.

# Run tests
make test

# Coverage
make coverage

# Benchmarks
make bench

❤️ Contributing

Contributions are not being accepted for a period of time. Updates will be provided when ready.

Please file an issue if you have problems or requests.

📜 License

MIT License. See LICENSE.md for details.

Commit count: 0

cargo fmt