Skip to content

Commit f6dffed

Browse files
Verify and implement the NGINX 8 KiB header line limit (#3574)
* Verify and implement the NGINX 8 KiB header line limit Observed against real Vuforia on 2026-09-08: both the Target API and the Query API return NGINX's "400 Request Header Or Cookie Too Large" HTML response for any header line of 8191 bytes or more, and accept a line of 8190 bytes. The mock now does the same, checked before any other validation, and a test runs the cases against every backend. The related behaviors which were observed but are not implemented (an Envoy cookie allowance, the AWS load balancer's 16 KiB limit and the Query API application server's 431) are recorded in the differences document, and the never-attempted entry is removed. Closes #3571 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Retry the concrete requests timeout and connection errors in CI pytest-retry checks whether the failing exception's type is in the filtered tuple, so a subclass of a listed type is never retried. The tuple listed requests' Timeout, but requests raises ReadTimeout and ConnectTimeout, so a TLS handshake timeout against real Vuforia failed a job outright instead of being retried. List the concrete types, and requests' ConnectionError, explicitly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 1e48d6c commit f6dffed

13 files changed

Lines changed: 400 additions & 23 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ jobs:
6565
- tests/mock_vws/test_content_length.py::TestIncorrect::test_not_integer
6666
- tests/mock_vws/test_content_length.py::TestIncorrect::test_too_large
6767
- tests/mock_vws/test_content_length.py::TestIncorrect::test_too_small
68+
- tests/mock_vws/test_header_size.py::TestOversizedHeaderLine::test_header_too_large
69+
- tests/mock_vws/test_header_size.py::TestOversizedHeaderLine::test_cookie_too_large
70+
- tests/mock_vws/test_header_size.py::TestOversizedHeaderLine::test_large_header_within_limit
6871
- tests/mock_vws/test_database_summary.py::TestDatabaseSummary::test_success
6972
- tests/mock_vws/test_database_summary.py::TestDatabaseSummary::test_active_images
7073
- tests/mock_vws/test_database_summary.py::TestDatabaseSummary::test_failed_images

docs/source/differences-to-vws.rst

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,26 @@ The mock uses the fixed sample value ``us-east-2, us-west-2`` for
134134
``x-aws-region`` response headers. The regions returned by the real Vuforia
135135
Web Services can differ, so tests should not rely on the mock's exact value.
136136

137+
.. _differences-nginx-error-cases:
138+
137139
NGINX Error cases
138140
-----------------
139141

140-
Vuforia uses NGINX.
141-
This has error handling which is not duplicated in the mock.
142-
For example, Vuforia is documented as returning a 400 (``BAD REQUEST``) response if a header or cookie is given which is larger than 8 KiB.
143-
144-
.. admonition:: Unverified assumption
145-
146-
:ref:`unverified-nginx-oversized-header-or-cookie`
142+
Vuforia uses NGINX in front of both the Target API and the Query API.
143+
NGINX reads each request header line into an 8 KiB buffer, and returns a 400 (``BAD REQUEST``) response with an HTML body titled ``400 Request Header Or Cookie Too Large`` for a line which does not fit.
144+
The line's terminating CRLF also counts towards the buffer, so the longest accepted line is 8190 bytes, where a line is the header name, a colon, a space and the value.
145+
This was observed against real Vuforia on 2026-09-08.
146+
147+
The mock returns that response for any header line longer than 8190 bytes.
148+
The mock does not implement the following related behaviors, which were observed in the same session:
149+
150+
* The Target API's Envoy layer lets a ``Cookie`` line slightly over the limit through.
151+
A ``Cookie`` line of 8193 bytes was accepted and one of 8300 bytes was rejected.
152+
* The Target API's AWS load balancer rejects a header line of 16384 bytes or more itself, with a shorter HTML body and a ``Server: awselb/2.0`` header.
153+
A ``Cookie`` line of that size passes the load balancer and is rejected by NGINX instead.
154+
* The Query API's application server rejects a request whose headers total about 8 KiB with a 431 (``REQUEST HEADER FIELDS TOO LARGE``) HTML response before the NGINX limit is reached.
155+
With the headers which a query normally has, a header line of 7500 bytes was accepted and one of 8000 bytes was rejected this way.
156+
* The Model Target Web API, the OAuth2 token endpoint and reco counts report downloads in the mock do not apply the limit.
147157

148158
Result codes
149159
------------

docs/source/unverified-behavior.rst

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,6 @@ The mock does not implement this, so a user of the mock sees a successful respon
7777

7878
A database with more than a million images would verify this, which a test account cannot hold.
7979

80-
.. _unverified-nginx-oversized-header-or-cookie:
81-
82-
Large headers and cookies
83-
-------------------------
84-
85-
:Category: never-attempted
86-
:API: Cross-cutting request handling
87-
88-
Vuforia runs behind NGINX, which is documented as returning a 400 (``BAD REQUEST``) response for a header or a cookie larger than 8 KiB.
89-
The mock does not implement this, and no test sends such a request to either.
90-
91-
Sending a request with a header larger than 8 KiB to a real database would verify this.
92-
9380
.. _unverified-reco-counts-report-not-ready:
9481

9582
A reco counts report which is not ready

newsfragments/3571.change

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The mock now returns NGINX's ``400 Request Header Or Cookie Too Large`` response for any request header line longer than 8190 bytes, as real Vuforia does.

src/mock_vws/_mock_common.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import uuid
77
from collections.abc import Iterable, Mapping
88
from dataclasses import dataclass
9-
from typing import Any, override
9+
from typing import Any, Final, override
1010

1111
from beartype import beartype
1212

@@ -144,3 +144,28 @@ def json_dump(*, body: dict[str, Any]) -> str:
144144
JSON dump of data in the same way that Vuforia dumps data.
145145
"""
146146
return json.dumps(obj=body, separators=(",", ":"))
147+
148+
149+
# NGINX, which sits in front of both Vuforia APIs, reads each header line
150+
# into an 8 KiB buffer which also holds the line's terminating CRLF.
151+
# A line of 8190 bytes is accepted and a line of 8191 bytes is rejected.
152+
MAX_HEADER_LINE_LENGTH: Final[int] = 8190
153+
154+
155+
@beartype
156+
def has_oversized_header_line(*, request_headers: Mapping[str, str]) -> bool:
157+
"""Whether any header line is too long for NGINX's header buffer.
158+
159+
A header line is the header name, a colon, a space and the value, as
160+
sent on the wire.
161+
162+
Args:
163+
request_headers: The headers sent with the request.
164+
165+
Returns:
166+
Whether any header line is longer than ``MAX_HEADER_LINE_LENGTH``.
167+
"""
168+
return any(
169+
len(f"{name}: {value}".encode()) > MAX_HEADER_LINE_LENGTH
170+
for name, value in request_headers.items()
171+
)

src/mock_vws/_query_validators/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
validate_date_in_range,
2727
)
2828
from .fields_validators import validate_extra_fields
29+
from .header_size_validators import validate_header_lines_not_too_large
2930
from .image_validators import (
3031
validate_image_dimensions,
3132
validate_image_field_given,
@@ -49,13 +50,17 @@ def run_query_validators(
4950
) -> None:
5051
"""Run all validators.
5152
53+
NGINX rejects a request with an over-long header line before it reaches
54+
Vuforia, so that is checked first.
55+
5256
Args:
5357
request_path: The path of the request.
5458
request_headers: The headers sent with the request.
5559
request_body: The body of the request.
5660
request_method: The HTTP method of the request.
5761
databases: All Vuforia databases.
5862
"""
63+
validate_header_lines_not_too_large(request_headers=request_headers)
5964
validate_content_length_header_is_int(request_headers=request_headers)
6065
validate_content_length_header_not_too_large(
6166
request_headers=request_headers,

src/mock_vws/_query_validators/exceptions.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,43 @@ def __init__(self) -> None:
457457
}
458458

459459

460+
@beartype
461+
class RequestHeaderOrCookieTooLargeError(ValidatorError):
462+
"""Exception raised when a request header line is too long for NGINX.
463+
464+
NGINX rejects the request before it reaches the Vuforia application,
465+
so this takes precedence over every other validation, including
466+
authorization.
467+
"""
468+
469+
def __init__(self) -> None:
470+
"""Initialize an NGINX request header too large response."""
471+
super().__init__()
472+
self.status_code = HTTPStatus.BAD_REQUEST
473+
self.response_text = "".join(
474+
f"{line}\r\n"
475+
for line in (
476+
"<html>",
477+
(
478+
"<head><title>400 Request Header Or Cookie Too Large"
479+
"</title></head>"
480+
),
481+
"<body>",
482+
"<center><h1>400 Bad Request</h1></center>",
483+
"<center>Request Header Or Cookie Too Large</center>",
484+
"<hr><center>nginx</center>",
485+
"</body>",
486+
"</html>",
487+
)
488+
)
489+
self.headers = {
490+
**_BASE_HEADERS,
491+
"Content-Type": "text/html",
492+
"Date": http_date(),
493+
"Content-Length": str(object=len(self.response_text)),
494+
}
495+
496+
460497
@beartype
461498
class RequestEntityTooLargeError(ValidatorError):
462499
"""Exception raised when the given image file size is too large."""
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Validators for the size of request headers."""
2+
3+
import logging
4+
from collections.abc import Mapping
5+
6+
from beartype import beartype
7+
8+
from mock_vws._mock_common import has_oversized_header_line
9+
from mock_vws._query_validators.exceptions import (
10+
RequestHeaderOrCookieTooLargeError,
11+
)
12+
13+
_LOGGER = logging.getLogger(name=__name__)
14+
15+
16+
@beartype
17+
def validate_header_lines_not_too_large(
18+
*,
19+
request_headers: Mapping[str, str],
20+
) -> None:
21+
"""Validate that no header line is too long for NGINX.
22+
23+
Args:
24+
request_headers: The headers sent with the request.
25+
26+
Raises:
27+
RequestHeaderOrCookieTooLargeError: A header line is longer than
28+
NGINX's header buffer.
29+
"""
30+
if has_oversized_header_line(request_headers=request_headers):
31+
_LOGGER.warning(msg="A request header line is too large.")
32+
raise RequestHeaderOrCookieTooLargeError

src/mock_vws/_services_validators/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
validate_authorization,
1414
)
1515
from .context import ValidatorContext
16+
from .header_size_validators import validate_header_lines_not_too_large
1617
from .request_rate_limiter import RequestRateLimiter
1718
from .routes import match_route
1819

@@ -29,7 +30,9 @@ def run_services_validators[DatabaseT: AnyDatabase](
2930
) -> DatabaseT:
3031
"""Run the validators which apply to the request.
3132
32-
Every request is authorized first, because the validators which follow
33+
NGINX rejects a request with an over-long header line before it reaches
34+
Vuforia, so that is checked first.
35+
Every request is then authorized, because the validators which follow
3336
are given the database which the request's server keys belong to. Which
3437
validators follow, and in which order, is decided by the route the
3538
request was made to. See :py:mod:`mock_vws._services_validators.routes`.
@@ -45,6 +48,7 @@ def run_services_validators[DatabaseT: AnyDatabase](
4548
Returns:
4649
The database which the request's server keys belong to.
4750
"""
51+
validate_header_lines_not_too_large(request_headers=request_headers)
4852
validate_auth_header_exists(request_headers=request_headers)
4953
validate_auth_header_has_signature(request_headers=request_headers)
5054
validate_access_key_exists(

src/mock_vws/_services_validators/exceptions.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,43 @@ def __init__(self) -> None: # pragma: no cover
356356
}
357357

358358

359+
@beartype
360+
class RequestHeaderOrCookieTooLargeError(ValidatorError):
361+
"""Exception raised when a request header line is too long for NGINX.
362+
363+
NGINX rejects the request before it reaches the Vuforia application,
364+
so this takes precedence over every other validation, including
365+
authorization.
366+
"""
367+
368+
def __init__(self) -> None:
369+
"""Initialize an NGINX request header too large response."""
370+
super().__init__()
371+
self.status_code = HTTPStatus.BAD_REQUEST
372+
self.response_text = "".join(
373+
f"{line}\r\n"
374+
for line in (
375+
"<html>",
376+
(
377+
"<head><title>400 Request Header Or Cookie Too Large"
378+
"</title></head>"
379+
),
380+
"<body>",
381+
"<center><h1>400 Bad Request</h1></center>",
382+
"<center>Request Header Or Cookie Too Large</center>",
383+
"<hr><center>nginx</center>",
384+
"</body>",
385+
"</html>",
386+
)
387+
)
388+
self.headers = {
389+
**_STANDARD_HEADERS,
390+
"Content-Type": "text/html",
391+
"Date": http_date(),
392+
"Content-Length": str(object=len(self.response_text)),
393+
}
394+
395+
359396
@beartype
360397
class ContentLengthHeaderNotIntError(ValidatorError):
361398
"""

0 commit comments

Comments
 (0)