hyper icon indicating copy to clipboard operation
hyper copied to clipboard

Original Header Cases API

Open seanmonstar opened this issue 4 years ago • 14 comments

This is a tracking issue for the feature to allow for sending and receiving the exact casing of header names, instead of the forced lowercasing of http. In most situations, simply forcing to lowercase is the better option, as it allows for a more performant HeaderMap, and is the required format for newer versions of HTTP (HTTP/2 and 3). However, there are rare occasions when being able to see the exact casing is needed.

Some initial support is available in hyper, and is exposed in the C API. But a public Rust API is not yet available.

  • [x] Initial internal support
  • [x] Expose in C API (#2278)
  • [x] Provide builder options to enable support in Rust, useful to proxies (#2480)
  • [ ] Design and export the public extension API

seanmonstar avatar Nov 16 '21 19:11 seanmonstar

any recent progress? I'm also having issues with header casing

kulame avatar Aug 15 '22 04:08 kulame

We update with progress as it happens, it's all here. I'd recommend filing a bug with the server you're interacting with, it is wrongly assuming that HTTP headers have meaning.

seanmonstar avatar Aug 15 '22 04:08 seanmonstar

in my case, the openconenct client assume the HTTP header is "X-CSTP-*" and it never convert to lowercase.

the client code is openconnect client

		if (strncmp(buf, "X-DTLS-", 7) &&
		    strncmp(buf, "X-CSTP-", 7) &&
		    strncmp(buf, "X-DTLS12-", 9))
			continue;

bad situation this is client code, there are millions of users relying on this client to use VPN. it is impossible to make these users upgrade their openconnect client.

so the server must be return the uppercase header.

kulame avatar Aug 15 '22 05:08 kulame

We've also been bitten by this unfortunately. We know and realize that HTTP headers should be lower case.

Alas, some of our customers have compatibility requirements because they've been sending them as upper case (or camel case, depending) and some of their customers rely on them being of specific casing.

I'd be really grateful for even an experimental feature that we could enable to support this, because our current (terrible) alternative is to maintain our own Hyper fork. :(

tasn avatar Nov 22 '22 02:11 tasn

The last item on the todo list above says "Design and export the public extension API". If I understand correctly, "extension" there refers to the http crate's extension concept. That would imply that this continues to be a hyper feature that uses extensions as a hack to encode header cases (and possibly order) separately from the actual header map of the http::Request / http::Response. IMHO that is a rather inelegant solution, and it becomes a non-solution as soon as other HTTP implementations (e.g. curl via the isahc crate) enter the picture. Are alternative solutions at the http crate level up for consideration¹?

¹ for example (though I haven't thought this through all the way): adding a defaulted generic parameter for the headermap type to http::Request, http::Response and thus allowing different header maps that chose other trade-offs than https to be written while having interoparability with other crates using http, if those are updated to be generic over the headermap type

svix-jplatte avatar Feb 02 '24 17:02 svix-jplatte

The idea of making all the types in http actually be traits has been thought of a few times, and it becomes extremely complicated for users when all the things are generic.

trait Request {
    type Method;
    type RequestTarget;
    type Version;
    type Headers: Headers;
}

trait Headers {
    type Name;
    type Value;
    type Iter;
    type IterMut;
    // etc
}

I have liked the idea of them being traits, but it hasn't seemed practical to users. It'd be cool if we could come up with a solution. But, that is all better for a different topic.


This issue is about the support in hyper. So, I'm going to hide these comments.

seanmonstar avatar Feb 05 '24 15:02 seanmonstar

How about this as a public API?

pub trait HttpMessageExt {
    fn headers_case_sensitive(&self) -> CaseSensitiveHeaderMap<'_>;
    fn headers_case_sensitive_mut(&mut self) -> CaseSensitiveHeaderMapMut<'_>;
}

impl<T> HttpMessageExt for http::Request<T> { /* ... */ }
impl<T> HttpMessageExt for http::Response<T> { /* ... */ }

// CaseSensitiveHeaderMap and CaseSensitiveHeaderMapMut methods would be more or less obvious, I guess?

For CaseSensitiveHeaderMapMut to work, we'd need extra API in http to allow mutably borrowing headers and extensions simultaneously, but I think that would "just" be a matter of

// request.rs
impl<T> Request<T> {
    pub fn parts_mut(&mut self) -> (PartsMut<'_>, &mut T) { /* ... */ }
}

pub struct PartsMut<'a> {
    /// The request's method
    pub method: &'a mut Method,

    /// The request's URI
    pub uri: &'a mut Uri,

    /// The request's version
    pub version: &'a mut Version,

    /// The request's headers
    pub headers: &'a mut HeaderMap<HeaderValue>,

    /// The request's extensions
    pub extensions: &'a mut Extensions,

    _priv: (),
}

// response.rs - equivalent with slightly different fields

svix-jplatte avatar Mar 28 '24 15:03 svix-jplatte

Hm. What would be the specific way a user would interact with this? What would the methods of HeaderCaseMap look like?

seanmonstar avatar Mar 29 '24 13:03 seanmonstar

Some examples

let headers = response.headers_case_sensitive();
// Check for a header using the exact provided casing
if let Some(val) = message.headers_case_sensitive().get("Foo-Bar") {
    // read val: HeaderValue
}

let mut headers = request.headers_case_sensitive();
// Insert a header using the exact provided casing
headers.insert("Foo-Bar", <header value>);

Methods-wise, I could see this type having all of the methods (not constructors of course) of HeaderMap plus even a few _ignore_case ones (if you want to check an existing header ignoring casing to insert a new one with specific casing from one map object), but starting small, these seems like the important ones:

impl<'a> CaseSensitiveHeaderMap<'a> {
    pub fn contains_key(&self, key: impl AsCaseSensitiveHeaderName) -> bool;
    pub fn get(&self, key: impl AsaseSensitiveHeaderName) -> Option<&'a HeaderValue>;
    // GetAll<'a>: IntoIterator<Item = &'a HeaderValue>
    pub fn get_all(&self, key: impl AsCaseSensitiveHeaderName) -> GetAll<'a>;
    // Iter<'a>: Iterator<Item = (&'a CaseSensitiveHeaderName, &'a HeaderValue)
    pub fn iter(&self) -> Iter<'a>;
}

impl<'a> CaseSensitiveHeaderMapMut<'a> {
    // same methods as CaseSensitiveHeaderMap
    pub fn append(&mut self, key: impl IntoCaseSensitiveHeaderName, value: HeaderValue) -> bool;
    pub fn insert(&mut self, key: impl IntoCaseSensitiveHeaderName, value: HeaderValue) -> Option<HeaderValue>;
    // also try_append, try_insert
    pub fn get_mut(&mut self, key: impl IntoCaseSensitiveHeaderName) -> Option<&mut HeaderValue);
    pub fn remove(&mut self, key: impl IntoCaseSensitiveHeaderName) -> Option<HeaderValue>;
    // IterMut<'a>: Iterator<Item = (&'a CaseSensitiveHeaderName, &'a mut HeaderValue)>
    pub fn iter_mut(&mut self) -> IterMut<'_>;
}

pub trait AsCaseSensitiveHeaderName: hdr_as::Sealed {}
pub trait IntoCaseSensitiveHeaderName: hdr_into::Sealed {}

// similar impls, sealed traits as for upstream traits

#[repr(transparent)]
struct CaseSensitiveHeaderName([u8]);

svix-jplatte avatar Apr 02 '24 09:04 svix-jplatte