NAME
API::Docker::Error::Timeout - Read timeout while waiting for the Docker Engine
VERSION
version 0.004
SYNOPSIS
# Stop waiting after two seconds of silence instead of hanging forever.
my $out = '';
eval {
$docker->containers->attach($id,
stream => 1,
stdout => 1,
read_timeout => 2,
on_frame => sub { $out .= $_[0]{data} },
);
};
if (my $err = $@) {
die $err unless ref $err
&& $err->isa('API::Docker::Error::Timeout');
# Every complete frame reached the callback before the timeout; the
# summary says how many.
warn 'stopped after ' . $err->summary->{delivered} . ' frames';
}
DESCRIPTION
API::Docker::Role::HTTP croaks with an object of this class when a request was given a "read_timeout" in API::Docker::Role::HTTP and the daemon then went quiet for longer than it -- and, with "phase" set to 'connect', when a request was given a "connect_timeout" in API::Docker::Role::HTTP and the socket never came up within it.
The rest of this describes the read timeout, which is the one that has something to hand back. A connect timeout carries no "partial" and no "summary", for the reason "phase" gives: nothing was ever sent.
It is an idle timeout, not a deadline: the clock is the time since the last byte arrived, so a stream that keeps producing runs as long as it likes and one that stalls is cut off. That is the distinction the endpoints this exists for need -- a hung /containers/{id}/attach has already delivered its buffered frames before it stalls, so "nothing yet" would never have fired.
Why it is fatal, on every path
A timeout is not information about the response; it is the absence of it. The transport cannot know whether the daemon was about to send the rest, so it cannot decide for the caller that what arrived is usable -- and every return shape this distribution promises would hide the question if it tried. ndjson promises an ArrayRef of events, raw promises the response bytes, the default promises the decoded body: a truncated value satisfies all three and is indistinguishable from a complete one. A half tarball that looks whole is a worse outcome than the hang it replaced.
That holds for the callback streams too, even though they have already handed the caller every complete unit. Returning normally there would run the stream handler's finish step, which is written for a daemon that closed: it treats a trailing partial line as a complete final event, and reports leftover bytes as a frame the daemon cut in half. Neither statement is true of a timeout. One rule -- a timeout is fatal -- also keeps read_timeout meaning the same thing whether or not on_event/on_frame/on_chunk is in use.
Nothing is lost with the exception: "partial" carries the bytes a buffered read had collected, "summary" the count a streamed one had delivered. A caller who wants "collect what there is, then stop" writes the eval in the SYNOPSIS; a caller who wants to fail loudly gets that without writing anything.
It is still the string it replaces
Like API::Docker::Error::HTTP and API::Docker::Error::Stream, this class overloads stringification (with fallback => 1, so comparison, concatenation, sprintf and matching all work through it) and produces what a plain croak would have died with: the reason, followed by Carp's own at FILE line N. location suffix, naming the same frame.
The boolean overload is explicit rather than derived from the string, so it cannot be made false by its own message.
message
The reason on its own, without the location suffix: the request it belongs to, the timeout that expired and how much had arrived before it did.
The request is named without its query string, for the same reason the >= 400 croak names it that way -- /build carries its buildargs there, which can hold credentials and have no business in an exception.
location
Carp's location suffix ( at FILE line N.\n), captured at the point the error was raised so it names the same frame a plain croak would have named. Kept apart from "message" so a caller can have the reason without it.
endpoint
The request that timed out, as "GET /v1.47/containers/json" -- method and path, no query string.
phase
Which of the two bounds fired: 'read' for "read_timeout" in API::Docker::Role::HTTP, 'connect' for "connect_timeout" in API::Docker::Role::HTTP. 'read' is the default, so an object built without it describes what every one of them used to describe.
One class rather than two, because the question a caller catches this to ask -- "did the request finish in time?" -- has the same answer either way, and a second class would make every such caller name two of them or find their common base. What differs is one bit: whether the daemon was ever reached. That bit is this attribute.
It is also the only thing that tells a 'connect' timeout apart from a 'read' one that expired before the first byte: "partial" is the empty string and "summary" is undef for both.
timeout
The number of seconds of silence that triggered this, i.e. the effective read_timeout of the request -- or, when "phase" is 'connect', the connect_timeout that expired. Not the total time the request took: a stream that sent something every second for an hour and then stopped reports the same value as one that never said anything.
partial
The response body bytes that had arrived when the timeout fired, for a request whose body was being buffered -- the empty string when none had.
These are not a body: nothing was decoded, no chunk framing was verified and the content may stop mid-value. They are here so a caller who wants them can have them rather than because the transport thinks they are usable.
Always the empty string for a streamed request, which keeps no body by design. Nothing is lost there either: every byte that arrived went through the same decoding as every other byte, so the units it completed reached the callback and are counted in "summary" before this is raised.
That holds by construction rather than by rescue, which is worth knowing if you are reading the transport. The read that expires carries nothing -- sysread returns what it received and leaves errno alone, and fails with EAGAIN only when it received nothing at all. It used to be otherwise: PerlIO's read() could come back with part of what it was asked for and EAGAIN together, so the bytes of the expiring read had to be fed to the callback before the exception went up or a caller would have been handed nothing even though the whole response had arrived.
Also empty when "phase" is 'connect': there was no response to have part of.
summary
For a request streaming through on_event, on_frame or on_chunk: the same { delivered => N, stopped => 0 } HashRef the call would have returned, describing what reached the callback before the timeout. undef for a buffered request.
Every unit it counts was complete and was delivered; a unit still arriving when the clock ran out was not, and is not counted and not delivered.
as_string
my $text = $err->as_string; # same as "$err"
The message and the location suffix, concatenated. This is what the stringification overload returns.
SEE ALSO
API::Docker::Role::HTTP - Raises this error; see its
read_timeoutattribute and optionAPI::Docker::Error::HTTP - Raised instead when the daemon answered, with a status of 400 or above
API::Docker::Error::Stream - Raised instead for a failure reported inside a stream the daemon already answered with HTTP 200
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-api-docker/issues.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.