-
Notifications
You must be signed in to change notification settings - Fork 38
Description
Binary download fails when server responds with content-encoding
Description
The coder.coder-remote extension fails to download the CLI binary from /bin/coder-<os>-<arch> when the server responds with Content-Encoding: br (Brotli compression). The extension aborts with "Unable to download binary: aborted" because Content-Length is absent from the compressed response.
Steps to Reproduce
- Deploy Coder server behind any standard setup (tested with AWS ALB on EKS)
- Connect from Kiro IDE using the
coder.coder-remoteextension - Extension attempts to download
/bin/coder-windows-amd64.exe(~51 MB) - Download fails immediately
Expected Behavior
The binary downloads successfully and the SSH tunnel is established.
Actual Behavior
Extension logs:
Downloading binary from /bin/coder-windows-amd64.exe
Got status code 200
Got invalid or missing content length
Released download lock
Unable to download binary: aborted
Root Cause
The extension's HTTP client sends Accept-Encoding: gzip, deflate, br. The Coder server honors this and responds with Brotli-compressed content:
HTTP/2 200
content-encoding: br
x-original-content-length: 53395640Because the compressed size isn't known upfront, Content-Length is dropped (chunked encoding on HTTP/1.1, implicit streaming on HTTP/2). The extension aborts when Content-Length is missing.
Without Accept-Encoding, the same endpoint returns the binary uncompressed with Content-Length: 53395640 and works fine.
Verification
# Without compression - Content-Length present c✅
curl -s -o /dev/null -D - "https://<hostname>/bin/coder-linux-amd64" 2>&1 | grep -iE 'content-length|content-encoding|transfer-encoding'
# content-length: 53395640
# x-original-content-length: 53395640
# With compression - Content-Length missing ❌
curl -s -o /dev/null -D - -H "Accept-Encoding: gzip, deflate, br" "https://<hostname>/bin/coder-linux-amd64" 2>&1 | grep -iE 'content-length|content-encoding|transfer-encoding'
# content-encoding: br
# x-original-content-length: 53395640Confirmed the compression happens at the Coder server itself (not the ALB) by testing directly against localhost:7080 inside the pod.
Source Code Analysis
Server: coderd/coderd.go - compression middleware
The Coder server wraps the site handler (which includes /bin/*) with compressHandler():
r.NotFound(csp.Mw(compressHandler(httpmw.HSTS(api.SiteHandler, ...))).ServeHTTP)compressHandler uses chi/middleware.NewCompressor with Brotli, zstd, and gzip for content types text/*, application/*, image/*. Since http.FileServer serves binaries as application/octet-stream, they match the application/* pattern and get compressed.
Server: site/site.go - binary handler
The binHandler already sets X-Original-Content-Length with the uncompressed size, and the code comments explicitly acknowledge the problem:
// http.FileServer will not set Content-Length when performing chunked
// transport encoding, which is used for large files like our binaries
// so stream compression can be used.
//
// Clients like IDE extensions and the desktop apps can compare the
// value of this header with the amount of bytes written to disk after
// decompression to show progress. Without this, they cannot show
// progress without disabling compression.So the server team is aware that compression drops Content-Length and added X-Original-Content-Length as a workaround - but the extension doesn't use it.
Extension: src/core/cliManager.ts - download method
The extension sends Accept-Encoding: gzip explicitly, but axios also adds br, deflate by default. The download method reads content-length from the response:
const rawContentLength = resp.headers["content-length"] as unknown;
const contentLength = Number.parseInt(
typeof rawContentLength === "string" ? rawContentLength : "",
);When Content-Length is missing (compressed response), contentLength becomes NaN. The download logs a warning ("Got invalid or missing content length") and shows "unknown" for progress. The stream then aborts - the exact mechanism is unclear (possibly axios's maxContentLength/maxBodyLength defaults interacting with a NaN content length), but the extension reports "Unable to download binary: aborted".
Suggested Fixes
Extension side (coder/vscode-coder) - simplest fix
Send Accept-Encoding: identity when downloading the CLI binary. There's no benefit to compressing a compiled binary, and this sidesteps the entire problem - no compression, no missing Content-Length, no stream issues:
headers: {
"Accept-Encoding": "identity",
"If-None-Match": "${etag}",
},As a secondary improvement, read X-Original-Content-Length as a fallback for progress display, which the server already provides:
const rawContentLength = resp.headers["content-length"]
?? resp.headers["x-original-content-length"];Server side (coder/coder) - exclude binaries from compression
The compressHandler wraps the entire site handler including /bin/*. Since binaries are already compressed (or incompressible), compressing them wastes CPU and breaks clients. The fix is to either:
- Exclude
application/octet-streamfrom the compressor's content type list - Or wrap only the non-binary routes with
compressHandler, leaving/bin/*uncompressed - Or set
Content-Encoding: identityinbinHandlerbefore the response is written, which would prevent the compressor middleware from re-encoding
Workaround
The following script downloads the binary manually, bypassing the extension's download logic. This confirms the binary endpoint works fine - the issue is specifically in the extension's HTTP client.
#!/bin/bash
# fix-coder-binary.sh
# Downloads the Coder CLI binary manually for Kiro when the extension
# fails with "Unable to download binary: aborted" due to missing
# Content-Length header from the Coder server.
#
# Usage: bash fix-coder-binary.sh <coder-hostname>
set -e
CODER_HOST="${1:?Usage: bash fix-coder-binary.sh <coder-hostname>}"
# Detect OS and architecture
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) OS="windows" ;;
Linux) OS="linux" ;;
Darwin) OS="darwin" ;;
*) echo "Unsupported OS: $(uname -s)"; exit 1 ;;
esac
case "$(uname -m)" in
x86_64|amd64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
*) echo "Unsupported arch: $(uname -m)"; exit 1 ;;
esac
BINARY="coder-${OS}-${ARCH}"
[ "$OS" = "windows" ] && BINARY="${BINARY}.exe"
# Determine Kiro globalStorage path
if [ "$OS" = "windows" ]; then
TARGET_DIR="${APPDATA}/Kiro/User/globalStorage/coder.coder-remote/${CODER_HOST}/bin"
elif [ "$OS" = "darwin" ]; then
TARGET_DIR="${HOME}/Library/Application Support/Kiro/User/globalStorage/coder.coder-remote/${CODER_HOST}/bin"
else
TARGET_DIR="${HOME}/.config/Kiro/User/globalStorage/coder.coder-remote/${CODER_HOST}/bin"
fi
mkdir -p "${TARGET_DIR}"
URL="https://${CODER_HOST}/bin/${BINARY}"
DEST="${TARGET_DIR}/${BINARY}"
echo "Downloading ${BINARY} from ${CODER_HOST}..."
echo " URL: ${URL}"
echo " Dest: ${DEST}"
curl -fL -o "${DEST}" "${URL}"
chmod +x "${DEST}" 2>/dev/null || true
SIZE=$(stat -c%s "${DEST}" 2>/dev/null || stat -f%z "${DEST}" 2>/dev/null)
echo "Done. Downloaded $(awk 'BEGIN{printf "%.1f", '${SIZE}/1048576}') MB"Must be re-run when the Coder server version changes.
Environment
- Coder server: v2.30.2+fa050ee
- Extension: coder.coder-remote v1.12.2 (from OpenVSX)
- Remote SSH: jeanp413/open-vscode-ssh v0.0.49
- Client: Kiro IDE 0.10.32 (Windows)
- Infrastructure: AWS EKS with ALB ingress (not the cause)