Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **`bin/rails claude_agent_sdk:install_cli` (and `CLIInstaller.install` whenever `Encoding.default_internal` is set) no longer fails with `Encoding::UndefinedConversionError`** (issue #177). The download was opened with `File::BINARY`, which is 0 outside Windows, so the file was in text mode; Rails sets `default_internal` to UTF-8, and each write then transcoded the binary chunks and raised on the first byte that is not valid UTF-8. The file is now opened with `binmode: true`.

## [1.4.1] - 2026-10-25

### Changed
Expand Down
7 changes: 6 additions & 1 deletion lib/claude_agent_sdk/cli_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,15 @@ def fetch_text(url, limit:)
# place, and a rename only orders metadata. Without the sync, a power
# loss shortly after an install could leave the published name
# pointing at an empty or partial file.
#
# binmode, not File::BINARY: that flag is 0 outside Windows, so the
# file would be in text mode, and with Encoding.default_internal set
# (Rails sets it to UTF-8) every write would transcode the binary
# chunks and raise on the first byte that is not valid UTF-8 (#177).
def download_to(url, path, max_bytes: nil)
with_response(url) do |response|
written = 0
File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::BINARY, 0o600) do |file|
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o600, binmode: true) do |file|
response.read_body do |chunk|
written += chunk.bytesize
if over?(written, max_bytes)
Expand Down
20 changes: 20 additions & 0 deletions spec/unit/cli_installer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,26 @@ def stub_response(*chunks)
expect(File.binread(target)).to eq('abcdef')
end

# Issue #177: Rails sets default_internal to UTF-8, and a text-mode file
# transcodes each write. File::BINARY is 0 outside Windows.
it 'writes the bytes unchanged when Encoding.default_internal is set' do
payload = "\xB7\x00\xFF\xCF".b
stub_response(payload[0, 2], payload[2, 2])
target = File.join(tmp_dir, 'binary')
saved = Encoding.default_internal
verbose = $VERBOSE
$VERBOSE = nil # the setter warns under -W

begin
Encoding.default_internal = Encoding::UTF_8
expect(http.download_to('https://example.com/claude', target)).to eq(target)
ensure
Encoding.default_internal = saved
$VERBOSE = verbose
end
expect(File.binread(target)).to eq(payload)
end

it 'aborts mid-stream once max_bytes is exceeded' do
stub_response('a' * 4, 'b' * 4)
target = File.join(tmp_dir, 'too-big')
Expand Down
Loading