Skip to content

fix: transfer timeouts and nested directory creation - #79

Merged
ilyabrin merged 2 commits into
releasefrom
fix/transfer-timeouts
Sep 19, 2026
Merged

ilyabrin merged 2 commits into
releasefrom
fix/transfer-timeouts

Conversation

@ilyabrin

Copy link
Copy Markdown
Owner

Why

Two bugs surfaced while wiring this package into a TUI client.

Uploads and downloads died after 30 seconds

Config.DefaultTimeout is assigned to http.Client.Timeout, an absolute
deadline over the entire request including the response body. Streamed
transfers ran on that client, so the 30s default killed any transfer that
took longer, roughly anything over 35 MB on a 10 Mbit/s link. A caller
passing a 30-minute context saw no benefit: whichever deadline fires first
wins, and it was always the client's.

uploadFileMultipart already had the right fix locally, with a comment
describing exactly this hazard. It just never reached uploadFileSingle
or the download path, and since UploadFileFromPathWithProgress leaves
ChunkSize at 0, the common upload path was always the unfixed one.

CreateDir could not create nested paths

The Yandex Disk API creates a single level per request, so CreateDir
failed on a/b/c unless every parent already existed. This was tracked by
a todo on the function.

What changed

  • Client.transferClient() returns a client sharing the configured
    transport but carrying no Timeout; transferContext() applies a
    30-minute fallback only when the caller set no deadline of their own.
    Both are now used by single upload, multipart upload and download.
  • CreateDirAll creates missing parents, mirroring os.MkdirAll and
    treating existing directories as success. CreateDir is unchanged and
    keeps os.Mkdir semantics.
  • ErrorResponse.StatusCode records the HTTP status behind an error
    (json:"-", populated by this package), and ErrorResponse.AlreadyExists()
    distinguishes a "directory is already there" conflict from a real one.

Compatibility

No breaking changes. Signatures are untouched; StatusCode is a new field
on an existing struct and CreateDirAll / AlreadyExists are additions.
Suitable for a patch or minor release.

Tests

transfer_timeout_test.go drives real httptest servers that stream
bodies more slowly than DefaultTimeout. Both transfer tests were
confirmed to fail against the previous code with
context deadline exceeded (Client.Timeout ...) and to pass after the
fix; a third test checks that a caller's own short deadline still aborts
the transfer.

createdirall_test.go covers segment-by-segment creation, existing
parents, idempotency, path forms (disk: prefix, relative, redundant
slashes), propagation of genuine errors, and rejection of traversal and
empty paths.

go test -race ./... passes.

Config.DefaultTimeout lands in http.Client.Timeout, which is an absolute
deadline covering the whole request including the body. That is right for
the small JSON calls the API is mostly made of, but it also applied to
streamed transfers: with the 30s default, every upload or download slower
than 30 seconds was aborted mid-flight, no matter how generous a deadline
the caller had put on ctx.

uploadFileMultipart already worked around this with a private client that
carries no Timeout. Lift that into Client.transferClient and a
transferContext helper, and use both from uploadFileSingle and
DownloadFileToPath, so every path that streams a body is bounded by the
context instead of the wall clock.

Callers who set their own deadline keep it; those who do not fall back to
30 minutes, as chunked uploads already did.
The API creates one directory level per request, so CreateDir could not
create "newDir/subDir/anotherDir", which was the long-standing todo on it.
Add CreateDirAll, which walks the path from the shallowest segment to the
deepest and issues one request per missing level, mirroring os.MkdirAll:
existing directories are left alone rather than reported as errors.

Telling "already exists" apart from a real conflict needs the HTTP status,
which ErrorResponse did not carry, so record it in a new StatusCode field
(json:"-", filled in by this package) and expose the check as
ErrorResponse.AlreadyExists.

CreateDir keeps its single-level semantics, matching os.Mkdir.
@ilyabrin
ilyabrin merged commit e3a378d into release Sep 19, 2026
11 checks passed
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