Skip to content

Add support for multiple headers with same key#777

Merged
bitspittle merged 8 commits intovarabyte:devfrom
k4i6:feature/776-multiple-header-entries-with-same-key
Apr 9, 2026
Merged

Add support for multiple headers with same key#777
bitspittle merged 8 commits intovarabyte:devfrom
k4i6:feature/776-multiple-header-entries-with-same-key

Conversation

@k4i6
Copy link
Copy Markdown

@k4i6 k4i6 commented Mar 16, 2026

Related Issue: #776

  • Add support for multiple header entries with same name for api responses in the backend

Breaking changes

  • The api for headers within Response on the server changed from MutableMap<String, String> to the new interface Headers

Additional Notes

  • The client api is unchanged: Since the fetch api doesn't allow multiple header entries with the same key, the current implementation seems sufficient to me

@bitspittle
Copy link
Copy Markdown
Member

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!

@k4i6
Copy link
Copy Markdown
Author

k4i6 commented Mar 23, 2026

@bitspittle friendly reminder: Did you already have a chance to look into this?

@bitspittle
Copy link
Copy Markdown
Member

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.

@k4i6
Copy link
Copy Markdown
Author

k4i6 commented Mar 24, 2026

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.

@bitspittle
Copy link
Copy Markdown
Member

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 :)

Copy link
Copy Markdown
Member

@bitspittle bitspittle left a comment

Choose a reason for hiding this comment

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

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> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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>
 */

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for your suggestion, I've added a header comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

@k4i6
Copy link
Copy Markdown
Author

k4i6 commented Apr 8, 2026

@bitspittle Thank you very much for your feedback!
I've made some updates and addressed your comments above.

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.

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?
https://developer.mozilla.org/en-US/docs/Web/API/Headers

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?

I'm more than happy to remove the MutableMap<String, String> implementation, since it cleans up the code as you say.

@k4i6 k4i6 requested a review from bitspittle April 8, 2026 08:23
Copy link
Copy Markdown
Member

@bitspittle bitspittle left a comment

Choose a reason for hiding this comment

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

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 =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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 ->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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 {
/**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

@bitspittle
Copy link
Copy Markdown
Member

bitspittle commented Apr 9, 2026

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? https://developer.mozilla.org/en-US/docs/Web/API/Headers

Maybe I should stop being clever and switch back to using the official Headers object. Instead, I am sure I was trying to be idiomatic by exposing headers as a Map<String, Any>, as you can see here:

headers: Map<String, Any>? = FetchDefaults.Headers,

I think the fix is either to use the standard Headers class directly or change the API to Map<String, List<String>> -- I need to look into it (and then worry about backwards compat :)

Copy link
Copy Markdown
Member

@bitspittle bitspittle left a comment

Choose a reason for hiding this comment

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

Looks great. Thanks again! Especially for your patience.

It's late here so I'll merge this PR tomorrow.

Copy link
Copy Markdown
Member

@bitspittle bitspittle left a comment

Choose a reason for hiding this comment

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

All looks great! Submitting shortly.

@bitspittle bitspittle merged commit 2e0a613 into varabyte:dev Apr 9, 2026
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.

2 participants