-
You must be signed in to change notification settings -
HTTP Early Hints #326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTTP Early Hints #326
Conversation
|
||
*pos++ = 0; | ||
|
||
pos = ngx_http_v2_write_name(pos, header[i].key.data, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that the following line should be "implemented" here:
#define NGX_HTTP_V2_LINK_INDEX 45
In the case of EarlyHints, the only header that is relevant is Link
.
A similar issue exists for HTTP/3, where the header should utilize the static table for HPACK/QPACK encoding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@patryk4815 thanks, this makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we do this, it should be done for all responses, not just early hints. And it will probably require direct referencing Link
headers from the headers structure. Overall, a good optimization for the future.
Updated the patch. Change: For the record, client may receive multiple early hints from different backends in case of main header http or network error. |
Reworked this. Now it's two patches. The first one adds early hints proxy. A new directive
The second patch adds
|
@arut have you had any thoughts how could this be supported for FastCGI proxy? I'm php-fpm maintainer and we would be open to do some tweaks in our implementation for that. |
Not yet, but we'll take a look at this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the record, the conclusion from our elsewhere conversation is to proceed with the 1st commit only, while keeping in mind the 2nd commit as a possible further improvement.
Some generic comments on the 1st commit log.
Upstream: early hints proxy.
Such summary looks somewhat vague, what about:
Added support for early hints in HTTP responses.
The change implements parsing early hints response from the proxied server.
s/parsing/processing/? as it is not just about parsing
But then it would make the sentence superfluous to the next one.
So you might consider to remove this sentence for brevity.
By default, early hints are enabled for HTTP/2 and
HTTP/3 and disabled for earlier HTTP versions due to protocol
The change actually disables it for the HTTP/1.1 protocol by default, and unconditionally disables for older protocols, such as HTTP/1.0. This may need a further clarification in this regard.
You may also want to mention that this is done to meet RFC 8297 security considerations, optionally replacing the "non-early-hints-aware" vague part.
# early_hints $arg_client_supports_early_hints;
While technically correct, the example could be somewhat more usable.
proxy_pass http://bar.example.com;
just example.com should be as good
Its sounds like we are talking about client-side here., while it's all about the proxy side.
ok
Rewritten this part and added RFC quotes.
OK, added an example implementing this recommendation.
ok |
Also, implemented these changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some 1st round comments
src/http/ngx_http_upstream.c
Outdated
} | ||
} | ||
|
||
goto read_header; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This results in the extra recv() call in a typical case of early hints sent separately.
Any reasons to do so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's what happens normally after u->process_header()
returns NGX_AGAIN
. I don't think it makes sense to optimize this only for early hints.
static ngx_int_t ngx_http_early_hints_filter(ngx_http_request_t *r); | ||
static ngx_int_t ngx_http_header_filter(ngx_http_request_t *r); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: these two filters are declared and defined (and used) in a different order.
@@ -50,6 +51,9 @@ static u_char ngx_http_server_string[] = "Server: nginx" CRLF; | |||
static u_char ngx_http_server_full_string[] = "Server: " NGINX_VER CRLF; | |||
static u_char ngx_http_server_build_string[] = "Server: " NGINX_VER_BUILD CRLF; | |||
|
|||
static ngx_str_t ngx_http_early_hints_status_line | |||
= ngx_string("103 Early Hints"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: lines with assignments are wrapped after the assignment sign:
$ grep -r '^static ngx_str_t .* =$' src
src/http/modules/ngx_http_log_module.c:static ngx_str_t ngx_http_combined_fmt =
src/http/modules/ngx_http_secure_link_module.c:static ngx_str_t ngx_http_secure_link_expires_name =
$ grep -r '^static u_char .* =$' src
src/mail/ngx_mail_smtp_handler.c:static u_char smtp_invalid_pipelining[] =
src/http/ngx_http_special_response.c:static u_char ngx_http_error_full_tail[] =
src/http/ngx_http_special_response.c:static u_char ngx_http_error_build_tail[] =
src/http/ngx_http_special_response.c:static u_char ngx_http_error_tail[] =
src/http/ngx_http_special_response.c:static u_char ngx_http_msie_padding[] =
src/http/ngx_http_special_response.c:static u_char ngx_http_msie_refresh_head[] =
src/http/ngx_http_special_response.c:static u_char ngx_http_msie_refresh_tail[] =
src/http/modules/ngx_http_grpc_module.c:static u_char ngx_http_grpc_connection_start[] =
but:
$ grep -E -r '^ {4}= ' src
$ grep -E -r '^ *= ' src
$
/* "HTTP/1.x" and status line */ | ||
b->last = ngx_cpymem(b->last, "HTTP/1.1 ", sizeof("HTTP/1.x ") - 1); | ||
|
||
b->last = ngx_copy(b->last, ngx_http_early_hints_status_line.data, | ||
ngx_http_early_hints_status_line.len); | ||
|
||
*b->last++ = CR; *b->last++ = LF; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes a copy (in several passes) of constant objects known in advance.
Instead, they could be references.
PoC how this could be improved to eliminate copies:
diff --git a/src/http/ngx_http_header_filter_module.c b/src/http/ngx_http_header_filter_module.c
index d1425c385..32a66859e 100644
--- a/src/http/ngx_http_header_filter_module.c
+++ b/src/http/ngx_http_header_filter_module.c
@@ -51,8 +51,8 @@ static u_char ngx_http_server_string[] = "Server: nginx" CRLF;
static u_char ngx_http_server_full_string[] = "Server: " NGINX_VER CRLF;
static u_char ngx_http_server_build_string[] = "Server: " NGINX_VER_BUILD CRLF;
-static ngx_str_t ngx_http_early_hints_status_line
- = ngx_string("103 Early Hints");
+static u_char ngx_http_early_hints_string[] =
+ "HTTP/1.1 103 Early Hints" CRLF;
static ngx_str_t ngx_http_status_lines[] = {
@@ -635,10 +635,12 @@ ngx_http_early_hints_filter(ngx_http_request_t *r)
size_t len;
ngx_buf_t *b;
ngx_uint_t i;
- ngx_chain_t out;
+ ngx_chain_t out, hl;
ngx_list_part_t *part;
ngx_table_elt_t *header;
+ static ngx_buf_t hb;
+
if (r->http_version < NGX_HTTP_VERSION_11) {
return NGX_OK;
}
@@ -672,24 +674,13 @@ ngx_http_early_hints_filter(ngx_http_request_t *r)
return NGX_OK;
}
- len += sizeof("HTTP/1.x ") - 1
- + ngx_http_early_hints_status_line.len + sizeof(CRLF) - 1
- /* the end of the early hints */
- + sizeof(CRLF) - 1;
+ len += sizeof(CRLF) - 1;
b = ngx_create_temp_buf(r->pool, len);
if (b == NULL) {
return NGX_ERROR;
}
- /* "HTTP/1.x" and status line */
- b->last = ngx_cpymem(b->last, "HTTP/1.1 ", sizeof("HTTP/1.x ") - 1);
-
- b->last = ngx_copy(b->last, ngx_http_early_hints_status_line.data,
- ngx_http_early_hints_status_line.len);
-
- *b->last++ = CR; *b->last++ = LF;
-
part = &r->headers_out.headers.part;
header = part->elts;
@@ -716,20 +707,33 @@ ngx_http_early_hints_filter(ngx_http_request_t *r)
*b->last++ = CR; *b->last++ = LF;
}
- ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
- "%*s", (size_t) (b->last - b->pos), b->pos);
+ if (hb.temporary == 0) {
+ hb.start = ngx_http_early_hints_string;
+ hb.last = hb.start + sizeof(ngx_http_early_hints_string) - 1;
+ hb.end = hb.last;
+ hb.temporary = 1;
+ }
+
+ hb.pos = hb.start;
+
+ hl.buf = &hb;
+ hl.next = &out;
+
+ ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "%*s%*s",
+ (size_t) (hb.last - hb.pos), hb.pos,
+ (size_t) (b->last - b->pos), b->pos);
/* the end of HTTP early hints */
*b->last++ = CR; *b->last++ = LF;
- r->header_size = b->last - b->pos;
+ r->header_size = (hb.last - hb.pos) + (b->last - b->pos);
b->flush = 1;
out.buf = b;
out.next = NULL;
- return ngx_http_write_filter(r, &out);
+ return ngx_http_write_filter(r, &hl);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like too much code for saving 26 bytes (of which 16 bytes are used by ngx_chain_t
anyway). Also, an additional chain link leads to more code executed down the filter chain. I agree that the entire 103 status line can be a single string, but an additional buffer complicates things too much.
src/http/ngx_http_core_module.c
Outdated
ngx_int_t rc; | ||
ngx_http_core_loc_conf_t *clcf; | ||
|
||
if (r != r->main || r->header_sent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
r->header_sent
should not happen here, similar to ngx_http_send_header()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed separately, I have added the same check for r->header_sent
as in ngx_http_send_header()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, added a check for r->post_action
similar to ngx_http_send_header()
and moved r != r->main
check to filter handlers, similar to the main filter chain.
src/http/ngx_http_core_module.c
Outdated
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); | ||
|
||
if (clcf->early_hints == NULL) { | ||
/* by default enabled for http/2+ */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- the proper spelling is HTTP/2
- anyway, the comment doesn't seem to be useful
src/http/ngx_http_core_module.c
Outdated
@@ -3636,6 +3677,7 @@ ngx_http_core_create_loc_conf(ngx_conf_t *cf) | |||
clcf->recursive_error_pages = NGX_CONF_UNSET; | |||
clcf->chunked_transfer_encoding = NGX_CONF_UNSET; | |||
clcf->etag = NGX_CONF_UNSET; | |||
clcf->early_hints = NGX_CONF_UNSET_PTR; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unsorted here and below, in the merge phase
at least I'd put it consistently before xor after server_tokens
(ngx_http_core_loc_conf_s seems to be perfectly unsorted, doesn't worth wasting time on this one)
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, | ||
"http2 early hints filter"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's the only early filter with entry logging
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTTP/2 header filter is also the only header filter like that. Anyway, removed the logging for better compliance with other header filters.
After an internal discussion, avoided calling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 1st commit makes early hints integration in the grpc module pretty easy:
diff --git a/src/http/modules/ngx_http_grpc_module.c b/src/http/modules/ngx_http_grpc_module.c
index 80046d6a4..bc3cc1351 100644
--- a/src/http/modules/ngx_http_grpc_module.c
+++ b/src/http/modules/ngx_http_grpc_module.c
@@ -1869,7 +1869,7 @@ ngx_http_grpc_process_header(ngx_http_request_t *r)
return NGX_HTTP_UPSTREAM_INVALID_HEADER;
}
- if (status < NGX_HTTP_OK) {
+ if (status < NGX_HTTP_OK && status != NGX_HTTP_EARLY_HINTS) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"upstream sent unexpected :status \"%V\"",
status_line);
@@ -4413,6 +4413,7 @@ ngx_http_grpc_create_loc_conf(ngx_conf_t *cf)
conf->upstream.pass_request_body = 1;
conf->upstream.force_ranges = 0;
conf->upstream.pass_trailers = 1;
+ conf->upstream.pass_early_hints = 1;
conf->upstream.preserve_output = 1;
conf->headers_source = NGX_CONF_UNSET_PTR;
It is made unconditional following pass_trailers
.
Again, this raises a question whether we actually need the proxy_pass_early_hints
directive. There doesn't seem to be any cautions to make receiving early hints upstream-side optional, which also meets HTTP semantics (RFC 9110):
A client MUST be able to parse one or more 1xx responses received
prior to a final response, even if the client does not expect one.
A user agent MAY ignore unexpected 1xx responses.
The change implements processing upstream early hints response in ngx_http_proxy_module and ngx_http_grpc_module. A new directive "early_hints" enables sending early hints to the client. By default, sending early hints is disabled. Example: map $http_sec_fetch_mode $early_hints { navigate $http2$http3; } early_hints $early_hints; proxy_pass http://example.com;
@arut It seems that |
A revamped version described here: #326 (comment)
Original text:
The change implements parsing Early Hints response from the proxied server and forwarding it to the client. Also, an Early Hints response could be sent to the client by the upstream module before choosing a proxied server. This happens if there are headers to send, which can be configured or added by third-party modules using the early hint filters.
A new parameter "early" is added to the "add_header" directive. The parameter instructs nginx to send this header as part of Early Hints response. The header is also sent in the main response. Also, a new directive "early_hints" is added, which accepts a predicate to enable Early Hints.
Example:
upd: added
early_hints
directive and renamed the parameter toearly
.How to avoid issues for clients that don't support Early Hints (thanks @bavshin-f5 ).