Fossil User Forum

warning: ”content-length” missing from 200 keep-alive reply
Login

warning: ”content-length” missing from 200 keep-alive reply

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.