-
Notifications
You must be signed in to change notification settings - Fork 3
Use http::HeaderMap to store response headers #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f57370f
463ec41
ae7819c
b9195ce
c9b9a14
d75b36a
a798e8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| use std::{collections::HashMap, fmt::Debug}; | ||
|
|
||
| use http::HeaderMap; | ||
| use serde::Deserialize; | ||
| use url::Url; | ||
|
|
||
| use crate::login::WpApiDetails; | ||
| use crate::WpApiError; | ||
|
|
||
| use self::endpoint::WpEndpointUrl; | ||
|
|
@@ -55,23 +57,45 @@ impl Debug for WpNetworkRequest { | |
| } | ||
|
|
||
| // Has custom `Debug` trait implementation | ||
| #[derive(uniffi::Record)] | ||
| #[derive(uniffi::Object)] | ||
| pub struct WpNetworkResponse { | ||
| pub body: Vec<u8>, | ||
| pub status_code: u16, | ||
| // TODO: We probably want to implement a specific type for these headers instead of using a | ||
| // regular HashMap. | ||
| // | ||
| // It could be something similar to `reqwest`'s [`header`](https://docs.rs/reqwest/latest/reqwest/header/index.html) | ||
| // module. | ||
| pub header_map: Option<HashMap<String, String>>, | ||
| body: Vec<u8>, | ||
| status_code: u16, | ||
| header_map: HeaderMap, | ||
| } | ||
|
|
||
| #[uniffi::export] | ||
| impl WpNetworkResponse { | ||
| #[uniffi::constructor] | ||
| pub fn new( | ||
| body: Vec<u8>, | ||
| status_code: u16, | ||
| header_map: Option<HashMap<String, String>>, | ||
| ) -> Self { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we want to return a Following up on this comment, why creating a The way this is handled right now with Returning
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think this error case warrant a panic? My thinking is that this method is designed to be only used by the native wrappers, which would always do the right thing to pass in valid headers (transforming headers in native HTTP clients to a HashMap is very simple and reliable). So in reality, it should never crash in production. If it crashes, I imagine it'd be very likely a programming error, instead of a WordPress site returns an invalid HTTP header in their response. If we were to add an error here, I'd say declare it as a standalone error type instead of putting it into
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are hoping that we won't be the only consumers of this library, so I think it's important for us to properly handle this issue. However, I am not sure about the error type, can I think on it and get back to you?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @crazytonyli I've separated this into its own issue. #134 Since I am working on a major change at the moment, I'd like to merge this as soon as possible. |
||
| let header_map: HeaderMap = header_map | ||
| .and_then(|m| (&m).try_into().ok()) | ||
| .unwrap_or_default(); | ||
|
|
||
| Self { | ||
| body, | ||
| status_code, | ||
| header_map, | ||
| } | ||
| } | ||
|
|
||
| pub fn parse_api_details_response(&self) -> Result<WpApiDetails, WpApiError> { | ||
| serde_json::from_slice(&self.body).map_err(|err| WpApiError::ParsingError { | ||
| reason: err.to_string(), | ||
| response: self.body_as_string(), | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| impl WpNetworkResponse { | ||
| pub fn get_link_header(&self, name: &str) -> Option<Url> { | ||
| self.header_map | ||
| .as_ref() | ||
| .map(|h_map| h_map.get(LINK_HEADER_KEY))? | ||
| .get(LINK_HEADER_KEY) | ||
| .and_then(|v| v.to_str().ok()) | ||
| .and_then(|link_header| parse_link_header::parse_with_rel(link_header).ok()) | ||
| .and_then(|link_map| { | ||
| link_map | ||
|
|
@@ -187,11 +211,11 @@ mod tests { | |
| #[case] expected_prev_link_header: Option<&str>, | ||
| #[case] expected_next_link_header: Option<&str>, | ||
| ) { | ||
| let response = WpNetworkResponse { | ||
| body: Vec::with_capacity(0), | ||
| status_code: 200, | ||
| header_map: Some([("Link".to_string(), link.to_string())].into()), | ||
| }; | ||
| let response = WpNetworkResponse::new( | ||
| Vec::with_capacity(0), | ||
| 200, | ||
| Some([("Link".to_string(), link.to_string())].into()), | ||
| ); | ||
|
|
||
| assert_eq!( | ||
| expected_prev_link_header.and_then(|s| Url::parse(s).ok()), | ||
|
|
@@ -202,4 +226,49 @@ mod tests { | |
| response.get_link_header("next") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_headers_case_insentive() { | ||
| let headers: HashMap<String, String> = [ | ||
| ("server".to_string(), "nginx".to_string()), | ||
| ("x-nananana".to_string(), "Batcache-Hit".to_string()), | ||
| ( | ||
| "date".to_string(), | ||
| "Thu, 30 May 2024 23:52:17 GMT".to_string(), | ||
| ), | ||
| ( | ||
| "content-type".to_string(), | ||
| "text/html; charset=UTF-8".to_string(), | ||
| ), | ||
| ( | ||
| "strict-transport-security".to_string(), | ||
| "max-age=31536000".to_string(), | ||
| ), | ||
| ("vary".to_string(), "Accept-Encoding".to_string()), | ||
| ( | ||
| "Link".to_string(), | ||
| "<http://localhost/wp-json/wp/v2/posts?page=2>; rel=\"next\"".to_string(), | ||
| ), | ||
| ] | ||
| .into(); | ||
| let response = WpNetworkResponse::new(Vec::with_capacity(0), 200, Some(headers)); | ||
|
|
||
| assert_eq!(response.header_map.get("Server").unwrap(), "nginx"); | ||
| assert_eq!( | ||
| response.header_map.get("X-Nananana").unwrap(), | ||
| "Batcache-Hit" | ||
| ); | ||
| assert_eq!( | ||
| response.header_map.get("Date").unwrap(), | ||
| "Thu, 30 May 2024 23:52:17 GMT" | ||
| ); | ||
| assert_eq!( | ||
| response.header_map.get("Content-Type").unwrap(), | ||
| "text/html; charset=UTF-8" | ||
| ); | ||
| assert_eq!( | ||
| response.header_map.get("link").unwrap(), | ||
| "<http://localhost/wp-json/wp/v2/posts?page=2>; rel=\"next\"" | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the constructor is taking a
HashMapas an argument, how would that address the issue from #121:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In #121 (comment), I mentioned that using a
HashMapto accept headers here won't be an issue on Apple platforms. I believe that's the case for OkHttp too. Because they merge repeated headers into one header. The issue here is storing headers in aHashMap. It should be okay to take aHashMapas an argument.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am very sorry, but I don't really understand the difference between the two. Can you elaborate with an example?
How is storing it is different from taking it as an argument? If we are storing exactly the same thing as we take as an argument (in the
uniffi::Recordcase before this change) how can that be different from taking it as an argument, converting it toHeaderMapand storing it? There is no mutation after it's stored, so I am a bit confused :(It's quite late here, and I am only replying now because of our time zone difference, but I'll take another look at this tomorrow. Maybe it'll click then 🤞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing that would be really helpful - if it's easy to do - is to add a test to the existing implementation as a separate PR to showcase what can go wrong with it. Then, it'll be much easier to understand why this solution addresses it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean like adding a failing test case: #133?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two potential issues here regarding how headers are stored in
WpNetworkResponse.linkheader incurl -sI 'https://longreads.com'.WpNetworkResponseshould be able to store those multiple link headers."Server": "Nginx"intoWpNetworkResponse, when we query the header likeresponse.headers.get("server"), it should returnNginx.Issue 1 won't occur in this library, because this library doesn't handle the raw HTTP response. It takes a
HashMapwhich is created by iOS/Android's HTTP client. As long as those HTTP clients can process repeated HTTP headers, it'd be all good for this library to take those headers in. For example, in the case ofcurl -sI 'https://longreads.com', theHashMappassed from iOS to the Rust library won't contain multiplelinkheader: it will be one entry forlinkwhose value is all the link header value. See #121 (comment) for details.Issue 2 does occur in this library, as demonstrated in #133.
In this PR, the
WpNetworkResponsenow stores anHeaderMapinstance, notHashMapinstance. TheHashMapargument is converted intoHeaderMapin the initializer.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was missing this bit, because I missed this comment when I was looking at the issue yesterday. I am really sorry to have wasted your time and I appreciate your patience in explaining the issue to me 🙇♂️
I'll dismiss my review and restart my review with this knowledge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries! Thanks for the review.