Skip to content

Unable to download binary: aborted. Binary download fails when server responds with content-encoding #810

@filterany

Description

@filterany

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

  1. Deploy Coder server behind any standard setup (tested with AWS ALB on EKS)
  2. Connect from Kiro IDE using the coder.coder-remote extension
  3. Extension attempts to download /bin/coder-windows-amd64.exe (~51 MB)
  4. 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: 53395640

Because 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: 53395640

Confirmed 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:

  1. Exclude application/octet-stream from the compressor's content type list
  2. Or wrap only the non-binary routes with compressHandler, leaving /bin/* uncompressed
  3. Or set Content-Encoding: identity in binHandler before 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions