_treatAsCustomHeaderTypes = treatAsCustomHeaderTypes;
}
+ internal Dictionary<HeaderDescriptor, HeaderStoreItemInfo> HeaderStore => _headerStore;
+
public void Add(string name, string value)
{
Add(GetHeaderDescriptor(name), value);
// HeaderName1: Value1, Value2
// HeaderName2: Value1
// ...
- StringBuilder sb = new StringBuilder();
- foreach (var header in GetHeaderStrings())
+ var sb = new StringBuilder();
+ foreach (KeyValuePair<string, string> header in GetHeaderStrings())
{
- sb.Append(header.Key);
- sb.Append(": ");
-
- sb.AppendLine(header.Value);
+ sb.Append(header.Key).Append(": ").AppendLine(header.Value);
}
return sb.ToString();
yield break;
}
- foreach (var header in _headerStore)
+ foreach (KeyValuePair<HeaderDescriptor, HeaderStoreItemInfo> header in _headerStore)
{
string stringValue = GetHeaderString(header.Key, header.Value);
private IEnumerator<KeyValuePair<string, IEnumerable<string>>> GetEnumeratorCore()
{
- foreach (var header in _headerStore)
+ foreach (KeyValuePair<HeaderDescriptor, HeaderStoreItemInfo> header in _headerStore)
{
HeaderDescriptor descriptor = header.Key;
HeaderStoreItemInfo info = header.Value;
}
}
- // The following is the same general code as the above GetEnumerator, but returning the
- // HeaderDescriptor and values string[], rather than the key name and a values enumerable.
-
- internal IEnumerable<KeyValuePair<HeaderDescriptor, string[]>> GetHeaderDescriptorsAndValues()
- {
- return _headerStore != null && _headerStore.Count > 0 ?
- GetHeaderDescriptorsAndValuesCore() :
- Array.Empty<KeyValuePair<HeaderDescriptor, string[]>>();
- }
-
- private IEnumerable<KeyValuePair<HeaderDescriptor, string[]>> GetHeaderDescriptorsAndValuesCore()
- {
- foreach (var header in _headerStore)
- {
- HeaderDescriptor descriptor = header.Key;
- HeaderStoreItemInfo info = header.Value;
-
- // Make sure we parse all raw values before returning the result. Note that this has to be
- // done before we calculate the array length (next line): A raw value may contain a list of
- // values.
- if (!ParseRawHeaderValues(descriptor, info, false))
- {
- // We have an invalid header value (contains invalid newline chars). Delete it.
- _headerStore.Remove(descriptor);
- }
- else
- {
- string[] values = GetValuesAsStrings(descriptor, info);
- yield return new KeyValuePair<HeaderDescriptor, string[]>(descriptor, values);
- }
- }
- }
-
#endregion
#region IEnumerable Members
return;
}
- foreach (var header in sourceHeaders._headerStore)
+ foreach (KeyValuePair<HeaderDescriptor, HeaderStoreItemInfo> header in sourceHeaders._headerStore)
{
// Only add header values if they're not already set on the message. Note that we don't merge
// collections: If both the default headers and the message have set some values for a certain
// The values array may not be full because some values were excluded
if (currentIndex < length)
{
- string[] trimmedValues = new string[currentIndex];
- Array.Copy(values, 0, trimmedValues, 0, currentIndex);
- values = trimmedValues;
+ values = values.AsSpan(0, currentIndex).ToArray();
}
}
else
return values;
}
- private static int GetValueCount(HeaderStoreItemInfo info)
+ internal static int GetValuesAsStrings(HeaderDescriptor descriptor, HeaderStoreItemInfo info, ref string[] values)
{
- Debug.Assert(info != null);
+ Debug.Assert(values != null);
+ int length = GetValueCount(info);
+
+ if (length > 0)
+ {
+ if (values.Length < length)
+ {
+ values = new string[length];
+ }
- int valueCount = 0;
- UpdateValueCount<string>(info.RawValue, ref valueCount);
- UpdateValueCount<string>(info.InvalidValue, ref valueCount);
- UpdateValueCount<object>(info.ParsedValue, ref valueCount);
+ int currentIndex = 0;
+ ReadStoreValues<string>(values, info.RawValue, null, null, ref currentIndex);
+ ReadStoreValues<object>(values, info.ParsedValue, descriptor.Parser, null, ref currentIndex);
+ ReadStoreValues<string>(values, info.InvalidValue, null, null, ref currentIndex);
+ Debug.Assert(currentIndex == length);
+ }
- return valueCount;
+ return length;
}
- private static void UpdateValueCount<T>(object valueStore, ref int valueCount)
+ private static int GetValueCount(HeaderStoreItemInfo info)
{
- if (valueStore == null)
- {
- return;
- }
+ Debug.Assert(info != null);
- List<T> values = valueStore as List<T>;
- if (values != null)
- {
- valueCount += values.Count;
- }
- else
- {
- valueCount++;
- }
+ int valueCount = Count<string>(info.RawValue);
+ valueCount += Count<string>(info.InvalidValue);
+ valueCount += Count<object>(info.ParsedValue);
+ return valueCount;
+
+ static int Count<T>(object valueStore) =>
+ valueStore is null ? 0 :
+ valueStore is List<T> list ? list.Count :
+ 1;
}
private static void ReadStoreValues<T>(string[] values, object storeValue, HttpHeaderParser parser,
#region Private Classes
- private class HeaderStoreItemInfo
+ internal class HeaderStoreItemInfo
{
- private object _rawValue;
- private object _invalidValue;
- private object _parsedValue;
-
- internal object RawValue
- {
- get { return _rawValue; }
- set { _rawValue = value; }
- }
-
- internal object InvalidValue
- {
- get { return _invalidValue; }
- set { _invalidValue = value; }
- }
+ internal HeaderStoreItemInfo() { }
- internal object ParsedValue
- {
- get { return _parsedValue; }
- set { _parsedValue = value; }
- }
+ internal object RawValue { get; set; }
+ internal object InvalidValue { get; set; }
+ internal object ParsedValue { get; set; }
internal bool CanAddValue(HttpHeaderParser parser)
{
- Debug.Assert(parser != null,
- "There should be no reason to call CanAddValue if there is no parser for the current header.");
+ Debug.Assert(parser != null, "There should be no reason to call CanAddValue if there is no parser for the current header.");
// If the header only supports one value, and we have already a value set, then we can't add
// another value. E.g. the 'Date' header only supports one value. We can't add multiple timestamps
// supporting 1 value. When the first value gets parsed, CanAddValue returns true and we add the
// parsed value to ParsedValue. When the second value is parsed, CanAddValue returns false, because
// we have already a parsed value.
- return ((parser.SupportsMultipleValues) || ((_invalidValue == null) && (_parsedValue == null)));
- }
-
- internal bool IsEmpty
- {
- get { return ((_rawValue == null) && (_invalidValue == null) && (_parsedValue == null)); }
+ return parser.SupportsMultipleValues || ((InvalidValue == null) && (ParsedValue == null));
}
- internal HeaderStoreItemInfo()
- {
- }
+ internal bool IsEmpty => (RawValue == null) && (InvalidValue == null) && (ParsedValue == null);
}
#endregion
}
private const int AcceptCharsetSlot = 1;
private const int AcceptEncodingSlot = 2;
private const int AcceptLanguageSlot = 3;
- private const int ExpectSlot = 4;
- private const int IfMatchSlot = 5;
- private const int IfNoneMatchSlot = 6;
- private const int TransferEncodingSlot = 7;
- private const int UserAgentSlot = 8;
- private const int NumCollectionsSlots = 9;
+ private const int IfMatchSlot = 4;
+ private const int IfNoneMatchSlot = 5;
+ private const int TransferEncodingSlot = 6;
+ private const int UserAgentSlot = 7;
+ private const int NumCollectionsSlots = 8;
private object[] _specialCollectionsSlots;
private HttpGeneralHeaders _generalHeaders;
+ private HttpHeaderValueCollection<NameValueWithParametersHeaderValue> _expect;
private bool _expectContinueSet;
#region Request Headers
private T GetSpecializedCollection<T>(int slot, Func<HttpRequestHeaders, T> creationFunc)
{
- // 9 properties each lazily allocate a collection to store the value(s) for that property.
+ // 8 properties each lazily allocate a collection to store the value(s) for that property.
// Rather than having a field for each of these, store them untyped in an array that's lazily
- // allocated. Then we only pay for the 72 bytes for those fields when any is actually accessed.
- object[] collections = _specialCollectionsSlots ?? (_specialCollectionsSlots = new object[NumCollectionsSlots]);
- object result = collections[slot];
- if (result == null)
- {
- collections[slot] = result = creationFunc(this);
- }
- return (T)result;
+ // allocated. Then we only pay for the 64 bytes for those fields when any is actually accessed.
+ _specialCollectionsSlots ??= new object[NumCollectionsSlots];
+ return (T)(_specialCollectionsSlots[slot] ??= creationFunc(this));
}
public HttpHeaderValueCollection<MediaTypeWithQualityHeaderValue> Accept =>
GetSpecializedCollection(UserAgentSlot, thisRef => new HttpHeaderValueCollection<ProductInfoHeaderValue>(KnownHeaders.UserAgent.Descriptor, thisRef));
private HttpHeaderValueCollection<NameValueWithParametersHeaderValue> ExpectCore =>
- GetSpecializedCollection(ExpectSlot, thisRef => new HttpHeaderValueCollection<NameValueWithParametersHeaderValue>(KnownHeaders.Expect.Descriptor, thisRef, HeaderUtilities.ExpectContinue));
+ _expect ??= new HttpHeaderValueCollection<NameValueWithParametersHeaderValue>(KnownHeaders.Expect.Descriptor, this, HeaderUtilities.ExpectContinue);
#endregion
}
/// <summary>Encodes a "Literal Header Field without Indexing - New Name".</summary>
- public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string[] values, string separator, Span<byte> destination, out int bytesWritten)
+ public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan<string> values, string separator, Span<byte> destination, out int bytesWritten)
{
// From https://tools.ietf.org/html/rfc7541#section-6.2.2
// ------------------------------------------------------
return false;
}
- public static bool EncodeStringLiterals(string[] values, string separator, Span<byte> destination, out int bytesWritten)
+ public static bool EncodeStringLiterals(ReadOnlySpan<string> values, string separator, Span<byte> destination, out int bytesWritten)
{
bytesWritten = 0;
private ArrayBuffer _outgoingBuffer;
private ArrayBuffer _headerBuffer;
+ /// <summary>Reusable array used to get the values for each header being written to the wire.</summary>
+ private string[] _headerValues = Array.Empty<string>();
+
private int _currentWriteSize; // as passed to StartWriteAsync
private readonly HPackDecoder _hpackDecoder;
_headerBuffer.Commit(bytesWritten);
}
- private void WriteLiteralHeader(string name, string[] values)
+ private void WriteLiteralHeader(string name, ReadOnlySpan<string> values)
{
- if (NetEventSource.IsEnabled) Trace($"{nameof(name)}={name}, {nameof(values)}={string.Join(", ", values)}");
+ if (NetEventSource.IsEnabled) Trace($"{nameof(name)}={name}, {nameof(values)}={string.Join(", ", values.ToArray())}");
int bytesWritten;
while (!HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, values, HttpHeaderParser.DefaultSeparator, _headerBuffer.AvailableSpan, out bytesWritten))
_headerBuffer.Commit(bytesWritten);
}
- private void WriteLiteralHeaderValues(string[] values, string separator)
+ private void WriteLiteralHeaderValues(ReadOnlySpan<string> values, string separator)
{
- if (NetEventSource.IsEnabled) Trace($"{nameof(values)}={string.Join(separator, values)}");
+ if (NetEventSource.IsEnabled) Trace($"{nameof(values)}={string.Join(separator, values.ToArray())}");
int bytesWritten;
while (!HPackEncoder.EncodeStringLiterals(values, separator, _headerBuffer.AvailableSpan, out bytesWritten))
{
if (NetEventSource.IsEnabled) Trace("");
- foreach (KeyValuePair<HeaderDescriptor, string[]> header in headers.GetHeaderDescriptorsAndValues())
+ if (headers.HeaderStore is null)
+ {
+ return;
+ }
+
+ foreach (KeyValuePair<HeaderDescriptor, HttpHeaders.HeaderStoreItemInfo> header in headers.HeaderStore)
{
- Debug.Assert(header.Value.Length > 0, "No values for header??");
+ int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref _headerValues);
+ Debug.Assert(headerValuesCount > 0, "No values for header??");
+ ReadOnlySpan<string> headerValues = _headerValues.AsSpan(0, headerValuesCount);
KnownHeader knownHeader = header.Key.KnownHeader;
if (knownHeader != null)
if (header.Key.KnownHeader == KnownHeaders.TE)
{
// HTTP/2 allows only 'trailers' TE header. rfc7540 8.1.2.2
- foreach (string value in header.Value)
+ foreach (string value in headerValues)
{
if (string.Equals(value, "trailers", StringComparison.OrdinalIgnoreCase))
{
// For all other known headers, send them via their pre-encoded name and the associated value.
WriteBytes(knownHeader.Http2EncodedName);
string separator = null;
- if (header.Value.Length > 1)
+ if (headerValues.Length > 1)
{
HttpHeaderParser parser = header.Key.Parser;
if (parser != null && parser.SupportsMultipleValues)
}
}
- WriteLiteralHeaderValues(header.Value, separator);
+ WriteLiteralHeaderValues(headerValues, separator);
}
}
else
{
// The header is not known: fall back to just encoding the header name and value(s).
- WriteLiteralHeader(header.Key.Name, header.Value);
+ WriteLiteralHeader(header.Key.Name, headerValues);
}
}
}
private readonly byte[] _writeBuffer;
private int _writeOffset;
private int _allowedReadLineBytes;
+ /// <summary>Reusable array used to get the values for each header being written to the wire.</summary>
+ private string[] _headerValues = Array.Empty<string>();
private ValueTask<int>? _readAheadTask;
private int _readAheadTaskLock = 0; // 0 == free, 1 == held
private async Task WriteHeadersAsync(HttpHeaders headers, string cookiesFromContainer)
{
- foreach (KeyValuePair<HeaderDescriptor, string[]> header in headers.GetHeaderDescriptorsAndValues())
+ if (headers.HeaderStore != null)
{
- if (header.Key.KnownHeader != null)
+ foreach (KeyValuePair<HeaderDescriptor, HttpHeaders.HeaderStoreItemInfo> header in headers.HeaderStore)
{
- await WriteBytesAsync(header.Key.KnownHeader.AsciiBytesWithColonSpace).ConfigureAwait(false);
- }
- else
- {
- await WriteAsciiStringAsync(header.Key.Name).ConfigureAwait(false);
- await WriteTwoBytesAsync((byte)':', (byte)' ').ConfigureAwait(false);
- }
-
- Debug.Assert(header.Value.Length > 0, "No values for header??");
- if (header.Value.Length > 0)
- {
- await WriteStringAsync(header.Value[0]).ConfigureAwait(false);
-
- if (cookiesFromContainer != null && header.Key.KnownHeader == KnownHeaders.Cookie)
+ if (header.Key.KnownHeader != null)
{
- await WriteTwoBytesAsync((byte)';', (byte)' ').ConfigureAwait(false);
- await WriteStringAsync(cookiesFromContainer).ConfigureAwait(false);
-
- cookiesFromContainer = null;
+ await WriteBytesAsync(header.Key.KnownHeader.AsciiBytesWithColonSpace).ConfigureAwait(false);
+ }
+ else
+ {
+ await WriteAsciiStringAsync(header.Key.Name).ConfigureAwait(false);
+ await WriteTwoBytesAsync((byte)':', (byte)' ').ConfigureAwait(false);
}
- // Some headers such as User-Agent and Server use space as a separator (see: ProductInfoHeaderParser)
- if (header.Value.Length > 1)
+ int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref _headerValues);
+ Debug.Assert(headerValuesCount > 0, "No values for header??");
+ if (headerValuesCount > 0)
{
- HttpHeaderParser parser = header.Key.Parser;
- string separator = HttpHeaderParser.DefaultSeparator;
- if (parser != null && parser.SupportsMultipleValues)
+ await WriteStringAsync(_headerValues[0]).ConfigureAwait(false);
+
+ if (cookiesFromContainer != null && header.Key.KnownHeader == KnownHeaders.Cookie)
{
- separator = parser.Separator;
+ await WriteTwoBytesAsync((byte)';', (byte)' ').ConfigureAwait(false);
+ await WriteStringAsync(cookiesFromContainer).ConfigureAwait(false);
+
+ cookiesFromContainer = null;
}
- for (int i = 1; i < header.Value.Length; i++)
+ // Some headers such as User-Agent and Server use space as a separator (see: ProductInfoHeaderParser)
+ if (headerValuesCount > 1)
{
- await WriteAsciiStringAsync(separator).ConfigureAwait(false);
- await WriteStringAsync(header.Value[i]).ConfigureAwait(false);
+ HttpHeaderParser parser = header.Key.Parser;
+ string separator = HttpHeaderParser.DefaultSeparator;
+ if (parser != null && parser.SupportsMultipleValues)
+ {
+ separator = parser.Separator;
+ }
+
+ for (int i = 1; i < headerValuesCount; i++)
+ {
+ await WriteAsciiStringAsync(separator).ConfigureAwait(false);
+ await WriteStringAsync(_headerValues[i]).ConfigureAwait(false);
+ }
}
}
- }
- await WriteTwoBytesAsync((byte)'\r', (byte)'\n').ConfigureAwait(false);
+ await WriteTwoBytesAsync((byte)'\r', (byte)'\n').ConfigureAwait(false);
+ }
}
if (cookiesFromContainer != null)
[Theory]
[InlineData("\u05D1\u05F1")]
[InlineData("jp\u30A5")]
- public async Task SendAsync_InvalidHeader_Throw(string value)
+ public async Task SendAsync_InvalidCharactersInHeader_Throw(string value)
{
await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
{
});
}
- [Fact]
- public async Task SendAsync_SpecialCharacterHeader_Success()
+ [Theory]
+ [InlineData("x-Special_name", "header name with underscore", true)] // underscores in header
+ [InlineData("Date", "invaliddateformat", false)] // invalid format for header but added with TryAddWithoutValidation
+ [InlineData("Accept-CharSet", "text/plain, text/json", false)] // invalid format for header but added with TryAddWithoutValidation
+ [InlineData("Content-Location", "", false)] // invalid format for header but added with TryAddWithoutValidation
+ [InlineData("Max-Forwards", "NotAnInteger", false)] // invalid format for header but added with TryAddWithoutValidation
+ public async Task SendAsync_SpecialHeaderKeyOrValue_Success(string key, string value, bool parsable)
{
- string headerValue = "header name with underscore";
await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
{
+ bool contentHeader = false;
using (HttpClient client = CreateHttpClient())
{
var message = new HttpRequestMessage(HttpMethod.Get, uri) { Version = VersionFromUseHttp2 };
- message.Headers.TryAddWithoutValidation("x-Special_name", "header name with underscore");
+ if (!message.Headers.TryAddWithoutValidation(key, value))
+ {
+ message.Content = new StringContent("");
+ contentHeader = message.Content.Headers.TryAddWithoutValidation(key, value);
+ }
(await client.SendAsync(message).ConfigureAwait(false)).Dispose();
}
+
+ // Validate our test by validating our understanding of a header's parsability.
+ HttpHeaders headers = contentHeader ? (HttpHeaders)
+ new StringContent("").Headers :
+ new HttpRequestMessage().Headers;
+ if (parsable)
+ {
+ headers.Add(key, value);
+ }
+ else
+ {
+ Assert.Throws<FormatException>(() => headers.Add(key, value));
+ }
},
async server =>
{
HttpRequestData requestData = await server.HandleRequestAsync(HttpStatusCode.OK);
-
- string header = requestData.GetSingleHeaderValue("x-Special_name");
- Assert.Equal(header, headerValue);
+ Assert.Equal(value, requestData.GetSingleHeaderValue(key));
});
}
{
var buffer = new ArrayBuffer(4);
FillAvailableSpaceWithOnes(buffer);
+ string[] headerValues = Array.Empty<string>();
- foreach (KeyValuePair<HeaderDescriptor, string[]> header in headers.GetHeaderDescriptorsAndValues())
+ foreach (KeyValuePair<HeaderDescriptor, HttpHeaders.HeaderStoreItemInfo> header in headers.HeaderStore)
{
+ int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref headerValues);
+ Assert.InRange(headerValuesCount, 0, int.MaxValue);
+ ReadOnlySpan<string> headerValuesSpan = headerValues.AsSpan(0, headerValuesCount);
+
KnownHeader knownHeader = header.Key.KnownHeader;
if (knownHeader != null)
{
// For all other known headers, send them via their pre-encoded name and the associated value.
WriteBytes(knownHeader.Http2EncodedName);
string separator = null;
- if (header.Value.Length > 1)
+ if (headerValuesSpan.Length > 1)
{
HttpHeaderParser parser = header.Key.Parser;
if (parser != null && parser.SupportsMultipleValues)
}
}
- WriteLiteralHeaderValues(header.Value, separator);
+ WriteLiteralHeaderValues(headerValuesSpan, separator);
}
else
{
// The header is not known: fall back to just encoding the header name and value(s).
- WriteLiteralHeader(header.Key.Name, header.Value);
+ WriteLiteralHeader(header.Key.Name, headerValuesSpan);
}
}
buffer.Commit(bytes.Length);
}
- void WriteLiteralHeaderValues(string[] values, string separator)
+ void WriteLiteralHeaderValues(ReadOnlySpan<string> values, string separator)
{
int bytesWritten;
while (!HPackEncoder.EncodeStringLiterals(values, separator, buffer.AvailableSpan, out bytesWritten))
buffer.Commit(bytesWritten);
}
- void WriteLiteralHeader(string name, string[] values)
+ void WriteLiteralHeader(string name, ReadOnlySpan<string> values)
{
int bytesWritten;
while (!HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, values, HttpHeaderParser.DefaultSeparator, buffer.AvailableSpan, out bytesWritten))