Skip to content

fix(export): remove the file a failed export left behind - #336

Open
harsh-thakkar7 wants to merge 1 commit into
petertzy:mainfrom
harsh-thakkar7:fix/export-cleanup-orphaned-temp-files
Open

harsh-thakkar7 wants to merge 1 commit into
petertzy:mainfrom
harsh-thakkar7:fix/export-cleanup-orphaned-temp-files

Conversation

@harsh-thakkar7

Copy link
Copy Markdown
Contributor

Summary

A failed DOCX or PDF export leaves its output file behind in the temporary
directory. Nothing ever removes it.

Cause

_make_output_path creates the output file before the exporter runs, because
tempfile.mkstemp both allocates the name and creates the file:

fd, path = tempfile.mkstemp(suffix=suffix)
os.close(fd)
return path

So by the time export_docx / export_pdf call the exporter the file is already
on disk. Their except clauses then only converted the exception into a 500:

    except Exception as exc:
        raise HTTPException(status_code=500, detail=str(exc))

Two consecutive failures in one temp directory on main:

tmp3er68z13.docx   0 bytes
tmpt26yg_8i.pdf    0 bytes

Neither was removed. A partial write leaves a partial file rather than an empty
one, so the litter is not always zero-length either.

Fix

_discard_failed_export removes the file on failure — but only when the path was
generated for this request
:

    if not requested_path:
        _remove_file(out_path)

That asymmetry is deliberate. A caller who passed output_path may be holding a
partial file they want to inspect, and silently deleting a file someone asked to
have written is worse than leaving it. So an exporter that fails partway through
writing to a caller-supplied path still leaves that partial file; only the files
we created ourselves are cleaned up.

export_html needs no change — it renders before allocating the path, so a
failure cannot orphan anything.

Verification

  • pytest: 270 passed → 274 passed on the full backend suite, no new failures.
  • ruff check . clean; ruff format --check . reports no offenders.
  • 8 of 8 mutants killed, including the two that matter most here:
    • removing the not requested_path guard (i.e. deleting unconditionally) is
      caught by test_failed_export_keeps_a_caller_supplied_path;
    • cleaning up on success as well is caught by
      test_successful_pdf_export_still_keeps_its_file.

The tests drive the routes through TestClient with the exporter replaced by one
that writes a partial file and then raises, so they cover the partial-output case
rather than just the empty-file case.

_make_output_path creates the output file up front, so by the time the exporter
runs the file already exists. export_docx and export_pdf then only converted an
exporter exception into a 500, leaving the artifact behind: two consecutive
failures in one temp directory produced tmp3er68z13.docx and tmpt26yg_8i.pdf,
both 0 bytes, neither removed by anything.

Clean up the generated file on failure. Deliberately asymmetric: a caller who
passed output_path may be holding a partial file they want to look at, so only
paths we generated are removed. An exporter that fails partway through writing
to a caller-supplied path still leaves that partial file, which is the lesser
evil next to deleting a file the caller asked to have.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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