| Crates.io | bunner_cors_rs |
| lib.rs | bunner_cors_rs |
| version | 0.1.2 |
| created_at | 2025-10-07 17:34:45.324198+00 |
| updated_at | 2025-10-12 13:29:06.437739+00 |
| description | A lightweight CORS (Cross-Origin Resource Sharing) core library for Rust |
| homepage | https://github.com/parkrevil/bunner-cors-rs |
| repository | https://github.com/parkrevil/bunner-cors-rs |
| max_upload_size | |
| id | 1872078 |
| size | 438,289 |
English | 한국어
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.
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"
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
Corsinstance once at application startup and reuse it.
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 |
originSpecifies which origins to allow.
Origin::AnyAllows all origins.
use bunner_cors_rs::{CorsOptions, Origin};
let options = CorsOptions::new();
Access-Control-Allow-Origin: *
Vary: Origin
[!IMPORTANT]
Origin::Anycannot be used whencredentials: true.
Origin::exactUse 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::listExplicitly 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_strFlexible 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::predicateAllows 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::disabledDisables 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::customDirectly 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::Anywhencredentials: true, a runtime error occurs. According to the CORS standard, credentials and wildcard origins cannot be used together.
methodsSpecifies 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_headersSpecifies 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::Anycannot be used whencredentials: true."*"cannot be included in the allowed headers list. UseAllowedHeaders::Anyif you need a wildcard.
exposed_headersSpecifies 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::Anycannot be used whencredentials: true."*"cannot be mixed with other header names.
credentialsSpecifies 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_ageSpecifies the preflight response cache time in seconds.
let options = CorsOptions::new()
.max_age(3600);
Access-Control-Max-Age: 3600
[!NOTE]
Some(0)sends theAccess-Control-Max-Age: 0header.Nonedoes not send the header.
allow_null_originSpecifies 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_networkAllows 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: trueand a specific origin configuration are required.
timing_allow_originSpecifies 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::Anycannot be used whencredentials: true.
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 |
Cors::check() can return a CorsError.
| Error | Description |
|---|---|
InvalidOriginAnyWithCredentials |
When Origin::custom callback returns OriginDecision::Any in a credentials: true situation (violates CORS standard) |
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)?;
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() |
PreflightAcceptedOPTIONS 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;
}
_ => {}
}
PreflightRejectedReturns 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();
}
SimpleAcceptedSimple 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);
}
SimpleRejectedSimple 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();
}
NotApplicableCORS processing not needed. Do not add CORS headers.
Framework-specific examples are in the /examples directory.
cargo run --example axum
curl -X GET -H "Origin: http://api.example.com" -I http://127.0.0.1:5001/greet
cargo run --example actix
curl -X GET -H "Origin: http://api.example.com" -I http://127.0.0.1:5002/greet
cargo run --example hyper
curl -X GET -H "Origin: http://api.example.com" -I http://127.0.0.1:5003/greet
This library includes unit tests, integration tests, property-based tests, and snapshot tests.
# Run tests
make test
# Coverage
make coverage
# Benchmarks
make bench
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.
MIT License. See LICENSE.md for details.