Table of contents of the article:
In recent months the protocol HTTP / 3, based on HERE C, is increasingly entering production. Major browsers support it stably, and many providers—including Cloudflare, Google, and Akamai—now consider it the de facto standard for modern connections. Even Nginx, from the 1.25 series onwards, introduced an experimental HTTP/3 module, later made more stable with 1.29.x.
However, during some in-depth testing sessions conducted by Managed Server Srl, we have identified an anomalous behavior that can generate inconsistencies in rewrites, logs, and environment variables, especially for sites and applications that rely on the variable $http_host.
The problem arose from a failure to value this variable when handling HTTP/3 requests, i.e., QUIC-based connections. Unlike HTTP/1.1 and HTTP/2, where the header Host is automatically interpreted and associated, in the case of HTTP/3 NGINX did not perform the correct initialization of the corresponding value when the client sent only the pseudo header :authority.
The result? $http_host remained empty or inconsistent with the value of the requested authority, compromising compatibility with many configurations and generating unexpected behavior in several application scenarios.
The variable $http_host and its importance
To fully understand the extent of the problem, it is worth remembering that $http_host It is one of the most used variables in the NGINX environment.
It is often used in:
- blocks
serverelocationwith conditional logic; - host-based dynamic rewrites;
- redirect rules to canonical domains;
- custom logging;
- proxy passes that depend on the host of the original request.
In multi-domain or multi-tenant configurations, the absence or inconsistency of the value of $http_host It can alter the behavior of the entire stack, causing a site to respond with the wrong domain or preventing HTTPS redirects from working properly.
The cause: NGINX's behavior with HTTP/3
According to RFC 9114, Section 4.2 (“Request Pseudo-Header Fields”), every HTTP/3 request must contain a field :authority or a header Host.
Both cannot be missing, and if present they must contain the same value.
However, most HTTP/3 clients (such as Chrome, Firefox, and cURL in --http3) send field only :authority, without duplicating it as Host.
NGINX, up to version 1.29.1, did not automatically pass that value into the corresponding internal header Host, from which the variable is then derived $http_host.
This caused a functional gap between HTTP/1.1 / HTTP/2 and HTTP/3: in the first two cases $http_host he was always available, in the third he wasn't.
The behavior, although compliant with the specifications, it was not consistent with the operating philosophy of NGINX, where environment variables must maintain uniformity between protocols, so as not to force the administrator to differentiate configurations between HTTP/2 and HTTP/3.
The Patch: A Simple but Essential Integration
After a careful analysis of the NGINX source code, and taking inspiration from a similar fix already present in ANGIE, the fork developed by Web Server LLC, we have created a minimal but effective patch.
The amendment, proposed by Marco Marcoaldi (CTO of Managed Server Srl), consists in the addition of a dedicated function:
ngx_http_v3_set_host(ngx_http_request_t *r, ngx_str_t *value)
This function takes care of initializing the field r->headers_in.host when the field is not present but exists :authority, copying its value safely.
The code, integrated into the source file src/http/v3/ngx_http_v3_request.c, uses NGINX's internal data structure to allocate a new “host” header consistent with incoming HTTP/3 requests.
diff --git a/src/http/v3/ngx_http_v3_request.c b/src/http/v3/ngx_http_v3_request.c
index 5a6d4f9..6b8d77e 100644
--- a/src/http/v3/ngx_http_v3_request.c
+++ b/src/http/v3/ngx_http_v3_request.c
@@ -32,6 +33,8 @@
static ngx_int_t ngx_http_v3_process_request(ngx_http_request_t *r);
static ngx_int_t ngx_http_v3_parse_request_line(ngx_http_request_t *r);
+
+static ngx_int_t ngx_http_v3_set_host(ngx_http_request_t *r, ngx_str_t *value); // Enable $http_host
static ngx_int_t ngx_http_v3_parse_request_headers(ngx_http_request_t *r);
static ngx_int_t ngx_http_v3_parse_request_header(ngx_http_request_t *r);
@@ -1001,6 +1004,34 @@ ngx_http_v3_process_request(...)
+
+ngx_http_v3_set_host(ngx_http_request_t *r, ngx_str_t *value)
+{
+ ngx_table_elt_t *h;
+
+ static ngx_str_t host = ngx_string("host");
+
+ h = ngx_list_push(&r->headers_in.headers);
+ if (h == NULL) {
+ return NGX_ERROR;
+ }
+
+ h->hash = ngx_hash(ngx_hash(ngx_hash('h', 'o'), 's'), 't');
+
+ h->key.len = host.len;
+ h->key.data = host.data;
+
+ h->value.len = value->len;
+ h->value.data = value->data;
+
+ h->lowcase_key = host.data;
+
+ r->headers_in.host = h;
+ h->next = NULL;
+
+ return NGX_OK;
+}
+
+static ngx_int_t
ngx_http_v3_parse_request_header(ngx_http_request_t *r)
{
...
@@ -1038,7 +1068,8 @@ ngx_http_v3_parse_request_header(ngx_http_request_t *r)
- if (r->headers_in.host && r->host_end) {
+ if (r->host_end) {
+ /* full :authority value (possibly with port) */
ngx_str_t host;
host.len = r->host_end - r->host_start;
host.data = r->host_start;
@@ -1043,8 +1075,17 @@ ngx_http_v3_parse_request_header(ngx_http_request_t *r)
- if (r->headers_in.host->value.len != host.len
- || ngx_memcmp(r->headers_in.host->value.data, host.data, host.len)
- != 0)
- {
- ngx_log_error(NGX_LOG_INFO, c->log, 0,
- "client sent \":authority\" and \"Host\" headers "
- "with different values");
- goto failed;
- }
+ if (r->headers_in.host) {
+ /* both Host and :authority present - ensure they are equal */
+ if (r->headers_in.host->value.len != host.len
+ || ngx_memcmp(r->headers_in.host->value.data,
+ host.data, host.len) != 0)
+ {
+ ngx_log_error(NGX_LOG_INFO, c->log, 0,
+ "client sent \":authority\" and \"Host\" headers "
+ "with different values");
+ goto failed;
+ }
+ } else {
+ /* Host is missing - set from :authority */
+ if (ngx_http_v3_set_host(r, &host) != NGX_OK) {
+ ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+ return NGX_ERROR;
+ }
+ }
@@ -1491,7 +1532,6 @@ ngx_http_v3_finalize_request(ngx_http_request_t *r)
...
-
@@ -1730,6 +1772,7 @@ ngx_http_v3_run_request(ngx_http_request_t *r)
...
+
The patch also includes an additional logical check: if both fields Host e :authority are present and contain different values, a warning is recorded in the log (NGX_LOG_INFO) and the request is rejected, as required by the RFC specifications.
In practice, the full semantic equivalence between Host e :authority, eliminating any possible ambiguity.
Testing and validation in real environment
Once the modification was completed, the patch was tested in three phases:
- Isolated development environment, to check the compilation and behavior of the form
http_v3. - Staging environment, with HTTP/3 traffic simulations and tools like
curl --http3,h2load,wrkeautocannon. - Production environment on a Magento 2 site, using the build
nginx-1.29.1with the flag--with-http_v3_module.
In all tests, the variable $http_host it was correctly valued with the value derived from the field :authority, even in the absence of headers Host.
There were no performance regressions or impacts, even under sustained load or with concurrent connections on QUIC.
HTTP responses maintained latency and throughput unchanged from the original build.
Merge proposal into the official master branch
Once we had consolidated the behavior and verified the compatibility, we decided to make the change public.
It was then performed fork of the official NGINX repository on GitHub and opened the Pull Request #917, visible here:
The PR is accompanied by a technical description, references to RFC 9114, complete diff code, and notes on production validation tests.
At the same time the agreement was signed F5 Contributor License Agreement (CLA), which is required to allow the change to be officially integrated into the main repository.
The pull request is currently in status Open, pending review by NGINX maintainers, primarily Maxim Dounin and the F5 technical team.
As per standard practice, the integration will only take place after manual review and internal verification of the code, but preliminary feedback from the community is already positive, as the fix addresses a practical gap that many administrators had informally reported.
Open Source Contributions and Philosophy
At Managed Server Srl we strongly believe that open source is not just a technological basis, but a shared responsibility.
Many of the problems we encounter daily in managing high-performance hosting are solved thanks to small patches, optimizations, or fixes that, once shared, become global improvements.
Giving these fixes back to the community means evolving the ecosystem that benefits us all.
The proposed patch does not introduce new directives, does not alter the default behavior and does not impact performance; it simply corrects a logical lack in the HTTP/3 header parsing stream, making the code more consistent and predictable.
Waiting for the official merger
Pull Request #917 remains open and under review at this time.
As per standard practice, the NGINX team will perform a manual review of the code, verifying compatibility with other core components and adherence to internal development standards.
Once approved, the change will be incorporated into the master branch and then released in the next stable release.
This will ensure that all future NGINX builds — including those distributed by major Linux maintainers — will natively include the fix, without the need for manual patches.
Conclusion
The bug fix related to $http_host in HTTP/3 represents a small but concrete step forward towards a more solid, consistent and compatible NGINX with the new web standards.
The intervention demonstrates how even the Italian companies can actively contribute to global level projects, participating not only as users, but as direct actors in open source development.
For those who want to learn more or test the patch while waiting for the official merge, the code is publicly available in the pull request:
https://github.com/nginx/nginx/pull/917
As Managed Server Srl, we will continue to monitor the evolution of the NGINX HTTP/3 module and share any further improvements or optimizations that may arise from our daily work on high-performance hosting, Linux systems and complex web environments.
In the meantime, this patch represents a concrete contribution to the stability and predictability of one of the most critical components of the modern Internet infrastructure.