use crate::{Error, ErrorKind, RawRequest, Result};
use http::{HeaderMap, HeaderName, HeaderValue};
#[derive(Clone, Default)]
pub struct GatewayRouting {
headers: HeaderMap,
bytes: usize,
}
crate::redaction::redacted_debug!(GatewayRouting);
impl GatewayRouting {
pub fn new() -> Self {
Self::default()
}
pub fn header(mut self, name: &str, value: &str) -> Result<Self> {
if name.len() > 128 || value.len() > 8192 {
return Err(Error::new(ErrorKind::Limit));
}
let name = HeaderName::from_bytes(name.as_bytes())
.map_err(|_| Error::new(ErrorKind::Validation))?;
let allowed = name.as_str() == "openai-beta"
|| name
.as_str()
.strip_prefix("x-portkey-")
.is_some_and(|suffix| {
!suffix.is_empty()
&& suffix.bytes().all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-'
})
&& suffix != "api-key"
});
if !allowed || self.headers.contains_key(&name) {
return Err(Error::new(ErrorKind::Validation));
}
let bytes = self
.bytes
.checked_add(name.as_str().len())
.and_then(|n| n.checked_add(value.len()))
.ok_or_else(|| Error::new(ErrorKind::Limit))?;
if self.headers.len() >= 32 || bytes > 12 * 1024 {
return Err(Error::new(ErrorKind::Limit));
}
let mut value =
HeaderValue::from_str(value).map_err(|_| Error::new(ErrorKind::Validation))?;
value.set_sensitive(true);
self.headers.insert(name, value);
self.bytes = bytes;
Ok(self)
}
pub(crate) fn apply(&self, request: &mut RawRequest) -> Result<()> {
request.routing_headers(&self.headers)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RuntimeReplay {
#[default]
Never,
Explicit(RuntimeReplayPermit),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RuntimeReplayPermit {
retries: usize,
}
impl RuntimeReplay {
pub fn bounded(retries: usize) -> Result<Self> {
if retries > 5 {
return Err(Error::new(ErrorKind::Configuration));
}
Ok(Self::Explicit(RuntimeReplayPermit { retries }))
}
pub(crate) fn apply(self, request: &mut RawRequest) -> Result<()> {
request.runtime_replay(match self {
Self::Never => None,
Self::Explicit(permit) => Some(permit.retries),
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn routing_is_case_insensitive_bounded_and_redacted() {
for name in [
"authorization",
"X-Portkey-Api-Key",
"host",
"x-tsg-id",
"cookie",
] {
assert!(GatewayRouting::new().header(name, "secret").is_err());
}
let routing = GatewayRouting::new()
.header("X-Portkey-Provider", "private")
.unwrap();
assert!(!format!("{routing:?}").contains("private"));
assert!(routing.header("x-portkey-provider", "other").is_err());
assert!(
GatewayRouting::new()
.header("openai-beta", "x\r\nY:z")
.is_err()
);
assert!(
GatewayRouting::new()
.header("openai-beta", &"x".repeat(8193))
.is_err()
);
assert!(RuntimeReplay::bounded(6).is_err());
assert_eq!(RuntimeReplay::default(), RuntimeReplay::Never);
}
}