HeaderMap Iteration Semantics

February 1, 2026 in Programming5 minutes

This Rust program is syntactically correct, but there’s a mistake in it. Can you spot it?

 1use http::header::{CONTENT_LENGTH, HOST, HeaderMap};
 2
 3fn main() {
 4    let headers: HeaderMap = get_headermap();
 5
 6    for (name, value) in headers {
 7        if let Some(name) = name {
 8            println!("{:?}: {:?}", name, value);
 9        }
10    }
11}
12
13// get_headermap() implementation omitted from this example

This is a simplified form of a bug I introduced a while back while adding a really large new feature to a service that, among other things, is parsing HTTP requests from other instances of itself.

It’s not a mistake in syntax; rather, it’s the result of a semantic misunderstanding of how http::headers::HeaderMap iteration works.

If you haven’t spotted the bug, take a moment to think about what the output might be. For that, you’ll need the implementation of get_headermap(), which gives us a pretty big hint:

1fn get_headermap() -> HeaderMap {
2    let mut headers = HeaderMap::new();
3
4    headers.insert(HOST, "first-example.com".parse().unwrap());
5    headers.insert(HOST, "second-example.com".parse().unwrap());
6    headers.insert(CONTENT_LENGTH, "123".parse().unwrap());
7
8    headers
9}

If we run the completed program, we get:

"host": "second-example.com"
"content-length": "123"

What happened to first-example.com?

IntoIterator

We’re able to use the for syntax directly on our HeaderMap called headers thanks to the IntoIterator trait. This trait allows types to define how they should be converted to an iterator.

As you can see in the docs, this trait requires three things:

  • An associated type Item - the type that is yielded on each iteration
  • Another associated type IntoIter - this one having a bound on another trait Iterator
  • A method into_iter() which actually returns the Iterator.

If you’ve poked around with iterators in Rust, you’re likely familiar with much of this.

How HeaderMap implements IntoIterator

Any time you’re relying on the implementation of a trait in Rust, you have to be very careful about the specific type you’re using for satisfaction of that trait. It turns out HeaderMap actually implements IntoIterator in three ways:

  1. impl<'a, T> IntoIterator for &'a HeaderMap<T> - for iterating over a reference to a HeaderMap
  2. impl<'a, T> IntoIterator for &'a mut HeaderMap<T> - for iterating over a mutable reference to a HeaderMap
  3. impl<T> IntoIterator for HeaderMap<T> - a consuming iterator which actually pulls keys and values out of the HeaderMap in arbitrary order.

In our above example, we were not iterating over &headers or &mut headers - we were simply iterating over headers, which meant the third option applies in this case. Every time the HeaderMap iterator yields a new value, it’s being moved out and into our scope where we print it to the terminal.

However, this is not the source of the bug - if we were doing something fishy with ownership, this is something the compiler is well-equipped to tell us about. Rather, it’s in how the yielded Option is set:

For each yielded item that has None provided for the HeaderName, then the associated header name is the same as that of the previously yielded item. The first yielded item will have HeaderName set.

This is likely a performance optimization to avoid moving a duplicated HeaderName value twice. You’re expected to maintain awareness of the last time a Some() was returned here, and if None is returned in subsequent iterations, it uses the same HeaderName. In other words, “ditto”. So, our loop did iterate three times, it just skipped over the second header when considering whether or not to println.

The into_iter() docs have some illustrative examples, particularly with multiple values per key, which is relevant here.

Solution

In this case, while it didn’t matter too much whether or not I was consuming the values from the HeaderMap (it wasn’t used again after this operation anyways), the more correct thing to do is iterate by reference, using the impl<'a, T> IntoIterator for &'a HeaderMap<T> that was mentioned above.

This makes our loop much simpler since this impl yields HeaderName and not Option<HeaderName>:

1for (name, value) in &headers {
2    println!("{:?}: {:?}", name, value);
3}

Conclusion

In my case, I believe the real culprit was a lazy copy+paste from another function which checks the membership of a key in the same HeaderMap. So the first lesson for me was “don’t be lazy” - especially in really large changes like this (this was one of those big features that had to change a bunch of things all at once).

However, it did also get me thinking that this is the kind of mistake an LLM can also easily make. So it’s important to get in the habit of understanding more than syntax, and truly understanding the semantics of the APIs you use.

Thankfully the bug in question was located in a context where multiple values per header key was not something that ever happens - otherwise this would have been well-caught in tests.

Beyond that, I thought this was a great case study for the more general Rust concept of understanding trait implementations for a type - particularly when considering the added dimension of “owned vs borrowed”. It can be at times easy to forget that trait implementations are specific to one of the three ownership cases.

Matt Oswalt avatar
Matt Oswalt

Principal Systems Engineer at Cloudflare focused on traffic engineering, Rust, and highly available distributed systems.

Writes primarily about systems programming, Linux networking, machine learning, and lessons learned from building and operating large-scale production systems.