JuliusBrussee/caveman · error

%s (%s): trailing PEM block is truncated after %d certificat

Error message

%s (%s): trailing PEM block is truncated after %d certificate(s), so the bundle is incomplete and must not be half-trusted

What it means

Thrown by rootsWithCAFile (chhttp.go:188) after the PEM decode loop ends: the leftover bytes still contain a '-----BEGIN' marker, meaning the file ends with a PEM header whose block was never completed. A truncated trailing certificate would be silently dropped, producing a half-trusted pool, so the loader refuses the whole bundle instead. The count of successfully added certificates is included.

Source

Thrown at shared/platform/chhttp/chhttp.go:188

	rest := bundle
	for {
		var block *pem.Block
		block, rest = pem.Decode(rest)
		if block == nil {
			break
		}
		if block.Type != "CERTIFICATE" {
			continue
		}
		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return nil, fmt.Errorf("%s (%s): certificate %d is unparseable, so the bundle is incomplete and must not be half-trusted: %w", caFileEnv, path, added+1, err)
		}
		roots.AddCert(cert)
		added++
	}
	if bytes.Contains(rest, []byte("-----BEGIN")) {
		return nil, fmt.Errorf("%s (%s): trailing PEM block is truncated after %d certificate(s), so the bundle is incomplete and must not be half-trusted", caFileEnv, path, added)
	}
	if added == 0 {
		return nil, fmt.Errorf("%s (%s) contains no valid PEM certificate", caFileEnv, path)
	}
	return roots, nil
}

// errTransport fails every request with the configuration error that produced
// it. A client constructor cannot return an error, and falling back to the
// default transport would silently trade a rejected TLS configuration for
// unpinned verification — so the client is built, and refuses to send.
type errTransport struct{ err error }

func (t errTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, t.err }

// queryTransport is the shared connection pool behind ClickHouse query clients.
//
// http.DefaultTransport keeps only DefaultMaxIdleConnsPerHost (2) idle

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Check the tail of the bundle: the last block must end with a complete -----END CERTIFICATE----- line followed by a newline.
  2. Re-download or regenerate the bundle from the source CA and compare checksums.
  3. Strip the truncated trailing block and re-test, or restore from a known-good copy.
  4. Add a CI check that validates the bundle end-to-end (openssl verifies every block) before deployment.

Example fix

# before: truncated tail
-----BEGIN CERTIFICATE-----
MIIB...(cut off mid-bas

# after: complete block
-----BEGIN CERTIFICATE-----
MIIB...full base64...
-----END CERTIFICATE-----
Defensive patterns

Strategy: validation

Validate before calling

// after decoding all blocks, leftover '-----BEGIN' means truncation
rest := data
for {
    var blk *pem.Block
    blk, rest = pem.Decode(rest)
    if blk == nil { break }
}
if bytes.Contains(rest, []byte("-----BEGIN")) {
    return fmt.Errorf("bundle truncated: trailing PEM header without complete block")
}

Try / catch

err := validateBundle(path)
if err != nil {
    log.Fatalf("config: %v", err) // treat as fatal: refuse partial trust
}

Prevention

When it happens

Trigger: The CA-file bundle's final PEM block is missing its END line, has a broken base64 body, or was cut off mid-write - pem.Decode returns nil for it, the loop breaks, and the remaining bytes still contain '-----BEGIN'.

Common situations: File truncated by a partial download or a full disk during generation; editor save that dropped the last lines; cat of two files where the second was still being written; a bundle piped through a command whose output was limited (head, log truncation).

Understand the failure class

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/a3f101cc3145d6e2. Report an issue: GitHub.