Skip to content

fix: report why a temp file could not be written - #480

Merged
tas50 merged 1 commit into
mainfrom
fix/temp-file-error-reporting
Aug 23, 2026
Merged

fix: report why a temp file could not be written#480
tas50 merged 1 commit into
mainfrom
fix/temp-file-error-reporting

Conversation

@tas50

@tas50 tas50 commented Aug 23, 2026

Copy link
Copy Markdown
Member

The bug

create_temp_file assigned the opened file back over its own file parameter, then closed it in an ensure:

begin
  path = ::File.dirname(file)
  ::FileUtils.mkdir_p(path) unless ::Dir.exist?(path)
  file = ::File.open(file, "w")      # String -> File
  file.write(contents)
rescue IOError => e
  raise "Failed to write temp file. Error Details: #{e}"
ensure
  file.close unless file.nil?        # if the open raised, this is still a String
end

Two things go wrong together:

  1. If opening fails, file is still the path String, so the ensure calls String#close and raises NoMethodError — which replaces the real error.
  2. The rescue catches IOError, but opening a file fails with Errno::*, which are SystemCallErrornot IOError. So the intended message never appeared either.

Confirmed

Every failure path produced the same useless error:

parent is a file      -> NoMethodError: undefined method 'close' for an instance of String
directory read-only   -> NoMethodError: undefined method 'close' for an instance of String
target is a directory -> NoMethodError: undefined method 'close' for an instance of String

Container::Linux#execute and Container::Windows#execute both write their command through this, so it reaches users as a Docker failure. With .kitchen not writable — which is what a read-only checkout, or a CI job with a read-only mount, gives you — a real kitchen exec said:

>>>>>> Message: Docker failed to execute command on container. Error Details:
       Failed to execute command on Linux container.
       undefined method 'close' for an instance of String

Nothing there points at a filesystem permission.

The fix

File.write, which opens, writes and closes in one call — no handle to leak, no reassignment to trip over, no ensure needed. SystemCallError is rescued alongside IOError, and the message names the path.

The same run now reports:

>>>>>> Message: Docker failed to execute command on container. Error Details:
       Failed to execute command on Linux container.
       Failed to write temp file ./.kitchen/temp/docker-7fa49201-....sh.
       Error Details: Permission denied @ dir_s_mkdir - ./.kitchen/temp

And the normal path is unchanged — kitchen exec … -c "echo hello" still prints hello.

Specs

New spec/file_helper_spec.rb: writes contents, creates the parent directory (./.kitchen/temp does not exist on a fresh checkout), truncates an existing file, leaves no open handle, and — for each failure mode — reports the real cause, names the path, and never raises NoMethodError.

One note on the parent-is-a-file case: the errno differs by platform (macOS reports EEXIST from mkdir, Linux ENOTDIR), so that example asserts the underlying cause reaches the user rather than pinning an exact string. That is the thing that was lost.

$ bundle exec rake
43 files inspected, no offenses detected
282 examples, 0 failures

$ bundle exec rake doc
clean, 100.00% documented

🤖 Generated with Claude Code

create_temp_file assigned the opened file back over its own file parameter and
closed it in an ensure:

    file = ::File.open(file, "w")
    file.write(contents)
  rescue IOError => e
    raise "Failed to write temp file. Error Details: #{e}"
  ensure
    file.close unless file.nil?

When opening failed, file was still the path String, so the ensure called
String#close and raised NoMethodError -- discarding the real error. The rescue
could not help: it caught IOError, while opening a file fails with Errno
classes, which are SystemCallError and not IOError.

Every failure produced the same useless message. Confirmed against a
read-only directory, a parent that is not a directory, and a target that is
itself a directory:

    parent is a file      -> NoMethodError: undefined method 'close' for an instance of String
    directory read-only   -> NoMethodError: undefined method 'close' for an instance of String
    target is a directory -> NoMethodError: undefined method 'close' for an instance of String

Container::Linux#execute and Container::Windows#execute both write their
command through this, so it reached users as a Docker failure. With .kitchen
not writable, which is what a read-only checkout gives you, kitchen exec said:

    Docker failed to execute command on container. Error Details: Failed to
    execute command on Linux container. undefined method 'close' for an
    instance of String

Written with File.write now, which opens, writes and closes in one call, so
there is no handle to leak and no reassignment to trip over. SystemCallError is
rescued alongside IOError, and the message names the path. The same run now
reports:

    Failed to write temp file ./.kitchen/temp/docker-7fa49201-....sh.
    Error Details: Permission denied @ dir_s_mkdir - ./.kitchen/temp

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tas50
tas50 merged commit 70565e5 into main Aug 23, 2026
107 checks passed
@tas50
tas50 deleted the fix/temp-file-error-reporting branch August 23, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant