@@ -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: \" " 
188+             " \"  (raw bytes: \" " 
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" 
@@ -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