warning: "content-length" missing from 200 keep-alive reply
(1) By Florian Balmer (florian.balmer) on 2026-01-07 15:50:20 [link] [source]
Today, something with my Apache setup was changed, and I encountered an old problem, again:
> fossil clone ...
...
warning: "content-length" missing from 200 keep-alive reply
...
It's the same problem reported here and fixed here. I'm able to
mitigate the problem by setting the ap_trust_cgilike_cl environment variable,
as explained in the Serving via CGI document.
What's new this time is that Apache seems to omit the "Content-Length:" header from replies with "Connection: Keep-Alive" set, and the linked Fossil fix doesn't work in this case.
Somewhat to my surprise, I found that the following patch fixes the problem:
Index: src/http.c
==================================================================
--- src/http.c
+++ src/http.c
@@ -749,11 +749,11 @@
if( iRecvLen != iLength ){
fossil_warning("response truncated: got %d bytes of %d",
iRecvLen, iLength);
goto write_err;
}
- }else if( closeConnection ){
+ }else if( 1 ){
/* Read content until end-of-file */
int iRecvLen; /* Received length of the reply payload */
unsigned int nReq = 1000;
unsigned int nPrior = 0;
do{
That is, ignoring the closeConnection flag in this block and simply
reading to EOF seems to work both on Windows 11 (OpenSSL 3.6.0) and FreeBSD
(OpenSSL 3.0.18).
My questions:
- Are there any experts who can explain why my "fix" works?
- If this solution turns out to be robust, should it go into Fossil?
(2) By Florian Balmer (florian.balmer) on 2026-01-09 16:30:47 in reply to 1 [source]
Meanwhile, I realized that the fix was later modified to only read to "EOF" if the connection is not keep-alive, as advised by the Apache developers.
I'm still not sure why this works, maybe because Fossil always closes and reopens connections during cloning and syncing, regardless of the keep-alive flag (which is the implicit default for HTTP/1.1).
Not sure what changed with my Apache setup (I have a shared web hosting), but
not both of the suggested solutions to the problem are valid any longer,
i.e. fiddling with the Apache config to add the ap_trust_cgilike_cl
environment variable is now mandatory.
I'm now testing the patch pasted below to always indicate the content length as a fallback with a custom header. Fossil has meanwhile become the indispensable base tool for all my computing activities, and I want it to work in all cases, without any fiddling with the server, because: on a fresh machine, to be able to fiddle with my server, I can simply clone my main script and dotfiles repository, and then ... no, wait 🤔
Is there any interest for the patch below to be integrated into the main line of Fossil?
Status.......: ACTIVE
Baseline.....: *
Cherry-pick..:
Required.....:
More robust fix to cloning and syncing if the "Content-Length" header is lacking
from CGI replies. Refer to the comments in the source code for more information.
Index: src/cgi.c
==================================================================
--- src/cgi.c
+++ src/cgi.c
@@ -260,10 +260,34 @@
void cgi_set_content_type(const char *zType){
int i;
for(i=0; zType[i]>='+' && zType[i]<='z'; i++){}
zReplyMimeType = fossil_strndup(zType, i);
}
+
+/* Send a custom "X-Fossil-Content-Length" header to specify the reply body size
+** as a fallback in case the web server removes the "Content-Length" header from
+** CGI replies (the Apache web server seems to do this for security reasons, but
+** it's possible to get it back by setting the "ap_trust_cgilike_cl" environment
+** variable to "yes" [0]). This enhances the fix to make Fossil work without the
+** "Content-Length" header [1-3] in situations where the web server replies with
+** "Connection: keep-alive", the implicit default of HTTP/1.1. The Apache people
+** advised against reading to EOF for keep-alive connections [4], but the method
+** seems to work anyway, probably because Fossil opens a new connection for each
+** HTTP transmission during cloning and syncing. The fallback header is added so
+** that cloning and syncing work out-of-the-box in all cases without the need to
+** fiddle with the web server configuration.
+**
+** [0]: https://httpd.apache.org/docs/2.4/env.html#:~:text=ap_trust_cgilike_cl
+** [1]: https://fossil-scm.org/home/info/a8e33fb161
+** [2]: https://fossil-scm.org/home/info/71919ad1b5
+** [3]: https://fossil-scm.org/home/info/f4ffefe708
+** [4]: https://bz.apache.org/bugzilla/show_bug.cgi?id=68905
+*/
+static int fSendXContentLength = 0;
+void cgi_send_x_content_length(int fEnabled){
+ fSendXContentLength = fEnabled;
+}
/*
** Erase any existing reply content. Replace is with a pNewContent.
**
** This routine erases pNewContent. In other words, it move pNewContent
@@ -572,10 +596,13 @@
blob_appendf(&hdr, "Content-Range: bytes %d-%d/%d\r\n",
rangeStart, rangeEnd-1, total_size);
total_size = rangeEnd - rangeStart;
}
blob_appendf(&hdr, "Content-Length: %d\r\n", total_size);
+ if( fSendXContentLength ){
+ blob_appendf(&hdr, "X-Fossil-Content-Length: %d\r\n", total_size);
+ }
}else{
total_size = 0;
}
blob_appendf(&hdr, "\r\n");
cgi_fwrite(blob_buffer(&hdr), blob_size(&hdr));
Index: src/http.c
==================================================================
--- src/http.c
+++ src/http.c
@@ -602,10 +602,13 @@
}
if( iHttpVersion<0 ) iHttpVersion = 1;
closeConnection = 0;
}else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
for(i=15; fossil_isspace(zLine[i]); i++){}
+ iLength = atoi(&zLine[i]);
+ }else if( fossil_strnicmp(zLine, "x-fossil-content-length:", 24)==0 ){
+ for(i=24; fossil_isspace(zLine[i]); i++){}
iLength = atoi(&zLine[i]);
}else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
int j; /* Position of next separator (comma or zero terminator). */
int k = 0; /* Position after last non-space char for current value. */
i = 11;
Index: src/xfer.c
==================================================================
--- src/xfer.c
+++ src/xfer.c
@@ -1300,10 +1300,11 @@
login_check_credentials();
cgi_check_for_malice();
memset(&xfer, 0, sizeof(xfer));
blobarray_zero(xfer.aToken, count(xfer.aToken));
cgi_set_content_type(g.zContentType);
+ cgi_send_x_content_length(1);
cgi_reset_content();
if( db_schema_is_outofdate() ){
@ error database\sschema\sis\sout-of-date\son\sthe\sserver.
return;
}
(3) By Stephan Beal (stephan) on 2026-01-09 16:54:52 in reply to 2 [link] [source]
I'm still not sure why this works, maybe because Fossil always closes and reopens connections during cloning and syncing, regardless of the keep-alive flag (which is the implicit default for HTTP/1.1).
Fossil does do that. Each round trip is a new connection.
Is there any interest for the patch below to be integrated into the main line of Fossil?
FWIW, i'm ambivalent about it. It if improves portability, great :).
(4) By Richard Hipp (drh) on 2026-01-09 17:00:19 in reply to 3 [link] [source]
On trunk is a much simpler patch that just set the connection to auto-close if the server fails to send a content-length.
(5) By Florian Balmer (florian.balmer) on 2026-01-09 17:05:34 in reply to 3 [link] [source]
I think Richard has already committed a better solution that addresses my problem!
This also fixes arbitrary URL test downloads with fossil
test-httpmsg from my server so they don't just output
"content-length" missing from 200 keep-alive reply and no
payload at all!
Thanks!
(6) By Andy Bradford (andybradford) on 2026-01-10 04:30:44 in reply to 3 [link] [source]
> Fossil does do that. Each round trip is a new connection. Minor clarification; while this is true for HTTP(S) connections, for the SSH transport, only one SSH connection is established for the duration of the entire sync operation and each round-trip just reuses the same SSH connection to negotiate a new HTTP session.