"Structurally valid" is meaningless in this context. The DCHECK is a type error in the extensions code, not a problem with the value. In both HTTP itself and //net, the type of a header value and reason string is byte string, not Unicode string. There is no guarantee or expectation that non-ASCII bytes are UTF-8. How to interpret those byte strings is the caller's responsibility, and the caller must be prepared to receive all possible byte strings. It's unfortunate that we have this sharp edge, and that they're just tossed into std::string, and that they look text-like, but sadly HTTP and //net come from a long history of systems being sloppy about what encoding is in use, so here we are.
"Decode as UTF-8 or return list of integers if not UTF-8" is a perfectly injective JSON representation of an HTTP header. It's just not the one that the web platform uses, and has a really weird discontinuity once your byte string doesn't happen to be UTF-8.
As for what I'm describing, that wasn't to encode as UTF-32. I just described the process as a sequence of code points abstractly because JSON contains Unicode strings. We happen to represent those Unicode strings in C++ as UTF-8, but thinking of them as Unicode strings makes it clearer why some byte strings are unrepresentable if you try to decode them as UTF-8. In particular, that JSON data will immediately get sent to JavaScript, where Unicode strings are represented as UTF-16. So describing it as either UTF-8 or UTF-16 would be weird. (So, in that vein, yes your code is ultimately converting 0xE2 0x98 0x83 because it needs to end up in a JavaScript string somehow. Your code turns it into a JavaScript string that represents U+2603, as it happens in UTF-16. The web-platform-consistent representation would be to instead turn it into a different JavaScript string that represents U+00E2 U+0098 U+0083, again in UTF-16 as it happens.)
If you would prefer to phrase it in terms of UTF-8, sure, your current implementation is the identity function restricted to valid UTF-8 encodings, whereas the web-platform-consistent transformation is to decode as Latin-1/ISO-8859-1 and then encode as UTF-8. Just that's a weird way to think of it, because it will still get decoded as UTF-8 and then encoded as UTF-16 when it makes its way to the extension's JavaScript, and it's less clear why your identity function fundamentally needed to be restricted.