Add support for multiple headers with same key#777
Conversation
|
Thanks for the PR. I will be busy for a few more days, so apologies for the delay in advance. I'll add reminders to take a look at this by the end of this week, but if you don't hear from me by the weekend, you can definitely reach out! |
|
@bitspittle friendly reminder: Did you already have a chance to look into this? |
|
Major apologies! I did not forget. And I should have given you an update. I hate sounding like I'm making excuses, but last week was crazy and this week will likely be too. I do have a daily reminder to review this PR so I promise it won't get dropped. You are free to skip the rest of this reply! But I'll explain here what's been going on to give you an idea if you're curious. I also want to emphasize that this stuff is temporary chaos that should be settling down soon. We're fostering three dogs here -- and last Tuesday we had a parasite scare, so we've been managing that with meds while also cleaning all sheets and dog beds and surfaces in our house to reduce the chance it will happen again. One of the three also got neutered last week in the middle of all this, something else we've had to manage. He already wants to play with everyone else, and we can't let him, which is... fun. It requires full time management basically. At the same time, we also got a ton of interest for all three dogs which means a lot of calling and vetting people, which will continue through the next few days. Finally, my wife has a minor surgical procedure Thursday this week, so we're juggling prep for that right now. By next week, we expect two if not all three dogs to be adopted and the wife to be back in commission, at which point I plan to block off time to really focus on all my coding tasks that have fallen behind on. I still need to get on top of some of my lingering Kotter work, because otherwise I'll be too scatter-brained to give your PR the attention it needs, because I assume this area will need a bit of thought regarding backwards compatability. If I end up getting more time than I expect, I will look at this early. But we're looking at midweek next week at this point. My apologies for that. |
|
Thanks for the update, no worries at all — I completely understand, and it sounds like you’ve had your hands full! Wishing your wife a smooth procedure and recovery, and I hope things settle down soon with the dogs too. |
|
Well, good news and bad news! The good news is the medical procedure went very well and results were best case scenario. Bad news is, and this was our first experience with this, but the adopter who took one of our dogs turned out to have lied to us about almost everything, and we spent the weekend getting her back after realizing it. Of the three dogs, this one is a border collie, which among our fosters accounted for about 90% of our time and energy.... So we're back to the adoption drawing board with her, and this will definitely affect the free time I was planning on having this week. I'll make progress on my projects as I get time to do so. Hopefully I'll still be getting to this review in the not-so-distant future. But I wanted to give you heads up that things are in a bit of a continued disarray here for now :) |
bitspittle
left a comment
There was a problem hiding this comment.
Oh, now that I've looked at this, let me apologize. This PR was much more focused than I expected. I was originally assuming it would be a much larger change, touching the APIs on the frontend side. Here, we're only affecting the backend code.
(The really scary change will need to be made to Fetch.kt, HttpFetcher.kt, and ApiFetcher.kt. But that's not what this PR is about, so this is much easier to review.)
Note that I wouldn't say this PR closes #776 as we still need to update the frontend code as well. In fact, that's probably the more important change that needs to happen. But I'm glad you haven't started working on it yet, because I need to also make some sweeping changes in there first.
I really like the Headers class design. I appreciate for backwards compatibility you are inheriting MutableMap<String, String>, but honestly, I would be OK if you didn't. I think so few people actually create Kobweb backends that we don't have to worry about backwards compatibility so much in this case. It might also be easier to review the code without all the deprecations. What do you think?
Otherwise, the biggest comment is we need to split apart the immutable interface and the mutable one. Take a look at my comment and let me know if you have any questions or thoughts about it.
| @@ -0,0 +1,90 @@ | |||
| package com.varabyte.kobweb.api.http | |||
|
|
|||
| class Headers : MutableMap<String, String> { | |||
There was a problem hiding this comment.
Let's add a header comment here. Since most people don't like writing one, I'll suggest some text for you, but you can take it as is, modify it, or write your own.
/**
* An collection of HTTP header values.
*
* In a majority of cases, HTTP headers are normally a collection of simple key/value pairs,
* but per official spec, a header field can technically contain multiple values. As a result,
* users are expected to [append] new values, not simply set them.
*
* Also, according to the RFC, header field names are case-insensitive. In other words, accessing
* "Content-Type" is functionally identical to "content-type".
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-5">HTTP Semantics Section 5</a>
*/
There was a problem hiding this comment.
Thanks for your suggestion, I've added a header comment.
There was a problem hiding this comment.
The Kobweb backend APIs use an immutable / mutable pattern (e.g. Response is an immutable interface but MutableResponse is the actual implementation)
So for here, I think we need
interface Headers : Map<String, String> {
// Immutable API
...
}
class MutableHeaders : MutableMap<String, String>, Headers {
// your implementation here
}Note
There's also an alternate approach where you wrap a mutable class with an immutable one:
class Headers(private val wrapped: MutableHeaders) { ... }
but I think for consistency with the rest of the codebase, by default I'd stick with the first approach.
Then we can do this:
interface Request {
val headers: Headers
}
class MutableRequest {
val headers: MutableHeaders
}There was a problem hiding this comment.
I've split the class into an immutable interface with mutable implementation and used the new class within the request, in addition to the response.
|
@bitspittle Thank you very much for your feedback!
Since the Fetch API implements headers as a simple key/value map, I think the interface from kobweb on client side regarding headers is already sufficient as it is. Or am I missing something?
I'm more than happy to remove the MutableMap<String, String> implementation, since it cleans up the code as you say. |
bitspittle
left a comment
There was a problem hiding this comment.
I think this looks great. I'm happy with the way the Headers class looks now, thank you!
For this pass of the review, I looked a bit more carefully at the calling sites, so there are two additional comments; once those are resolved, I am pretty sure this PR will be ready to go.
| check(status in VALID_REDIRECT_STATUS_CODES) { "Redirect status code is invalid ($status); must be one of $VALID_REDIRECT_STATUS_CODES" } | ||
| this.status = status | ||
| headers["Location"] = | ||
| val location = |
There was a problem hiding this comment.
So I looked into this a bit more. It turns out, as a header "Location" is always supposed to be a single URI value:
https://www.rfc-editor.org/rfc/rfc9110.html#name-location
The field value consists of a single URI-reference. When it has the form of a relative reference ([URI], Section 4.2), the final value is computed by resolving it against the target URI ([URI], Section 5).
So I think this implies we keep the set operator around and use it here. Unless I'm mistaken, append shouldn't be used everywhere.
There was a problem hiding this comment.
Makes sense to me, I've readded the set operator
| val response = apiJar.apis.handle("/$pathStr", request) | ||
| response.headers.forEach { (key, value) -> | ||
| call.response.headers.append(key, value) | ||
| response.headers.names.forEach { name -> |
There was a problem hiding this comment.
I feel like we should keep supporting iteration. Any reason that Headers can't implement Iterable<Pair<String, List<String>>? (Or Iterable<Map.Entry<String, List<String>> or introduce some new Header.Entry data class with header and values properties)
There was a problem hiding this comment.
I went for the Iterable<Pair<String, List<String>>, thanks for the suggestion
| * @see <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-5">HTTP Semantics Section 5</a> | ||
| */ | ||
| interface Headers { | ||
| /** |
There was a problem hiding this comment.
No action required: The header comments you left for the following properties and functions are absolutely fine and I have no problem with them going in at all exactly like this.
You can also cut them if you want, as I think in this case the code is pretty readable without them.
I'm just mentioning this as an option in case you wouldn't have added them for yourself but wanted to add them for me (and I had asked for a header comment for the entire class).
There was a problem hiding this comment.
I've actually added them to keep it consistent with the Request interface.
However, thanks for the hint: I've gone through them again and removed the ones which are adding no additional information from my point of view.
Maybe I should stop being clever and switch back to using the official I think the fix is either to use the standard |
bitspittle
left a comment
There was a problem hiding this comment.
Looks great. Thanks again! Especially for your patience.
It's late here so I'll merge this PR tomorrow.
bitspittle
left a comment
There was a problem hiding this comment.
All looks great! Submitting shortly.
Related Issue: #776
Breaking changes
headerswithinResponseon the server changed fromMutableMap<String, String>to the new interfaceHeadersAdditional Notes