Skip to content

Conversation

crazytonyli
Copy link
Contributor

Fixes #121.

This PR only changes headers in WpNetworkResponse, not WpNetworkRequest. It's fine for now to leave WpNetworkRequest as HashMap, because it's created internally in rust. But it'd be good to keep the implementation consistent between request and response types.

Changes

This PR uses http:HeaderMap in WpNetworkResponse. Thus, we have to expose WpNetworkResponse as a reference type instead of value type. I don't see a huge issue in this type change. One benefit it brings is that now we move all those standalong parse_xxx(response: WpNetworkResponse) functions into WpNetworkResponse.

@crazytonyli crazytonyli requested review from jkmassel and oguzkocer May 31, 2024 00:26
@crazytonyli crazytonyli changed the title Use HeaderMap to store response headers Use http::HeaderMap to store response headers May 31, 2024
pub fn new(
body: Vec<u8>,
status_code: u16,
header_map: Option<HashMap<String, String>>,
Copy link
Contributor

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 HashMap as an argument, how would that address the issue from #121:

HashMap is not suitable for storing HTTP headers, since header name can repeat.

Copy link
Contributor Author

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 HashMap to 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 a HashMap. It should be okay to take a HashMap as an argument.

Copy link
Contributor

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::Record case before this change) how can that be different from taking it as an argument, converting it to HeaderMap and 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 🤞

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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.

  1. A server may send multiple headers in response. i.e. the link header in curl -sI 'https://longreads.com'. WpNetworkResponse should be able to store those multiple link headers.
  2. Headers need to be case insensitive. If we put a map "Server": "Nginx" into WpNetworkResponse, when we query the header like response.headers.get("server"), it should return Nginx.

Issue 1 won't occur in this library, because this library doesn't handle the raw HTTP response. It takes a HashMap which 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 of curl -sI 'https://longreads.com', the HashMap passed from iOS to the Rust library won't contain multiple link header: it will be one entry for link whose 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 WpNetworkResponse now stores an HeaderMap instance, not HashMap instance. The HashMap argument is converted into HeaderMap in the initializer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Headers need to be case insensitive. If we put a map "Server": "Nginx" into WpNetworkResponse, when we query the header like response.headers.get("server"), it should return Nginx.

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@oguzkocer oguzkocer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thus, we have to expose WpNetworkResponse as a reference type instead of value type. I don't see a huge issue in this type change.

It's generally simpler to work with the Record types with uniffi, so whenever we can help it, we should prefer it over the Object type. With Object types, we are not able to expose the fields end always have to use functions - to opposite is true with Record types. The tricky bit with functions is that, we'll either have to expose things as a reference with Arc or reallocate them.

This is not necessarily a problem, but rather, a consideration to keep in mind when we are switching from Record to Object types.

One result of such a change would be that we'll expose limited functionality to the consumers, which - again - is not a problem. But, it's something to keep in mind. Not allowing consumers to use WpNetworkResponse outside of the provided functionality might even be a good thing.


I've left a line comment about constructor still taking a HashMap as an argument, so I believe the PR doesn't address the issue it intends to.

One option might be to accept Vec<(K, V)> in the constructor and build the HeaderMap that way. If we are ok with making WpNetworkObject a uniffi::Object, (which I am happy to give it a try) I don't see any reason that wouldn't work.

Also note that there is a TODO in WpNetworkResponse to address the issue by using a custom type for this. However, I think the custom type would only be necessary if we keep it as a uniffi::Record. So, if we intend to merge this PR after the issue is addressed, I think we can remove it.

As a final note, it'd be good to have a unit test that adds multiple values with the same key and validates that both values are there for the given key. Since this PR is addressing a specific issue, let's make sure that it does so through a unit test.


I was just about to submit my review, but I wanted to double check the documentation for HeaderMap. I see that there is a TryFrom<&'a HashMap<K, V>> but not from Vec<(K, V)> (although the extend function might work for that 🤔 ) which is giving me pause. I think it'd be good to double check that HeaderMap supports multiple values per key.

According to specification, multiple values per headers are supported as a comma separated list. So, I don't think libraries/clients have to support multiple value per key. And I feel like maybe we shouldn't either, if comma separated list is enough.

@crazytonyli
Copy link
Contributor Author

crazytonyli commented May 31, 2024

I think it'd be good to double check that HeaderMap supports multiple values per key.

I don't think it does. But as I mentioned in my previous comment, it won't be an issue as long as the wrappers handle repeated headers properly.

I'm happy to change the initializer if you think that's the best way to go. But Vec<(String, String)> won't work, because it can't be exported as uniffi API. Maybe we'll need to use Vec<Vec<String>>.

@oguzkocer
Copy link
Contributor

I'm happy to change the initializer if you think that's the best way to go.

Oh, I wasn't actually trying to suggest that. I was trying to better understand the issue and I thought looking into the implemented traits would be useful.

Let me check the TryFrom<&'a HashMap<K, V>> implementation to see if this would work for us. The possible issue I am seeing it with it is that we'd have to return a Result<Self, E> instead of Self.

@oguzkocer oguzkocer dismissed their stale review May 31, 2024 16:28

Due to a misunderstanding of the goals of the PR

Copy link
Contributor

@oguzkocer oguzkocer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crazytonyli Thanks for addressing the issue and thank you again for your patience in explaining it to me.

I left some suggestions. Let me know what you think!

// 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>>,
headers: Arc<HeaderMap>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to use Arc<T> for this since it's not supposed to be exposed to FFI through a function.

Comment on lines 62 to 63
pub body: Vec<u8>,
pub status_code: u16,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we are "marking" this type as something that shouldn't be directly consumed, we can remove the pubs to clarify it further for Rust clients. This will prevent others from initializing it using the struct literal syntax and help compiler to point out the new associated function:

Screenshot 2024-05-31 at 12 32 14

Although technically the headers being non-public is enough for this ^ bit, still it's a good idea to fully encapsulate all fields in this case.


While you're making this change, could you remove the TODO in this type? Thanks!

body: Vec<u8>,
status_code: u16,
header_map: Option<HashMap<String, String>>,
) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to return a Result<Self, WpApiError> here with a new error variant ResponseHeaderParsingError. (or similar)

Following up on this comment, why creating a HeaderMap from HashMap could fail and I found a very simple case for it:

#[test]
fn test_err_empty_header_name() {
    assert!(http::HeaderName::try_from("".to_string()).is_err());
}

The way this is handled right now with ok() is that it'll get silently swallowed and the whole header map will be discarded. The problem is, there is no trace of it anywhere, making it a very difficult bug to address later on.


Returning Result<Self, WpApiError> will force clients to address this possibility. They can do the exact same thing if they'd like - catch this specific error and try building the WpNetworkResponse again by passing None for the headers argument. Even though this practically results in exactly the same object after construction, because it's done in a client, it makes it clear how the possibility is handled. I think it's important for us to do the "right" thing from the Rust library and then if we want to keep it simple, we can do this workaround in the clients.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 WpApiError. Adding to WpApiError means the apps/consumers would need to handle this error case, where they shouldn't need to.

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Self {
body,
status_code,
headers: headers.into(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I kind of like being consistent with the type names (HeaderMap in this case) when I can, so I'd have named this header_map in the WpNetworkResponse struct. (I am leaving the comment here because that line has a big discussion)

You can use variable shadowing to name the headers in line 80 as header_map to get this working cleanly.

);
}

#[rstest]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: We can mark this as #[test] since there is no rstest specific implementation involved.

body: Vec<u8>,
status_code: u16,
header_map: Option<HashMap<String, String>>,
) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

@oguzkocer oguzkocer merged commit bef8433 into trunk Jun 4, 2024
@oguzkocer oguzkocer deleted the refactor-network-response-header branch June 4, 2024 19:10
oguzkocer added a commit that referenced this pull request Jun 4, 2024
oguzkocer added a commit that referenced this pull request Jun 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP Headers can't be stored in a HashMap

2 participants