a2a_protocol_server/dispatch/cors.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Tom F.
3
4//! CORS (Cross-Origin Resource Sharing) configuration for A2A dispatchers.
5//!
6//! Browser-based A2A clients need CORS headers to interact with agents.
7//! [`CorsConfig`] provides configurable CORS support that can be applied to
8//! both [`RestDispatcher`](super::RestDispatcher) and
9//! [`JsonRpcDispatcher`](super::JsonRpcDispatcher).
10
11use std::convert::Infallible;
12
13use bytes::Bytes;
14use http_body_util::combinators::BoxBody;
15use http_body_util::{BodyExt, Full};
16
17/// CORS configuration for A2A dispatchers.
18///
19/// # Examples
20///
21/// ```
22/// use a2a_protocol_server::dispatch::cors::CorsConfig;
23///
24/// // Allow all origins (development/testing).
25/// let cors = CorsConfig::permissive();
26///
27/// // Restrict to a specific origin.
28/// let cors = CorsConfig::new("https://my-app.example.com");
29/// ```
30#[derive(Debug, Clone)]
31pub struct CorsConfig {
32 /// The `Access-Control-Allow-Origin` value.
33 pub allow_origin: String,
34 /// The `Access-Control-Allow-Methods` value.
35 pub allow_methods: String,
36 /// The `Access-Control-Allow-Headers` value.
37 pub allow_headers: String,
38 /// The `Access-Control-Max-Age` value in seconds.
39 pub max_age_secs: u32,
40}
41
42impl CorsConfig {
43 /// Creates a new [`CorsConfig`] with the given allowed origin.
44 #[must_use]
45 pub fn new(allow_origin: impl Into<String>) -> Self {
46 Self {
47 allow_origin: allow_origin.into(),
48 allow_methods: "GET, POST, PUT, DELETE, OPTIONS".into(),
49 allow_headers: "content-type, authorization, a2a-notification-token".into(),
50 max_age_secs: 86400,
51 }
52 }
53
54 /// Creates a permissive [`CorsConfig`] that allows all origins.
55 ///
56 /// Suitable for development or public APIs. For production use,
57 /// prefer [`CorsConfig::new`] with a specific origin.
58 #[must_use]
59 pub fn permissive() -> Self {
60 Self::new("*")
61 }
62
63 /// Applies CORS headers to an existing HTTP response.
64 pub fn apply_headers<B>(&self, resp: &mut hyper::Response<B>) {
65 let headers = resp.headers_mut();
66 // These `parse()` calls only fail on invalid header values containing
67 // control characters, which our constructors don't produce.
68 if let Ok(v) = self.allow_origin.parse() {
69 headers.insert("access-control-allow-origin", v);
70 }
71 if let Ok(v) = self.allow_methods.parse() {
72 headers.insert("access-control-allow-methods", v);
73 }
74 if let Ok(v) = self.allow_headers.parse() {
75 headers.insert("access-control-allow-headers", v);
76 }
77 if let Ok(v) = self.max_age_secs.to_string().parse() {
78 headers.insert("access-control-max-age", v);
79 }
80 }
81
82 /// Builds a preflight (OPTIONS) response with CORS headers.
83 #[must_use]
84 pub fn preflight_response(&self) -> hyper::Response<BoxBody<Bytes, Infallible>> {
85 let mut resp = hyper::Response::builder()
86 .status(204)
87 .body(Full::new(Bytes::new()).boxed())
88 .unwrap_or_else(|_| hyper::Response::new(Full::new(Bytes::new()).boxed()));
89 self.apply_headers(&mut resp);
90 resp
91 }
92}