@@ -19,6 +19,7 @@ internal partial class Http1Connection : HttpProtocol, IRequestProcessor
19
19
private const byte ByteAsterisk = ( byte ) '*' ;
20
20
private const byte ByteForwardSlash = ( byte ) '/' ;
21
21
private const string Asterisk = "*" ;
22
+ private const string ForwardSlash = "/" ;
22
23
23
24
private readonly HttpConnectionContext _context ;
24
25
private readonly IHttpParser < Http1ParsingHandler > _parser ;
@@ -268,16 +269,68 @@ private void OnOriginFormTarget(HttpMethod method, HttpVersion version, Span<byt
268
269
269
270
_requestTargetForm = HttpRequestTarget . OriginForm ;
270
271
272
+ if ( target . Length == 1 )
273
+ {
274
+ // If target.Length == 1 it can only be a forward slash (e.g. home page)
275
+ // and we know RawTarget and Path are the same and QueryString is Empty
276
+ RawTarget = ForwardSlash ;
277
+ Path = ForwardSlash ;
278
+ QueryString = string . Empty ;
279
+ // Clear parsedData as we won't check it if we come via this path again,
280
+ // an setting to null is fast as it doesn't need to use a GC write barrier.
281
+ _parsedRawTarget = _parsedPath = _parsedQueryString = null ;
282
+ return ;
283
+ }
284
+
271
285
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
272
286
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
273
287
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
274
288
275
289
try
276
290
{
291
+ var disableStringReuse = ServerOptions . DisableStringReuse ;
277
292
// Read raw target before mutating memory.
278
- RawTarget = target . GetAsciiStringNonNullCharacters ( ) ;
279
- QueryString = query . GetAsciiStringNonNullCharacters ( ) ;
280
- Path = PathNormalizer . DecodePath ( path , pathEncoded , RawTarget , query . Length ) ;
293
+ var previousValue = _parsedRawTarget ;
294
+ if ( disableStringReuse ||
295
+ previousValue == null || previousValue . Length != target . Length ||
296
+ ! StringUtilities . BytesOrdinalEqualsStringAndAscii ( previousValue , target ) )
297
+ {
298
+ // The previous string does not match what the bytes would convert to,
299
+ // so we will need to generate a new string.
300
+ RawTarget = _parsedRawTarget = target . GetAsciiStringNonNullCharacters ( ) ;
301
+
302
+ previousValue = _parsedQueryString ;
303
+ if ( disableStringReuse ||
304
+ previousValue == null || previousValue . Length != query . Length ||
305
+ ! StringUtilities . BytesOrdinalEqualsStringAndAscii ( previousValue , query ) )
306
+ {
307
+ // The previous string does not match what the bytes would convert to,
308
+ // so we will need to generate a new string.
309
+ QueryString = _parsedQueryString = query . GetAsciiStringNonNullCharacters ( ) ;
310
+ }
311
+ else
312
+ {
313
+ // Same as previous
314
+ QueryString = _parsedQueryString ;
315
+ }
316
+
317
+ if ( path . Length == 1 )
318
+ {
319
+ // If path.Length == 1 it can only be a forward slash (e.g. home page)
320
+ Path = _parsedPath = ForwardSlash ;
321
+ }
322
+ else
323
+ {
324
+ Path = _parsedPath = PathNormalizer . DecodePath ( path , pathEncoded , RawTarget , query . Length ) ;
325
+ }
326
+ }
327
+ else
328
+ {
329
+ // As RawTarget is the same we can reuse the previous parsed values.
330
+ RawTarget = _parsedRawTarget ;
331
+ Path = _parsedPath ;
332
+ QueryString = _parsedQueryString ;
333
+ }
281
334
}
282
335
catch ( InvalidOperationException )
283
336
{
@@ -312,9 +365,27 @@ private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target)
312
365
//
313
366
// Allowed characters in the 'host + port' section of authority.
314
367
// See https://tools.ietf.org/html/rfc3986#section-3.2
315
- RawTarget = target . GetAsciiStringNonNullCharacters ( ) ;
368
+
369
+ var previousValue = _parsedRawTarget ;
370
+ if ( ServerOptions . DisableStringReuse ||
371
+ previousValue == null || previousValue . Length != target . Length ||
372
+ ! StringUtilities . BytesOrdinalEqualsStringAndAscii ( previousValue , target ) )
373
+ {
374
+ // The previous string does not match what the bytes would convert to,
375
+ // so we will need to generate a new string.
376
+ RawTarget = _parsedRawTarget = target . GetAsciiStringNonNullCharacters ( ) ;
377
+ }
378
+ else
379
+ {
380
+ // Reuse previous value
381
+ RawTarget = _parsedRawTarget ;
382
+ }
383
+
316
384
Path = string . Empty ;
317
385
QueryString = string . Empty ;
386
+ // Clear parsedData for path and queryString as we won't check it if we come via this path again,
387
+ // an setting to null is fast as it doesn't need to use a GC write barrier.
388
+ _parsedPath = _parsedQueryString = null ;
318
389
}
319
390
320
391
private void OnAsteriskFormTarget ( HttpMethod method )
@@ -331,6 +402,9 @@ private void OnAsteriskFormTarget(HttpMethod method)
331
402
RawTarget = Asterisk ;
332
403
Path = string . Empty ;
333
404
QueryString = string . Empty ;
405
+ // Clear parsedData as we won't check it if we come via this path again,
406
+ // an setting to null is fast as it doesn't need to use a GC write barrier.
407
+ _parsedRawTarget = _parsedPath = _parsedQueryString = null ;
334
408
}
335
409
336
410
private void OnAbsoluteFormTarget ( Span < byte > target , Span < byte > query )
@@ -346,21 +420,49 @@ private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query)
346
420
// a server MUST accept the absolute-form in requests, even though
347
421
// HTTP/1.1 clients will only send them in requests to proxies.
348
422
349
- RawTarget = target . GetAsciiStringNonNullCharacters ( ) ;
423
+ var disableStringReuse = ServerOptions . DisableStringReuse ;
424
+ var previousValue = _parsedRawTarget ;
425
+ if ( disableStringReuse ||
426
+ previousValue == null || previousValue . Length != target . Length ||
427
+ ! StringUtilities . BytesOrdinalEqualsStringAndAscii ( previousValue , target ) )
428
+ {
429
+ // The previous string does not match what the bytes would convert to,
430
+ // so we will need to generate a new string.
431
+ RawTarget = _parsedRawTarget = target . GetAsciiStringNonNullCharacters ( ) ;
350
432
351
- // Validation of absolute URIs is slow, but clients
352
- // should not be sending this form anyways, so perf optimization
353
- // not high priority
433
+ // Validation of absolute URIs is slow, but clients
434
+ // should not be sending this form anyways, so perf optimization
435
+ // not high priority
354
436
355
- if ( ! Uri . TryCreate ( RawTarget , UriKind . Absolute , out var uri ) )
437
+ if ( ! Uri . TryCreate ( RawTarget , UriKind . Absolute , out var uri ) )
438
+ {
439
+ ThrowRequestTargetRejected ( target ) ;
440
+ }
441
+
442
+ _absoluteRequestTarget = uri ;
443
+ Path = _parsedPath = uri . LocalPath ;
444
+ // don't use uri.Query because we need the unescaped version
445
+ previousValue = _parsedQueryString ;
446
+ if ( disableStringReuse ||
447
+ previousValue == null || previousValue . Length != query . Length ||
448
+ ! StringUtilities . BytesOrdinalEqualsStringAndAscii ( previousValue , query ) )
449
+ {
450
+ // The previous string does not match what the bytes would convert to,
451
+ // so we will need to generate a new string.
452
+ QueryString = _parsedQueryString = query . GetAsciiStringNonNullCharacters ( ) ;
453
+ }
454
+ else
455
+ {
456
+ QueryString = _parsedQueryString ;
457
+ }
458
+ }
459
+ else
356
460
{
357
- ThrowRequestTargetRejected ( target ) ;
461
+ // As RawTarget is the same we can reuse the previous values.
462
+ RawTarget = _parsedRawTarget ;
463
+ Path = _parsedPath ;
464
+ QueryString = _parsedQueryString ;
358
465
}
359
-
360
- _absoluteRequestTarget = uri ;
361
- Path = uri . LocalPath ;
362
- // don't use uri.Query because we need the unescaped version
363
- QueryString = query . GetAsciiStringNonNullCharacters ( ) ;
364
466
}
365
467
366
468
internal void EnsureHostHeaderExists ( )
0 commit comments