NAME
API::Docker::Error::Truncated - The daemon closed before the response it announced was complete
VERSION
version 0.004
SYNOPSIS
# A tar the daemon stopped sending halfway is not a tar.
my $tar = eval { $docker->images->get_tar('busybox') };
if (my $err = $@) {
die $err unless ref $err
&& $err->isa('API::Docker::Error::Truncated');
warn 'got ' . length($err->partial) . ' of '
. $err->expected . ' bytes; retrying';
$tar = $docker->images->get_tar('busybox');
}
DESCRIPTION
API::Docker::Role::HTTP croaks with an object of this class when the daemon closed the connection in the middle of a response -- a status line with no terminator, a header block with no blank line to close it, a body shorter than its Content-Length, a chunk shorter than its own header, a chunk header cut in half, or a chunked body with no terminating zero chunk. It is raised in one more place that is not a closed connection but has the same consequence: a chunk size line that arrived in full and is not a hexadecimal number, which would otherwise be misread as a zero chunk and end the body early (see "phase").
It is a structural check, not a heuristic, and it asks one of two questions depending on how the piece is delimited. Where the response announced a length it compares what arrived against it. Where the framing is by terminator instead -- the head, and the chunk headers -- it asks whether the terminator came before the stream ended, which needs nothing to compare and is just as decidable. Neither is a guess about content: a header block that never closed is not a short one, it is an unfinished one.
A body delimited by nothing but the close -- attach, logs(follow => 1), /exec/{id}/start, the whole application/vnd.docker.raw-stream family -- announces no end and has no terminator either, so there an EOF is the end and this is never raised. Its head is framed like any other, and is checked like any other.
Why it is fatal
For the same reason API::Docker::Error::Timeout is, and the two are the same defect reached by different routes: a short body satisfies every return shape this role promises and is indistinguishable from a complete one. ndjson promises an ArrayRef of events and gets a shorter one; raw promises the response bytes and gets fewer of them; the default promises the decoded body and gets whatever the truncated bytes happened to parse as. A half tarball that looks whole is the worst of them, and it is the case this exists for.
Nothing is lost by raising it: "partial" carries the bytes a buffered read had collected and "summary" the count a streamed one had delivered, so a caller who wants what arrived can have it. What it cannot do any more is mistake it for everything.
What it is not
Not a timeout. Nothing waited and nothing expired -- the daemon answered, and then the stream ended early. API::Docker::Error::Timeout is raised when the daemon goes quiet for longer than a read_timeout, which is a bound the caller asked for; this needs no option and is on for every request.
Not a status. Where a status line arrived intact it said 200 and the response after it did not follow; where "phase" is 'status-line' there was no usable status to begin with. An engine that reports a failure the normal way raises API::Docker::Error::HTTP, and one that reports it inside an HTTP 200 stream raises API::Docker::Error::Stream. This is the third thing: no report at all, because the connection went away mid-sentence.
Nor is it the daemon answering nothing whatsoever. A connection that closed before a single byte of the status line is still the plain No response from Docker daemon croak it has always been -- there is no half-sent response to describe, and that string predates every error class here.
A response with a status of 400 or above raises this rather than API::Docker::Error::HTTP when its body is the one cut short, which is the same rule the timeout follows: the transport cannot tell a caller what the engine said when it did not finish saying it. Read "partial" for the part of the error body that did arrive.
It is still the string it replaces
Like the other three error classes here, this one 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.
Unlike the other three it replaces no string, because there was nothing here to replace -- a truncated response used to be returned rather than raised. That makes it the one exception in this distribution that existing code cannot have been catching, which is why it is a documented behaviour change and not a bug fix in passing.
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, where in the response framing the stream ended, and how much had arrived.
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 was cut short, as "GET /v1.47/images/get" -- method and path, no query string. The empty string for a reader driven directly with no request context, which is how the transport's own tests drive them.
phase
Which piece of the response framing the stream ended inside. One of:
'status-line'- the stream ended inside the status line, before the CRLF that terminates it. A status line with nothing after it parses perfectly well --'HTTP/1.1 200 OK'yields 200 andOK-- so the missing terminator is the only thing that says the daemon never finished writing it. Also a line that arrived in full but is not an HTTP status line at all -- a proxy's plain-text banner, an HTML error page -- which is no cut response, but is refused here for the reason the non-hexadecimal chunk size below is: its second word would otherwise be split out and read as the status'header-block'- the stream ended inside a header line, or where one belongs with the blank line that ends the field section never sent. The second covers a head with no fields at all: RFC 9112 section 2.1 requires the empty line whether there are twenty fields or none'content-length'- fewer bytes arrived than theContent-Lengthheader announced, or the header arrived in full but its value is not a number. The second is no cut response either: left as it stood it would read as0and a response that had a body would come back empty, the same body-shaped lie a truncation is'chunk-header'- the stream ended inside a chunk size line, or at a chunk boundary with no terminating zero chunk after it, or a chunk size line that arrived in full but is not a hexadecimal number. The last is not a cut response: the line is complete and terminated, buthexwould read its garbage as0-- the terminating zero chunk -- so the body would silently come back empty. It is caught here because the outcome is the same body-shaped lie a truncation is, not because the connection went away'chunk-data'- the stream ended inside a chunk, short of the size that chunk's own header announced'chunk-terminator'- a chunk's data arrived in full and the CRLF that ends it did not
Informational rather than something to branch on: every value means the same thing to a caller, which is that the response is incomplete. It is here because "which of the four" is the first question when a real engine starts raising this, and reading it off the object beats parsing "message".
expected
The byte count the framing announced for the piece that was cut short: the Content-Length for 'content-length', the chunk's own size for 'chunk-data'. undef for the four phases with no announcement to fall short of, which are the ones framed by a terminator instead.
received
How many of "expected" arrived. undef whenever "expected" is.
Note that this counts the piece, not the response: on a chunked body it is the bytes of the unfinished chunk, while "partial" holds every chunk before it as well.
partial
The response body bytes that had arrived when the stream ended, 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 beyond what was needed to find the truncation, and the content stops 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.
Also always the empty string when "phase" is 'status-line' or 'header-block': the response was cut before its body began, so there are no body bytes to hand over. The bytes of the head itself are deliberately not put here -- they are not a body, and "message" already says how far into which piece the stream got.
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 stream was cut off. undef for a buffered request.
stopped is always 0 here. A stream the caller ended with $stop->() leaves the rest of the response unread on purpose and is never truncation -- the check is skipped entirely once the callback has said stop.
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 "Failure in the middle of a response" in API::Docker::Role::HTTP
API::Docker::Error::Timeout - Raised instead when the daemon went quiet for longer than a
read_timeout, rather than closingAPI::Docker::Error::HTTP - Raised instead when the daemon answered, completely, 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.