@@ -164,15 +164,39 @@ static_assert(HEADER_HASH_TABLE.find("AcCePt-ChArSeT"_kj) == 1);
164164
165165static_assert (std::size(COMMON_HEADER_NAMES) == (MAX_COMMON_HEADER_ID + 1 ));
166166
167- inline constexpr void requireValidHeaderName (const jsg::ByteString& name) {
167+ inline constexpr void requireValidHeaderName (kj::StringPtr name) {
168+ if (HEADER_HASH_TABLE.find (name) != 0 ) {
169+ // Known common header, always valid
170+ return ;
171+ }
168172 for (char c: name) {
169173 JSG_REQUIRE (util::isHttpTokenChar (c), TypeError, " Invalid header name." );
170174 }
171175}
172176
177+ void maybeWarnIfBadHeaderString (kj::StringPtr str) {
178+ if (IoContext::hasCurrent ()) {
179+ auto & context = IoContext::current ();
180+ if (context.isInspectorEnabled ()) {
181+ if (!simdutf::validate_ascii (str.begin (), str.size ())) {
182+ // The string contains non-ASCII characters. While any 8-bit value is technically valid
183+ // in HTTP headers, we encode header strings as UTF-8, so we want to warn the user that
184+ // their header name/value may not be what they may expect based on what browsers do.
185+ auto utf8Hex =
186+ kj::strArray (KJ_MAP (b, str) { return kj::str (" \\ x" , kj::hex (kj::byte (b))); }, " " );
187+ context.logWarning (kj::str (" A header value contains non-ASCII characters: \" " , str,
188+ " \" (raw bytes: \" " , utf8Hex,
189+ " \" ). As a quirk to support Unicode, we are encoding "
190+ " values as UTF-8 in the header, but in a browser this would likely result in a "
191+ " TypeError exception. Consider encoding this string in ASCII for compatibility with "
192+ " browser implementations of the Fetch specification." ));
193+ }
194+ }
195+ }
196+ }
197+
173198// Left- and right-trim HTTP whitespace from `value`.
174- kj::String normalizeHeaderValue (jsg::Lock& js, jsg::ByteString value) {
175- JSG_REQUIRE (workerd::util::isValidHeaderValue (value), TypeError, " Invalid header value." );
199+ kj::String normalizeHeaderValue (kj::String value) {
176200 // Fast path: if empty, return as-is
177201 if (value.size () == 0 ) return kj::mv (value);
178202
@@ -183,9 +207,18 @@ kj::String normalizeHeaderValue(jsg::Lock& js, jsg::ByteString value) {
183207 while (begin < end && util::isHttpWhitespace (*(end - 1 ))) --end;
184208
185209 size_t newSize = end - begin;
186- if (newSize == value.size ()) return kj::mv (value);
210+ if (newSize == value.size ()) {
211+ JSG_REQUIRE (workerd::util::isValidHeaderValue (value), TypeError, " Invalid header value." );
212+ maybeWarnIfBadHeaderString (value);
213+ return kj::mv (value);
214+ }
187215
188- return kj::str (kj::ArrayPtr (begin, newSize));
216+ auto trimmed = kj::ArrayPtr (begin, newSize);
217+ JSG_REQUIRE (workerd::util::isValidHeaderValue (trimmed), TypeError, " Invalid header value." );
218+ maybeWarnIfBadHeaderString (value);
219+ // By attaching the original array to the trimmed view, we keep the original allocation alive
220+ // and prevent an unnecessary copy.
221+ return kj::str (trimmed.attach (value.releaseArray ()));
189222}
190223
191224constexpr bool isSetCookie (const Headers::HeaderKey& key) {
@@ -285,8 +318,7 @@ kj::uint Headers::HeaderCallbacks::hashCode(capnp::CommonHeaderName commondId) {
285318 return kj::hashCode (commondId);
286319}
287320
288- Headers::Headers (jsg::Lock& js, jsg::Dict<jsg::ByteString, jsg::ByteString> dict)
289- : guard(Guard::NONE) {
321+ Headers::Headers (jsg::Lock& js, jsg::Dict<kj::String, kj::String> dict): guard(Guard::NONE) {
290322 headers.reserve (dict.fields .size ());
291323 for (auto & field: dict.fields ) {
292324 append (js, kj::mv (field.name ), kj::mv (field.value ));
@@ -386,7 +418,7 @@ kj::Array<Headers::DisplayedHeader> Headers::getDisplayedHeaders(jsg::Lock& js)
386418}
387419
388420jsg::Ref<Headers> Headers::constructor (jsg::Lock& js, jsg::Optional<Initializer> init) {
389- using StringDict = jsg::Dict<jsg::ByteString, jsg::ByteString >;
421+ using StringDict = jsg::Dict<kj::String, kj::String >;
390422
391423 KJ_IF_SOME (i, init) {
392424 KJ_SWITCH_ONEOF (kj::mv (i)) {
@@ -398,7 +430,7 @@ jsg::Ref<Headers> Headers::constructor(jsg::Lock& js, jsg::Optional<Initializer>
398430 // It's important to note here that we are treating the Headers object
399431 // as a special case here. Per the fetch spec, we *should* be grabbing
400432 // the Symbol.iterator off the Headers object and interpreting it as
401- // a Sequence<Sequence<ByteString >> (as in the ByteStringPairs case
433+ // a Sequence<Sequence<kj::String >> (as in the StringPairs case
402434 // below). However, special casing Headers like we do here is more
403435 // performant and has other side effects such as preserving the casing
404436 // of header names that have been received.
@@ -415,7 +447,7 @@ jsg::Ref<Headers> Headers::constructor(jsg::Lock& js, jsg::Optional<Initializer>
415447 // implementation here, however, we are ignoring the Symbol.iterator so
416448 // the test fails.
417449 }
418- KJ_CASE_ONEOF (pairs, ByteStringPairs ) {
450+ KJ_CASE_ONEOF (pairs, StringPairs ) {
419451 auto dict = KJ_MAP (entry, pairs) {
420452 JSG_REQUIRE (entry.size () == 2 , TypeError,
421453 " To initialize a Headers object from a sequence, each inner sequence "
@@ -430,7 +462,7 @@ jsg::Ref<Headers> Headers::constructor(jsg::Lock& js, jsg::Optional<Initializer>
430462 return js.alloc <Headers>();
431463}
432464
433- kj::Maybe<kj::String> Headers::get (jsg::Lock& js, jsg::ByteString name) {
465+ kj::Maybe<kj::String> Headers::get (jsg::Lock& js, kj::String name) {
434466 requireValidHeaderName (name);
435467 return getUnguarded (js, name.asPtr ());
436468}
@@ -457,7 +489,7 @@ kj::Array<kj::StringPtr> Headers::getSetCookie() {
457489 return nullptr ;
458490}
459491
460- kj::Array<kj::StringPtr> Headers::getAll (jsg::ByteString name) {
492+ kj::Array<kj::StringPtr> Headers::getAll (kj::String name) {
461493 requireValidHeaderName (name);
462494
463495 if (!strcaseeq (name, " set-cookie" _kj)) {
@@ -470,7 +502,7 @@ kj::Array<kj::StringPtr> Headers::getAll(jsg::ByteString name) {
470502 return getSetCookie ();
471503}
472504
473- bool Headers::has (jsg::ByteString name) {
505+ bool Headers::has (kj::String name) {
474506 requireValidHeaderName (name);
475507 return headers.find (getHeaderKeyFor (name)) != kj::none;
476508}
@@ -480,23 +512,24 @@ bool Headers::hasCommon(capnp::CommonHeaderName idx) {
480512 return headers.find (idx) != kj::none;
481513}
482514
483- void Headers::set (jsg::Lock& js, jsg::ByteString name, jsg::ByteString value) {
515+ void Headers::set (jsg::Lock& js, kj::String name, kj::String value) {
484516 checkGuard ();
485517 requireValidHeaderName (name);
486- setUnguarded (js, kj::mv (name), normalizeHeaderValue (js, kj::mv (value)));
518+ setUnguarded (js, kj::mv (name), normalizeHeaderValue (kj::mv (value)));
487519}
488520
489521void Headers::setUnguarded (jsg::Lock& js, kj::String name, kj::String value) {
490522 auto key = getHeaderKeyFor (name);
491523 auto & header = headers.findOrCreate (key, [&]() {
492524 Header header (kj::mv (key));
493- if (header.getHeaderName () != name) {
525+ auto keyName = header.getKeyName ();
526+ if (keyName.size () != name.size () || keyName != name) {
494527 header.name = kj::mv (name);
495528 }
496529 return kj::mv (header);
497530 });
498- header.values .clear ( );
499- header.values . add ( kj::mv (value) );
531+ header.values .resize ( 1 );
532+ header.values [ 0 ] = kj::mv (value);
500533}
501534
502535void Headers::setCommon (capnp::CommonHeaderName idx, kj::String value) {
@@ -507,25 +540,26 @@ void Headers::setCommon(capnp::CommonHeaderName idx, kj::String value) {
507540 header.values .add (kj::mv (value));
508541}
509542
510- void Headers::append (jsg::Lock& js, jsg::ByteString name, jsg::ByteString value) {
543+ void Headers::append (jsg::Lock& js, kj::String name, kj::String value) {
511544 checkGuard ();
512545 requireValidHeaderName (name);
513- appendUnguarded (js, kj::mv (name), normalizeHeaderValue (js, kj::mv (value)));
546+ appendUnguarded (js, kj::mv (name), normalizeHeaderValue (kj::mv (value)));
514547}
515548
516549void Headers::appendUnguarded (jsg::Lock& js, kj::String name, kj::String value) {
517550 auto key = getHeaderKeyFor (name);
518551 auto & header = headers.findOrCreate (key, [&]() {
519552 Header header (kj::mv (key));
520- if (header.getHeaderName () != name) {
553+ auto keyName = header.getKeyName ();
554+ if (keyName.size () != name.size () || keyName != name) {
521555 header.name = kj::mv (name);
522556 }
523557 return kj::mv (header);
524558 });
525559 header.values .add (kj::mv (value));
526560}
527561
528- void Headers::delete_ (jsg::ByteString name) {
562+ void Headers::delete_ (kj::String name) {
529563 checkGuard ();
530564 requireValidHeaderName (name);
531565 headers.eraseMatch (getHeaderKeyFor (name));
0 commit comments